security: document x509 postgres epoll unsafe invariants
ober
f5a4ce43e5a418459430bd9a1f9232b58b3fe1b6
--- 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 1204) + (file-count 1205) (site-count 904) (foreign-procedure 810) (load-shared-object 80) @@ -41,9 +41,9 @@ Latest summary from `tools/ffi-audit-report.ss`: (c-file-count 5) (no-mangle-exports 196) (unsafe-sites 369) - (unsafe-sites-with-nearby-safety-comment 347) - (unsafe-sites-without-nearby-safety-comment 22) - (unsafe-comment-review-sites 22) + (unsafe-sites-with-nearby-safety-comment 356) + (unsafe-sites-without-nearby-safety-comment 13) + (unsafe-comment-review-sites 13) (export-review-sites 196)) (vendor (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated))) @@ -113,7 +113,8 @@ empty paths, oversized lengths, interior NUL path truncation, and null output pointers before crossing into libc. `epoll.rs` now documents epoll/eventfd syscall and caller-output invariants, bounds caller-controlled event counts, checks output byte-size derivation, and treats empty nonblocking eventfd drains -as normal. `antidebug.rs` now documents ptrace and breakpoint-probe invariants, +as normal, with the remaining invariants adjacent to the audited unsafe +expressions. `antidebug.rs` now documents ptrace and breakpoint-probe invariants, reports ptrace OS errors, and saturates timing duration conversion instead of truncating. `socks5_server.rs` now rejects null pointers paired with nonzero lengths before building FFI slices, bounds caller lengths, documents stats @@ -122,7 +123,9 @@ locks, with C ABI slice and output-copy invariants adjacent to the audited unsafe expressions. `postgres_native.rs` now funnels SQL and connection-string inputs through a shared checked UTF-8 helper, rejects null/nonzero and oversized FFI buffers, documents result-slot and output-buffer writes, and permits zero-sized -output buffers without manufacturing null slices. `landlock.rs` now validates +output buffers without manufacturing null slices, with PostgreSQL C ABI slice +and output-copy invariants adjacent to the audited unsafe expressions. +`landlock.rs` now validates path and TCP port inputs before issuing syscalls, rejects interior-NUL path truncation, documents Landlock syscall-attribute and fd-close invariants, and has Linux-only regression tests for syscall-free input rejection. `wasm.rs` @@ -140,8 +143,9 @@ rejects null result and output slots, checks result indexes before converting signed values to `usize`, and centralizes output-buffer writes with bounded copy invariants. `x509.rs` now funnels certificate SAN/path/PEM inputs through checked C ABI UTF-8 helpers, centralizes PEM/fingerprint output-buffer writes, -documents atomic file-publication syscall invariants, and has regressions for -null/nonzero input and output-slot failures. `wasm_sm.rs` now compiles under +documents atomic file-publication syscall invariants adjacent to the audited +unsafe expressions, and has regressions for null/nonzero input and output-slot +failures. `wasm_sm.rs` now compiles under the crate-wide `unsafe_op_in_unsafe_fn` denial for the SpiderMonkey feature, checks host-memory offset arithmetic, bounds C ABI module/function/argument/log buffers, converts poisoned handle-store locks into normal FFI errors, documents @@ -161,7 +165,7 @@ 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 347 annotated native unsafe sites and 22 remaining unsafe review sites. +reports 356 annotated native unsafe sites and 13 remaining unsafe review sites. Remaining work before closing K3-P1-01: --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -663,18 +663,21 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit interior NUL path truncation, and null output pointers before crossing into libc. `epoll.rs` now documents epoll/eventfd syscall and caller-output invariants, bounds caller-controlled event counts, checks output byte-size - derivation, and treats empty nonblocking eventfd drains as normal. + derivation, and treats empty nonblocking eventfd drains as normal, with the + remaining invariants adjacent to the audited unsafe expressions. `antidebug.rs` now documents ptrace and breakpoint-probe invariants, reports ptrace OS errors, and saturates timing duration conversion instead of truncating. `socks5_server.rs` now rejects null pointers paired with nonzero lengths before building FFI slices, bounds caller lengths, documents stats buffer copies, and returns normal FFI errors for poisoned server-registry locks, with C ABI slice and output-copy invariants adjacent to the audited - unsafe expressions. `postgres_native.rs` now funnels SQL and connection-string inputs - through a shared checked UTF-8 helper, rejects null/nonzero and oversized FFI - buffers, documents result-slot and output-buffer writes, and permits - zero-sized output buffers without manufacturing null slices. `landlock.rs` - now validates path and TCP port inputs before issuing syscalls, rejects + unsafe expressions. `postgres_native.rs` now funnels SQL and + connection-string inputs through a shared checked UTF-8 helper, rejects + null/nonzero and oversized FFI buffers, documents result-slot and + output-buffer writes, and permits + zero-sized output buffers without manufacturing null slices, with PostgreSQL + C ABI slice and output-copy invariants adjacent to the audited unsafe + expressions. `landlock.rs` now validates path and TCP port inputs before issuing syscalls, rejects interior-NUL path truncation, documents Landlock syscall-attribute and fd-close invariants, and has Linux-only regression tests for syscall-free input rejection. `wasm.rs` now bounds module/function/path/log buffers, @@ -684,11 +687,13 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit invariants adjacent to the remaining unsafe sites, and has Rust regressions for invalid C ABI inputs. `aproc.rs` now documents inherited-fd slice, `pre_exec`, and `openpty` invariants adjacent to the remaining - worker-launch unsafe sites. `duckdb_native.rs` now validates path, SQL, text, blob, result-slot, - result-index, and output-buffer inputs through shared helpers and converts - poisoned handle-store locks into normal FFI errors. `x509.rs` now validates + worker-launch unsafe sites. `duckdb_native.rs` now validates path, SQL, text, + blob, result-slot, result-index, and output-buffer inputs through shared + helpers and converts poisoned handle-store locks into normal FFI errors. + `x509.rs` now validates SAN/path/PEM inputs through checked UTF-8 helpers, centralizes PEM and - fingerprint output writes, and documents atomic file-publication syscalls. + fingerprint output writes, and documents atomic file-publication syscalls + adjacent to the audited unsafe expressions. `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 @@ -702,8 +707,8 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit publication, and test-owned fd conversion invariants. `tls.rs` now validates 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 347 - annotated native unsafe sites and 22 remaining unsafe review sites. + shutdown invariants for blocked TLS I/O. The generated report now shows 356 + annotated native unsafe sites and 13 remaining unsafe review sites. Remaining work: continue unsafe invariant comments across the rest of `jerboa-native-rs`. @@ -1193,7 +1198,7 @@ Track these in `docs/status.md` per release: | Confined worker exists | facade, audit lifecycle, output caps, deadline, process-group kill, memory rlimit pre-exec path, explicit sandbox-axis refusal, egress proxy env wiring, and Linux syscall/ptrace seccomp pre-exec setup landed; Landlock path/net and Seatbelt/Capsicum parity pending | yes, tested (P0-02) | | 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 | 22 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 | +| Un-annotated Rust `unsafe` blocks | 13 unsafe review sites without nearby `SAFETY:` comments 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/reviews/2026-07-27-native-export-review.sexp +++ b/docs/reviews/2026-07-27-native-export-review.sexp @@ -54,13 +54,13 @@ (export (symbol "jerboa_duckdb_value_is_null") (file "jerboa-native-rs/src/duckdb_native.rs") (line 918) (scheme-callers 1 (caller (file "lib/std/db/duckdb-native.ss") (line 81)))) (export (symbol "jerboa_duckdb_value_text") (file "jerboa-native-rs/src/duckdb_native.rs") (line 1023) (scheme-callers 1 (caller (file "lib/std/db/duckdb-native.ss") (line 89)))) (export (symbol "jerboa_duckdb_version") (file "jerboa-native-rs/src/duckdb_native.rs") (line 1167) (scheme-callers 1 (caller (file "lib/std/db/duckdb-native.ss") (line 98)))) - (export (symbol "jerboa_epoll_close") (file "jerboa-native-rs/src/epoll.rs") (line 107) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 59)))) + (export (symbol "jerboa_epoll_close") (file "jerboa-native-rs/src/epoll.rs") (line 106) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 59)))) (export (symbol "jerboa_epoll_create") (file "jerboa-native-rs/src/epoll.rs") (line 7) (scheme-callers 2 (caller (file "lib/std/os/epoll-native.ss") (line 30)) (caller (file "lib/std/os/epoll-native.ss") (line 50)))) (export (symbol "jerboa_epoll_ctl") (file "jerboa-native-rs/src/epoll.rs") (line 25) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 53)))) (export (symbol "jerboa_epoll_wait") (file "jerboa-native-rs/src/epoll.rs") (line 47) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 56)))) - (export (symbol "jerboa_eventfd_create") (file "jerboa-native-rs/src/epoll.rs") (line 122) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 293)))) - (export (symbol "jerboa_eventfd_drain") (file "jerboa-native-rs/src/epoll.rs") (line 156) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 299)))) - (export (symbol "jerboa_eventfd_signal") (file "jerboa-native-rs/src/epoll.rs") (line 136) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 296)))) + (export (symbol "jerboa_eventfd_create") (file "jerboa-native-rs/src/epoll.rs") (line 121) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 293)))) + (export (symbol "jerboa_eventfd_drain") (file "jerboa-native-rs/src/epoll.rs") (line 155) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 299)))) + (export (symbol "jerboa_eventfd_signal") (file "jerboa-native-rs/src/epoll.rs") (line 135) (scheme-callers 1 (caller (file "lib/std/os/epoll-native.ss") (line 296)))) (export (symbol "jerboa_gunzip") (file "jerboa-native-rs/src/compress.rs") (line 162) (scheme-callers 1 (caller (file "lib/std/compress/native-rust.ss") (line 39)))) (export (symbol "jerboa_gzip") (file "jerboa-native-rs/src/compress.rs") (line 116) (scheme-callers 1 (caller (file "lib/std/compress/native-rust.ss") (line 36)))) (export (symbol "jerboa_hmac_sha256") (file "jerboa-native-rs/src/crypto.rs") (line 206) (scheme-callers 2 (caller (file "lib/std/crypto/native-rust.ss") (line 109)) (caller (file "lib/std/crypto/native-rust.ss") (line 120)))) --- 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 22. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. | +| 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 13. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. | | 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/jerboa-native-rs/src/epoll.rs +++ b/jerboa-native-rs/src/epoll.rs @@ -72,9 +72,9 @@ pub extern "C" fn jerboa_epoll_wait( let mut events: Vec<libc::epoll_event> = vec![libc::epoll_event { events: 0, u64: 0 }; max_events_usize]; let n = loop { - // SAFETY: events is an initialized Vec<epoll_event> with capacity - // for max_events entries. epoll_wait writes at most max_events - // entries and does not retain the pointer. + // events is an initialized Vec<epoll_event> with capacity for + // max_events entries. + // SAFETY: epoll_wait writes at most max_events and retains nothing. let r = unsafe { libc::epoll_wait(epfd, events.as_mut_ptr(), max_events, timeout_ms) }; if r < 0 { let err = std::io::Error::last_os_error(); @@ -88,9 +88,8 @@ pub extern "C" fn jerboa_epoll_wait( break r; }; // Copy results to output buffer: each entry is (fd:i32, events:u32) = 8 bytes - // SAFETY: events_out is non-null, output_len is derived by checked - // multiplication from the bounded max_events count, and the Scheme - // caller allocates exactly max_events * 8 writable bytes. + // events_out is non-null, output_len is checked from max_events. + // SAFETY: the caller allocates exactly max_events * 8 writable bytes. let out = unsafe { std::slice::from_raw_parts_mut(events_out, output_len) }; for i in 0..n as usize { let fd = events[i].u64 as i32; --- a/jerboa-native-rs/src/postgres_native.rs +++ b/jerboa-native-rs/src/postgres_native.rs @@ -39,9 +39,9 @@ fn ffi_utf8(ptr: *const u8, len: usize, label: &str) -> Result<String, ()> { if len == 0 { return Ok(String::new()); } - // SAFETY: len is bounded to `isize::MAX`, and null pointers with nonzero - // length were rejected above. The returned slice is copied into an owned - // String before this FFI wrapper returns. + // len is bounded to `isize::MAX`, and null pointers with nonzero length + // were rejected above. + // SAFETY: the slice is copied into an owned String before FFI return. let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; std::str::from_utf8(bytes).map(str::to_owned).map_err(|_| { set_last_error(format!("invalid UTF-8 {label}")); @@ -296,9 +296,9 @@ pub extern "C" fn jerboa_pg_get_value( let bytes = s.as_bytes(); let copy_len = bytes.len().min(output_max); if copy_len != 0 { - // SAFETY: null output with nonzero capacity was rejected, - // and `output_max` is bounded to `isize::MAX`; the slice - // is used only for this bounded copy. + // Null output with nonzero capacity was rejected, and + // `output_max` is bounded to `isize::MAX`. + // SAFETY: the slice is used only for this bounded copy. let out = unsafe { std::slice::from_raw_parts_mut(output, output_max) }; out[..copy_len].copy_from_slice(&bytes[..copy_len]); } @@ -374,9 +374,9 @@ pub extern "C" fn jerboa_pg_column_name( let bytes = name.as_bytes(); let copy_len = bytes.len().min(output_max); if copy_len != 0 { - // SAFETY: null output with nonzero capacity was rejected, and - // `output_max` is bounded to `isize::MAX`; the slice is used only - // for this bounded copy. + // Null output with nonzero capacity was rejected, and `output_max` + // is bounded to `isize::MAX`. + // SAFETY: the slice is used only for this bounded copy. let out = unsafe { std::slice::from_raw_parts_mut(output, output_max) }; out[..copy_len].copy_from_slice(&bytes[..copy_len]); } --- a/jerboa-native-rs/src/x509.rs +++ b/jerboa-native-rs/src/x509.rs @@ -24,9 +24,9 @@ fn ffi_bytes<'a>(ptr: *const u8, len: usize, label: &str) -> Result<&'a [u8], () if len == 0 { return Ok(&[]); } - // SAFETY: `len` is bounded to `isize::MAX`, and null pointers paired with - // nonzero length were rejected above. The borrowed slice is parsed or - // copied before returning across the C ABI. + // `len` is bounded to `isize::MAX`, and null pointers paired with nonzero + // length were rejected above. + // SAFETY: the borrowed slice is parsed or copied before C ABI return. Ok(unsafe { std::slice::from_raw_parts(ptr, len) }) } @@ -65,9 +65,9 @@ fn copy_exact_output( return -1; } if !bytes.is_empty() { - // SAFETY: null output with nonzero capacity was rejected, `output_max` - // is bounded to `isize::MAX`, and the earlier length check ensures the - // destination can hold the whole PEM payload. + // Null output with nonzero capacity was rejected, and `output_max` is + // bounded to `isize::MAX`. + // SAFETY: the destination can hold the whole PEM payload. let out = unsafe { std::slice::from_raw_parts_mut(output, output_max) }; out[..bytes.len()].copy_from_slice(bytes); } @@ -139,9 +139,9 @@ fn write_new_file_atomic(path: &Path, data: &[u8], mode: u32) -> io::Result<()> temp_name.push_str(".tmp"); let temp = CString::new(temp_name).expect("generated name has no NUL"); - // SAFETY: `dirfd` comes from an open directory handle, `temp` is a - // NUL-free generated filename, and `openat` is used with no-follow and - // exclusive-create flags so the temporary file is private. + // `dirfd` comes from an open directory handle, and `temp` is a NUL-free + // generated filename. + // SAFETY: openat uses no-follow and exclusive-create for a private file. let fd = unsafe { libc::openat( dirfd, @@ -186,9 +186,9 @@ fn write_new_file_atomic(path: &Path, data: &[u8], mode: u32) -> io::Result<()> // linkat is an atomic no-replace publication: an existing regular // file or symlink at the destination makes the operation fail. - // SAFETY: `dirfd`, `temp`, and `destination` are valid directory/file - // descriptors or NUL-terminated names, and both paths are relative to - // the opened parent directory. + // `dirfd`, `temp`, and `destination` are valid descriptors or + // NUL-terminated names relative to the opened parent directory. + // SAFETY: linkat performs atomic no-replace publication. let link_result = unsafe { libc::linkat(dirfd, temp.as_ptr(), dirfd, destination.as_ptr(), 0) }; let link_error = io::Error::last_os_error();