On this page:
16.2.7.1 Using syntax-protect for Macro Results
16.2.7.2 Tainting Modes
16.2.7.3 Taints and Code Inspectors
16.2.7.4 Protected Exports
16.2.7 Code Inspectors and Syntax Taints

Modules often contain definitions that are meant only for use within the same module and not exported with provide. Still, a use of a macro defined in the module can expand into a reference of an unexported identifier. In general, such an identifier must not be extracted from the expanded expression and used in a different context, because using the identifier in a different context may break invariants of the macro’s module.

For example, the following module exports a macro go that expands to a use of unchecked-go:

"m.rkt"

#lang racket
(provide go)
 
(define (unchecked-go n x)
  ; to avoid disaster, n must be a number
  (+ n 17))
 
(define-syntax (go stx)
  (syntax-case stx ()
    [(_ x)
     #'(unchecked-go 8 x)]))

If the reference to unchecked-go is extracted from the expansion of (go 'a), then it might be inserted into a new expression, (unchecked-go #f 'a), leading to disaster. The datum->syntax procedure can be used similarly to construct references to an unexported identifier, even when no macro expansion includes a reference to the identifier.

Ultimately, protection of a module’s private bindings depends on changing the current code inspector by setting the current-code-inspector parameter. That’s because a code inspector controls access to a module’s internal state through functions like module->namespace. The current code inspector also gates access to the exports of unsafe modules like racket/unsafe/ops.

To some extent, a code inspector also constrains access to bindings via datum->syntax (see Code Inspectors for Trusted and Untrusted Code), but a code inspector does not control the use of expand or local-expand. To prevent misuse of its module’s bindings and imports in general, a macro should enable syntax taints via syntax-protect on its result syntax object.

16.2.7.1 Using syntax-protect for Macro Results

To prevent abuses of unexported identifiers, the go macro from the preceding example must explicitly protect its expansion by using syntax-protect:

(define-syntax (go stx)
  (syntax-case stx ()
    [(_ x)
     (syntax-protect #'(unchecked-go 8 x))]))

The syntax-protect function causes any syntax object that is extracted from the result of go to be tainted. The macro expander rejects tainted identifiers, so attempting to extract unchecked-go from the expansion of (go 'a) produces an identifier that cannot be used to construct a new expression (or, at least, not one that the macro expander will accept). The syntax-rules, syntax-id-rules, and define-syntax-rule forms automatically protect their expansion results.

More precisely, syntax-protect arms a syntax object with a dye pack. When a syntax object is armed, then syntax-e taints any syntax object in its result. Similarly, datum->syntax taints its result when its first argument is armed. Finally, if any part of a quoted syntax object is armed, then the corresponding part is tainted in the resulting syntax constant.

Of course, the macro expander itself must be able to disarm a dye pack on a syntax object, so that it can further expand an expression or its sub-expressions. When a syntax object is armed with a dye pack, the dye pack has an associated inspector that can be used to disarm the dye pack. A (syntax-protect stx) function call is actually a shorthand for (syntax-arm stx #f #t), which arms stx using a suitable inspector. The expander uses syntax-disarm and with its inspector on every expression before trying to expand or compile it.

In much the same way that the macro expander copies properties from a syntax transformer’s input to its output (see Syntax Object Properties), the expander copies dye packs from a transformer’s input to its output. Building on the previous example,

"n.rkt"

#lang racket
(require "m.rkt")
 
(provide go-more)
 
(define y 'hello)
 
(define-syntax (go-more stx)
  (syntax-protect #'(go y)))

the expansion of (go-more) introduces a reference to the unexported y in (go y), and the expansion result is armed so that y cannot be extracted from the expansion. Even if go did not use syntax-protect for its result (perhaps because it does not need to protect unchecked-go after all), the dye pack on (go y) is propagated to the final expansion (unchecked-go 8 y). The macro expander uses syntax-rearm to propagate dye packs from a transformer’s input to its output.

16.2.7.2 Tainting Modes

In some cases, a macro implementor intends to allow limited destructuring of a macro result without tainting the result. For example, given the following define-like-y macro,

"q.rkt"

#lang racket
 
(provide define-like-y)
 
(define y 'hello)
 
(define-syntax (define-like-y stx)
  (syntax-case stx ()
    [(_ id) (syntax-protect #'(define-values (id) y))]))

someone may use the macro in an internal definition:

(let ()
  (define-like-y x)
  x)

The implementor of the "q.rkt" module most likely intended to allow such uses of define-like-y. To convert an internal definition into a letrec binding, however, the define form produced by define-like-y must be deconstructed, which would normally taint both the binding x and the reference to y.

Instead, the internal use of define-like-y is allowed, because syntax-protect treats specially a syntax list that begins with define-values. In that case, instead of arming the overall expression, each individual element of the syntax list is armed, pushing dye packs further into the second element of the list so that they are attached to the defined identifiers. Thus, define-values, x, and y in the expansion result (define-values (x) y) are individually armed, and the definition can be deconstructed for conversion to letrec.

Just like syntax-protect, the expander rearms a transformer result that starts with define-values, by pushing dye packs into the list elements. As a result, define-like-y could have been implemented to produce (define id y), which uses define instead of define-values. In that case, the entire define form is at first armed with a dye pack, but as the define form is expanded to define-values, the dye pack is moved to the parts.

The macro expander treats syntax-list results starting with define-syntaxes in the same way that it treats results starting with define-values. Syntax-list results starting with begin are treated similarly, except that the second element of the syntax list is treated like all the other elements (i.e., the immediate element is armed, instead of its content). Furthermore, the macro expander applies this special handling recursively, in case a macro produces a begin form that contains nested define-values forms.

The default application of dye packs can be overridden by attaching a 'taint-mode property (see Syntax Object Properties) to the resulting syntax object of a macro transformer. If the property value is 'opaque, then the syntax object is armed and not its parts. If the property value is 'transparent, then the syntax object’s parts are armed. If the property value is 'transparent-binding, then the syntax object’s parts and the sub-parts of the second part (as for define-values and define-syntaxes) are armed. The 'transparent and 'transparent-binding modes trigger recursive property checking at the parts, so that armings can be pushed arbitrarily deeply into a transformer’s result.

16.2.7.3 Taints and Code Inspectors

Tools that are intended to be privileged (such as a debugging transformer) must disarm dye packs in expanded programs. Privilege is granted through code inspectors. Each dye pack records an inspector, and a syntax object can be disarmed using a sufficiently strong inspector.

When a module is declared, the declaration captures the current value of the current-code-inspector parameter. The captured inspector is used when syntax-protect is applied by a macro transformer that is defined within the module. A tool can disarm the resulting syntax object by supplying syntax-disarm with an inspector that is the same or a super-inspector of the module’s inspector. Untrusted code is ultimately run after setting current-code-inspector to a weaker inspector (after trusted code, such as debugging tools, have been loaded).

With this arrangement, macro-generating macros require some care, since the generating macro may embed syntax objects in the generated macro that need to have the generating module’s protection level, rather than the protection level of the module that contains the generated macro. To avoid this problem, use the module’s declaration-time inspector, which is accessible as (variable-reference->module-declaration-inspector (#%variable-reference)), and use it to define a variant of syntax-protect.

For example, suppose that the go macro is implemented through a macro:

#lang racket
(provide def-go)
 
(define (unchecked-go n x)
  (+ n 17))
 
(define-syntax (def-go stx)
  (syntax-case stx ()
    [(_ go)
     (syntax-protect
      #'(define-syntax (go stx)
          (syntax-case stx ()
            [(_ x)
             (syntax-protect #'(unchecked-go 8 x))])))]))

When def-go is used inside another module to define go, and when the go-defining module is at a different protection level than the def-go-defining module, the generated macro’s use of syntax-protect is not right. The use of unchecked-go should be protected at the level of the def-go-defining module, not the go-defining module.

The solution is to define and use go-syntax-protect, instead:

#lang racket
(provide def-go)
 
(define (unchecked-go n x)
  (+ n 17))
 
(define-for-syntax go-syntax-protect
  (let ([insp (variable-reference->module-declaration-inspector
               (#%variable-reference))])
    (lambda (stx) (syntax-arm stx insp))))
 
(define-syntax (def-go stx)
  (syntax-case stx ()
    [(_ go)
     (syntax-protect
      #'(define-syntax (go stx)
          (syntax-case stx ()
           [(_ x)
            (go-syntax-protect #'(unchecked-go 8 x))])))]))
16.2.7.4 Protected Exports

Sometimes, a module needs to export bindings to some modules—other modules that are at the same trust level as the exporting module—but prevent access from untrusted modules. Such exports should use the protect-out form in provide. For example, ffi/unsafe exports all of its unsafe bindings as protected in this sense.

Only modules loaded with an equally strong code inspector as an exporting module can use protected bindings from the exporting module. Operations like dynamic-require are granted access depending on the current code inspector as determined by current-code-inspector.

When a module re-exports a protected binding, it does not need to use protect-out again. Access is always determined by the code inspector of the module that originally defines a protected binding.