security: triage raw datum readers
ober
7379764cdaefebb65cecef67219c620bf42f93aa
--- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -215,7 +215,7 @@ when" must be answerable from `dist/release-evidence/` in minutes. | Exec restricted worker facade | `(std security worker)` | exists; pure env, deadline, process-group kill, output caps, fail-closed refused axes; pre-exec kernel controls pending | security-reference §5 | | Native async exec launcher (collect-safe) | `(std os aproc)` | exists — the primitive P0-02 should build on | [aproc.md](aproc.md) | | Parser hardening (depth/size/backtrack budgets: reader, JSON, XML, YAML, DNS, HTTP/2, WS, zlib, base64, hex, CSV, pregexp, format) | various | phases 1–4 done, 42 tests; **phase 5 (FFI audit) not started** | security-reference §7 | -| Safe deserialization (tagged-JSON envelope, no native FASL on untrusted paths) | `(std safe-fasl)`, `(std fasl)` (trusted-only) | exists; raw-FASL callers still need triage (P0-04) | [safety-guide.md](safety-guide.md) §10 | +| Safe deserialization (tagged-JSON envelope, no native FASL on untrusted paths) | `(std safe-fasl)`, `(std fasl)` (trusted-only) | exists; named raw-read/FASL paths have first triage; broader `load`/REPL/dev-surface classification remains | [safety-guide.md](safety-guide.md) §10 | | Crypto (Rust `ring`: AEAD, HMAC, PBKDF2, scrypt, ChaCha20, digests; CSPRNG from `/dev/urandom`; timing-safe compare) | `(std crypto native-rust)` et al. | exists; OpenSSL legacy paths still present; no Argon2id | security-reference §10 | | Secure memory (mlock, guard pages, DONTDUMP, DONTFORK, explicit_bzero) | `(std crypto secure-mem)` | exists; low-level pointer API only | security-reference §9 | | Lifetime-scoped secrets with auto-wipe | `(std security secret)` | exists; **absent from security-reference.md** — doc gap | `lib/std/security/secret.ss` | @@ -412,7 +412,14 @@ The allowlist is Jerboa's crown jewel; it currently rests on manual curation. failures. ### K3-P0-04 — Triage and close raw `read` / native FASL paths on untrusted input -**Serves:** G1. **Effort:** 3–5 days. +**Serves:** G1. **Effort:** 3–5 days. **Status:** first scanner-driven +batch landed 2026-07-27: gRPC request/response parsing, general `(std io)`, +and legacy lockfile readers use `jerboa-read`; package manifest/policy/lock +readers keep plain Scheme datum syntax but are size-capped, structurally +validated, and locally justified; PGO profile loading is size-capped and tagged +as trusted generated input; translator source reads are explicitly trusted +compiler input after string transforms. Remaining work: complete the broader +`load`/REPL/dev-surface classification and add a dedicated audit gate. `security-reference.md`'s checklist already names exemplars: `jerbuild.ss:308`, `lib/std/actor/distributed.ss:303`, `lib/std/fasl.ss:27`, @@ -888,7 +895,7 @@ Track these in `docs/status.md` per release: |---|---|---| | Build balance clean | `pattern.ss` repaired 2026-07-27; `source-balance` in `make audit` | always clean | | Confined worker exists | initial facade landed 2026-07-27; native pre-exec backend pending | yes, tested (P0-02) | -| Unclassified raw `read`/FASL sites | unknown (4+ named) | 0 | +| Unclassified raw `read`/FASL sites | first scanner-driven batch closed 2026-07-27; broader `load`/REPL/dev classification remains | 0 | | FFI bindings audited | 0 / 91 files | 100% with verdicts | | Un-annotated Rust `unsafe` blocks | unknown / 432 matches | 0 | | Fuzz corpora / crash regressions | 0 / 0 | per-parser corpora + every crash a test | --- a/lib/jerboa/lock.ss +++ b/lib/jerboa/lock.ss @@ -28,7 +28,8 @@ iota 1+ 1- partition make-date make-time) - (except (jerboa prelude) meta atom?)) + (except (jerboa prelude) meta atom?) + (only (jerboa reader) jerboa-read)) (defstruct lock-entry (name version hash deps)) (defstruct lockfile (entries)) @@ -85,7 +86,7 @@ (newline port)) (def (lockfile-read port) - (let ([sexp (read port)]) + (let ([sexp (jerboa-read port)]) (if (eof-object? sexp) (make-lockfile '()) (sexp->lockfile sexp)))) --- a/lib/std/compiler/pgo.ss +++ b/lib/std/compiler/pgo.ss @@ -35,6 +35,21 @@ (import (chezscheme) (only (jerboa core) def)) + (def max-profile-bytes (* 4 1024 1024)) + + (def (read-profile-text filename) + (call-with-input-file filename + (lambda (port) + (let ([out (open-output-string)]) + (let loop ([n 0]) + (let ([ch (read-char port)]) + (cond + [(eof-object? ch) (get-output-string out)] + [(>= n max-profile-bytes) + (error 'profile-load "profile file too large" filename)] + [else + (write-char ch out) + (loop (+ n 1))]))))))) ;; ----------------------------------------------------------------------- ;; State (symbol → integer call-count table) @@ -107,12 +122,12 @@ (def (profile-load filename) "Load profile data from FILENAME; returns alist" (if (file-exists? filename) - (call-with-input-file filename - (lambda (port) - (let ([form (read port)]) - (if (and (pair? form) (eq? (car form) 'jerboa-profile)) - (cadr form) - (error 'profile-load "malformed profile" filename))))) + (let* ([text (read-profile-text filename)] + [port (open-input-string text)] + [form (read port)]) ; jerboa-security: suppress bare-read-untrusted -- PGO profile is generated by profile-save, size-capped above, and tag-checked below + (if (and (pair? form) (eq? (car form) 'jerboa-profile)) + (cadr form) + (error 'profile-load "malformed profile" filename))) '())) (def (profile-load! filename) --- a/lib/std/io.ss +++ b/lib/std/io.ss @@ -9,11 +9,12 @@ read-all write-all with-input with-output) (import (chezscheme) - (only (jerboa core) def)) + (only (jerboa core) def) + (only (jerboa reader) jerboa-read)) (def (read-all port) (let lp ([acc '()]) - (let ([datum (read port)]) + (let ([datum (jerboa-read port)]) (if (eof-object? datum) (reverse acc) (lp (cons datum acc)))))) --- a/lib/std/net/grpc.ss +++ b/lib/std/net/grpc.ss @@ -3,7 +3,8 @@ ;;; ;;; Protocol: client sends (method arg ...) as an S-expression followed by newline. ;;; Server responds with (ok result) or (error message). -;;; Length-prefix framing is not needed since `read` handles S-expression boundaries. +;;; Messages are parsed with Jerboa's bounded reader; callers should still keep +;;; transport-level connection and request-size limits. (library (std net grpc) (export @@ -15,7 +16,8 @@ (import (chezscheme) (only (std native-loader) native-loader-ensure-libc-symbol!) - (only (jerboa core) def defstruct try catch finally)) + (only (jerboa core) def defstruct try catch finally) + (only (jerboa reader) jerboa-read)) ;; ---- C socket FFI ---- @@ -156,7 +158,7 @@ [inp (car ps)] [out (cdr ps)]) (let loop () - (let ([msg (try (read inp) + (let ([msg (try (jerboa-read inp) (catch (exn) (eof-object)))]) (unless (eof-object? msg) (let ([response (dispatch-rpc services msg)]) @@ -251,7 +253,7 @@ [out (grpc-client-rec-out client)]) (write (cons method args) out) (flush-output-port out) - (let ([resp (read inp)]) + (let ([resp (jerboa-read inp)]) (if (and (pair? resp) (eq? (car resp) 'ok)) (cadr resp) (if (and (pair? resp) (eq? (car resp) 'error)) --- a/lib/std/pkg/lock.ss +++ b/lib/std/pkg/lock.ss @@ -136,9 +136,11 @@ (when (> (string-length text) max-lock-bytes) (jpkg-error "lock: file too large")) (let* ([port (open-string-input-port text)] - [datum (try (read port) (catch (e) (jpkg-error "lock: unreadable")))]) + [datum (try (read port) ; jerboa-security: suppress bare-read-untrusted -- lock text is size-capped above and structurally validated below + (catch (e) (jpkg-error "lock: unreadable")))]) (when (eof-object? datum) (jpkg-error "lock: empty file")) - (let ([extra (try (read port) (catch (e) (jpkg-error "lock: trailing junk")))]) + (let ([extra (try (read port) ; jerboa-security: suppress bare-read-untrusted -- second read only rejects extra size-capped lock forms + (catch (e) (jpkg-error "lock: trailing junk")))]) (unless (eof-object? extra) (jpkg-error "lock: more than one top-level form"))) (check-data! datum) --- a/lib/std/pkg/manifest.ss +++ b/lib/std/pkg/manifest.ss @@ -85,13 +85,14 @@ ;; check-data!'s node budget below converts both into hard errors, so ;; no textual pre-scan is needed. (let* ([port (open-string-input-port text)] - [datum (try (read port) + [datum (try (read port) ; jerboa-security: suppress bare-read-untrusted -- manifest text is size-capped above and structurally validated below (catch (e) (jpkg-error "manifest: unreadable: ~a" (if (message-condition? e) (condition-message e) ""))))]) (when (eof-object? datum) (jpkg-error "manifest: empty file")) - (let ([extra (try (read port) (catch (e) (jpkg-error "manifest: trailing junk")))]) + (let ([extra (try (read port) ; jerboa-security: suppress bare-read-untrusted -- second read only rejects extra size-capped manifest forms + (catch (e) (jpkg-error "manifest: trailing junk")))]) (unless (eof-object? extra) (jpkg-error "manifest: more than one top-level form"))) (check-data! datum) --- a/lib/std/pkg/policy.ss +++ b/lib/std/pkg/policy.ss @@ -82,9 +82,11 @@ (when (> (string-length text) 65536) (jpkg-error "policy: file too large")) (let* ([port (open-string-input-port text)] - [datum (try (read port) (catch (e) (jpkg-error "policy: unreadable")))]) + [datum (try (read port) ; jerboa-security: suppress bare-read-untrusted -- policy text is size-capped above and structurally validated below + (catch (e) (jpkg-error "policy: unreadable")))]) (when (eof-object? datum) (jpkg-error "policy: empty file")) - (let ([extra (try (read port) (catch (e) (jpkg-error "policy: trailing junk")))]) + (let ([extra (try (read port) ; jerboa-security: suppress bare-read-untrusted -- second read only rejects extra size-capped policy forms + (catch (e) (jpkg-error "policy: trailing junk")))]) (unless (eof-object? extra) (jpkg-error "policy: more than one form"))) ;; shallow data check (let walk ([x datum] [depth 0]) --- a/src/jerboa/translator.ss +++ b/src/jerboa/translator.ss @@ -828,7 +828,7 @@ (define (read-all-from-string str) (let ([p (open-input-string str)]) (let loop ([acc '()]) - (let ([f (read p)]) + (let ([f (read p)]) ; jerboa-security: suppress bare-read-untrusted -- trusted compiler source after Jerboa string transforms (if (eof-object? f) (reverse acc) (loop (cons f acc)))))))