(load-relative "loadtest.ss") (Section 'sandbox) (require scheme/sandbox) ;; test call-in-nested-thread* (let () (define (kill) (kill-thread (current-thread))) (define (shut) (custodian-shutdown-all (current-custodian))) (define-syntax-rule (nested body ...) (call-in-nested-thread* (lambda () body ...))) (define-syntax-rule (nested* body ...) (call-in-nested-thread* (lambda () body ...) (lambda () 'kill) (lambda () 'shut))) (test 1 values (nested 1)) ;; propagates parameters (let ([p (make-parameter #f)]) (nested (p 1)) (test 1 p) (with-handlers ([void void]) (nested (p 2) (error "foo") (p 3))) (test 2 p)) ;; propagates kill-thread (test (void) thread-wait (thread (lambda () (nested (kill)) ;; never reach here (semaphore-wait (make-semaphore 0))))) ;; propagates custodian-shutdown-all (test (void) values (parameterize ([current-custodian (make-custodian)]) (nested (shut)))) ;; test handlers parameters (test 'kill (lambda () (nested* (kill)))) (test 'shut (lambda () (nested* (shut))))) (let ([ev void]) (define (make-evaluator! . args) (set! ev (apply make-evaluator args))) (define (make-base-evaluator! . args) (set! ev (apply make-evaluator 'scheme/base args))) (define (make-base-evaluator/reqs! reqs . args) (set! ev (apply make-evaluator 'scheme/base #:requires reqs args))) (define (make-module-evaluator! . args) (set! ev (apply make-module-evaluator args))) (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> 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 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-- (make-base-evaluator! (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 0.5 5) --eval-- x => 1 (id 1) => 1 (id (plus1 x)) => 1 (define id2 id) (id2 (id x)) => 1 blah =err> "before its definition" ;; using a string for an input "1" => 1 "(+ 1 2) x (define y 9) y (set! y 99) y" => 99 "bad\"string" =err> "expected a closing" "bad(string" =err> "expected a .\\)." "bad)string" =err> "unexpected .\\)." "(set! y 999) (string" =err> "expected a .\\)." y => 99 "(set! y 999) (if)" =err> "if: bad syntax" y => 999 ;; test limits (loop) =err> "out of time" --top-- (when (custodian-memory-accounting-available?) (t --eval-- (memory 3000000) =err> "out of memory")) ;; test parameter settings (tricky to get this right since ;; with-limits runs stuff in a different thread) (set-eval-limits ev #f #f) --eval-- (define p (make-parameter 0)) (p) => 0 (p 1) (p) => 1 (thread-wait (thread (lambda () (p 100)))) (p) => 1 --top-- (set-eval-limits ev 1 3) --eval-- (p) => 1 (p 2) (p) => 2 (thread-wait (thread (lambda () (p 100)))) (p) => 2 --top-- (set-eval-limits ev #f #f) --eval-- (p) => 2 ;; breaking --top-- (thread (lambda () (sleep 1) (break-evaluator ev))) --eval-- (sleep 2) =err> "user break" (printf "x = ~s\n" x) => (void) ;; termination --eval-- ,eof =err> "terminated .eof.$" 123 =err> "terminated .eof.$" ,eof =err> "terminated .eof.$" ;; other termination messages --top-- (make-base-evaluator!) (kill-evaluator ev) --eval-- 123 =err> "terminated .evaluator-killed.$" ;; nested calls are forbidden --top-- (make-base-evaluator!) --eval-- (,ev 1) =err> "nested evaluator call" ;; eval-limits apply to the sandbox creation too --top-- (parameterize ([sandbox-eval-limits '(0.25 5)]) (make-base-evaluator! '(sleep 2))) =err> "out of time" (when (custodian-memory-accounting-available?) (t --top-- (parameterize ([sandbox-eval-limits '(2 2)]) (make-base-evaluator! '(define a (for/list ([i (in-range 10)]) (collect-garbage) (make-bytes 500000))))) =err> "out of memor(?:y)")) ;; i/o --top-- (parameterize ([sandbox-input "3\n"] [sandbox-output 'string] [sandbox-error-output current-output-port]) (make-base-evaluator! '(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-- (parameterize ([sandbox-output 'string] [sandbox-error-output 'string]) (make-base-evaluator!)) --eval-- (begin (printf "a\n") (fprintf (current-error-port) "b\n")) --top-- (get-output ev) => "a\n" (get-error-output ev) => "b\n" --top-- (parameterize ([sandbox-input 'pipe] [sandbox-output 'bytes] [sandbox-error-output current-output-port] [sandbox-eval-limits '(0.25 10)]) (make-base-evaluator! '(define x 123))) --eval-- (begin (printf "x = ~s\n" x) (fprintf (current-error-port) "err\n")) --top-- (get-output ev) => #"x = 123\nerr\n" (put-input ev "blah\n") (put-input ev "blah\n") --eval-- (read-line) => "blah" (printf "line = ~s\n" (read-line)) --top-- (get-output ev) => #"line = \"blah\"\n" --eval-- (read-line) =err> "out of time" --top-- (put-input ev "blah\n") (put-input ev eof) --eval-- (read-line) => "blah" (read-line) => eof (read-line) => eof ;; test kill-evaluator here --top-- (kill-evaluator ev) => (void) --eval-- x =err> "terminated .evaluator-killed.$" y =err> "terminated .evaluator-killed.$" ,eof =err> "terminated .evaluator-killed.$" --top-- (let-values ([(i1 o1) (make-pipe)] [(i2 o2) (make-pipe)]) ;; o1 -> i1 -ev-> o2 -> i2 (parameterize ([sandbox-input i1] [sandbox-output o2]) (make-base-evaluator! '(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-- (make-base-evaluator! '(define id (lambda (x) x))) --eval-- (id 123) => 123 --top-- (make-base-evaluator! '(define id (lambda (x) x)) '(define fooo 999)) --eval-- (id fooo) => 999 ;; test source locations too --top-- (make-base-evaluator! 0 1 2 '(define foo)) =err> "program:4:0: define" ;; empty program for clean repls --top-- (make-evaluator! '(begin)) --eval-- (define x (+ 1 2 3)) => (void) x => 6 (define x (+ x 10)) => (void) x => 16 --top-- (make-base-evaluator!) --eval-- (define x (+ 1 2 3)) => (void) x => 6 (define x (+ x 10)) => (void) x => 16 --top-- (make-base-evaluator! '(define x (+ 1 2 3))) --eval-- (define x (+ x 10)) =err> "cannot re-define a constant" ;; whole program argument --top-- (make-module-evaluator! '(module foo scheme/base (define x 1))) --eval-- x => 1 --top-- (make-module-evaluator! '(module foo scheme/base (provide x) (define x 1))) --eval-- x => 1 (define x 2) =err> "cannot re-define a constant" ;; limited FS access, allowed for requires --top-- (let* ([tmp (make-temporary-file "sandboxtest~a" 'directory)] [strpath (lambda xs (path->string (apply build-path xs)))] [schemelib (strpath (collection-path "scheme"))] [list-lib (strpath schemelib "list.ss")] [list-zo (strpath schemelib "compiled" "list_ss.zo")] [test-lib (strpath tmp "sandbox-test.ss")] [test-zo (strpath tmp "compiled" "sandbox-test_ss.zo")] [test2-lib (strpath tmp "sandbox-test2.ss")] [test2-zo (strpath tmp "compiled" "sandbox-test2_ss.zo")]) (t --top-- (make-base-evaluator!) --eval-- ;; reading from collects is allowed (list? (directory-list ,schemelib)) (file-exists? ,list-lib) => #t (input-port? (open-input-file ,list-lib)) => #t ;; writing is forbidden (open-output-file ,list-lib) =err> "`write' access denied" ;; reading from other places is forbidden (directory-list ,tmp) =err> "`read' access denied" ;; no network too (require scheme/tcp) (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 scheme/base (define x 123) (provide x))))) (make-base-evaluator/reqs! `(,test-lib)) --eval-- x => 123 (length (with-input-from-file ,test-lib read)) => 5 ;; the directory is still not kosher (directory-list ,tmp) =err> "`read' access denied" --top-- ;; should work also for module evaluators ;; --> NO! Shouldn't make user code require whatever it wants ;; (make-module-evaluator! ;; `(module foo scheme/base (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, and write access to a single file (make-directory (build-path tmp "compiled")) (parameterize ([sandbox-path-permissions `((read ,tmp) (write ,test-zo) ,@(sandbox-path-permissions))]) (make-base-evaluator!)) --eval-- (length (with-input-from-file ,test-lib read)) => 5 (list? (directory-list ,tmp)) (open-output-file ,(build-path tmp "blah")) =err> "access denied" (delete-directory ,(build-path tmp "blah")) =err> "access denied" (list? (directory-list ,schemelib)) ;; we can read/write/delete list-zo, but we can't load bytecode from ;; it due to the code inspector (copy-file ,list-zo ,test-zo) => (void) (copy-file ,test-zo ,list-zo) =err> "access denied" (load/use-compiled ,test-lib) => (void) (require 'list) =err> "access from an uncertified context" (delete-file ,test-zo) => (void) (delete-file ,test-lib) =err> "`delete' access denied" --top-- ;; a more explicit test of bytcode loading, allowing rw access to the ;; complete tmp directory, but read-bytecode only for test2-lib (parameterize ([sandbox-path-permissions `((write ,tmp) (read-bytecode ,test2-lib) ,@(sandbox-path-permissions))]) (make-base-evaluator!)) --eval-- (define (cp from to) (when (file-exists? to) (delete-file to)) (copy-file from to)) (cp ,list-lib ,test-lib) (cp ,list-zo ,test-zo) (cp ,list-lib ,test2-lib) (cp ,list-zo ,test2-zo) ;; bytecode from test-lib is bad, even when we can read/write to it (load/use-compiled ,test-zo) (require 'list) =err> "access from an uncertified context" ;; bytecode from test2-lib is explicitly allowed (load/use-compiled ,test2-lib) (require 'list) => (void)) ((dynamic-require 'scheme/file 'delete-directory/files) tmp)) ;; languages and requires --top-- (make-evaluator! '(special r5rs) "(define x (eq? 'x 'X))") --eval-- x => #t --top-- (make-base-evaluator! "(define l null)") --eval-- (cond [null? l 0]) => 0 (last-pair l) =err> "reference to an identifier" --top-- (make-evaluator! '(special 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-- (make-base-evaluator/reqs! '(scheme/list)) --eval-- (last-pair '(1 2 3)) => '(3) (last-pair null) =err> "expected argument of type" ;; coverage --top-- (parameterize ([sandbox-coverage-enabled #t]) (make-base-evaluator! (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 #f)) (pair? (get-uncovered-expressions ev)) ; no-tests coverage still the same ;; misc parameters --top-- (parameterize ([sandbox-init-hook (let ([old (sandbox-init-hook)]) (lambda () (old) (compile-enforce-module-constants #f) (compile-allow-set!-undefined #t)))]) (make-base-evaluator! '(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 ;; test that output is also collected under the limit --top-- (parameterize ([sandbox-output 'bytes] [sandbox-error-output current-output-port] [sandbox-memory-limit 2] [sandbox-eval-limits '(0.25 1)]) (make-base-evaluator!)) ;; GCing is needed to allow these to happen (note: the memory limit is very ;; tight here, this test usually fails if the sandbox library is not ;; compiled) (let ([t (lambda () (t --eval-- (display (make-bytes 400000 65)) (collect-garbage) --top-- (bytes-length (get-output ev)) => 400000))]) ;; can go arbitrarily high here (for ([i (in-range 20)]) (t))) ;; test that killing the thread, shutting the custodian, or calling `exit' ;; works fine first try it without limits (limits imply a nested ;; thread/custodian) --top-- (let () (define (3x2-terminations) (t --top-- (make-base-evaluator!) --eval-- (kill-thread (current-thread)) =err> "terminated .thread-killed.$" --top-- (make-base-evaluator!) --eval-- (custodian-shutdown-all (current-custodian)) =err> "terminated .custodian-shutdown.$" --top-- (make-base-evaluator!) --eval-- (exit) =err> "terminated .exited.$" ;; now test that it's fine when called directly --top-- (make-base-evaluator!) (call-in-sandbox-context ev (lambda () (kill-thread (current-thread)))) =err> "terminated .thread-killed.$" (make-base-evaluator!) (call-in-sandbox-context ev (lambda () (custodian-shutdown-all (current-custodian)))) =err> "terminated .custodian-shutdown.$" (make-base-evaluator!) (call-in-sandbox-context ev exit) =err> "terminated .exited.$")) (define (test-terminations) ;; try without, then with per-expression limits (parameterize ([sandbox-eval-limits #f]) (3x2-terminations)) (3x2-terminations)) (test-terminations)) ;; when an expression is out of memory, the sandbox should stay alive --top-- (when (custodian-memory-accounting-available?) (t --top-- (parameterize ([sandbox-eval-limits '(2 5)] [sandbox-memory-limit 100]) (make-base-evaluator!)) --eval-- (define a '()) (define b 1) (length (for/fold ([v null]) ([i (in-range 20)]) ;; increases size of sandbox: it's reachable from it (outside of ;; this evaluation) because `a' is defined there (set! a (cons (make-bytes 500000) a)) (collect-garbage) ;; increases size of the current evaluation (cons (make-bytes 500000) v))) =err> "out of memo(?:ry)" b => 1)) )) (report-errs)