Racket provides protect-out
to prevent module exports from being used with eval (or a deconstructed syntax object), unless the module has enough privileges (aka, has a strong enough code inspector). The docs also provide a good example for what it does:
> (module nest racket
(provide num-eggs (protect-out num-chicks))
(define num-eggs 2)
(define num-chicks 3))
> (define weak-inspector (make-inspector (current-code-inspector)))
> (define (weak-eval x)
(parameterize ([current-code-inspector weak-inspector])
(define weak-ns (make-base-namespace))
(namespace-attach-module (current-namespace)
''nest
weak-ns)
(parameterize ([current-namespace weak-ns])
(namespace-require ''nest)
(eval x))))
> (require 'nest)
> (list num-eggs num-chicks)
'(2 3)
> (weak-eval 'num-eggs)
2
> (weak-eval 'num-chicks)
?: access disallowed by code inspector to protected variable
from module: 'nest
at: num-chicks
That is, eval
had a powerful enough code inspector (because it was called in the same scope that required the module originally) and therefore was able to get the export. However, weak-eval
did not, because it got the same instance of the module, but with a weaker inspector to use for eval
ing.
My question is, when should we use protect-out
? Should it always be used (or at least whenever possible)? Or is there a specific work flow that protect-out
is designed for?
Use protect-out
for unsafe exports, where unsafe means something that has the capability of violating the rules of the Racket language or virtual machine. In particular, it is often possible to crash the Racket VM by misusing unsafe features.
Examples of unsafe features:
unsafe-vector-set!
more or less lets you write to arbitrary memory locations, potentially violating the invariants of the GC, the invariants of Racket's value representations in memory, etcunsafe-vector-ref
seems less immediately dangerous, but you could use it for example to extract the value of a free variable from a closure, even though the Racket language does not allow inspecting the implementation of a procedureffi/unsafe
is unsafe, obviouslyHere's a less obvious example of an unsafe procedure:
#lang racket
(require ffi/unsafe ffi/unsafe/define)
(define-ffi-definer define-c #f) ;; searches libc, etc
(define-c fopen (_fun _path (_bytes = #"a") -> _pointer))
(define-c fclose (_fun _pointer -> _void))
(define-c fwrite (_fun _bytes _size _size _pointer -> _size))
(define (append-to-file path buf)
(define fp (fopen path))
(unless fp (error "couldn't open file"))
(fwrite buf (bytes-length buf) 1 fp)
(fclose fp))
(provide append-to-file)
Consider the append-to-file
procedure. It's defined using unsafe features (the FFI), but the FFI types _path
and _bytes
will reject bad Racket values, so it shouldn't be possible to crash Racket by using append-to-file
. The append-to-file
procedure is still unsafe (although perhaps not as catastrophically unsafe as unsafe-vector-set!
) because it circumvents Racket's security guard mechanism. The procedure is unsafe in another way because fopen
and fwrite
can block, and Racket code is not supposed to block.
The unsafe operations in Racket's core libraries are provided with protect-out
. If you use them to define your own unsafe operations, you should provide the derived unsafe operations using protect-out
too. If you use unsafe features to define a safe feature, then you can provide that normally (that is, without protect-out
), but think about it carefully first!
If you capture a privileged code inspector (either directly or indirectly via #%variable-reference
), you should also protect that, because it can be used to dynamically gain access to unsafe features.
The goal is this: If a namespace contains only modules that have property protected their unsafe exports, then any additional code evaluated with a weaker code inspector should not be able to violate the invariants of the Racket VM (eg make it crash, circumvent security guard checks, etc).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With