security: gate vendored jsqlite advisory review
Jaime Fournier
0168b463396753c6b3143d2ac20c4f47ce614635
--- a/.jerboa-system +++ b/.jerboa-system @@ -49,7 +49,7 @@ "*.sls" "*.so" "*.wpo" "*.boot"))) (dependencies - (vendored ("vendor/ChezScheme")) + (vendored ("vendor/ChezScheme" "vendor/jsqlite")) (native ("jerboa-native-rs" "browser-repl")) (external-tools ("cc" "cargo" "make" "tar" "ln" "rm" "mkdir" "find" "shasum|sha256sum"))) @@ -106,6 +106,7 @@ (signing planned) (sbom planned) (tcb ("Chez Scheme" "Jerboa stdlib" "jerboa-native-rs" "browser-repl" + "vendor/jsqlite" "support/multicall-main.c" "support/jerboa-embed.c" "support/jerboa-embed.h" "support/hardening-report.h" --- a/Makefile +++ b/Makefile @@ -76,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 jpkg-audit tcb-drift-check import-policy-check 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: 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 jpkg-audit vendor-jsqlite-security-check tcb-drift-check import-policy-check 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 \ @@ -137,6 +137,7 @@ help: @echo " clean-native Remove Rust build artifacts" @echo " audit Run production security/release gate checks" @echo " audit-native Run cargo audit on Rust native library" + @echo " vendor-jsqlite-security-check Verify vendored jsqlite TCB review freshness" @echo " pure-audit Scan jerboa-* repos for non-pure runtime surfaces" @echo " sbom Write local SBOM/toolchain evidence under dist/sbom" @echo " reproducibility-report Compare two local multicall builds" @@ -1742,6 +1743,9 @@ audit-native: jpkg-audit: build @$(SCHEME) --libdirs $(LIBDIRS) --script tools/jpkg-main.ss audit +vendor-jsqlite-security-check: + @support/check-vendor-jsqlite-security.sh + tcb-drift-check: @set -eu; \ if git diff --quiet -- .jerboa-system support/container-dependencies.lock \ @@ -1775,7 +1779,7 @@ unsafe-deserialize-check: import-policy-check: build @$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-import-policy.ss examples -audit: source-balance restrict-closure-check unsafe-deserialize-check import-policy-check security-audit audit-native jpkg-audit tcb-drift-check data-check security-production unification-check +audit: source-balance restrict-closure-check unsafe-deserialize-check import-policy-check security-audit audit-native jpkg-audit vendor-jsqlite-security-check tcb-drift-check data-check security-production unification-check test-gaps: $(SCHEME) --libdirs $(LIBDIRS) --script tests/test-gaps.ss --- a/docs/ffi-audit.md +++ b/docs/ffi-audit.md @@ -23,7 +23,7 @@ Latest summary from `tools/ffi-audit-report.ss`: (ffi-audit-report (schema "jerboa.ffi-audit/1") (scheme - (file-count 1202) + (file-count 1204) (site-count 904) (foreign-procedure 810) (load-shared-object 80) @@ -46,7 +46,7 @@ Latest summary from `tools/ffi-audit-report.ss`: (unsafe-comment-review-sites 433) (export-review-sites 231)) (vendor - (jsqlite (path "vendor/jsqlite") (status accepted-risk-pending-cve-gate))) + (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated))) (verdict needs-per-binding-review)) ``` @@ -79,4 +79,7 @@ Remaining work before closing K3-P1-01: - Reconcile the 231 Rust `#[no_mangle]` exports with live Scheme callers and unexport or document unused exports. - Keep `vendor/jsqlite` in the TCB until consumers move to the Rust sqlite - path or release gates include a dedicated CVE freshness check. + path. The accepted-risk decision is now recorded in + `docs/vendor-jsqlite-tcb.md`, and `make audit` runs + `vendor-jsqlite-security-check` to fail stale advisory reviews or drift + between the decision record and `vendor/jsqlite/README.md`. --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -304,7 +304,7 @@ when" must be answerable from `dist/release-evidence/` in minutes. | Goal | Where we stand | The gap | |---|---|---| -| G1 shrink target | Managed core is memory-safe; parsers budgeted; safe prelude exists; FFI audit inventory, per-site provisional verdicts, and FFI hazard scanner rules exist | FFI remediation remains: unsafe invariant comments, native export shrinking/justification, and the `vendor/jsqlite` decision | +| G1 shrink target | Managed core is memory-safe; parsers budgeted; safe prelude exists; FFI audit inventory, per-site provisional verdicts, FFI hazard scanner rules, and a gated `vendor/jsqlite` accepted-risk decision exist | FFI remediation remains: unsafe invariant comments and native export shrinking/justification | | G2 cap blast radius | Capabilities, taint, kernel sandbox, egress policy objects, worker facade, memory rlimit pre-exec path, egress proxy env wiring, authenticated actor transport/envelopes, Ring HTTP source taint, and WebSocket/fiber WebSocket source taint all exist | **No native pre-exec worker backend yet for kernel sandbox controls before child input** | | G3 find it first | 13 harnesses, scanner w/ rule DB, lint | No corpora, no crash regression, no scheduled fuzzing, no standing AI-red-team, no exploit-shaped regression suite | | G4 fail closed | Raw-fork launchers retired correctly; `allow-degraded?` explicit | New controls must keep the invariant; degraded-mode warnings must be test-locked | @@ -608,8 +608,11 @@ not started." integer width ambiguity, and pointer arithmetic bounds review. `jerboa-native-rs` now denies `unsafe_op_in_unsafe_fn`, so unsafe function bodies do not implicitly permit - unsafe operations. Remaining work: unsafe invariant comments, export - shrinking/justification, and the `vendor/jsqlite` CVE/replacement decision. + unsafe operations. `vendor/jsqlite` remains in the TCB with an accepted-risk + decision in `docs/vendor-jsqlite-tcb.md`; `make audit` now runs + `vendor-jsqlite-security-check`, which fails stale advisory reviews or drift + between the decision record and the vendored README compatibility target. + Remaining work: unsafe invariant comments and export shrinking/justification. ### K3-P1-02 — TOCTOU-safe filesystem capability checks **Serves:** G2. **Effort:** 1 week. @@ -1190,9 +1193,9 @@ fake confidence happens. - Secure memory has a checked `secure-bytevector` API; raw secure-region pointers remain for explicit FFI integration. - FFI audit phase 5 has a reproducible inventory, per-site provisional - verdicts, and committed hazard scanner rules; unsafe invariant comments, - export shrinking/justification, and the `vendor/jsqlite` TCB decision remain - open. + verdicts, committed hazard scanner rules, and a `vendor/jsqlite` accepted-risk + decision gated by `make audit`; unsafe invariant comments and export + shrinking/justification remain open. - No independent red-team evaluation yet (P2-04 starts the practice). - The confined exec worker facade exists as `(std security worker)`, with audit-log lifecycle events and native pre-exec memory rlimits on supported --- a/docs/release-security.md +++ b/docs/release-security.md @@ -38,8 +38,9 @@ make verify `make audit` is the pull-request gate. It checks required release/security files, Apache/NOTICE preservation for vendored Chez, high-confidence secret patterns, generated-artifact ignore policy, RustSec advisories with warnings -denied, `data/*.sexp` schema validity, the security profile tests, and the -non-artifact unification checks from `.jerboa-system`. +denied, vendored `jsqlite` advisory-review freshness, `data/*.sexp` schema +validity, the security profile tests, and the non-artifact unification checks +from `.jerboa-system`. For reproducible-build investigations, `jerbuild binary` honors `JERBUILD_BINARY_OBJ_DIR` as an opt-in fixed object directory. The path must be @@ -157,6 +158,10 @@ crates, C shims, external tools, and current FFI-site count. The audit gate also runs `tcb-drift-check`, which requires a `data/changelog.sexp` note when TCB-affecting manifests, C shims, native lockfiles, container locks, or vendored C inputs change. +The same audit gate runs `vendor-jsqlite-security-check`, which requires +`docs/vendor-jsqlite-tcb.md` to record a fresh accepted-risk review for the +vendored pure-Jerboa SQLite-compatible engine and to match the pinned SQLite +compatibility target in `vendor/jsqlite/README.md`. It also runs `support/check-release-path-leaks.sh` against `dist/jerboa` and requires `path_leak_status=pass` in `path-leaks.txt`. Release packaging runs the same checker against the final `.tar.gz`, so shipped artifacts cannot new file mode 100644 --- /dev/null +++ b/docs/vendor-jsqlite-tcb.md @@ -0,0 +1,39 @@ +# Vendored jsqlite TCB Decision + +Last advisory review: 2026-07-27 +Review max age: 90 days +SQLite compatibility target: 3.54.0 +Decision: accepted-risk +Runtime dependency: none on libsqlite3 + +`vendor/jsqlite` remains in the Jerboa TCB for now. It is a pure Jerboa +SQLite-compatible engine, not a dynamically linked copy of upstream +`libsqlite3`, so `cargo audit` and system SQLite package scanners do not cover +it. The release gate therefore treats it as an internal database engine with a +SQLite compatibility target and requires a fresh manual advisory review. + +The review must check at least: + +- SQLite's official CVE page: <https://www.sqlite.org/cves.html> +- NVD/OpenCVE-style entries for SQLite CVEs affecting versions at or below the + compatibility target. +- Whether each advisory depends on upstream C implementation memory safety, + optional extensions absent from jsqlite, SQL semantics that jsqlite implements, + or file-format behavior that jsqlite parses or writes. + +Current review notes: + +- `vendor/jsqlite/README.md` pins compatibility to SQLite 3.54.0. +- CVE-2025-6965 affects SQLite versions before 3.50.2 according to NVD, so the + pinned compatibility target is beyond the fixed upstream version. The jsqlite + review still needs semantic applicability checks because jsqlite reimplements + query planning/execution rather than linking upstream C. +- CVE-2025-70873 and similar zipfile/extension reports must be checked for + extension applicability; jsqlite does not currently expose SQLite's zipfile + extension as a runtime dependency. + +Release rule: + +`make audit` runs `support/check-vendor-jsqlite-security.sh`. The check fails +when this decision record is missing, the README compatibility target drifts, +or the advisory review is older than `Review max age`. new file mode 100755 --- /dev/null +++ b/support/check-vendor-jsqlite-security.sh @@ -0,0 +1,85 @@ +#!/bin/sh +set -eu + +doc="${1:-docs/vendor-jsqlite-tcb.md}" +readme="${2:-vendor/jsqlite/README.md}" + +python3 - "$doc" "$readme" <<'PY' +import datetime as dt +import re +import sys +from pathlib import Path + +doc_path = Path(sys.argv[1]) +readme_path = Path(sys.argv[2]) + +def fail(msg): + print(f"vendor-jsqlite-security: {msg}", file=sys.stderr) + sys.exit(1) + +def field(text, name): + match = re.search(rf"^{re.escape(name)}:\s*(.+)$", text, re.MULTILINE) + if not match: + fail(f"missing {name!r} field in {doc_path}") + return match.group(1).strip() + +if not doc_path.exists(): + fail(f"missing decision record {doc_path}") +if not readme_path.exists(): + fail(f"missing vendored README {readme_path}") + +doc = doc_path.read_text(encoding="utf-8") +readme = readme_path.read_text(encoding="utf-8") + +target = field(doc, "SQLite compatibility target") +decision = field(doc, "Decision") +runtime_dependency = field(doc, "Runtime dependency") +review_date_text = field(doc, "Last advisory review") +max_age_text = field(doc, "Review max age") + +if decision != "accepted-risk": + fail(f"unsupported decision {decision!r}; expected accepted-risk") +if runtime_dependency != "none on libsqlite3": + fail("runtime dependency field must state that libsqlite3 is not linked") + +readme_target = re.search(r"Pinned compatibility target:\s*\*\*SQLite\s+([0-9]+(?:\.[0-9]+)+)\*\*", readme) +if not readme_target: + fail("could not find pinned SQLite compatibility target in vendor/jsqlite/README.md") +if readme_target.group(1) != target: + fail(f"decision target {target} does not match README target {readme_target.group(1)}") + +for needle in ( + "https://www.sqlite.org/cves.html", + "CVE-2025-6965", + "CVE-2025-70873", +): + if needle not in doc: + fail(f"decision record must mention {needle}") + +try: + review_date = dt.date.fromisoformat(review_date_text) +except ValueError: + fail(f"invalid Last advisory review date {review_date_text!r}; expected YYYY-MM-DD") + +max_age_match = re.fullmatch(r"([0-9]+)\s+days", max_age_text) +if not max_age_match: + fail(f"invalid Review max age {max_age_text!r}; expected '<n> days'") +max_age_days = int(max_age_match.group(1)) +if max_age_days <= 0: + fail("Review max age must be positive") + +today = dt.date.today() +if review_date > today: + fail(f"Last advisory review {review_date} is in the future relative to {today}") +age_days = (today - review_date).days +if age_days > max_age_days: + fail( + f"Last advisory review is {age_days} days old; " + f"refresh docs/vendor-jsqlite-tcb.md before release" + ) + +print( + "vendor-jsqlite-security: OK " + f"(target SQLite {target}, review age {age_days} days, max {max_age_days})" +) +PY --- a/tools/ffi-audit-report.ss +++ b/tools/ffi-audit-report.ss @@ -329,7 +329,7 @@ (eq? (rust-site-verdict s) 'needs-export-review)) rust))) (vendor - (jsqlite (path "vendor/jsqlite") (status accepted-risk-pending-cve-gate))) + (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated))) (verdict needs-per-binding-review) ,@(if full? `((scheme-sites ,scheme*) (rust-sites ,rust*))