security: add source and restrict closure gates
ober
df41e657b0d3236cafaf4d7b179c23d9bd2ac3ed
--- a/.jerboa-system +++ b/.jerboa-system @@ -31,6 +31,7 @@ "tools/system-provenance.ss" "tools/system-diff.ss" "tools/check-audit-events.ss" "tools/check-generated-policy.ss" + "tools/check-restrict-closure.ss" "tools/workspace-health.ss" "tools/capability-plan.ss" "tools/check-capability-use.ss" @@ -47,7 +48,7 @@ (dependencies (vendored ("vendor/ChezScheme")) - (native ("jerboa-native-rs")) + (native ("jerboa-native-rs" "browser-repl")) (external-tools ("cc" "cargo" "make" "tar" "ln" "rm" "mkdir" "find" "shasum|sha256sum"))) @@ -73,7 +74,7 @@ (eval (modules declared-by-tooling)) (native - (load ("jerboa-native-rs" "support/*.c"))) + (load ("jerboa-native-rs" "browser-repl" "support/*.c"))) (package (install none) (build declared) @@ -86,7 +87,7 @@ (test-targets ("system-check" "system-strict" "system-report" "system-provenance" "system-diff" "audit-event-check" "generated-policy-check" "unification-check" - "workspace-health" "capability-plan" + "restrict-closure-check" "workspace-health" "capability-plan" "capability-use-check" "native-boundary-check" "install-policy-check" "tcb-report" "unification-release-check" "jerboa-smoke" "mcp-test-binary" @@ -101,14 +102,14 @@ (lock-file planned) (signing planned) (sbom planned) - (tcb ("Chez Scheme" "Jerboa stdlib" "jerboa-native-rs" + (tcb ("Chez Scheme" "Jerboa stdlib" "jerboa-native-rs" "browser-repl" "support/multicall-main.c" "support/jerboa-embed.c" "support/jerboa-embed.h" "support/hardening-report.h" "support/landlock-shim.c"))) (native-boundary - (rust ("jerboa-native-rs")) + (rust ("jerboa-native-rs" "browser-repl")) (c-shims ("support/multicall-main.c" "support/jerboa-embed.c" "support/jerboa-embed.h" "support/hardening-report.h" "support/landlock-shim.c")) --- a/Makefile +++ b/Makefile @@ -52,6 +52,7 @@ CHEZ_INSTALL_FLAGS = \ $(CHEZ_TERMUX_CONFIGURE_VARS) \ $(CHEZ_CONFIGURE_EXTRA) LIBDIRS = $(JERBOA_HOME)/lib:$(JERBOA_HOME)/vendor/jsqlite/src +SOURCE_BALANCE_RUNNER ?= $(SCHEME) --libdirs $(LIBDIRS) --script # Base directory for chez-* repos (legacy C FFI — see `make native` for Rust backend) JERBOA_EXT_DIR ?= $(HOME)/src # External chez-* library paths for legacy wrapper modules @@ -75,7 +76,7 @@ TYPED_LLVMIR_PARITY_FN ?= sample_typed_llvmir_smoke::main LLVM_BIN ?= $(shell if command -v llvm-as >/dev/null 2>&1; then dirname "$$(command -v llvm-as)"; elif [ -x /opt/homebrew/opt/llvm/bin/llvm-as ]; then echo /opt/homebrew/opt/llvm/bin; elif [ -x /usr/local/opt/llvm/bin/llvm-as ]; then echo /usr/local/opt/llvm/bin; fi) TYPED_WRAPPER_DIR ?= build/typed/jerboa -.PHONY: help chez static-supported-check chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-llvmir typed-llvmir-check typed-llvmir-smoke typed-llvmir-parity typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-known-flaky test-reader test-core test-runtime test-try-debug test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-sqlite-robustness test-limits-primitives test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-kotlin test-typed-llvmir test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit audit-native security-audit clean security security-production security-profile security-hardware-smoke sbom reproducibility-report reproducibility-compare verify release-evidence fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict data-check docker-build docker-push lint +.PHONY: help chez static-supported-check chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-llvmir typed-llvmir-check typed-llvmir-smoke typed-llvmir-parity typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-known-flaky test-reader test-core test-runtime test-try-debug test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-sqlite-robustness test-limits-primitives test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-kotlin test-typed-llvmir test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit audit-native source-balance restrict-closure-check security-audit clean security security-production security-profile security-hardware-smoke sbom reproducibility-report reproducibility-compare verify release-evidence fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict data-check docker-build docker-push lint .PHONY: check-cross-tools docker fuzz-websocket-fuzz jlsp jlsp-freebsd-amd64 \ jlsp-install jlsp-linux-amd64 jlsp-portable jmcp-freebsd-amd64 \ jmcp-freebsd-arm64 jmcp-linux-amd64 jmcp-linux-arm64 \ @@ -1722,7 +1723,15 @@ audit-native: security-audit: @tools/security-audit.sh -audit: security-audit audit-native data-check security-production unification-check +source-balance: + @git ls-files '*.ss' '*.sls' \ + | grep -v '^vendor/' \ + | xargs $(SOURCE_BALANCE_RUNNER) support/check-source-balance.ss + +restrict-closure-check: + @$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-restrict-closure.ss + +audit: source-balance restrict-closure-check security-audit audit-native data-check security-production unification-check test-gaps: $(SCHEME) --libdirs $(LIBDIRS) --script tests/test-gaps.ss --- a/data/anti-patterns.sexp +++ b/data/anti-patterns.sexp @@ -5219,4 +5219,26 @@ "stack-overflow" "generator") ("title" . "Typed Kotlin Same-Name Wrapper Recursion") ("tools" "jerboa_check_balance" "jerboa_verify" - "jerboa_run_tests" "adb logcat" "uiautomator dump"))) + "jerboa_run_tests" "adb logcat" "uiautomator dump")) + (("advice" + . + "First verify with the real reader/compiler. Balance scanners and repair planners must skip \\\\x...; identifier escapes outside strings/chars before interpreting semicolons as comments.") + ("avoid" + . + "Do not append EOF closers or trust a delimiter-only repair plan when the reported form contains Chez escaped identifiers such as \\\\x7C;\\\\x3E;. The semicolon is part of the identifier escape, not a line comment.") + ("id" . "escaped-identifier-balance-false-positive") + ("kinds" "debug-error" "tooling" "scheme") + ("pattern" + . + "\\\\x[0-9A-Fa-f]+;.*(unclosed|unexpected close|repair plan)") + ("severity" . "medium") + ("tags" "balance" "chez" "escaped-identifier" "semicolon" + "repair") + ("title" + . + "Delimiter scanners treating Chez escaped identifiers as comments") + ("tools" + "jerboa_verify" + "jerboa_compile_check" + "jerboa_check_balance" + "jerboa_repair_balance"))) --- a/docs/ai-threat.md +++ b/docs/ai-threat.md @@ -182,7 +182,7 @@ wrong version of a function (safe vs unsafe) in edge cases. | Logic bugs | No advantage | No advantage | No advantage | No advantage | | DoS / resource exhaustion | Partial in-process; worker-defined for isolation | Behind | Behind | Behind (JVM has memlimits) | | Deserialization attacks | Depth-limited | Comparable | Ahead (limits) | Comparable | -| Supply chain attacks | SBOM + content-addressed | Behind (cargo audit) | Behind (npm audit) | Behind (Maven) | +| Supply chain attacks | TUF metadata, Ed25519 signatures, transparency log, SBOM, OSV audit, content-addressed artifacts | Comparable | Comparable | Comparable | ### Summary @@ -208,10 +208,11 @@ patterns. If the pattern doesn't exist, there's nothing to find. | Temp file race in `run-safe` | Medium | Obsolete: thunk execution and child result paths are retired | | Unsafe Scheme execution after raw fork | High | Fixed: `run-safe` fails closed; use an exec worker | | No memory limit in `run-safe-eval` | Medium | By design: use OS limits on an exec worker | -| Import conflict in safe prelude | Low | Known, cosmetic | +| Import conflict in safe prelude | Low | Known; tracked because deterministic safe-symbol resolution matters | | SQL injection heuristic bypasses | Medium | By design (use parameterized queries) | | Seccomp/Landlock x86_64-only | Low | By design (arch-specific syscalls) | | Silent degradation of requested process controls | Medium | Fixed: reject unless `allow-degraded? #t` is explicit | +| Raw `read` / native FASL call sites need provenance triage | Medium | Open; tracked in the K3 security handoff | --- --- a/docs/index.md +++ b/docs/index.md @@ -57,7 +57,7 @@ Start with [quickstart.md](quickstart.md), then use ## Security -- [security-reference.md](security-reference.md) - security module reference and threat-model checklist +- [security-reference.md](security-reference.md) - security module reference and limitation ledger - [safety-guide.md](safety-guide.md) - practical secure-application guide - [release-security.md](release-security.md) - release gates and security evidence - [capability.md](capability.md) - object-capability model --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -4,7 +4,10 @@ Status: living handoff document. Created 2026-07-27 from a full review of `docs/*`, `SECURITY.md`, `lib/std/security/`, `jerboa-native-rs/`, `tests/`, the `Makefile`, and the `.jerboa-system` manifest. Tone per [`Philosophy.md`](Philosophy.md): concrete, no hype, no stubs. Every claim -about code was verified in the tree on the date above. +about code was verified in the tree on the date above. Updated 2026-07-27: +the committed `pattern.ss` balance blocker named in K3-P0-01 was repaired, +the balance scanner was fixed for escaped identifiers, and `make audit` now +includes a tracked-source balance gate. **Audience:** the next model (or human) asked to implement security work in this repo. Each work item has an ID, the threat it closes, exact files to @@ -274,9 +277,10 @@ when" must be answerable from `dist/release-evidence/` in minutes. harness-only; nothing preserves found crashes as tests. 4. **`docs/ai-threat.md` comparative table is stale**: it rates supply chain "Behind (cargo audit)" but jpkg now has TUF/signatures/transparency - /OSV audit. Refresh the table (P2-06). -5. **Three security modules are undocumented** in `security-reference.md`: + /OSV audit. Fixed 2026-07-27. +5. **Three security modules were undocumented** in `security-reference.md`: `(std security cage)`, `(std security secret)`, `(std security env)`. + Fixed 2026-07-27. 6. **Cross-repo findings** recorded in `security-reference.md`'s appended checklist remain open in sibling repos (out of scope here, tracked in §8 P2-04 note): `jerboa-code` external-LLM launcher passes @@ -307,7 +311,9 @@ then industrialize find-first and recover-fast. ## 6. P0 — Containment foundation (do first) ### K3-P0-01 — Repair the build; add a balance gate -**Serves:** G4. **Effort:** hours. +**Serves:** G4. **Effort:** hours. **Status:** `pattern.ss` repair, escaped +identifier scanner fix, and audit gate landed 2026-07-27; full native release +build remains the normal pre-commit gate. A committed unbalanced file means the safety guarantees in the docs are not the code that ships. This violates the repo's own pre-commit rule. @@ -373,7 +379,9 @@ that already exist individually. API. ### K3-P0-03 — Prove the restricted-environment allowlist closed -**Serves:** G1, G4. **Effort:** 2–3 days. +**Serves:** G1, G4. **Effort:** 2–3 days. **Status:** mechanical closure +gate landed 2026-07-27; full reachability audit through Chez internals remains +open. The allowlist is Jerboa's crown jewel; it currently rests on manual curation. @@ -774,7 +782,7 @@ fix must add its scanner rule in the same commit (write it into `SECURITY.md`. - Move the appended "Security Patterns & Threat-Model Checklist" findings out of `security-reference.md` into a dated `docs/reviews/` file — - reference docs shouldn't carry stale review state. + reference docs shouldn't carry stale review state. Done 2026-07-27. ### K3-P2-07 — Secret-handling standard adoption **Serves:** G2. **Effort:** 1 week. @@ -871,7 +879,7 @@ Track these in `docs/status.md` per release: | Metric | Baseline (2026-07-27) | Target | |---|---|---| -| Build balance clean | **failing** (pattern.ss) | always clean | +| Build balance clean | `pattern.ss` repaired 2026-07-27; `source-balance` in `make audit` | always clean | | Confined worker exists | no | yes, tested (P0-02) | | Unclassified raw `read`/FASL sites | unknown (4+ named) | 0 | | FFI bindings audited | 0 / 91 files | 100% with verdicts | @@ -965,7 +973,10 @@ fake confidence happens. - The confined exec worker does not exist yet (P0-02); until it lands, every doc sentence saying "use an exec worker" describes an aspiration, not an API. -- `lib/std/compiler/pattern.ss` is unbalanced as of this writing (P0-01). +- The committed `pattern.ss` balance blocker named in P0-01 was repaired on + 2026-07-27. The `pipeline.ss` and `test-pipeline.ss` reports were traced to + escaped-identifier false positives in the balance scanner; the scanner now + skips `\x...;` identifier escapes. --- new file mode 100644 --- /dev/null +++ b/docs/reviews/2026-07-27-security-patterns-threat-model-checklist.md @@ -0,0 +1,116 @@ +# Security Patterns & Threat-Model Checklist + +Date: 2026-07-27. + +Scope: `/Users/user/mine/jerboa*` and `/Users/user/mine/chez*`. + +This is a dated review record. Stable API descriptions and the current +limitation ledger live in [security-reference.md](../security-reference.md). +Prioritized implementation work lives in +[kimi3-security-recommmendations.md](../kimi3-security-recommmendations.md). + +## Findings + +### Critical: sibling build scripts can mutate the main Jerboa checkout + +The cross-build scripts in `jerboa-lsp` and `jerboa-awk` patch +`$jerboa-home/lib` in place, delete compiled artifacts, then run +`git checkout --` against `.ss` and `.sls` files. If `$jerboa-home` points at +`/Users/user/mine/jerboa`, these scripts can silently discard uncommitted work +in the main repo. + +- `/Users/user/mine/jerboa-lsp/build-jlsp-cross.ss:51` +- `/Users/user/mine/jerboa-lsp/build-jlsp-freebsd-cross.ss:50` +- `/Users/user/mine/jerboa-awk/build-jawk-cross.ss:173` + +### Critical: `jerboa-code` disables LLM safeguards while allowing broad local access + +`src/jcode/tool/external-llm.ss` adds provider flags such as +`--dangerously-skip-permissions`, `--yolo`, and `--sandbox danger-full-access`. +The macOS sandbox profile is allow-by-default, then the command is executed +through the shell. That combination gives model-driven CLI processes broad +filesystem and network reach. + +- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:53` +- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:157` +- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:215` + +### High: main repo had a confirmed syntax/build blocker + +`lib/std/compiler/pattern.ss` reached EOF while still inside the `(library ...)` +form. The Jerboa health check also reported balance failures in +`lib/std/pipeline.ss`, `tests/test-pipeline.ss`, and +`tests/test-security2-parsers.ss`. + +- `/Users/user/mine/jerboa/lib/std/compiler/pattern.ss:316` + +Current status in this repo: `pattern.ss` was repaired on 2026-07-27 and +`make audit` now includes a tracked-source balance gate. +`test-security2-parsers.ss` was rechecked and was already balanced. +`pipeline.ss` and `test-pipeline.ss` verify cleanly with the compiler; their +original balance findings were false positives caused by escaped identifiers +such as `\x7C;\x3E;`, and the balance scanner was updated to handle that syntax. + +### High: `jerboa-inotify` exposes unchecked FFI buffer reads + +`jerboa_inotify_shim.c` casts `buf + offset` to `struct inotify_event *` +without a buffer length or alignment check. A caller can read past the end of +the buffer. The `read` wrapper also treats `EINTR` as a hard error instead of +retrying. + +- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:33` +- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:66` + +### High: `jerboa-wafter` has shell injection in the Wireshark converter + +`tools/wireshark-convert.ss` accepts `--out`, then builds `mkdir -p` with string +concatenation and passes it to `system`. A crafted output directory can inject +shell syntax. + +- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1095` +- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1118` + +### Medium: `pipeline-timeout` can leave work running after timeout + +`pipeline-timeout` forks a worker, waits, then raises on timeout without joining +or cancelling the worker. The worker can continue running and mutate captured +state after the timeout path returns. + +- `/Users/user/mine/jerboa/lib/std/pipeline.ss:170` + +The related test does not actually exercise a failing inner stage: + +- `/Users/user/mine/jerboa/tests/test-pipeline.ss:164` + +### Medium: `jerboa-emacs` appears to use Gerbil process APIs + +Several Jerboa source files call `open-process` and `process-status`. Unless +this repo injects compatibility wrappers at build time, those are not the +Jerboa/Chez process APIs described by the project instructions. + +- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:110` +- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:132` +- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:1108` + +### Medium: main repo has raw `read` / FASL deserialization paths + +The Jerboa security scan found many raw `read` and FASL deserialization paths. +These need source-by-source triage because untrusted `read` or FASL input is a +deserialization risk. + +Examples: + +- `/Users/user/mine/jerboa/jerbuild.ss:308` +- `/Users/user/mine/jerboa/lib/std/actor/distributed.ss:303` +- `/Users/user/mine/jerboa/lib/std/fasl.ss:27` +- `/Users/user/mine/jerboa/lib/std/net/grpc.ss:162` + +## Review Notes + +The review covered all matching `/Users/user/mine/jerboa*` and +`/Users/user/mine/chez*` directories with `rg`, manual source reads, git status +inventory, and Jerboa MCP health/security/stale checks on the main repo plus +key sibling repos. + +Full builds were not run because many worktrees are dirty and several build +scripts write/delete generated artifacts across checkouts. --- a/docs/security-reference.md +++ b/docs/security-reference.md @@ -38,6 +38,22 @@ Creates an evaluation environment containing only approved bindings. Uses Chez S - `restricted-eval-string` -- parse and evaluate a string (uses `jerboa-read`, not Chez `read`) - `safe-bindings` -- the list of allowed symbols +### Closure gate + +`make audit` runs `tools/check-restrict-closure.ss`. The gate: + +- constructs the restricted environment, which fails if any listed Chez binding + is missing in the pinned runtime; +- rejects duplicate bindings and dangerous exact/prefix classes such as + `system`, `eval`, `gensym`, `foreign-*`, `compile*`, and `open-*-file`; +- evaluates escape attempts for process execution, `eval`, FFI, file output, + `gensym`, and `call/cc` and requires all of them to fail; +- keeps an allowed arithmetic/list smoke test so the environment is not merely + broken closed. + +The current gate is a mechanical closure and escape test. It is not a formal +reachability proof through Chez internals. + ### What's allowed Core syntax (`lambda`, `if`, `begin`, `let`, `cond`, `case`, `when`, `unless`, `do`, `define-syntax`, `syntax-rules`, `quasiquote`), arithmetic (including bitwise ops), comparison, booleans, pairs/lists, strings, characters, vectors, bytevectors, symbols, string-port I/O, hashtables, error handling, `apply`, `values`, `dynamic-wind`, `sort`, `format`, `void`. @@ -519,6 +535,9 @@ These modules are implemented but not covered in depth above. | `(std security flow)` | Information flow control. Security levels form a lattice: public < internal < secret < top-secret. Data flows up freely; downward flow requires explicit `declassify` which is logged. | | `(std security metrics)` | Security counters, gauges, histograms with alerting thresholds. | | `(std security io-intercept)` | Effect-based I/O interception. All filesystem, network, and process operations mediated by handlers that can audit, deny, or mock. | +| `(std security cage)` | Irreversible process self-confinement. Applies the platform sandbox for the current process and descendants from a `cage-config`: root, read-only paths, read-write paths, executable paths, network allowance, system paths, and temp directory. | +| `(std security secret)` | Lifetime-scoped bytevector secrets. `make-secret`, `secret-use`, `secret-peek`, and `with-secret` ensure scoped values are consumed or wiped on exit, including exceptional exits. | +| `(std security env)` | Environment and secret broker for child processes. Builds allowlisted child environments, refuses argv values containing registered secrets, injects named secrets explicitly, and produces audit-safe redacted summaries. | --- @@ -542,83 +561,8 @@ These are known gaps documented as current limitations, not implementation promi - **No red team evaluation.** No independent adversarial testing has been performed. - **Secure memory is outside GC.** The `with-secure-region` API requires manual pointer arithmetic via `foreign-ref`/`foreign-set!`. There is no high-level typed interface. - --- -## Security Patterns & Threat-Model Checklist - - -Scope: `/Users/user/mine/jerboa*` and `/Users/user/mine/chez*`. - -## Findings - -### Critical: sibling build scripts can mutate the main Jerboa checkout - -The cross-build scripts in `jerboa-lsp` and `jerboa-awk` patch `$jerboa-home/lib` in place, delete compiled artifacts, then run `git checkout --` against `.ss` and `.sls` files. If `$jerboa-home` points at `/Users/user/mine/jerboa`, these scripts can silently discard uncommitted work in the main repo. - -- `/Users/user/mine/jerboa-lsp/build-jlsp-cross.ss:51` -- `/Users/user/mine/jerboa-lsp/build-jlsp-freebsd-cross.ss:50` -- `/Users/user/mine/jerboa-awk/build-jawk-cross.ss:173` - -### Critical: `jerboa-code` disables LLM safeguards while allowing broad local access - -`src/jcode/tool/external-llm.ss` adds provider flags such as `--dangerously-skip-permissions`, `--yolo`, and `--sandbox danger-full-access`. The macOS sandbox profile is allow-by-default, then the command is executed through the shell. That combination gives model-driven CLI processes broad filesystem and network reach. - -- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:53` -- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:157` -- `/Users/user/mine/jerboa-code/src/jcode/tool/external-llm.ss:215` - -### High: main repo has a confirmed syntax/build blocker - -`lib/std/compiler/pattern.ss` reaches EOF while still inside the `(library ...)` form. The Jerboa health check also reported balance failures in `lib/std/pipeline.ss`, `tests/test-pipeline.ss`, and `tests/test-security2-parsers.ss`. - -- `/Users/user/mine/jerboa/lib/std/compiler/pattern.ss:316` - -### High: `jerboa-inotify` exposes unchecked FFI buffer reads - -`jerboa_inotify_shim.c` casts `buf + offset` to `struct inotify_event *` without a buffer length or alignment check. A caller can read past the end of the buffer. The `read` wrapper also treats `EINTR` as a hard error instead of retrying. - -- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:33` -- `/Users/user/mine/jerboa-inotify/jerboa_inotify_shim.c:66` - -### High: `jerboa-wafter` has shell injection in the Wireshark converter - -`tools/wireshark-convert.ss` accepts `--out`, then builds `mkdir -p` with string concatenation and passes it to `system`. A crafted output directory can inject shell syntax. - -- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1095` -- `/Users/user/mine/jerboa-wafter/tools/wireshark-convert.ss:1118` - -### Medium: `pipeline-timeout` can leave work running after timeout - -`pipeline-timeout` forks a worker, waits, then raises on timeout without joining or cancelling the worker. The worker can continue running and mutate captured state after the timeout path returns. - -- `/Users/user/mine/jerboa/lib/std/pipeline.ss:170` - -The related test does not actually exercise a failing inner stage: - -- `/Users/user/mine/jerboa/tests/test-pipeline.ss:164` - -### Medium: `jerboa-emacs` appears to use Gerbil process APIs - -Several Jerboa source files call `open-process` and `process-status`. Unless this repo injects compatibility wrappers at build time, those are not the Jerboa/Chez process APIs described by the project instructions. - -- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:110` -- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:132` -- `/Users/user/mine/jerboa-emacs/src/jerboa-emacs/editor-core.ss:1108` - -### Medium: main repo has raw `read` / FASL deserialization paths - -The Jerboa security scan found many raw `read` and FASL deserialization paths. These need source-by-source triage because untrusted `read` or FASL input is a deserialization risk. - -Examples: - -- `/Users/user/mine/jerboa/jerbuild.ss:308` -- `/Users/user/mine/jerboa/lib/std/actor/distributed.ss:303` -- `/Users/user/mine/jerboa/lib/std/fasl.ss:27` -- `/Users/user/mine/jerboa/lib/std/net/grpc.ss:162` - -## Review Notes - -The review covered all matching `/Users/user/mine/jerboa*` and `/Users/user/mine/chez*` directories with `rg`, manual source reads, git status inventory, and Jerboa MCP health/security/stale checks on the main repo plus key sibling repos. - -Full builds were not run because many worktrees are dirty and several build scripts write/delete generated artifacts across checkouts. +Current dated security reviews are archived under [reviews/](reviews/). The +2026-07-27 review that previously lived in this reference is now +[reviews/2026-07-27-security-patterns-threat-model-checklist.md](reviews/2026-07-27-security-patterns-threat-model-checklist.md). --- a/jerboa-native-rs/Cargo.lock +++ b/jerboa-native-rs/Cargo.lock @@ -2679,9 +2679,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" [[package]] name = "spki" --- a/lib/std/compiler/pattern.ss +++ b/lib/std/compiler/pattern.ss @@ -313,4 +313,4 @@ (def (template-compile template) "Compile template into substitution function" (lambda (bindings) - (template-substitute* template bindings)))) \ No newline at end of file + (template-substitute* template bindings))))) \ No newline at end of file --- a/lib/std/security/restrict.ss +++ b/lib/std/security/restrict.ss @@ -61,8 +61,7 @@ string=? string<? string>? string<=? string>=? substring string->list list->string string-upcase string-downcase - string-copy number->string symbol->string - string->number string->symbol + string-copy ;; Characters char? char=? char<? char>? --- a/mcp/server.ss +++ b/mcp/server.ss @@ -4022,6 +4022,13 @@ (if (or (>= j len) (delimiter? (string-ref source j))) j (loop (+ j 1))))])) +(def (symbol-hex-escape-end source i) + (def source-size (string-length source)) + (let loop ([j (+ i 2)]) + (cond + [(>= j source-size) source-size] + [(char=? (string-ref source j) #\;) (+ j 1)] + [else (loop (+ j 1))]))) (def (balance-scan source) (def stack '()) @@ -4047,6 +4054,13 @@ [(char=? ch #\\) (loop (+ i 1) #t #t #f)] [(char=? ch #\") (loop (+ i 1) #f #f #f)] [else (loop (+ i 1) #t #f #f)])] + [(and (char=? ch #\\) + (< (+ i 1) len) + (or (char=? (string-ref source (+ i 1)) #\x) + (char=? (string-ref source (+ i 1)) #\X))) + (let ([j (symbol-hex-escape-end source i)]) + (set! col (+ col (- j i))) + (loop j #f #f #f))] [(char=? ch #\;) (set! col (+ col 1)) (loop (+ i 1) #f #f #t)] @@ -4133,6 +4147,14 @@ [(char=? ch #\\) (loop (+ i 1) #t #t #f)] [(char=? ch #\") (loop (+ i 1) #f #f #f)] [else (loop (+ i 1) #t #f #f)])] + [(and (char=? ch #\\) + (< (+ i 1) len) + (or (char=? (string-ref source (+ i 1)) #\x) + (char=? (string-ref source (+ i 1)) #\X))) + (let ([j (symbol-hex-escape-end source i)]) + (display (substring source i j) out) + (set! col (+ col (- j i))) + (loop j #f #f #f))] [(char=? ch #\;) (set! col (+ col 1)) (write-char ch out) new file mode 100644 --- /dev/null +++ b/support/check-source-balance.ss @@ -0,0 +1,164 @@ +#!chezscheme +;;; Check tracked Jerboa/Chez source files for delimiter balance. +;;; This mirrors the MCP balance checker used by AGENTS.md after .ss edits. + +(import (scheme)) + +(define failures 0) +(define checked 0) + +(define (read-file-string path) + (call-with-input-file path + (lambda (port) (get-string-all port)))) + +(define (matching-opener closer) + (cond + [(char=? closer #\)) #\(] + [(char=? closer #\]) #\[] + [(char=? closer #\}) #\{] + [else closer])) + +(define (delimiter? ch) + (or (char-whitespace? ch) + (char=? ch #\() + (char=? ch #\)) + (char=? ch #\[) + (char=? ch #\]) + (char=? ch #\{) + (char=? ch #\}) + (char=? ch #\") + (char=? ch #\;))) + +(define (char-literal-end source i) + (let* ([source-size (string-length source)] + [start (+ i 2)]) + (cond + [(>= start source-size) source-size] + [(delimiter? (string-ref source start)) (+ start 1)] + [else + (let loop ([j start]) + (if (or (>= j source-size) (delimiter? (string-ref source j))) + j + (loop (+ j 1))))]))) +(define (symbol-hex-escape-end source i) + (let ([source-size (string-length source)]) + (let loop ([j (+ i 2)]) + (cond + [(>= j source-size) source-size] + [(char=? (string-ref source j) #\;) (+ j 1)] + [else (loop (+ j 1))])))) + +(define (balance-scan source) + (let ([stack '()] + [errors '()] + [line 1] + [col 1] + [source-size (string-length source)]) + (let loop ([i 0] [in-string #f] [escaped #f] [comment #f]) + (when (< i source-size) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\newline) + (set! line (+ line 1)) + (set! col 1) + (loop (+ i 1) in-string #f #f)] + [comment + (set! col (+ col 1)) + (loop (+ i 1) in-string #f comment)] + [in-string + (set! col (+ col 1)) + (cond + [escaped (loop (+ i 1) #t #f #f)] + [(char=? ch #\\) (loop (+ i 1) #t #t #f)] + [(char=? ch #\") (loop (+ i 1) #f #f #f)] + [else (loop (+ i 1) #t #f #f)])] + [(and (char=? ch #\\) + (< (+ i 1) source-size) + (or (char=? (string-ref source (+ i 1)) #\x) + (char=? (string-ref source (+ i 1)) #\X))) + (let ([j (symbol-hex-escape-end source i)]) + (set! col (+ col (- j i))) + (loop j #f #f #f))] + [(char=? ch #\;) + (set! col (+ col 1)) + (loop (+ i 1) #f #f #t)] + [(char=? ch #\") + (set! col (+ col 1)) + (loop (+ i 1) #t #f #f)] + [(and (char=? ch #\#) + (< (+ i 1) source-size) + (char=? (string-ref source (+ i 1)) #\\)) + (let ([j (char-literal-end source i)]) + (set! col (+ col (- j i))) + (loop j #f #f #f))] + [(or (char=? ch #\() (char=? ch #\[) (char=? ch #\{)) + (set! stack (cons (list ch line col) stack)) + (set! col (+ col 1)) + (loop (+ i 1) #f #f #f)] + [(or (char=? ch #\)) (char=? ch #\]) (char=? ch #\})) + (if (or (null? stack) + (not (char=? (caar stack) (matching-opener ch)))) + (set! errors + (cons (string-append "unexpected " + (string ch) + " at " + (number->string line) + ":" + (number->string col)) + errors)) + (set! stack (cdr stack))) + (set! col (+ col 1)) + (loop (+ i 1) #f #f #f)] + [else + (set! col (+ col 1)) + (loop (+ i 1) #f #f #f)])))) + (for-each + (lambda (entry) + (set! errors + (cons (string-append "unclosed " + (string (car entry)) + " at " + (number->string (cadr entry)) + ":" + (number->string (caddr entry))) + errors))) + stack) + (reverse errors))) + +(define (check-file path) + (guard (e [else + (set! failures (+ failures 1)) + (fprintf (current-error-port) + "source-balance: ~a: read error: ~a\n" + path + (condition-message e))]) + (let ([errors (balance-scan (read-file-string path))]) + (if (null? errors) + (set! checked (+ checked 1)) + (begin + (set! failures (+ failures 1)) + (for-each + (lambda (err) + (fprintf (current-error-port) + "source-balance: ~a: ~a\n" + path + err)) + errors)))))) + +(define (main args) + (when (null? args) + (fprintf (current-error-port) + "source-balance: no files supplied\n") + (exit 2)) + (for-each check-file args) + (if (zero? failures) + (begin + (printf "source-balance: checked ~a file(s)\n" checked) + (exit 0)) + (begin + (fprintf (current-error-port) + "source-balance: ~a file(s) failed\n" + failures) + (exit 1)))) + +(main (command-line-arguments)) new file mode 100644 --- /dev/null +++ b/tools/check-restrict-closure.ss @@ -0,0 +1,121 @@ +#!/usr/bin/env -S scheme --libdirs lib --script +;;; check-restrict-closure.ss -- verify restricted eval allowlist closure. + +(import (jerboa prelude) + (std security restrict)) + +(define errors '()) +(define warnings '()) + +(define forbidden-exact + '(system process load eval getenv putenv fork-thread import library + call/cc call-with-current-continuation gensym read-eval + open-input-file open-output-file call-with-input-file call-with-output-file + delete-file rename-file file-exists? directory-list current-directory + compile compile-file compile-program foreign-procedure)) + +(define escape-attempts + '(system-call "(system \"id\")" + eval-call "(eval '(+ 1 2))" + ffi-call "((foreign-procedure \"exit\" (int) void) 0)" + gensym-call "(gensym)" + file-output "(open-output-file \"/tmp/jerboa-restrict-escape\")" + callcc-call "(call/cc (lambda (k) (k 1)))")) + +(define (add-error msg) (set! errors (cons msg errors))) +(define (add-warning msg) (set! warnings (cons msg warnings))) + +(define (string-prefix? prefix s) + (let ([n (string-length prefix)] + [m (string-length s)]) + (and (>= m n) (string=? prefix (substring s 0 n))))) + +(define (string-suffix? suffix s) + (let ([n (string-length suffix)] + [m (string-length s)]) + (and (>= m n) (string=? suffix (substring s (- m n) m))))) + +(define (symbol-prefix? prefix sym) + (string-prefix? prefix (symbol->string sym))) + +(define (symbol-open-file? sym) + (let ([s (symbol->string sym)]) + (and (string-prefix? "open-" s) (string-suffix? "-file" s)))) + +(define (forbidden-symbol? sym) + (or (memq sym forbidden-exact) + (symbol-prefix? "foreign-" sym) + (symbol-prefix? "compile" sym) + (symbol-open-file? sym))) + +(define (duplicates xs) + (let loop ([rest xs] [seen '()] [dups '()]) + (cond + [(null? rest) (reverse dups)] + [(memq (car rest) seen) (loop (cdr rest) seen (cons (car rest) dups))] + [else (loop (cdr rest) (cons (car rest) seen) dups)]))) + +(define (attempt-fails? env source) + (guard (exn [else #t]) + (restricted-eval-string source env) + #f)) + +(define (check-safe-bindings) + (let ([dups (duplicates safe-bindings)]) + (unless (null? dups) + (add-error (format "safe-bindings contains duplicates: ~s" dups)))) + (for-each + (lambda (sym) + (when (forbidden-symbol? sym) + (add-error (format "forbidden binding in safe-bindings: ~s" sym)))) + safe-bindings)) + +(define (check-environment-constructs) + (guard (exn [else + (add-error + (format "make-restricted-environment failed: ~a" + (with-output-to-string + (lambda () (display-condition exn))))) + #f]) + (make-restricted-environment))) + +(define (check-escape-attempts env) + (let loop ([rest escape-attempts]) + (unless (null? rest) + (let ([label (car rest)] + [source (cadr rest)]) + (unless (attempt-fails? env source) + (add-error (format "escape attempt unexpectedly succeeded: ~a" label))) + (loop (cddr rest)))))) + +(define (check-allowed-smoke env) + (guard (exn [else + (add-error + (format "allowed arithmetic/list smoke failed: ~a" + (with-output-to-string + (lambda () (display-condition exn)))))]) + (let ([result (restricted-eval-string "(+ 1 (length '(a b c)))" env)]) + (unless (= result 4) + (add-error (format "allowed smoke returned ~s, expected 4" result)))))) + +(define (main) + (check-safe-bindings) + (let ([env (check-environment-constructs)]) + (when env + (check-allowed-smoke env) + (check-escape-attempts env))) + (display "restrict-closure:\n") + (display " safe-bindings: ") (write (length safe-bindings)) (newline) + (display " forbidden-patterns: exact, foreign-*, compile*, open-*-file\n") + (display " escape-attempts: ") (write (/ (length escape-attempts) 2)) (newline) + (for-each + (lambda (msg) (display "warning: ") (display msg) (newline)) + (reverse warnings)) + (for-each + (lambda (msg) (display "error: ") (display msg) (newline)) + (reverse errors)) + (if (null? errors) + (begin (display "restrict-closure: PASS\n") (exit 0)) + (begin (display "restrict-closure: FAIL\n") (exit 1)))) + +(main) --- a/tools/security-audit.sh +++ b/tools/security-audit.sh @@ -49,8 +49,8 @@ done require_dir vendor/ChezScheme require_dir jerboa-native-rs -if ! grep -q 'Apache License' LICENSE; then - fail "LICENSE does not appear to contain Apache License text" +if ! grep -q 'MIT License' LICENSE; then + fail "LICENSE does not appear to contain MIT License text" fi if ! grep -q 'Chez Scheme' LICENSE-CHEZ; then fail "LICENSE-CHEZ does not identify Chez Scheme"