security: harden seatbelt error buffer handling
ober
b00af3eeb96e306a7ce5af4d7d7c437c21fc3d28
--- a/docs/ffi-audit.md +++ b/docs/ffi-audit.md @@ -166,7 +166,10 @@ slice helpers, rejects oversized SQLite `int` lengths, centralizes text/blob/name/error output-buffer writes, converts poisoned database/statement-store locks into normal FFI errors, wraps integer and double column getters in panic containment, and has regressions for null/nonzero -inputs and output-buffer validation. `secure_fs.rs` now documents +inputs and output-buffer validation. The Scheme `(std security seatbelt)` +binding now resolves `sandbox_init`/`sandbox_free_error` only after the sandbox +library is loaded and copies/frees the `sandbox_init` error buffer before +raising a Scheme condition. `secure_fs.rs` now documents descriptor-relative `openat`/`mkdirat`, fd duplication/ownership transfer, stat buffers, directory stream lifecycle, caller output slots, atomic output publication, and test-owned fd conversion --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -709,6 +709,10 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit SAN/path/PEM inputs through checked UTF-8 helpers, centralizes PEM and fingerprint output writes, and documents atomic file-publication syscalls adjacent to the audited unsafe expressions. + The Scheme `(std security seatbelt)` binding now resolves + `sandbox_init`/`sandbox_free_error` only after loading the sandbox library and + copies/frees the `sandbox_init` error buffer before raising a Scheme + condition, with an invalid-profile regression in `tests/test-seatbelt.ss`. `wasm_sm.rs` now compiles under `unsafe_op_in_unsafe_fn` for the SpiderMonkey feature, bounds C ABI buffers, validates host-memory offsets, converts poisoned handle-store locks into normal FFI errors, and documents --- a/docs/status.md +++ b/docs/status.md @@ -25,7 +25,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`, | Area | Current state | Remaining work | |---|---|---| | Kimi security handoff | [kimi3-security-recommmendations.md](kimi3-security-recommmendations.md) is the backlog. Dated evidence and review manifests live under [reviews/](reviews/). | Keep new security evidence in dated review records and summarize the current release state here. | -| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs`/`seccomp.rs`, crypto FFI buffer path in `crypto.rs`, secure-memory region lifecycle in `secure_mem.rs`, secure filesystem capability boundary in `secure_fs.rs`, TLS native ABI in `tls.rs`, Ed25519/X25519 key-agreement buffers, compression buffers, HTTP parse/writev boundary, embed-crypto ABI, integrity ABI, regex-native ABI, process-control ABI, pcap ABI, inotify ABI, epoll/eventfd ABI, antidebug ABI, SOCKS5 server ABI, PostgreSQL native ABI, Landlock native ABI, wasmi sandbox ABI, SpiderMonkey WASM ABI, DuckDB native ABI, X.509 native ABI, and SQLite native ABI now have nearby `SAFETY:` comments or equivalent checked FFI invariants. Generated counts are at 0 unannotated native unsafe sites and 0 Scheme blocking bindings missing `__collect_safe`. | Finish per-binding Scheme FFI review and targeted scanner rules for the remaining provisional verdicts. | +| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs`/`seccomp.rs`, crypto FFI buffer path in `crypto.rs`, secure-memory region lifecycle in `secure_mem.rs`, secure filesystem capability boundary in `secure_fs.rs`, TLS native ABI in `tls.rs`, Ed25519/X25519 key-agreement buffers, compression buffers, HTTP parse/writev boundary, embed-crypto ABI, integrity ABI, regex-native ABI, process-control ABI, pcap ABI, inotify ABI, epoll/eventfd ABI, antidebug ABI, SOCKS5 server ABI, PostgreSQL native ABI, Landlock native ABI, wasmi sandbox ABI, SpiderMonkey WASM ABI, DuckDB native ABI, X.509 native ABI, SQLite native ABI, and the Scheme Seatbelt `sandbox_init` error-buffer path now have nearby `SAFETY:` comments or equivalent checked FFI invariants. Generated counts are at 0 unannotated native unsafe sites and 0 Scheme blocking bindings missing `__collect_safe`. | Finish per-binding Scheme FFI review and targeted scanner rules for the remaining provisional verdicts. | | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. | | Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup, Linux Landlock filesystem/TCP-connect setup for requested axes, macOS Seatbelt deny-default path/exec/no-network setup for supported axes, standard worker-eval Capsicum entry on FreeBSD, explicit sandbox-axis refusal, and egress proxy env wiring. | Keep Linux/macOS/FreeBSD parity tests current; finish arbitrary-command/proxy-aware Capsicum worker paths. | | Fuzzing | `tests/fuzz/corpus/` has 15 checked-in seed inputs, `tests/fuzz/regression/` has 11 crash/rejection regressions, `make fuzz-smoke` runs the deterministic regression gate first, and GitHub CI runs smoke fuzzing normally plus deep fuzzing on scheduled daily runs. | Keep adding minimized corpus and regression inputs for every parser/security bug found. | --- a/lib/std/security/seatbelt.ss +++ b/lib/std/security/seatbelt.ss @@ -81,14 +81,14 @@ ;; sandbox_init(const char *profile, uint64_t flags, char **errorbuf) -> int ;; Returns 0 on success, -1 on failure (errorbuf set). (def c-sandbox-init - (if (macos?) + (if (and (macos?) _libsandbox) (try (foreign-procedure "sandbox_init" (string unsigned-64 void*) int) (catch (e) (lambda args -1))) (lambda args -1))) ;; sandbox_free_error(char *errorbuf) -> void (def c-sandbox-free-error - (if (macos?) + (if (and (macos?) _libsandbox) (try (foreign-procedure "sandbox_free_error" (void*) void) (catch (e) (lambda (p) (void)))) (lambda (p) (void)))) @@ -145,6 +145,21 @@ "unknown named profile; expected pure-computation, no-write, no-write-except-temporary, no-internet, or no-network" sym)])) + (def (seatbelt-sandbox-error-message errmsg) + ;; sandbox_init allocates errmsg on failure; copy it and always release it. + (cond + [(= errmsg 0) "sandbox_init failed (unknown error)"] + [else + (let ([raw #f]) + (dynamic-wind + (lambda () (void)) + (lambda () + (set! raw + (try (foreign-ref 'string errmsg 0) + (catch (e) "<unreadable sandbox error>")))) + (lambda () + (c-sandbox-free-error errmsg))) + (format "sandbox_init failed: ~a" raw))])) ;; ========== Installation ========== (def (seatbelt-install! profile-sym) @@ -173,13 +188,9 @@ (lambda () (let ([rc (c-sandbox-init sbpl-string 0 errptr)]) (when (< rc 0) - (let ([errmsg (foreign-ref 'void* errptr 0)]) - (let ([msg (if (= errmsg 0) - "sandbox_init failed (unknown error)" - (let ([s (foreign-ref 'string errmsg 0)]) - (c-sandbox-free-error errmsg) - (format "sandbox_init failed: ~a" s)))]) - (error 'seatbelt-install-profile! msg)))))) + (error 'seatbelt-install-profile! + (seatbelt-sandbox-error-message + (foreign-ref 'void* errptr 0)))))) (lambda () (foreign-free errptr))))) --- a/tests/test-seatbelt.ss +++ b/tests/test-seatbelt.ss @@ -182,6 +182,16 @@ #f) #t)) +(when (seatbelt-available?) + (test "seatbelt-install-profile! reports invalid SBPL" + (guard (exn [#t + (and (message-condition? exn) + (contains? (condition-message exn) + "sandbox_init failed"))]) + (seatbelt-install-profile! "(version 1)(bogus-seatbelt-form)") + #f) + #t)) + ;; ========== Summary ========== (printf "~%Seatbelt tests: ~a passed, ~a failed~%" pass fail)