How to create the most permissive security guard for a racket sandbox?

Hi folks! For a plugin/bridge system with a Go program, I'm using make-evaluator from sandbox but for the time being would like to allow the full racket/gui language with access to any file. So I'm using this:

(sandbox-init-hook
 (lambda ()
   (sandbox-path-permissions
    (list (list 'execute (byte-regexp #".*"))))
   (sandbox-security-guard
    (make-security-guard
     (current-security-guard)
     (lambda (caller path permissions)
       (void))
     (lambda (caller host port sort)
       (void))
     (lambda (caller path content)
       (void))))))
     
(define evaluator
 (make-evaluator
  'racket/gui
  #f
  #:requires '(racket/base racket/gui racket/random)))

But I still get an error when I use this evaluator to load a program:

. . ../../../../../../../racket/share/pkgs/sandbox-lib/racket/sandbox.rkt:754:18: link: access disallowed by code inspector to unexported variable
  variable: make-ffi-definer-transformer
  from module: "/home/nemo/racket/collects/ffi/unsafe/define.rkt"

How can I allow any requires, any file access, any network access, and any links? I'm aware this defeats the whole purpose of sandbox but for testing and getting things working I really need full access first.

The plan is to later implement access restrictions based on GUI feedback on the plugins at the Go side, similar to asking for permissions on Android, and pass these over to the evaluator definition.

Do you mean a program writtin in Go, or a program to play Go?

I wish people would use less ambiguous names for the languages they make up.

-- hendrik

I thought this is clear from the context. It's a bridge to a program written in Go, I'm experimenting with Racket for providing the plugins. Currently, I'm using my own Lisp but it was never designed for serious applications and won't suffice in the long run. Since I want my extension language to be a robust Lisp dialect, it's either Racket, CommonLisp (SBCL), or some scheme like Chez. Racket has various advantages.

The problem is that sandboxed evaluation with relaxed access rules appears to be quite complicated in Racket. I need to 1. Compile a list of functions at runtime and provide them to every plugin module, 2. load a bunch of Racket code or modules that get executed in a shared environment, 3. call Racket functions from Go. Step 1 works but I'm not sure how to best provide the functions to plugins. Calling Go functions in Racket also works fine. But the sandboxed evaluation of incoming expressions in Racket is unfortunately complicated. I would already be happy and very grateful if someone could explain to me why the following code yields an error:

#lang racket
(require zeromq)
(require json)
(require racket/random)
(require racket/gui)
(require racket/sandbox)

(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 #".*"))))
   (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 evaluator
 (make-evaluator
  'racket/gui
  #f
  #:requires '(racket/base racket/gui racket/random)))

(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)
    (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)))])
        (list OK (jsonify (evaluator expr)))))
    (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))

The error:

. . ../../../../../../../racket/share/pkgs/sandbox-lib/racket/sandbox.rkt:754:18: link: access disallowed by code inspector to unexported variable
  variable: make-ffi-definer-transformer
  from module: "/home/nemo/racket/collects/ffi/unsafe/define.rkt"

Why? I thought the sandbox init hook makes sure the evaluator definition has the security guard defined in the init hook. Is that wrong? How do I define an evaluator that allows full access to any file? What does the "link" error mean?

1 Like

I've found the issue for this particular problem. The error message wasn't helpful but it turned out that sandbox-path-permissions needs to include 'read-bytecode:

(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))))))

Now it does execute simple commands. I first use (current-directory <path>) to set the directory to each plugin path and then try (load "program.rkt") to execute the plugin initialization code. Unfortunately, now the error is even more obscure: eval-linklet: cannot use unsafe linklet loaded with non-original code inspector

1 Like

Sorry for replying to my own questions but in case someone reads this later, I think I've got it working by setting the sandbox code-inspector to (current-code-inspector). For some reason, this does not work in sandbox-init-hook, which has been puzzling me. But it works using parameterize as follows:

(define (create-evaluator)
  (define inspector (current-code-inspector))
  (parameterize ([sandbox-make-code-inspector
                  (lambda () inspector)])
    (make-evaluator
     'racket/gui
     #f
     #:requires '(racket/base racket/gui racket/random))))

(define evaluator (create-evaluator))

When I use this evaluator with the above sandbox-init-hook, I can use (dynamic-require "module-name.rkt" 0) and it will load and execute the plugin code at local path "module-name.rkt". A message-box in the GUI does not work even though the module starts with #lang racket/gui, it does something but the message box doesn't show up and it times out. However, I'm sure I can fix this.

1 Like