security: seed fuzz regression corpus

ober

ba8ea87260b392e7ada04e0376259e1a58d61672

diff --git a/Makefile b/Makefile
index 9a34739..6195747 100644
--- 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)
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index ccde5c1..bc62c8b 100644
--- 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
diff --git a/docs/testing-and-infrastructure.md b/docs/testing-and-infrastructure.md
index e621f0c..89fccec 100644
--- 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
diff --git a/tests/fuzz/corpus/json/nested.json b/tests/fuzz/corpus/json/nested.json
new file mode 100644
index 0000000..d913ddb
--- /dev/null
+++ b/tests/fuzz/corpus/json/nested.json
@@ -0,0 +1 @@
+{"a":[1,true,false,null,{"b":"c"}]}
diff --git a/tests/fuzz/corpus/json/unicode.json b/tests/fuzz/corpus/json/unicode.json
new file mode 100644
index 0000000..e0360b7
--- /dev/null
+++ b/tests/fuzz/corpus/json/unicode.json
@@ -0,0 +1 @@
+"\u0041\n\t"
diff --git a/tests/fuzz/corpus/reader/basic-list.sexp b/tests/fuzz/corpus/reader/basic-list.sexp
new file mode 100644
index 0000000..7842802
--- /dev/null
+++ b/tests/fuzz/corpus/reader/basic-list.sexp
@@ -0,0 +1 @@
+(alpha (beta . gamma) [1 2 3])
diff --git a/tests/fuzz/corpus/reader/comments.sexp b/tests/fuzz/corpus/reader/comments.sexp
new file mode 100644
index 0000000..59e5cee
--- /dev/null
+++ b/tests/fuzz/corpus/reader/comments.sexp
@@ -0,0 +1 @@
+#;discarded (kept "value" #u8(1 2 3))
diff --git a/tests/fuzz/corpus/uri/basic.txt b/tests/fuzz/corpus/uri/basic.txt
new file mode 100644
index 0000000..51d8875
--- /dev/null
+++ b/tests/fuzz/corpus/uri/basic.txt
@@ -0,0 +1 @@
+https://example.com/path?q=v#frag
diff --git a/tests/fuzz/corpus/uri/traversal-shape.txt b/tests/fuzz/corpus/uri/traversal-shape.txt
new file mode 100644
index 0000000..3f5b14e
--- /dev/null
+++ b/tests/fuzz/corpus/uri/traversal-shape.txt
@@ -0,0 +1 @@
+http://host/../../etc/passwd
diff --git a/tests/fuzz/regression/json/lone-surrogate.json b/tests/fuzz/regression/json/lone-surrogate.json
new file mode 100644
index 0000000..6dcea84
--- /dev/null
+++ b/tests/fuzz/regression/json/lone-surrogate.json
@@ -0,0 +1 @@
+"\uD800"
diff --git a/tests/fuzz/regression/reader/sharp-dot.sexp b/tests/fuzz/regression/reader/sharp-dot.sexp
new file mode 100644
index 0000000..9c26016
--- /dev/null
+++ b/tests/fuzz/regression/reader/sharp-dot.sexp
@@ -0,0 +1 @@
+#.(+ 1 2)
diff --git a/tests/fuzz/regression/sandbox/foreign-procedure.ssfrag b/tests/fuzz/regression/sandbox/foreign-procedure.ssfrag
new file mode 100644
index 0000000..fbd5bb9
--- /dev/null
+++ b/tests/fuzz/regression/sandbox/foreign-procedure.ssfrag
@@ -0,0 +1 @@
+(foreign-procedure "getpid" () int)
diff --git a/tests/fuzz/regression/sandbox/system.ssfrag b/tests/fuzz/regression/sandbox/system.ssfrag
new file mode 100644
index 0000000..7c2c4b3
--- /dev/null
+++ b/tests/fuzz/regression/sandbox/system.ssfrag
@@ -0,0 +1 @@
+(system "true")
diff --git a/tests/fuzz/regression/uri/javascript-url.txt b/tests/fuzz/regression/uri/javascript-url.txt
new file mode 100644
index 0000000..d2a7213
--- /dev/null
+++ b/tests/fuzz/regression/uri/javascript-url.txt
@@ -0,0 +1 @@
+javascript:alert(1)
diff --git a/tests/test-fuzz-regression.ss b/tests/test-fuzz-regression.ss
new file mode 100644
index 0000000..ddbf33a
--- /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))