GUI in sandbox not working

I'm still trying to get my plugin system working (Racket plugins in a Golang application). The problem is that it does work for ordinary code, but it does not seem to work for GUI. The gracket executable is started as a sub-process in Go and loaded requiring paths to the two modules needed for plugin support using the -t flag. It also uses the -e flag to start the server with (init)(start-server). It then initializes all plugins by entering their directory and using dynamic-require like in the following string.

(parameterize ([current-directory "path/to/dir/"])(dynamic-require "program.rkt" 0))

This is sent to the server running in Racket and executed using a sandbox evaluator in which racket/gui is required and the GUI is enabled. It executes the code in the evaluator and registers the plugin as it should. However, if I try to display a message-box, the process halts waiting for the button press but nothing is displayed on screen. No window is displayed on Linux/GTK. But it does wait for the button press, halting everything. (If I send it to queue-callback, the program continues but nothing is displayed either.) It's as if all GUI related things disappear. I'm capturing the output, no error is returned from gracket.

In earlier versions of the system, when I just loaded plugin code into an unrestricted gracket, it did show the message-box just fine, as you would expect. So what's going wrong here?

Here is the code of a test plugin:

#lang racket/gui

;;;; This is a template for defining an action. Please study the Projects extension manual
;;;; and the Racket documentation for more information.
;;;;
;;;; An action is an instance of the Racket class action%, as defined in the above required file.
;;;; The contents of this module are evaluated and it should define an instance of action%
;;;; and register it using (register-action action). After it has been registered, it will be
;;;; available in the main application under the given prefix and name.

(require "../../actions.rkt")

(register-action 
 (new action%
      (prefix "debug")
      (name "test")
      (run-proc
       (lambda ()
         (display "This is a test action merely used for debugging.\n")))))

(message-box "Test" "Let's see if that works.")

;;;; NOTE: The <name> and <prefix> init-fields of the action% instance must
;;;; coincide with the directory names in which the plugin resides, namely
;;;; in subdirectory actions/racket/<prefix>/<name>/program.rkt within the
;;;; Projects directory. This file must be named "program.rkt" and is loaded
;;;; automatically during plugin initialization.

Here is the entire Racket side of the plugin system:

#lang racket
(require zeromq)
(require json)
(require racket/random)
(require racket/gui)
(require racket/sandbox)
(require "actions.rkt")

(provide init start-server evaluator)

;;; BASE SYSTEM

;;;; SECURITY: The following security guard allows all access!
(sandbox-init-hook
 (lambda ()
   (sandbox-path-permissions
    (list (list 'execute (byte-regexp #".*"))
          (list 'read-bytecode (byte-regexp #".*"))))
   (sandbox-security-guard
    (make-security-guard
     (current-security-guard)
     (lambda (caller path permissions)
       (display (format "~s ~s ~s\n" caller path permissions))
       (void))
     (lambda (caller host port sort)
       (void))
     (lambda (caller path content)
       (void))))))

(define (create-evaluator)
  (define inspector (current-code-inspector))
  (parameterize ([sandbox-make-code-inspector
                  (lambda () inspector)]
                 [sandbox-eval-limits (list #f #f)]
                 [sandbox-gui-available #t]
                 [sandbox-output 'string])
    (make-evaluator
     'racket/gui
     #f
     #:requires '(racket/base racket/gui racket/random "actions.rkt"))))

(define evaluator #f)

(define done-channel (make-channel))

(define requester (make-parameter #f))

;;; init initializes the server, connecting to Go on localhost port 8739
;;; via TCP and using (get-registered-functions) to obtain all Go functions.
;;; It then dynamically evaluates wrappers so all these functions are available in
;;; the Racket system. Afterwards, it starts a server listening on 8740 with a
;;; simple REP 0MQ socket. When it receives input, it attempts to evaluate it
;;; and returns the result as JSON. That means that the result of the function that is evaluated
;;; must be a legal jsonexpr, i.e. it e.g. cannot be a symbol but must be a string.
;;;
(define (init [prefix #f])
  (unless (requester)
    (set! evaluator (create-evaluator))
    (requester (zmq-socket 'req #:connect "tcp://localhost:8739"))
    (zmq-send (requester) (jsexpr->string '("get-registered-functions")))
    (define response (zmq-recv-string (requester)))
    (map
     (lambda (f)
       (define name (if prefix (string-append (symbol->string prefix) "-" f) f))
       (define-remote-function (requester) f name)
       (string->symbol name))
     (car (string->jsexpr response)))))

;;; define-remote-function defines a remote function based on a signature obtained from the Go server.
(define (define-remote-function requester remote-name local-name)
  (zmq-send requester (jsexpr->string `("get-signature" ,remote-name)))
  (define response (string->jsexpr (zmq-recv-string requester)))
  (when (not (equal? (third response) 'null))
    (error "Plugin initialization failed: " (third response)))
  (define (get-args response)
    (build-list (length (car response)) (lambda (i) (gensym "arg"))))
  (let ((args (get-args response)))
    (evaluator `(define (,(string->symbol local-name) ,@args)
                  (zmq-send ,requester  (jsexpr->string (list ,remote-name ,@args)))
                  (string->jsexpr (zmq-recv-string ,requester))))))

(define OK 0)
(define ERR_NOT_JSON 2)
(define ERR_USER 3)

;;; start-server starts the Racket side server, a simple 'REP socket listening on 8740.
;;; The Go RacketServer connects to this port and uses this for evaluating Racket expressions (one at a time)
;;; on the Racket side, obtaining the result. The result of the function must therefore be a valid JSON value
;;; such as 'null, a string, a number, etc.
(define (start-server)
  (define (jsonify datum)
    (cond
      ((void? datum) (json-null))
      ((symbol? datum) (symbol->string datum))
      (else datum)))
  (define identity (crypto-random-bytes 5))
  (define socket (zmq-socket 'rep 
                             #:identity identity #:bind "tcp://*:8740"))
  (zmq-bind socket)
  (let loop ((msg (zmq-recv socket)))
    (define expr (with-input-from-bytes msg (lambda () (read))))
    (define response
      (with-handlers ([exn:fail?
                       (λ (e)
                         (print (format "Racket action error: ~s" e))
                         (list ERR_USER (format "~s" e)(jsonify (get-output evaluator))))])
        (list OK (jsonify (evaluator expr))(jsonify (get-output evaluator)))))
    (cond
      ((jsexpr? response)
       (zmq-send socket (jsexpr->bytes response)))
      (else
       (zmq-send socket (jsexpr->bytes
                         (list ERR_NOT_JSON
                               (format "The remote call result was not a valid JSON expression: ~s"
                                       response))))
       (print (format "Function response for ~s is not a valid JSON expression: ~s" expr response))))
    (unless (zmq-closed? socket)
      (loop (zmq-recv socket))))
  (channel-put done-channel #t))


1 Like

Does it make a difference if you use gui-dynamic-require https://docs.racket-lang.org/gui/Dynamic_Loading.html#%28def._%28%28lib._racket%2Fgui%2Fdynamic..rkt%29._gui-dynamic-require%29%29 ? Might not be the right thing here, not sure.

Unfortunately not. If I use e.g. ((gui-dynamic-require 'message-box) "Test" "This is a test") in the plugin code, it works when running it in DrRacket, but no window is shown if running as a subprocess in Go. However, the plugin code is executed with (dynamic-require "program.rkt" 0) evaluated in the sandbox evaluator with path access and GUI enabled. register-action just adds it to a list, and (list-actions) lists it. I think the problem comes from the dynamic-require argument 0. I'm not very familiar with Racket's phases.

Any other way of loading the plugin code using this evaluator results in an error. For example, using (load "program.rkt") instead gives:

eval:5:0: list-actions: namespace mismatch;\n cannot locate module instance\n  module: \"/home/nemo/.local/share/Projects/actions.rkt\"\n  use phase: 0\n  definition phase: 0\n  in: list-actions" #<continuation-mark-set> (#<syntax:eval:5:0 list-actions>))

Curiously, the plugin is processed and registered in this case. But a later call of (list-actions) via the ZMQ socket results in this error, even though it uses the same sandbox evaluator.

Whatever I've tried so far, I either got this problem or the above problem where the code is executed in some phase but GUI elements are not visible.

I guess as a workaround for now I could simply use eval with a namespace obtained from a namespace anchor. This worked fine before. But without sandboxing the value of the plugin system will be highly diminished. I was planning to add file access and network access restrictions for plugins again later, which would e.g. allow users to exchange them more freely.

This is just a guess, but GUI's on Linux require a network access to the X server, or whatever else is used for the actual display. This can either be a TCP socket or a Unix domain socket. This means your sanbox evaluator must allow network connections to the display server.

Edit: there's a sandbox-network-guard parameter, which, by default, forbids all network connections, and you don't seem to set it anywhere in your code.

Alex.

1 Like