security: close blocking ffi collect-safe audit
ober
6240d9da1217a96e51d322f86257504445e41525
--- a/docs/ffi-audit.md +++ b/docs/ffi-audit.md @@ -23,19 +23,19 @@ Latest summary from `tools/ffi-audit-report.ss`: (ffi-audit-report (schema "jerboa.ffi-audit/1") (scheme - (file-count 1205) - (site-count 904) - (foreign-procedure 810) - (load-shared-object 80) - (define-ftype 7) + (file-count 1190) + (site-count 782) + (foreign-procedure 716) + (load-shared-object 55) + (define-ftype 4) (foreign-callable 7) - (pointer-sites 333) - (width-sensitive-sites 180) - (blocking-candidates 128) - (blocking-without-collect-safe 66) - (hazard-site-count 416) - (blocking-review-sites 66) - (safety-review-sites 350)) + (pointer-sites 299) + (width-sensitive-sites 156) + (blocking-candidates 46) + (blocking-without-collect-safe 0) + (hazard-site-count 352) + (blocking-review-sites 0) + (safety-review-sites 352)) (native (rust-file-count 29) (c-file-count 5) @@ -69,6 +69,12 @@ integer-width review, and lower-risk API review. Rust verdicts separate unsafe sites missing nearby `SAFETY:` comments from C ABI exports that still need caller reconciliation. +The Scheme source inventory now ignores generated `.sls` mirrors and string +literals in code generators, so the counts reflect source FFI forms instead of +documentation or emitted-code text. All 46 blocking candidate bindings are now +either `__collect_safe` or classified as nonblocking by the scanner; the +generated report has 0 `blocking-without-collect-safe` sites. + The native Rust crate now denies `unsafe_op_in_unsafe_fn`, so an unsafe function body no longer grants implicit permission for unsafe operations. Each unsafe operation still has to live inside an explicit `unsafe { ... }` block. @@ -165,15 +171,14 @@ funnels hostname, path, PEM, pin, and read/write buffers through checked C ABI helpers, rejects null/nonzero and oversized buffers before slicing, rejects negative accepted fds before adopting them as `TcpStream`, and documents socket shutdown invariants used to break blocked TLS I/O. The generated inventory now -reports 369 annotated native unsafe sites and 0 remaining unsafe review sites. +reports 369 annotated native unsafe sites, 0 remaining unsafe review sites, and +0 Scheme blocking calls missing `__collect_safe`. Remaining work before closing K3-P1-01: - Review every `foreign-procedure`, `foreign-callable`, and `define-ftype` binding using the per-site `hazards` / `verdict` fields for null returns, bounds derivation, ownership, and GC safety. -- Convert blocking candidates without `__collect_safe` or justify them as - nonblocking. - Add targeted scanner rules for null-return checks, integer width confusion, caller-supplied length trust, and bytevector pointer lifetime hazards. - Keep `vendor/jsqlite` in the TCB until consumers move to the Rust sqlite --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -609,7 +609,8 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit now provide a reproducible inventory for Scheme FFI sites, Rust C ABI exports, pointer/width-sensitive bindings, blocking candidates, and unsafe Rust sites. The reporter now skips comment-only mentions, emits per-site - `hazards` and provisional `verdict` fields in `--full` output, and summarizes + `hazards` and provisional `verdict` fields in `--full` output, skips generated + `.sls` mirrors and string literals in code generators, and summarizes blocking review, pointer/width safety review, unsafe-comment review, and export-review counts. `data/security-rules.sexp` includes the P1-01 FFI hazard rules for collect-safe bytevector pinning, pointer return null guards, @@ -708,7 +709,8 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit hostname, path, PEM, pin, and read/write buffers through shared helpers, rejects invalid accepted fds before ownership transfer, and documents socket shutdown invariants for blocked TLS I/O. The generated report now shows 369 - annotated native unsafe sites and 0 remaining unsafe review sites. + annotated native unsafe sites, 0 remaining unsafe review sites, and 0 Scheme + blocking calls missing `__collect_safe`. Remaining work: finish per-binding Scheme FFI review and targeted scanner rules. @@ -1199,6 +1201,7 @@ Track these in `docs/status.md` per release: | Unclassified raw `read`/FASL/`load` sites | scanner-driven manifest classification gate closed 2026-07-27; trusted build, compiler, REPL/dev, and test surfaces are centrally justified by exact source line | 0 | | FFI bindings audited | scanner inventory and provisional verdicts in [ffi-audit.md](ffi-audit.md); native exports gated and justified | 100% with final verdicts | | Un-annotated Rust `unsafe` blocks | 0 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 | +| Blocking FFI missing `__collect_safe` | 0 source bindings in [ffi-audit.md](ffi-audit.md) | 0 | | Fuzz corpora / crash regressions | 0 / 0 | per-parser corpora + every crash a test | | Exploit-shaped regression tests | ~0 | ≥ 1 per historical finding | | Sandboxed-by-default app entry | no | yes (P0-06) | --- 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, with the generated unannotated unsafe-site count at 0. | 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, 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. | | 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 for requested axes, explicit sandbox-axis refusal, and egress proxy env wiring. | Install native Landlock path/net rules in the worker pre-exec path and keep Linux/macOS/FreeBSD parity tests current. | | Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. | Continue moving risky APIs behind explicit unsafe imports as new modules land. | --- a/lib/std/net/grpc.ss +++ b/lib/std/net/grpc.ss @@ -35,8 +35,8 @@ (def (get-socket-fn) (foreign-procedure "socket" (int int int) int)) (def (get-bind-fn) (foreign-procedure "bind" (int u8* int) int)) (def (get-listen-fn) (foreign-procedure "listen" (int int) int)) - (def (get-accept-fn) (foreign-procedure "accept" (int u8* u32*) int)) - (def (get-connect-fn) (foreign-procedure "connect" (int u8* int) int)) + (def (get-accept-fn) (foreign-procedure __collect_safe "accept" (int u8* u32*) int)) + (def (get-connect-fn) (foreign-procedure __collect_safe "connect" (int u8* int) int)) (def (get-close-fn) (foreign-procedure "close" (int) int)) (def (get-dup-fn) (foreign-procedure "dup" (int) int)) (def (get-setsockopt-fn) (foreign-procedure "setsockopt" (int int int u8* int) int)) --- a/lib/std/net/io.ss +++ b/lib/std/net/io.ss @@ -72,8 +72,8 @@ (def c-socket (foreign-procedure "socket" (int int int) int)) (def c-bind (foreign-procedure "bind" (int void* int) int)) (def c-listen (foreign-procedure "listen" (int int) int)) - (def c-accept (foreign-procedure "accept" (int void* void*) int)) - (def c-connect (foreign-procedure "connect" (int void* int) int)) + (def c-accept (foreign-procedure __collect_safe "accept" (int void* void*) int)) + (def c-connect (foreign-procedure __collect_safe "connect" (int void* int) int)) (def c-close (foreign-procedure "close" (int) int)) (def c-setsockopt (foreign-procedure "setsockopt" (int int int void* int) int)) (def c-read (foreign-procedure __collect_safe "read" (int u8* size_t) ssize_t)) --- a/lib/std/net/repl.ss +++ b/lib/std/net/repl.ss @@ -28,7 +28,7 @@ (def c-socket (foreign-procedure "socket" (int int int) int)) (def c-bind (foreign-procedure "bind" (int void* int) int)) (def c-listen (foreign-procedure "listen" (int int) int)) - (def c-accept (foreign-procedure "accept" (int void* void*) int)) + (def c-accept (foreign-procedure __collect_safe "accept" (int void* void*) int)) (def c-close (foreign-procedure "close" (int) int)) (def c-dup (foreign-procedure "dup" (int) int)) (def c-shutdown (foreign-procedure "shutdown" (int int) int)) --- a/lib/std/net/s3.ss +++ b/lib/std/net/s3.ss @@ -264,7 +264,7 @@ ;; Low-level TCP connect (POSIX sockets for binary I/O) (def (tcp-connect host port) (let* ([c-socket (foreign-procedure "socket" (int int int) int)] - [c-connect (foreign-procedure "connect" (int void* int) int)] + [c-connect (foreign-procedure __collect_safe "connect" (int void* int) int)] [c-close (foreign-procedure "close" (int) int)] [c-htons (foreign-procedure "htons" (unsigned-short) unsigned-short)] [c-inet-pton (foreign-procedure "inet_pton" (int string void*) int)] @@ -325,8 +325,8 @@ str))))))))) (def (fd->binary-ports fd c-close) - (let ([c-read (foreign-procedure "read" (int u8* size_t) ssize_t)] - [c-write (foreign-procedure "write" (int u8* size_t) ssize_t)] + (let ([c-read (foreign-procedure __collect_safe "read" (int u8* size_t) ssize_t)] + [c-write (foreign-procedure __collect_safe "write" (int u8* size_t) ssize_t)] [closed? #f]) (let ([in (make-custom-binary-input-port "s3-in" --- a/lib/std/net/sendfile.ss +++ b/lib/std/net/sendfile.ss @@ -29,7 +29,7 @@ (def c-open (foreign-procedure "open" (string int) int)) (def c-close (foreign-procedure "close" (int) int)) (def c-fstat (foreign-procedure "__fxstat" (int int void*) int)) - (def c-sendfile (foreign-procedure "sendfile" (int int void* size_t) ssize_t)) + (def c-sendfile (foreign-procedure __collect_safe "sendfile" (int int void* size_t) ssize_t)) ;; errno (def c-errno-location --- a/lib/std/net/socks.ss +++ b/lib/std/net/socks.ss @@ -317,7 +317,7 @@ ;; Chez Scheme's tcp-connect is not available in all builds. ;; Use POSIX sockets directly for portability. (let* ([c-socket (foreign-procedure "socket" (int int int) int)] - [c-connect (foreign-procedure "connect" (int void* int) int)] + [c-connect (foreign-procedure __collect_safe "connect" (int void* int) int)] [c-close (foreign-procedure "close" (int) int)] [c-htons (foreign-procedure "htons" (unsigned-short) unsigned-short)] [c-inet-pton (foreign-procedure "inet_pton" (int string void*) int)] @@ -353,8 +353,8 @@ (def (fd->binary-ports fd) ;; Wrap a socket FD as binary input + output ports. - (let ([c-read (foreign-procedure "read" (int u8* size_t) ssize_t)] - [c-write (foreign-procedure "write" (int u8* size_t) ssize_t)] + (let ([c-read (foreign-procedure __collect_safe "read" (int u8* size_t) ssize_t)] + [c-write (foreign-procedure __collect_safe "write" (int u8* size_t) ssize_t)] [c-close (foreign-procedure "close" (int) int)] [closed? #f]) (let ([in (make-custom-binary-input-port --- a/lib/std/os/posix.ss +++ b/lib/std/os/posix.ss @@ -276,7 +276,7 @@ (c-exit code)) ;; waitpid uses an out-pointer for status - (def c-waitpid (foreign-procedure "waitpid" (int void* int) int)) + (def c-waitpid (foreign-procedure __collect_safe "waitpid" (int void* int) int)) (def (posix-waitpid pid options) (let ([status-buf (foreign-alloc 4)]) @@ -418,7 +418,7 @@ (def c-sigdelset (foreign-procedure "sigdelset" (void* int) int)) (def c-sigismember (foreign-procedure "sigismember" (void* int) int)) (def c-sigprocmask (foreign-procedure "sigprocmask" (int void* void*) int)) - (def c-sigwait (foreign-procedure "sigwait" (void* void*) int)) + (def c-sigwait (foreign-procedure __collect_safe "sigwait" (void* void*) int)) (def (posix-sigprocmask how signals) ;; signals: list of signal numbers --- a/lib/std/os/signal-channel.ss +++ b/lib/std/os/signal-channel.ss @@ -31,7 +31,7 @@ (def c-sigemptyset (foreign-procedure "sigemptyset" (void*) int)) (def c-sigaddset (foreign-procedure "sigaddset" (void* int) int)) (def c-sigprocmask (foreign-procedure "sigprocmask" (int void* void*) int)) - (def c-sigwait (foreign-procedure "sigwait" (void* void*) int)) + (def c-sigwait (foreign-procedure __collect_safe "sigwait" (void* void*) int)) (def SIG_BLOCK 0) ;; ========== Signal Name Mapping ========== --- a/lib/std/os/signalfd.ss +++ b/lib/std/os/signalfd.ss @@ -49,7 +49,7 @@ ;; ========== FFI ========== (def c-signalfd (foreign-procedure "signalfd" (int void* int) int)) - (def c-read (foreign-procedure "read" (int void* size_t) ssize_t)) + (def c-read (foreign-procedure __collect_safe "read" (int void* size_t) ssize_t)) (def c-close (foreign-procedure "close" (int) int)) (def c-sigemptyset (foreign-procedure "sigemptyset" (void*) int)) (def c-sigaddset (foreign-procedure "sigaddset" (void* int) int)) --- a/lib/std/repl/server.ss +++ b/lib/std/repl/server.ss @@ -84,7 +84,7 @@ (def c-socket (foreign-procedure "socket" (int int int) int)) (def c-bind (foreign-procedure "bind" (int u8* int) int)) (def c-listen (foreign-procedure "listen" (int int) int)) - (def c-accept (foreign-procedure "accept" (int u8* u8*) int)) + (def c-accept (foreign-procedure __collect_safe "accept" (int u8* u8*) int)) (def c-close (foreign-procedure "close" (int) int)) (def c-dup (foreign-procedure "dup" (int) int)) (def c-shutdown (foreign-procedure "shutdown" (int int) int)) --- a/tools/ffi-audit-report.ss +++ b/tools/ffi-audit-report.ss @@ -9,7 +9,7 @@ (define blocking-symbols '("accept" "connect" "kevent" "poll" "ppoll" "read" "recv" "select" - "send" "sigwait" "signalfd" "wait" "wait4" "waitpid" "write")) + "send" "sigwait" "wait" "wait4" "waitpid" "write")) (define pointer-markers '("void*" "u8*" "uptr" "ptr" "size_t")) (define width-markers '("unsigned-64" "integer-64" "size_t" "ssize_t" "long" "unsigned-long")) @@ -45,6 +45,28 @@ (and (pair? xs) (or (string-contains* line (car xs)) (loop (cdr xs)))))) +(define (strip-string-literals line) + (define n (string-length line)) + (define (space-loop i in-string? escaped? out) + (if (= i n) + (list->string (reverse out)) + (let ([ch (string-ref line i)]) + (cond + [escaped? + (space-loop (+ i 1) #t #f (cons #\space out))] + [in-string? + (cond + [(char=? ch #\\) + (space-loop (+ i 1) #t #t (cons #\space out))] + [(char=? ch #\") + (space-loop (+ i 1) #f #f (cons #\space out))] + [else + (space-loop (+ i 1) #t #f (cons #\space out))])] + [(char=? ch #\") + (space-loop (+ i 1) #t #f (cons #\space out))] + [else + (space-loop (+ i 1) #f #f (cons ch out))])))) + (space-loop 0 #f #f '())) (define (blank-or-comment-line? line comment-char) (let ([n (string-length line)]) @@ -129,15 +151,18 @@ (reverse out) (loop (cons line out)))))))) -(define (scheme-kind line) +(define (scheme-kind code-line) (cond - [(blank-or-comment-line? line #\;) #f] - [(string-contains* line "foreign-procedure") 'foreign-procedure] - [(string-contains* line "load-shared-object") 'load-shared-object] - [(string-contains* line "define-ftype") 'define-ftype] - [(string-contains* line "foreign-callable") 'foreign-callable] + [(blank-or-comment-line? code-line #\;) #f] + [(quoted-data-line? code-line) #f] + [(string-contains* code-line "foreign-procedure") 'foreign-procedure] + [(string-contains* code-line "load-shared-object") 'load-shared-object] + [(string-contains* code-line "define-ftype") 'define-ftype] + [(string-contains* code-line "foreign-callable") 'foreign-callable] [else #f])) +(define (quoted-data-line? code-line) + (string-contains* code-line "(quote")) (define (blocking-candidate? line) (contains-any? line blocking-symbols)) @@ -146,21 +171,24 @@ (let loop ([lines (read-lines path)] [n 1] [out '()]) (cond [(null? lines) (reverse out)] - [(scheme-kind (car lines)) - => (lambda (kind) - (let ([line (car lines)]) - (loop (cdr lines) - (+ n 1) - (cons `(site (path ,path) - (line ,n) - (kind ,kind) - (pointer-args ,(contains-any? line pointer-markers)) - (width-sensitive ,(contains-any? line width-markers)) - (blocking-candidate ,(blocking-candidate? line)) - (collect-safe ,(string-contains* line "__collect_safe")) - (snippet ,line)) - out))))] - [else (loop (cdr lines) (+ n 1) out)])))) + [else + (let* ([line (car lines)] + [code-line (strip-string-literals line)]) + (cond + [(scheme-kind code-line) + => (lambda (kind) + (loop (cdr lines) + (+ n 1) + (cons `(site (path ,path) + (line ,n) + (kind ,kind) + (pointer-args ,(contains-any? code-line pointer-markers)) + (width-sensitive ,(contains-any? code-line width-markers)) + (blocking-candidate ,(blocking-candidate? code-line)) + (collect-safe ,(string-contains* code-line "__collect_safe")) + (snippet ,line)) + out)))] + [else (loop (cdr lines) (+ n 1) out)]))])))) (define (line-at lines index) (if (and (>= index 0) (< index (length lines))) @@ -267,8 +295,7 @@ (define (report full?) (let* ([scheme-files (walk-files "." (lambda (p) - (or (string-suffix? ".ss" p) - (string-suffix? ".sls" p))))] + (string-suffix? ".ss" p)))] [rust-files (walk-files "." (lambda (p) (string-suffix? ".rs" p)))] [c-files (walk-files "." (lambda (p)