new sandbox in mzlib

svn: r5873
This commit is contained in:
Eli Barzilay 2007-04-06 08:56:23 +00:00
parent 239e56f93d
commit 433c9a57ec
7 changed files with 827 additions and 586 deletions

View File

@ -493,7 +493,7 @@
markup-prefix prefix-re))))
(when create-text? (make-directory "grading") (write-text))
(when value-printer (current-value-printer value-printer))
(when coverage? (coverage-enabled #t))
(when coverage? (sandbox-coverage-enabled #t))
(set-run-status "checking submission")
(cond
[(not eval?) (let () body ...)]

View File

@ -605,8 +605,12 @@ They are provided in a few layers, each layer provides new
functionality in addition to the lower one. These modules are (in
order):
* sandbox.ss -- basic sandbox evaluation utilities, can be used
independently from the handin-server.
* mzlib/sandbox.ss -- basic sandbox evaluation utilities. This is in
MzLib since it can be used independently. (See the MzLib manual for
details.)
* sandbox.ss -- a wrapper that configures MzLib's sandbox for the
handin server.
* utils.ss -- additional utilities for dealing with handin
submissions, as well as a few helpers for testing code.
@ -621,195 +625,8 @@ The following sections describe each of these modules.
_sandbox.ss_
------------
The main function that is implemented in this module is
`make-evaluator'. Most of the functionality that is provided is used
by this function.
> mred?
A boolean that is bound to `#t' if we're currently running in MrEd,
`#f' if in plain MzScheme. The idea is that you can use this module
both from MzScheme or, if needed, from MrEd. (Higher levels
("utils.ss" and "checker.ss"), need to be used with MrEd.)
> coverage-enabled
A boolean parameter that controls whether coverage testing is
enabled in `make-evaluator'-created functions. If it set to true,
the "handin-server/private/coverage.ss" module will be used to
detect uncovered expressions. This information is collected after
the input port has been evaluated, so it is not affected by testing
code that is not part of the submission. To retrieve the collected
information, apply the evaluation function on the special
`get-uncovered-expressions' value below. The resulting value is a
list of uncovered expressions, with at most one per position+span
(which means that the contents may be unreliable, but the position
is). The default is `#f'.
> namespace-specs
A parameter that holds a list of values that specify how to create a
namespace for evaluation in `make-evaluator'. The first item in the
list is a thunk that creates the namespace, and the rest are require
specs for modules that are to be attached to the created namespace.
The default is `make-namespace' and `(lib "posn.ss" "lang")' if
running in MzScheme, or `make-namespace-with-mred' and
`(lib "cache-image-snip.ss" "mrlib")' as well as the posn library.
(The module specs are needed for sharing module instantiations, for
example, without the above, posn values in testing code will be a
different type from posn values in tested code.)
> sandbox-reader
A parameter that holds a function that reads all expressions from
the current-input-port. It is used to read the submission source.
It must return a list of syntax objects, and it must use the symbol
`program' as the input source (that is, something like a loop that
consumes the input using `(read-syntax 'program)'). The default
reader is using a plain `read-syntax' -- it does so while setting
`read-case-sensitive' to `#t', and `read-decimal-as-inexact' to `#f'
(both are sensible choices for testing code).
> sandbox-override-collection-paths
A parameter that holds a list of collection directories. A
submission evaluator that is created by `make-evaluator' will put
these directories (ones tat actually exist) in front of the
collections in `current-library-collection-paths' -- so you can put
collection overrides there. The default is an `overridden-collects'
directory in the handin-server collection, which comes with a few
common overrides for teachpacks that use the GUI.
> sandbox-security-guard
A parameter that holds a security guard that is used by all
evaluations that happen in a `make-evaluator' function. The default
value is a security guard that forbids all I/O, except for things in
`sandbox-path-permissions' (see below).
> sandbox-path-permissions
This parameter configures the behavior of the default sandbox
security guard by listing path and access modes that are allowed.
The contents of this parameter is a list of specs, each one is a
list of an access mode (a symbol) and a path spec that is granted
this access. The access mode symbol is one of: 'execute, 'write,
'read, or 'exists, where each of these implies that modes that
follow are also permitted (eg, 'read allows reading or checking for
existence). The path spec is either a path as a byte string (must
be resolved and simplified) that will match exactly that path, or a
byte-regexp that applies for all matching paths. The default value
is a list of 'read permissions for the library collection paths.
Note that when an evaluator is created by `make-evaluator', the list
is augmented with permissions for accessing non-`lib' teachpack
requires and language module.
> sandbox-input
A parameter that specifies the input for evaluations that happen in
a `make-evaluator' function. It defaults to `#f', which makes such
functions work in a context where no input is available. It can be
set to:
* an input port, which will be used as is;
* a string or a byte string that will be used as the complete input;
* a path that names a file holding the input.
> sandbox-output
A parameter that specifies the output for evaluations that happen in
a `make-evaluator' function. It defaults to `#f', which simply
discards all such output. It can also be set to:
* an output port, which will be used as is;
* the symbol 'bytes, which will make `get-output' (see below) return
the complete output as a byte string;
* the symbol 'string, similar to the above, but uses a string;
* the symbol 'pipe, which will make it use a pipe for output, and
`get-output' returns the input end of the pipe.
(Note that error output is *not* redirected.)
> sandbox-eval-limits
A parameter that determines the default limits on each use of a
`make-evaluator' function. Its value should be a list of two
numbers, the first is a timeout value in seconds, and the second is
for setting a memory limit in megabytes. Either one can be `#f' for
disabling the corresponding limit (or the parameter can be set to
`#f' to disble both). When limits are set `with-limits' (see blow)
is wrapped around any use of an evaluator, so consuming too much
time or memory results in an exception. After an evaluator is
generated, its limits can be modified using `set-eval-limits' below
(the parameter value is used to initialize a new evaluator).
> (make-evaluator language teachpack-paths input-program)
This is the main entry point for the sandbox module.
This function Creates an evaluator function for evaluating
expressions in the designated `language', after loading teachpacks
that are specified in `teachpack-paths', and after evaluating the
code in the `input-program'.
The `input-program' holds the input program in the same way as the
`sandbox-input' parameter (but it cannot be `#f'). The contents of
this input is read using the `sandbox-reader', with line-counting
enabled.
The `language' can be:
* a symbol indicating a built-in language (currently, only
'mzscheme), or a teaching language -- one of 'beginner,
'beginner-abbr, 'intermediate, 'intermediate-lambda, or 'advanced.
* a list that begins with a 'lib, 'file, or 'planet symbol, which
stands for the language defined by this (quoted) module
specification, or a string specifying a relative module filename
directly.
* a list that begins with a 'begin symbol means that the code will
not be evaluated in a module context at all, it will simply be
evaluated in a new namespace, after evaluating this list.
The `teachpack-paths' list specifies additional code to load, can be
one of:
* paths to teachpacks to load into the module.
* a list that begins with a 'begin symbol is arbitrary code that is
prefixed into the submitted program.
The actual evaluation of expressions (both the program and later
evaluations) happens under the `sandbox-security-guard'
restrictions, and if MrEd is used -- in a newly created eventspace.
See also `with-limits' below for adding resource limits, and
`get-uncovered-expressions' above for enforcing test coverage.
> (get-output evaluator)
When this is used with an evaluator that was created with
`make-evaluator', it will return the output of the evaluator. (It
can also be provided as an argument to the evaluator, with the same
result.) The result depends on the value of the `sandbox-output'
parameter at the time the evaluator was created: if it was `#f' then
`get-output' will return `#f', if it was the symbol `pipe' then
`get-output' returns an input port that is being fed by the pipe,
and if it was the symbol `bytes' or `string' then `get-output'
returns the accumulated output and resets the evaluator's output to
a new output string or byte string (so each call returns a piece of
the evaluator's output).
> (get-uncovered-expressions evaluator)
When this is used with an evaluator that was created with
`make-evaluator', it will return a list of uncovered syntax
objects. (It can also be provided as an argument to the evaluator,
with the same result.)
> (set-eval-limits evaluator sec mb)
Changes the per-expression limits that the evaluator uses. This
procedure should be used to modify an evaluator limits -- changing
the `sandbox-eval-limits' parameter (see above) does not affect
existing evaluators. See also `with-limits' below.
> (call-with-limits sec mb thunk)
This function executes the given thunk with memory and time
restrictions: if execution consumes more than `mb' megabytes or more
that `sec' seconds, then the computation is aborted and an error is
thrown. Otherwise the result of the thunk is returned (a value,
multiple values, or raise an exception). Each of the two limits can
be `#f' to disable it. (Note: memory limits requires running in a
3m executable.)
This is used in `make-evaluator' functions, according to the
`sandbox-eval-limits' setting and uses of `set-eval-limits': each
expression evaluation is protected from timeouts and memory
problems. This means that you normally would not use it -- but you
may want to limit a whole testing session instead of each expression
(eg, when you want to run tests faster).
> (with-limits sec mb body ...)
A macro version of the above.
This is just a wrapper around the sandbox engine from MzLib. It
configures it for use with the handin server.
_utils.ss_

View File

@ -1,396 +1,16 @@
(module sandbox mzscheme
(require (lib "string.ss") (lib "list.ss") (lib "port.ss")
(lib "moddep.ss" "syntax"))
(require (lib "sandbox.ss"))
(provide (all-from (lib "sandbox.ss")))
(provide mred?
coverage-enabled
namespace-specs
sandbox-reader
sandbox-override-collection-paths
sandbox-security-guard
sandbox-path-permissions
sandbox-input
sandbox-output
sandbox-eval-limits
get-output
get-uncovered-expressions
set-eval-limits
make-evaluator
call-with-limits
with-limits)
(sandbox-namespace-specs
(let ([specs (sandbox-namespace-specs)])
`(,(car specs)
,@(cdr specs)
(lib "posn.ss" "lang")
,@(if mred? '((lib "cache-image-snip.ss" "mrlib")) '()))))
(define mred?
(with-handlers ([void (lambda (_) #f)])
(dynamic-require '#%mred-kernel #f)
#t))
(define-syntax mz/mr ; use a value for mzscheme, or pull a mred binding
(syntax-rules ()
[(mz/mr mzval mrsym)
(if mred? (dynamic-require '(lib "mred.ss" "mred") 'mrsym) mzval)]))
;; Configuration ------------------------------------------------------------
(define sandbox-input (make-parameter #f))
(define sandbox-output (make-parameter #f))
(define sandbox-eval-limits (make-parameter '(30 10))) ; 30sec, 10mb
(define coverage-enabled (make-parameter #f))
(define namespace-specs
(make-parameter
(let ([mods '((lib "posn.ss" "lang"))]
[mred-mods '((lib "cache-image-snip.ss" "mrlib"))])
`(,(mz/mr make-namespace make-namespace-with-mred)
,@mods ,@(if mred? mred-mods '())))))
(define (default-sandbox-reader)
(parameterize ([read-case-sensitive #t] [read-decimal-as-inexact #f])
(let loop ([l '()])
(let ([expr (read-syntax 'program)])
(if (eof-object? expr)
(reverse! l)
(loop (cons expr l)))))))
(define sandbox-reader (make-parameter default-sandbox-reader))
(define sandbox-override-collection-paths
(make-parameter (list (build-path (collection-path "handin-server")
"overridden-collects"))))
;; Security Guard -----------------------------------------------------------
(define sep (bytes-ref (path->bytes (simplify-path "/")) 0)) ; '\' on windows
(define (simplify-path* path)
(simplify-path
(expand-path
(path->complete-path (if (bytes? path) (bytes->path path) path)))))
(define permission-order '(execute write read exists))
(define (perm<=? p1 p2)
(memq p1 (memq p2 permission-order)))
(define dir-path->bytes-re
(let* ([sep-re (regexp-quote (bytes sep))]
[last-sep (byte-regexp (bytes-append sep-re #"?$"))])
(lambda (path)
(byte-regexp (regexp-replace last-sep
(path->bytes (simplify-path* path))
(bytes-append #"(?:$|" sep-re #")"))))))
(define (get-lib-permissions libs)
(let* ([sep-re (regexp-quote (bytes sep))]
[last-sep (byte-regexp (bytes-append sep-re #"?$"))])
(map (lambda (p) (list 'read (dir-path->bytes-re p))) libs)))
(define sandbox-path-permissions
(make-parameter (get-lib-permissions (current-library-collection-paths))))
(define (path-ok? bpath ok)
(cond [(bytes? ok) (equal? bpath ok)]
[(byte-regexp? ok) (regexp-match? ok bpath)]
[else (error 'path-ok? "bad path spec: ~e" ok)]))
(define default-sandbox-guard
(let ([orig-security (current-security-guard)])
(make-security-guard
orig-security
(lambda (what path modes)
(when path
(let ([needed (let loop ([order permission-order])
(cond [(null? order)
(error 'default-sandbox-guard
"unknown access modes: ~e" modes)]
[(memq (car order) modes) (car order)]
[else (loop (cdr order))]))]
[bpath (parameterize ([current-security-guard orig-security])
(path->bytes (simplify-path* path)))])
(unless (ormap (lambda (perm)
(and (perm<=? needed (car perm))
(path-ok? bpath (cadr perm))))
(sandbox-path-permissions))
(error what "file access denied ~a" (cons path modes))))))
(lambda (what . xs) (error what "network access denied: ~e" xs)))))
(define sandbox-security-guard (make-parameter default-sandbox-guard))
;; computes permissions that are needed for require specs (`read' for all
;; files and "compiled" subdirs, `exists' for the base-dir)
(define (module-specs->path-permissions mods)
(define paths (module-specs->non-lib-paths mods))
(define bases
(let loop ([paths paths] [bases '()])
(if (null? paths)
(reverse! bases)
(let-values ([(base name dir?) (split-path (car paths))])
(let ([base (simplify-path* base)])
(loop (cdr paths)
(if (member base bases) bases (cons base bases))))))))
(append (map (lambda (p) (list 'read (path->bytes p))) paths)
(map (lambda (b)
(list 'read (dir-path->bytes-re (build-path b "compiled"))))
bases)
(map (lambda (b)
(list 'exists (path->bytes (path->directory-path b))))
bases)))
;; takes a module-spec list and returns all module paths that are needed
;; ==> ignores (lib ...) modules
(define (module-specs->non-lib-paths mods)
(define (lib? x)
(if (module-path-index? x)
(let-values ([(m base) (module-path-index-split x)]) (lib? m))
(and (pair? x) (eq? 'lib (car x)))))
(let loop ([todo (filter values
(map (lambda (mod)
(and (not (lib? mod))
(simplify-path*
(resolve-module-path mod #f))))
mods))]
[r '()])
(cond
[(null? todo) r]
[(member (car todo) r) (loop (cdr todo) r)]
[else
(let ([path (car todo)])
(loop (map (lambda (i)
(simplify-path* (resolve-module-path-index i path)))
(filter (lambda (i)
(and (module-path-index? i) (not (lib? i))))
(apply append
(call-with-values
(lambda ()
(module-compiled-imports
(get-module-code (car todo))))
list))))
(cons path r)))])))
;; Execution ----------------------------------------------------------------
(define (make-evaluation-namespace)
(let* ([specs (namespace-specs)]
[new-ns ((car specs))]
[orig-ns (current-namespace)]
[mods (cdr specs)]
[resolve (current-module-name-resolver)])
(for-each (lambda (mod) (dynamic-require mod #f)) mods)
(let ([modsyms (map (lambda (mod) (resolve mod #f #f)) mods)])
(parameterize ([current-namespace new-ns])
(for-each (lambda (ms) (namespace-attach-module orig-ns ms))
modsyms)))
new-ns))
(define (input->port inp)
(cond [(input-port? inp) inp]
[(string? inp) (open-input-string inp)]
[(bytes? inp) (open-input-bytes inp)]
[(path? inp) (open-input-file inp)]
[else (error 'input->port "bad input: ~e" inp)]))
(define (read-code inp)
(parameterize ([current-input-port (input->port inp)])
(port-count-lines! (current-input-port))
((sandbox-reader))))
(define (require-perms language teachpacks)
(let* ([requires
(if (and (pair? teachpacks) (eq? 'begin (car teachpacks)))
(apply append
(map cdr
(filter
(lambda (x)
(let ([fst (and (pair? x) (car x))])
(eq? 'require
(if (syntax? fst) (syntax-e fst) fst))))
(cdr teachpacks))))
teachpacks)]
[requires
(if (or (and (pair? language) (memq (car language) '(file planet)))
(string? language))
(cons language requires)
requires)])
(module-specs->path-permissions requires)))
(define (evaluate-program language teachpacks input-program uncovered!)
(let* ([body (read-code input-program)]
[body (append (if (and (pair? teachpacks)
(eq? 'begin (car teachpacks)))
(cdr teachpacks)
(map (lambda (tp)
`(,#'require ,(if (pair? tp) tp `(file ,tp))))
teachpacks))
body)]
[body (cond [(and (symbol? language)
(memq language '(beginner
beginner-abbr
intermediate
intermediate-lambda
advanced)))
`(module m
(lib ,(case language
[(beginner) "htdp-beginner.ss"]
[(beginner-abbr) "htdp-beginner-abbr.ss"]
[(intermediate) "htdp-intermediate.ss"]
[(intermediate-lambda)
"htdp-intermediate-lambda.ss"]
[(advanced) "htdp-advanced.ss"])
"lang")
,@body)]
[(or (and (pair? language) (eq? 'lib (car language)))
(symbol? language))
`(module m ,language ,@body)]
[(or (and (pair? language)
(memq (car language) '(file planet)))
(string? language))
`(module m ,language ,@body)]
[(and (pair? language)
(eq? 'begin (car language)))
`(begin ,language ,@body)]
[else (error 'make-evaluator
"Bad language specification: ~e"
language)])]
[ns (current-namespace)])
(when uncovered!
(eval '(require (lib "coverage.ss" "handin-server" "private"))))
(eval body)
(when (and (pair? body) (eq? 'module (car body))
(pair? (cdr body)) (symbol? (cadr body)))
(let ([mod (cadr body)])
(eval `(require ,mod))
(current-namespace (module->namespace mod))))
(when uncovered!
(uncovered! (filter (lambda (x) (eq? 'program (syntax-source x)))
(parameterize ([current-namespace ns])
(eval '(get-uncovered-expressions))))))))
(define current-eventspace (mz/mr (make-parameter #f) current-eventspace))
(define make-eventspace (mz/mr void make-eventspace))
(define run-in-bg (mz/mr thread queue-callback))
(define null-input (open-input-bytes #""))
(define (get-uncovered-expressions eval) (eval get-uncovered-expressions))
(define (get-output eval) (eval get-output))
(define (set-eval-limits eval . args) (apply (eval set-eval-limits) args))
(define (make-evaluator language teachpacks input-program)
(define coverage? (coverage-enabled))
(define uncovered-expressions #f)
(define input-ch (make-channel))
(define result-ch (make-channel))
(define output #f)
(define limits (sandbox-eval-limits))
(define (user-process)
;; First read program and evaluate it as a module:
(with-handlers ([void (lambda (exn) (channel-put result-ch exn))])
(evaluate-program
language teachpacks input-program
(and coverage? (lambda (exprs) (set! uncovered-expressions exprs))))
(channel-put result-ch 'ok))
;; Now wait for interaction expressions:
(let loop ()
(let ([expr (channel-get input-ch)])
(unless (eof-object? expr)
(with-handlers ([void (lambda (exn)
(channel-put result-ch (cons 'exn exn)))])
(let* ([sec (and limits (car limits))]
[mb (and limits (cadr limits))]
[run (if (or sec mb)
(lambda () (with-limits sec mb (eval expr)))
(lambda () (eval expr)))])
(channel-put result-ch
(cons 'vals (call-with-values run list)))))
(loop))))
(let loop ()
(channel-put result-ch '(exn . nothing-more-to-evaluate))
(loop)))
(define (user-eval expr)
(channel-put input-ch expr)
(let ([r (channel-get result-ch)])
(if (eq? (car r) 'exn) (raise (cdr r)) (apply values (cdr r)))))
(define (evaluator expr)
(cond [(eq? expr get-output)
(if (procedure? output) (user-eval `(,output)) output)]
[(eq? expr get-uncovered-expressions)
uncovered-expressions]
[(eq? expr set-eval-limits)
(lambda args (set! limits args))]
[else (user-eval expr)]))
(parameterize
([current-namespace (make-evaluation-namespace)]
[current-inspector (make-inspector)]
[current-library-collection-paths
(filter directory-exists?
(append (sandbox-override-collection-paths)
(current-library-collection-paths)))]
[exit-handler (lambda x (error 'exit "user code cannot exit"))]
[current-input-port
(let ([inp (sandbox-input)]) (if inp (input->port inp) null-input))]
[current-output-port
(let ([out (sandbox-output)])
(cond [(not out) (open-output-nowhere)]
[(output-port? out) (set! output out) out]
[(eq? out 'pipe)
(let-values ([(i o) (make-pipe)]) (set! output i) o)]
[(memq out '(bytes string))
(let-values
([(open get)
(if (eq? out 'bytes)
(values open-output-bytes get-output-bytes)
(values open-output-string get-output-string))])
(let ([o (open)])
(set! output (lambda ()
(let ([o1 o])
(set! o (open))
(current-output-port o)
(get-output-bytes o1))))
o))]
[else (error 'make-evaluator "bad output: ~e" out)]))]
[sandbox-path-permissions
(append (sandbox-path-permissions)
(get-lib-permissions (sandbox-override-collection-paths))
(require-perms language teachpacks))]
[current-security-guard (sandbox-security-guard)])
;; Note the above definition of `current-eventspace': in MzScheme, it is
;; a parameter that is not used at all. Also note that creating an
;; eventspace starts a thread that will eventually run the callback code
;; (which evaluates the program in `run-in-bg') -- so this
;; parameterization must be nested in the above, or it will not use the
;; new namespace.
(parameterize ([current-eventspace (make-eventspace)])
(run-in-bg user-process)
(let ([r (channel-get result-ch)])
(if (eq? r 'ok)
;; Initial program executed ok, so return an evaluator:
evaluator
;; Program didn't execute:
(raise r))))))
;; Resources ----------------------------------------------------------------
(define (call-with-limits sec mb thunk)
(let ([cust (make-custodian)]
[ch (make-channel)])
(when mb (custodian-limit-memory cust (* mb 1024 1024) cust))
(let* ([work (parameterize ([current-custodian cust])
(thread (lambda ()
(channel-put ch
(with-handlers ([void (lambda (e)
(list raise e))])
(call-with-values thunk
(lambda vs (cons values vs))))))))]
[watch (thread (lambda ()
(channel-put ch
(if (sync/timeout sec work) 'memory 'time))))]
[r (channel-get ch)])
(custodian-shutdown-all cust)
(kill-thread watch)
(if (list? r)
(apply (car r) (cdr r))
(error 'with-limit "out of ~a" r)))))
(define-syntax with-limits
(syntax-rules ()
[(with-limits sec mb body ...)
(call-with-limits sec mb (lambda () body ...))]))
(sandbox-override-collection-paths
(cons (build-path (collection-path "handin-server") "overridden-collects")
(sandbox-override-collection-paths)))
)

View File

@ -1,5 +1,6 @@
;; Use the stacktrace interface from errortrace to find uncovered expressions.
(module coverage mzscheme
;; This file is is used in the context of sandboxed code, it uses the
;; stacktrace interface from errortrace to find uncovered expressions.
(module sandbox-coverage mzscheme
(require (lib "stacktrace.ss" "errortrace") (lib "unit.ss") (lib "list.ss"))
;; Test coverage run-time support

532
collects/mzlib/sandbox.ss Normal file
View File

@ -0,0 +1,532 @@
(module sandbox mzscheme
(require (lib "string.ss") (lib "list.ss") (lib "port.ss")
(lib "moddep.ss" "syntax"))
(provide mred?
sandbox-init-hook
sandbox-reader
sandbox-input
sandbox-output
sandbox-error-output
sandbox-coverage-enabled
sandbox-namespace-specs
sandbox-override-collection-paths
sandbox-security-guard
sandbox-path-permissions
sandbox-network-guard
sandbox-eval-limits
kill-evaluator
get-output
get-error-output
get-uncovered-expressions
set-eval-limits
make-evaluator
call-with-limits
with-limits
exn:fail:resource?
exn:fail:resource-resource)
(define mred?
(with-handlers ([void (lambda (_) #f)])
(dynamic-require '#%mred-kernel #f)
#t))
(define-syntax mz/mr ; use a value for mzscheme, or pull a mred binding
(syntax-rules ()
[(mz/mr mzval mrsym)
(if mred? (dynamic-require '(lib "mred.ss" "mred") 'mrsym) mzval)]))
;; Configuration ------------------------------------------------------------
(define sandbox-init-hook (make-parameter void))
(define sandbox-input (make-parameter #f))
(define sandbox-output (make-parameter #f))
(define sandbox-error-output (make-parameter #t))
(define sandbox-eval-limits (make-parameter '(30 10))) ; 30sec, 10mb
(define sandbox-coverage-enabled (make-parameter #f))
(define sandbox-namespace-specs
(make-parameter `(,(mz/mr make-namespace make-namespace-with-mred)
#| no modules here by default |#)))
(define (default-sandbox-reader source)
(let loop ([l '()])
(let ([expr (read-syntax source)])
(if (eof-object? expr)
(reverse! l)
(loop (cons expr l))))))
(define sandbox-reader (make-parameter default-sandbox-reader))
(define sandbox-override-collection-paths (make-parameter '()))
(define teaching-langs
'(beginner beginner-abbr intermediate intermediate-lambda advanced))
;; Security Guard -----------------------------------------------------------
(define sep (bytes-ref (path->bytes (simplify-path "/")) 0)) ; '\' on windows
(define (simplify-path* path)
(simplify-path (expand-path (path->complete-path
(cond [(bytes? path) (bytes->path path)]
[(string? path) (string->path path)]
[else path])))))
(define permission-order '(execute write delete read exists))
(define (perm<=? p1 p2)
(memq p1 (memq p2 permission-order)))
;; gets a path (can be bytes/string), returns a regexp for that path that
;; matches also subdirs (if it's a directory)
(define path->bregexp
(let* ([sep-re (regexp-quote (bytes sep))]
[last-sep (byte-regexp (bytes-append sep-re #"?$"))]
[suffix-re (bytes-append #"(?:$|" sep-re #")")])
(lambda (path)
(if (byte-regexp? path)
path
(let* ([path (path->bytes (simplify-path* path))]
[path (regexp-quote (regexp-replace last-sep path #""))])
(byte-regexp (bytes-append #"^" path suffix-re)))))))
(define sandbox-path-permissions
(make-parameter '()
(lambda (new)
(map (lambda (perm) (cons (car perm) (map path->bregexp (cdr perm))))
new))))
(define sandbox-network-guard
(make-parameter (lambda (what . xs)
(error what "network access denied: ~e" xs))))
(define default-sandbox-guard
(let ([orig-security (current-security-guard)])
(make-security-guard
orig-security
(lambda (what path modes)
(when path
(let ([needed (let loop ([order permission-order])
(cond [(null? order)
(error 'default-sandbox-guard
"unknown access modes: ~e" modes)]
[(memq (car order) modes) (car order)]
[else (loop (cdr order))]))]
[bpath (parameterize ([current-security-guard orig-security])
(path->bytes (simplify-path* path)))])
(unless (ormap (lambda (perm)
(and (perm<=? needed (car perm))
(regexp-match (cadr perm) bpath)))
(sandbox-path-permissions))
(error what "file access denied ~a" (cons path modes))))))
(lambda args (apply (sandbox-network-guard) args)))))
(define sandbox-security-guard (make-parameter default-sandbox-guard))
;; computes permissions that are needed for require specs (`read' for all
;; files and "compiled" subdirs, `exists' for the base-dir)
(define (module-specs->path-permissions mods)
(define paths (module-specs->non-lib-paths mods))
(define bases
(let loop ([paths paths] [bases '()])
(if (null? paths)
(reverse! bases)
(let-values ([(base name dir?) (split-path (car paths))])
(let ([base (simplify-path* base)])
(loop (cdr paths)
(if (member base bases) bases (cons base bases))))))))
(append (map (lambda (p) `(read ,(path->bytes p))) paths)
(map (lambda (b) `(read ,(build-path b "compiled"))) bases)
(map (lambda (b) `(exists ,b)) bases)))
;; takes a module-spec list and returns all module paths that are needed
;; ==> ignores (lib ...) modules
(define (module-specs->non-lib-paths mods)
(define (lib? x)
(if (module-path-index? x)
(let-values ([(m base) (module-path-index-split x)]) (lib? m))
(and (pair? x) (eq? 'lib (car x)))))
(let loop ([todo (filter values
(map (lambda (mod)
(and (not (lib? mod))
(simplify-path*
(resolve-module-path mod #f))))
mods))]
[r '()])
(cond
[(null? todo) r]
[(member (car todo) r) (loop (cdr todo) r)]
[else
(let ([path (car todo)])
(loop (map (lambda (i)
(simplify-path* (resolve-module-path-index i path)))
(filter (lambda (i)
(and (module-path-index? i) (not (lib? i))))
(apply append
(call-with-values
(lambda ()
(module-compiled-imports
(get-module-code (car todo))))
list))))
(cons path r)))])))
;; Resources ----------------------------------------------------------------
(define-struct (exn:fail:resource exn:fail) (resource))
(define (call-with-limits sec mb thunk)
(let ([cust (make-custodian)]
[ch (make-channel)]
;; use this to copy parameter changes from the sub-thread
[p current-preserved-thread-cell-values])
(when mb (custodian-limit-memory cust (* mb 1024 1024) cust))
(let* ([work (parameterize ([current-custodian cust])
(thread (lambda ()
(channel-put ch
(with-handlers ([void (lambda (e)
(list (p) raise e))])
(call-with-values thunk
(lambda vs (list* (p) values vs))))))))]
[watch (thread (lambda ()
(channel-put ch
(if (sync/timeout sec work) 'memory 'time))))]
[r (channel-get ch)])
(custodian-shutdown-all cust)
(kill-thread watch)
(if (list? r)
;; apply parameter changes first
(begin (p (car r)) (apply (cadr r) (cddr r)))
(raise (make-exn:fail:resource (format "with-limit: out of ~a" r)
(current-continuation-marks)
r))))))
(define-syntax with-limits
(syntax-rules ()
[(with-limits sec mb body ...)
(call-with-limits sec mb (lambda () body ...))]))
;; Execution ----------------------------------------------------------------
(define (literal-identifier=? x y)
(or (module-identifier=? x y)
(eq? (if (syntax? x) (syntax-e x) x) (if (syntax? y) (syntax-e y) y))))
(define (make-evaluation-namespace)
(let* ([specs (sandbox-namespace-specs)]
[new-ns ((car specs))]
[orig-ns (current-namespace)]
[mods (cdr specs)]
[resolve (current-module-name-resolver)])
(for-each (lambda (mod) (dynamic-require mod #f)) mods)
(let ([modsyms (map (lambda (mod) (resolve mod #f #f)) mods)])
(parameterize ([current-namespace new-ns])
(for-each (lambda (ms) (namespace-attach-module orig-ns ms))
modsyms)))
new-ns))
(define (require-perms language requires)
(define (find-requires forms)
(let loop ([forms (reverse forms)] [reqs '()])
(if (null? forms)
reqs
(loop (cdr forms)
(syntax-case* (car forms) (require) literal-identifier=?
[(require specs ...)
(append (syntax-object->datum #'(specs ...)) reqs)]
[_else reqs])))))
(let* ([requires (if (and (pair? requires) (eq? 'begin (car requires)))
(find-requires (cdr requires))
requires)]
[requires (cond [(string? language) (cons language requires)]
[(not (pair? language)) requires]
[(memq (car language) '(file planet))
(cons language requires)]
[(eq? (car language) 'begin)
(append (find-requires (cdr language)) requires)]
[else 'require-perms
"bad language spec: ~e" language])])
(module-specs->path-permissions requires)))
(define (input->port inp)
;; returns #f when it can't create a port
(cond [(input-port? inp) inp]
[(string? inp) (open-input-string inp)]
[(bytes? inp) (open-input-bytes inp)]
[(path? inp) (open-input-file inp)]
[else #f]))
;; Gets an input spec returns a list of syntaxes. The input can be a list of
;; sexprs/syntaxes, or a list with a single input port spec
;; (path/string/bytes) value.
(define (input->code inps source n)
(if (null? inps)
'()
(let ([p (input->port (car inps))])
(cond [(and p (null? (cdr inps)))
(port-count-lines! p)
(parameterize ([current-input-port p])
((sandbox-reader) source))]
[p (error 'input->code "ambiguous inputs: ~e" inps)]
[else (let loop ([inps inps] [n n] [r '()])
(if (null? inps)
(reverse! r)
(loop (cdr inps) (and n (add1 n))
;; 1st at line#1, pos#1, 2nd at line#2, pos#2 etc
;; (starting from the `n' argument)
(cons (datum->syntax-object
#f (car inps)
(list source n (and n 0) n (and n 1)))
r))))]))))
(define ((init-for-language language))
(cond [(eq? language 'r5rs)
(read-case-sensitive #f)
(read-square-bracket-as-paren #f)
(read-curly-brace-as-paren #f)]
[(memq language teaching-langs)
(read-case-sensitive #t)
(read-decimal-as-inexact #f)]))
;; Returns a single (module ...) or (begin ...) expression (a `begin' list
;; will be evaluated one by one -- the language might not have a `begin').
(define (build-program language requires input-program)
(let* ([body (append (if (and (pair? requires) (eq? 'begin (car requires)))
(cdr requires)
(map (lambda (r) (list #'require r))
requires))
(input->code input-program 'program 1))]
[use-lang (lambda (lang) `(module program ,lang . ,body))])
(cond [(memq language teaching-langs)
(use-lang `(lib ,(format "htdp-~a.ss" language) "lang"))]
[(eq? language 'r5rs)
(use-lang `(lib "lang.ss" "r5rs"))]
[(or (and (pair? language) (memq (car language) '(lib file planet)))
(symbol? language) (string? language))
(use-lang language)]
[(and (pair? language) (eq? 'begin (car language)))
(append language body)]
[else (error 'make-evaluator "bad language spec: ~e" language)])))
;; Like a toplevel (eval `(begin ,@exprs)), but the language that is used may
;; not have a begin.
(define (eval* exprs)
(if (null? exprs)
(void)
(let ([deftag (default-continuation-prompt-tag)])
(let loop ([expr (car exprs)] [exprs (cdr exprs)])
(if (null? exprs)
(eval expr)
(begin
(call-with-continuation-prompt
(lambda () (eval expr))
deftag
(lambda (x) (abort-current-continuation deftag x)))
(loop (car exprs) (cdr exprs))))))))
(define (evaluate-program program limits uncovered!)
(when uncovered!
(eval `(,#'require (lib "sandbox-coverage.ss" "mzlib" "private"))))
;; the actual evaluation happens under specified limits, if given
(let ([run (if (and (pair? program) (eq? 'begin (car program)))
(lambda () (eval* (cdr program)))
(lambda () (eval program)))]
[sec (and limits (car limits))]
[mb (and limits (cadr limits))])
(if (or sec mb) (call-with-limits sec mb run) (run)))
(let ([ns (syntax-case* program (module) literal-identifier=?
[(module mod . body)
(identifier? #'mod)
(let ([mod #'mod])
(eval `(,#'require ,mod))
(module->namespace (syntax-e mod)))]
[_else #f])])
(when uncovered!
(let ([get (let ([ns (current-namespace)])
(lambda () (eval '(get-uncovered-expressions) ns)))])
(uncovered! (list (get) get))))
(when ns (current-namespace ns))))
(define current-eventspace (mz/mr (make-parameter #f) current-eventspace))
(define make-eventspace (mz/mr void make-eventspace))
(define run-in-bg (mz/mr thread queue-callback))
(define null-input (open-input-bytes #""))
(define (kill-evaluator eval) (eval kill-evaluator))
(define (get-output eval) (eval get-output))
(define (get-error-output eval) (eval get-error-output))
(define (get-uncovered-expressions eval . args)
(apply (eval get-uncovered-expressions) args))
(define (set-eval-limits eval . args)
(apply (eval set-eval-limits) args))
(define-syntax parameterize*
(syntax-rules ()
[(parameterize* ([p1 v1] [p v] ...) body ...)
(parameterize ([p1 v1]) (parameterize* ([p v] ...) body ...))]
[(parameterize* () body ...)
(begin body ...)]))
(define (make-evaluator* init-hook require-perms program-or-maker)
(define cust (make-custodian))
(define coverage? (sandbox-coverage-enabled))
(define uncovered #f)
(define input-ch (make-channel))
(define result-ch (make-channel))
(define output #f)
(define error-output #f)
(define limits (sandbox-eval-limits))
(define user-running? #t)
(define (kill-me)
(when user-running? (set! user-running? #f) (custodian-shutdown-all cust))
(void))
(define (user-process)
(with-handlers ([void (lambda (exn) (channel-put result-ch exn))])
;; first set up the environment
(init-hook)
((sandbox-init-hook))
;; now read and evaluate the input program
(evaluate-program
(if (procedure? program-or-maker) (program-or-maker) program-or-maker)
limits
(and coverage? (lambda (es+get) (set! uncovered es+get))))
(channel-put result-ch 'ok))
;; finally wait for interaction expressions
(let loop ([n 1])
(let ([expr (channel-get input-ch)])
(when (eof-object? expr) (channel-put result-ch expr) (kill-me))
(let ([code (input->code (list expr) 'eval n)])
(with-handlers ([void (lambda (exn)
(channel-put result-ch (cons 'exn exn)))])
(let* ([sec (and limits (car limits))]
[mb (and limits (cadr limits))]
[run (if (or sec mb)
(lambda () (with-limits sec mb (eval* code)))
(lambda () (eval* code)))])
(channel-put result-ch
(cons 'vals (call-with-values run list))))))
(loop (add1 n)))))
(define (user-eval expr)
(let ([r (if user-running?
(begin (channel-put input-ch expr) (channel-get result-ch))
eof)])
(cond [(eof-object? r) (error 'evaluator "terminated")]
[(eq? (car r) 'exn) (raise (cdr r))]
[else (apply values (cdr r))])))
(define get-uncovered
(case-lambda
[() (get-uncovered #t)]
[(prog?) (get-uncovered prog? 'program)]
[(prog? src)
(unless uncovered
(error 'get-uncovered-expressions "no coverage information"))
(let ([uncovered (if prog? (car uncovered) ((cadr uncovered)))])
(if src
(filter (lambda (x) (equal? src (syntax-source x))) uncovered)
uncovered))]))
(define (evaluator expr)
(cond [(eq? expr kill-evaluator) (kill-me)]
[(eq? expr get-output)
(if (procedure? output) (user-eval `(,output)) output)]
[(eq? expr get-error-output)
(if (procedure? error-output)
(user-eval `(,error-output)) error-output)]
[(eq? expr get-uncovered-expressions) get-uncovered]
[(eq? expr set-eval-limits) (lambda args (set! limits args))]
[else (user-eval expr)]))
(define linked-outputs? #f)
(define (make-output what out set-out! allow-link?)
(cond [(not out) (open-output-nowhere)]
[(and (eq? #t out) allow-link?)
(set! linked-outputs? #t) (current-output-port)]
[(output-port? out) out]
[(eq? out 'pipe) (let-values ([(i o) (make-pipe)]) (set-out! i) o)]
[(memq out '(bytes string))
(let-values ([(open get)
(if (eq? out 'bytes)
(values open-output-bytes get-output-bytes)
(values open-output-string get-output-string))])
(let ([o (open)])
(set-out! (lambda ()
(let ([o1 o])
(set! o (open))
(current-output-port o)
(when linked-outputs? (current-error-port o))
(get o1))))
o))]
[else (error 'make-evaluator "bad sandox-~a spec: ~e" what out)]))
(parameterize* ; the order in these matters
([current-custodian cust]
[current-namespace (make-evaluation-namespace)]
[current-inspector (make-inspector)]
[exit-handler (lambda x (error 'exit "user code cannot exit"))]
[current-input-port
(let ([inp (sandbox-input)])
(if inp
(or (input->port inp)
(error 'make-evaluator "bad sandbox-input: ~e" inp))
null-input))]
[current-output-port (make-output 'output (sandbox-output)
(lambda (o) (set! output o))
#f)]
[current-error-port (make-output 'error-output (sandbox-error-output)
(lambda (o) (set! error-output o))
#t)]
[current-library-collection-paths
(filter directory-exists?
(append (sandbox-override-collection-paths)
(current-library-collection-paths)))]
[sandbox-path-permissions
(append (map (lambda (p) `(read ,p))
(current-library-collection-paths))
require-perms
(sandbox-path-permissions))]
[current-security-guard (sandbox-security-guard)]
;; Note the above definition of `current-eventspace': in MzScheme, it
;; is an unused parameter. Also note that creating an eventspace
;; starts a thread that will eventually run the callback code (which
;; evaluates the program in `run-in-bg') -- so this parameterization
;; must be nested in the above (which is what paramaterize* does), or
;; it will not use the new namespace.
[current-eventspace (make-eventspace)])
(run-in-bg user-process)
(let ([r (channel-get result-ch)])
(if (eq? r 'ok)
;; initial program executed ok, so return an evaluator
evaluator
;; program didn't execute
(raise r)))))
(define make-evaluator
(case-lambda
;; `input-program' is either a single argument specifying a file/string,
;; or multiple arguments for a sequence of expressions
[(language requires . input-program)
(let (;; make it possible to provide #f for no language and no requires
[lang (or language '(begin))]
;; make it possible to use simple paths to files to require
[reqs (cond [(not requires) '()]
[(not (list? requires))
(error 'make-evaluator "bad requires: ~e" requires)]
[else (map (lambda (r) (if (pair? r) r `(file ,r)))
requires)])])
(make-evaluator* (init-for-language lang)
(require-perms lang reqs)
(lambda () (build-program lang reqs input-program))))]
;; this is for a complete module input program
[(input-program)
(let ([prog (input->code (list input-program) 'program #f)])
(unless (= 1 (length prog))
(error 'make-evaluator "expecting a single `module' program; ~a"
(if (zero? (length prog))
"no program expressions given"
"got more than a single expression")))
(syntax-case* (car prog) (module) literal-identifier=?
[(module modname lang body ...)
(make-evaluator*
void
(require-perms (syntax-object->datum #'lang)
(cons 'begin (syntax->list #'(body ...))))
(car prog))]
[_else (error 'make-evaluator "expecting a `module' program; got ~e"
(syntax-object->datum (car prog)))]))]))
)

View File

@ -11,7 +11,7 @@
[(t E => :rt-err:) (err/rt-test E)]
[(t E => :st-err:) (syntax-test #'E)]
[(t (f x ...) => res) (test res f x ...)]
[(t R => E more ...) (begin (t R => E) (t more ...))]
[(t E => R more ...) (begin (t E => R) (t more ...))]
[(t R <= E more ...) (t E => R more ...)]))
;; make sure that lambda/kw behaves as lambda

View File

@ -0,0 +1,271 @@
(load-relative "loadtest.ss")
(Section 'sandbox)
(require (lib "sandbox.ss"))
(let ([ev void])
(define (run thunk)
(with-handlers ([void (lambda (e) (list 'exn: e))])
(call-with-values thunk (lambda vs (cons 'vals: vs)))))
(define (run* thunk)
(with-handlers ([void (lambda (e) (list 'exn: e))])
(call-with-values thunk
(case-lambda [(x) (and x #t)] [vs (cons 'vals: vs)]))))
(define (e-match? re run thunk)
(let ([x (run thunk)])
(if (and (list? x) (= 2 (length x)) (eq? 'exn: (car x)) (exn? (cadr x)))
(let ([m (exn-message (cadr x))])
(or (regexp-match? re m) (list 'bad-exception-message: m)))
x)))
(define-syntax thunk (syntax-rules () [(thunk b ...) (lambda () b ...)]))
(define-syntax t
(syntax-rules (--eval-- --top-- => <= =err> <err=)
[(t -?-) (void)]
[(t -?- --eval-- more ...) (t --eval-- more ...)]
[(t -?- --top-- more ...) (t --top-- more ...)]
[(t --eval-- E) (test #t run* (thunk (ev `E)))]
[(t --top-- E) (test #t run* (thunk E))]
[(t --eval-- E => R) (test `(vals: ,R) run (thunk (ev `E)))]
[(t --top-- E => R) (test `(vals: ,R) run (thunk E))]
[(t --eval-- E =err> R) (test #t e-match? R run (thunk (ev `E)))]
[(t --top-- E =err> R) (test #t e-match? R run (thunk E))]
[(t -?- E => R more ...) (begin (t -?- E => R) (t -?- more ...))]
[(t -?- E =err> R more ...) (begin (t -?- E =err> R) (t -?- more ...))]
[(t -?- R <= E more ...) (t -?- E => R more ...)]
[(t -?- R <err= E more ...) (t E =err> R more ...)]
;; last so it doesn't match the above
[(t -?- E more ...) (begin (t -?- E) (t -?- more ...))]))
(define (make-prog . lines)
(apply string-append (map (lambda (l) (string-append l "\n")) lines)))
(t
;; basic stuff, limits
--top--
(set! ev (make-evaluator 'mzscheme '()
(make-prog "(define x 1)"
"(define (id x) x)"
"(define (plus1 x) x)"
"(define (loop) (loop))"
"(define (memory x) (make-vector x))")))
(set-eval-limits ev 1 1)
--eval--
x => 1
(id 1) => 1
(id (plus1 x)) => 1
(loop) =err> "out of time"
(memory 1000000) =err> "out of memory"
(printf "x = ~s\n" x) => (void)
,eof =err> "terminated"
x =err> "terminated"
,eof =err> "terminated"
;; i/o
--top--
(set! ev (parameterize ([sandbox-input "3\n"] [sandbox-output 'string])
(make-evaluator 'mzscheme '() '(define x 123))))
--eval-- (printf "x = ~s\n" x) => (void)
--top-- (get-output ev) => "x = 123\n"
--eval-- (printf "x = ~s\n" x) => (void)
--top-- (get-output ev) => "x = 123\n"
--eval-- (printf "x*2 = ~s\n" (+ x x)) => (void)
(printf "x*10 = ~s\n" (* 10 x)) => (void)
--top-- (get-output ev) => "x*2 = 246\nx*10 = 1230\n"
--eval-- (printf "x*(read) = ~s\n" (* x (read))) => (void)
--top-- (get-output ev) => "x*(read) = 369\n"
--eval-- (begin (printf "a\n") (fprintf (current-error-port) "b\n"))
--top-- (get-output ev) => "a\nb\n"
(get-error-output ev) => #f
--top--
(set! ev (parameterize ([sandbox-output 'string]
[sandbox-error-output 'string])
(make-evaluator 'mzscheme '())))
--eval-- (begin (printf "a\n") (fprintf (current-error-port) "b\n"))
--top-- (get-output ev) => "a\n"
(get-error-output ev) => "b\n"
;; test kill-evaluator here
--top--
(kill-evaluator ev) => (void)
--eval--
x =err> "terminated"
y =err> "terminated"
,eof =err> "terminated"
--top--
(let-values ([(i1 o1) (make-pipe)] [(i2 o2) (make-pipe)])
;; o1 -> i1 -ev-> o2 -> i2
(set! ev (parameterize ([sandbox-input i1] [sandbox-output o2])
(make-evaluator 'mzscheme '() '(define x 123))))
(t --eval-- (printf "x = ~s\n" x) => (void)
--top-- (read-line i2) => "x = 123"
--eval-- (printf "x = ~s\n" x) => (void)
--top-- (read-line i2) => "x = 123"
--eval-- (printf "x*2 = ~s\n" (+ x x)) => (void)
(printf "x*10 = ~s\n" (* 10 x)) => (void)
--top-- (read-line i2) => "x*2 = 246"
(read-line i2) => "x*10 = 1230"
(fprintf o1 "3\n")
--eval-- (printf "x*(read) = ~s\n" (* x (read))) => (void)
--top-- (read-line i2) => "x*(read) = 369"
))
;; sexprs as a program
--top--
(set! ev (make-evaluator 'mzscheme '() '(define id (lambda (x) x))))
--eval--
(id 123) => 123
--top--
(set! ev (make-evaluator 'mzscheme '() '(define id (lambda (x) x))
'(define fooo 999)))
--eval--
(id fooo) => 999
;; test source locations too
--top--
(make-evaluator 'mzscheme '() 0 1 2 '(define foo))
=err> "program:4:0: define"
;; empty program for clean repls
--top--
(set! ev (make-evaluator '(begin) '()))
--eval--
(define x (+ 1 2 3)) => (void)
x => 6
(define x (+ x 10)) => (void)
x => 16
--top--
(set! ev (make-evaluator 'mzscheme '()))
--eval--
(define x (+ 1 2 3)) => (void)
x => 6
(define x (+ x 10)) => (void)
x => 16
--top--
(set! ev (make-evaluator 'mzscheme '() '(define x (+ 1 2 3))))
--eval--
(define x (+ x 10)) =err> "cannot change identifier"
;; whole program argument
--top--
(set! ev (make-evaluator '(module foo mzscheme (define x 1))))
--eval--
x => 1
--top--
(set! ev (make-evaluator '(module foo mzscheme (provide x) (define x 1))))
--eval--
x => 1
(define x 2) =err> "cannot change identifier"
;; limited FS access, allowed for requires
--top--
(when (directory-exists? "/tmp") ; non-collects place to play with
(let* ([mzlib (path->string (collection-path "mzlib"))]
[list-lib (path->string (build-path mzlib "list.ss"))]
[test-lib "/tmp/sandbox-test.ss"])
(t --top--
(set! ev (make-evaluator 'mzscheme '()))
--eval--
;; reading from collects is allowed
(list (directory-list ,mzlib))
(file-exists? ,list-lib) => #t
(input-port? (open-input-file ,list-lib)) => #t
;; writing is forbidden
(open-output-file ,list-lib) =err> "file access denied"
;; reading from other places is forbidden
(directory-list "/tmp") =err> "file access denied"
;; no network too
(tcp-listen 12345) =err> "network access denied"
--top--
;; reading from a specified require is fine
(with-output-to-file test-lib
(lambda ()
(printf "~s\n" '(module sandbox-test mzscheme
(define x 123) (provide x))))
'replace)
(set! ev (make-evaluator 'mzscheme `(,test-lib)))
--eval--
x => 123
(length (with-input-from-file ,test-lib read)) => 5
;; the directory is still not kosher
(directory-list "/tmp") =err> "file access denied"
--top--
;; should work also for module evaluators
(set! ev (make-evaluator `(module foo mzscheme
(require (file ,test-lib)))))
--eval--
x => 123
(length (with-input-from-file ,test-lib read)) => 5
;; the directory is still not kosher
(directory-list "/tmp") =err> "file access denied"
--top--
;; explicitly allow access to /tmp
(set! ev (parameterize ([sandbox-path-permissions
`((read #rx#"^/tmp(?:/|$)")
,@(sandbox-path-permissions))])
(make-evaluator 'mzscheme '())))
--eval--
(length (with-input-from-file ,test-lib read)) => 5
(list? (directory-list "/tmp"))
(open-output-file "/tmp/blah") =err> "file access denied"
(delete-directory "/tmp/blah") =err> "file access denied"
)))
;; languages and requires
--top--
(set! ev (make-evaluator 'r5rs '() "(define x (eq? 'x 'X))"))
--eval--
x => #t
--top--
(set! ev (make-evaluator 'mzscheme '() "(define l null)"))
--eval--
(cond [null? l 0]) => 0
(last-pair l) =err> "reference to an identifier"
--top--
(set! ev (make-evaluator 'beginner '() (make-prog "(define l null)"
"(define x 3.5)")))
--eval--
(cond [null? l 0]) =err> "expected an open parenthesis"
--top--
(eq? (ev "6") (ev "(sub1 (* 2 3.5))"))
(eq? (ev "6") (ev "(sub1 (* 2 x))"))
--top--
(set! ev (make-evaluator 'mzscheme '((lib "list.ss")) '()))
--eval--
(last-pair '(1 2 3)) => '(3)
(last-pair null) =err> "expected argument of type"
;; coverage
--top--
(set! ev (parameterize ([sandbox-coverage-enabled #t])
(make-evaluator 'mzscheme '()
(make-prog "(define (foo x) (+ x 1))"
"(define (bar x) (+ x 2))"
"(equal? (foo 3) 4)"))))
(pair? (get-uncovered-expressions ev))
(pair? (get-uncovered-expressions ev #t))
--eval--
(foo 3) => 4
(bar 10) => 12
--top--
(null? (get-uncovered-expressions ev #t))
(pair? (get-uncovered-expressions ev)) ; no-tests coverage still the same
;; misc parameters
--top--
(set! ev (parameterize ([sandbox-init-hook
(let ([old (sandbox-init-hook)])
(lambda ()
(old)
(compile-enforce-module-constants #f)
(compile-allow-set!-undefined #t)))])
(make-evaluator 'mzscheme '() '(define x 123))))
--eval--
(set! x 456) ; would be an error without the `enforce' parameter
x => 456
(set! y 789) ; would be an error without the `set!' parameter
y => 789
)
)