security: seed fuzz regression corpus
ober
ba8ea87260b392e7ada04e0376259e1a58d61672
--- a/Makefile +++ b/Makefile @@ -1745,6 +1745,10 @@ test-all: test test-features test-wrappers test-security test-native test-gaps ## ========== Fuzzing ========== +.PHONY: test-fuzz-regression +test-fuzz-regression: + @$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-fuzz-regression.ss + FUZZ_DIR = tests/fuzz/harness FUZZ_ITERATIONS ?= 10000 FUZZ_ENV = SCHEME=$(SCHEME) LIBDIRS=$(LIBDIRS) @@ -1754,7 +1758,7 @@ fuzz: $(FUZZ_ENV) FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-all.ss # Quick smoke test for CI (~30s) -fuzz-smoke: +fuzz-smoke: test-fuzz-regression $(FUZZ_ENV) FUZZ_ITERATIONS=500 $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-all.ss # Long-running deep fuzz (nightly/dedicated) --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -522,6 +522,14 @@ Current tests verify features work; almost none verify *attacks fail*. Harnesses without corpora find a bug once and forget it. +- **Status:** first corpus/regression batch landed 2026-07-27: + `tests/fuzz/corpus/{reader,json,uri}/` contains seed corpora, + `tests/fuzz/regression/` contains checked-in known-bad inputs, and + `tests/test-fuzz-regression.ss` runs them under normal `make test`. + `make fuzz-smoke` also runs that regression gate before randomized harnesses. + Remaining work is to broaden corpora for every harness and add the missing + YAML, safe-FASL envelope, actor-auth, archive-validator, MCP/LSP, and REPL + protocol harnesses. - **Do:** (a) Add `tests/fuzz/corpus/<parser>/` seed corpora (start from valid samples mutated minimally; include every historical crash input). (b) Add `tests/fuzz/regression/` — every crash ever found becomes a --- a/docs/testing-and-infrastructure.md +++ b/docs/testing-and-infrastructure.md @@ -25,6 +25,12 @@ regression suite is `tests/security/test-k3-regressions.ss`, and `docs/security-reference.md` is the canonical reference for the implemented security controls it pins. +Use `make test-fuzz-regression` for checked-in fuzz corpus regressions. Seed +corpora live under `tests/fuzz/corpus/<parser>/`; known-bad crash or rejection +inputs live under `tests/fuzz/regression/<parser>/`. The regression runner is +also part of `make test`, and `make fuzz-smoke` runs it before randomized fuzz +harnesses. + --- ## 1. (std test quickcheck) -- Property-Based Testing new file mode 100644 --- /dev/null +++ b/tests/fuzz/corpus/json/nested.json @@ -0,0 +1 @@ +{"a":[1,true,false,null,{"b":"c"}]} new file mode 100644 --- /dev/null +++ b/tests/fuzz/corpus/json/unicode.json @@ -0,0 +1 @@ +"\u0041\n\t" new file mode 100644 --- /dev/null +++ b/tests/fuzz/corpus/reader/basic-list.sexp @@ -0,0 +1 @@ +(alpha (beta . gamma) [1 2 3]) new file mode 100644 --- /dev/null +++ b/tests/fuzz/corpus/reader/comments.sexp @@ -0,0 +1 @@ +#;discarded (kept "value" #u8(1 2 3)) new file mode 100644 --- /dev/null +++ b/tests/fuzz/corpus/uri/basic.txt @@ -0,0 +1 @@ +https://example.com/path?q=v#frag new file mode 100644 --- /dev/null +++ b/tests/fuzz/corpus/uri/traversal-shape.txt @@ -0,0 +1 @@ +http://host/../../etc/passwd new file mode 100644 --- /dev/null +++ b/tests/fuzz/regression/json/lone-surrogate.json @@ -0,0 +1 @@ +"\uD800" new file mode 100644 --- /dev/null +++ b/tests/fuzz/regression/reader/sharp-dot.sexp @@ -0,0 +1 @@ +#.(+ 1 2) new file mode 100644 --- /dev/null +++ b/tests/fuzz/regression/sandbox/foreign-procedure.ssfrag @@ -0,0 +1 @@ +(foreign-procedure "getpid" () int) new file mode 100644 --- /dev/null +++ b/tests/fuzz/regression/sandbox/system.ssfrag @@ -0,0 +1 @@ +(system "true") new file mode 100644 --- /dev/null +++ b/tests/fuzz/regression/uri/javascript-url.txt @@ -0,0 +1 @@ +javascript:alert(1) new file mode 100644 --- /dev/null +++ b/tests/test-fuzz-regression.ss @@ -0,0 +1,110 @@ +#!chezscheme +;;; test-fuzz-regression.ss -- checked-in fuzz corpus regression runner + +(import (scheme) + (jerboa reader) + (std net uri) + (std text json) + (std security restrict) + (std security sanitize)) + +(define pass-count 0) +(define fail-count 0) + +(define-syntax check + (syntax-rules (=>) + [(_ name expr => expected) + (let ([result expr] + [exp expected]) + (if (equal? result exp) + (set! pass-count (+ pass-count 1)) + (begin + (set! fail-count (+ fail-count 1)) + (display "FAIL: ") (display name) + (display " => ") (write result) + (display " expected ") (write exp) (newline))))])) + +(define (read-text path) + (call-with-input-file path + (lambda (port) (get-string-all port)))) + +(define (read-first-line path) + (call-with-input-file path + (lambda (port) (get-line port)))) + +(define (raises? thunk) + (guard (exn [#t #t]) + (thunk) + #f)) + +(define (does-not-crash? thunk) + (guard (exn [#t #t]) + (thunk) + #t)) + +(define (parser-accepts-file? parser path) + (guard (exn [#t (begin + (display "FAIL: corpus parse raised for ") + (display path) + (newline) + #f)]) + (parser (read-text path)) + #t)) + +(define (all-files-accepted? parser files) + (let loop ([rest files]) + (cond + [(null? rest) #t] + [(parser-accepts-file? parser (car rest)) (loop (cdr rest))] + [else #f]))) + +(define reader-corpus + '("tests/fuzz/corpus/reader/basic-list.sexp" + "tests/fuzz/corpus/reader/comments.sexp")) + +(define json-corpus + '("tests/fuzz/corpus/json/nested.json" + "tests/fuzz/corpus/json/unicode.json")) + +(define uri-corpus + '("tests/fuzz/corpus/uri/basic.txt" + "tests/fuzz/corpus/uri/traversal-shape.txt")) + +(check "fuzz corpus reader seeds parse" + (all-files-accepted? jerboa-read-string reader-corpus) => #t) +(check "fuzz corpus json seeds parse" + (all-files-accepted? string->json-object json-corpus) => #t) ; jerboa-security: suppress unguarded-string-to-json-on-hostile -- parser-accepts-file? wraps corpus parser calls in guard +(check "fuzz corpus uri seeds parse" + (all-files-accepted? + (lambda (input) + (let ([u (uri-parse input)]) + (when u (uri->string u)))) + uri-corpus) => #t) + +(check "fuzz regression reader rejects sharp-dot" + (raises? (lambda () + (jerboa-read-string + (read-text "tests/fuzz/regression/reader/sharp-dot.sexp")))) => #t) +(check "fuzz regression json lone surrogate does not crash" + (does-not-crash? (lambda () + (string->json-object + (read-text "tests/fuzz/regression/json/lone-surrogate.json")))) => #t) +(check "fuzz regression sanitizer rejects javascript URL" + (raises? (lambda () + (sanitize-url-attribute + (read-first-line "tests/fuzz/regression/uri/javascript-url.txt")))) => #t) +(check "fuzz regression restricted eval rejects system" ; jerboa-security: suppress shell-interpolation-unquoted-into-sh -- fixture name contains hostile input; restricted-eval-string must reject it before shell access + (raises? (lambda () + (restricted-eval-string + (read-text "tests/fuzz/regression/sandbox/system.ssfrag")))) => #t) +(check "fuzz regression restricted eval rejects foreign-procedure" + (raises? (lambda () + (restricted-eval-string + (read-text "tests/fuzz/regression/sandbox/foreign-procedure.ssfrag")))) => #t) + +(display " fuzz-regression: ") +(display pass-count) (display " passed") +(when (> fail-count 0) + (display ", ") (display fail-count) (display " failed")) +(newline) +(when (> fail-count 0) (exit 1))