security: document secure filesystem ffi

ober

9f6c2aff1a6576281b70452e5fd53180d91c2abf

diff --git a/docs/ffi-audit.md b/docs/ffi-audit.md
index 60abeaa..e267a7b 100644
--- a/docs/ffi-audit.md
+++ b/docs/ffi-audit.md
@@ -40,10 +40,10 @@ Latest summary from `tools/ffi-audit-report.ss`:
     (rust-file-count 29)
     (c-file-count 5)
     (no-mangle-exports 196)
-    (unsafe-sites 397)
-    (unsafe-sites-with-nearby-safety-comment 243)
-    (unsafe-sites-without-nearby-safety-comment 154)
-    (unsafe-comment-review-sites 154)
+    (unsafe-sites 395)
+    (unsafe-sites-with-nearby-safety-comment 299)
+    (unsafe-sites-without-nearby-safety-comment 96)
+    (unsafe-comment-review-sites 96)
     (export-review-sites 196))
   (vendor
     (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated)))
@@ -145,8 +145,13 @@ 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. The generated inventory now reports 243
-annotated native unsafe sites and 154 remaining unsafe review sites.
+inputs and output-buffer validation. `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
+invariants around its secure filesystem capability boundary. The generated
+inventory now reports 299 annotated native unsafe sites and 96 remaining unsafe
+review sites.
 
 Remaining work before closing K3-P1-01:
 
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index a46234f..4c3241e 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -146,7 +146,7 @@ never pattern-based:
   and the unsafe pattern unrepresentable — that's most of P0/P1 below.
 - **The FFI boundary is our `unsafe`.** 91 files under `lib/std/` mention
   `foreign-procedure`; the current [FFI audit](ffi-audit.md) tracks 196
-  native `#[no_mangle]` sites and 397 Rust unsafe sites. That boundary is
+  native `#[no_mangle]` sites and 395 Rust unsafe sites. That boundary is
   where a K3-class adversary will concentrate, because it is the only place
   memory-corruption patterns *exist* in a Jerboa application.
   [`Philosophy.md`](Philosophy.md) Principle 4 and open tension #5 both
@@ -268,7 +268,7 @@ Keep this section to baseline facts only.
 | Files under `lib/std/` mentioning `foreign-procedure` | **91** | `grep -rl foreign-procedure lib/std \| wc -l` |
 | Native `#[no_mangle]` sites in `jerboa-native-rs/src` | **196** | `make ffi-audit-report` |
 | Actual native C ABI exports after cfg filtering | **190** | `make native-export-review-check` |
-| Rust unsafe sites in `jerboa-native-rs/src` | **397** | `make ffi-audit-report` |
+| Rust unsafe sites in `jerboa-native-rs/src` | **395** | `make ffi-audit-report` |
 | Fuzz harnesses | 13 | `ls tests/fuzz/harness` |
 | Checked-in fuzz seed corpora | **0** | `tests/fuzz/` contains only `harness/` |
 | Security test files | 21 | Appendix A |
@@ -690,9 +690,12 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit
   `sqlite.rs` native wrapper now validates path, SQL, text, blob, result
   output, and error output buffers through shared helpers, rejects oversized
   SQLite lengths, converts poisoned handle-store locks into normal FFI errors,
-  and wraps integer and double column getters in panic containment. The
-  generated report now shows 243 annotated native unsafe sites and 154
-  remaining unsafe review sites.
+  and wraps integer and double column getters in panic containment.
+  `secure_fs.rs` now documents descriptor-relative open/create/stat/list/read
+  syscalls, fd ownership transfer, caller output slots, atomic output
+  publication, and test-owned fd conversion invariants. The generated report
+  now shows 299 annotated native unsafe sites and 96 remaining unsafe review
+  sites.
   Remaining work: continue unsafe invariant comments across the rest of
   `jerboa-native-rs`.
 
@@ -1182,7 +1185,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 | 154 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 |
+| Un-annotated Rust `unsafe` blocks | 96 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) |
@@ -1197,7 +1200,7 @@ Track these in `docs/status.md` per release:
 ```bash
 # Posture facts used in this document (re-run to refresh):
 grep -rl 'foreign-procedure' lib/std | wc -l        # 91
-make ffi-audit-report                              # native no_mangle 196; unsafe 397
+make ffi-audit-report                              # native no_mangle 196; unsafe 395
 make native-export-review-check                    # actual exports 190
 ls tests/fuzz/harness                                # 13 harnesses
 ls vendor/                                           # ChezScheme, ChezScheme-patches, jsqlite
diff --git a/docs/reviews/2026-07-27-native-export-review.sexp b/docs/reviews/2026-07-27-native-export-review.sexp
index baa9061..567c151 100644
--- a/docs/reviews/2026-07-27-native-export-review.sexp
+++ b/docs/reviews/2026-07-27-native-export-review.sexp
@@ -113,20 +113,20 @@
     (export (symbol "jerboa_seccomp_lock") (file "jerboa-native-rs/src/seccomp.rs") (line 179) (scheme-callers 2 (caller (file "lib/std/os/seccomp.ss") (line 34)) (caller (file "lib/std/os/seccomp.ss") (line 36))))
     (export (symbol "jerboa_seccomp_lock_strict") (file "jerboa-native-rs/src/seccomp.rs") (line 196) (scheme-callers 1 (caller (file "lib/std/os/seccomp.ss") (line 36))))
     (export (symbol "jerboa_secure_alloc") (file "jerboa-native-rs/src/secure_mem.rs") (line 30) (scheme-callers 1 (caller (file "lib/std/crypto/secure-mem.ss") (line 35))))
-    (export (symbol "jerboa_secure_dir_close") (file "jerboa-native-rs/src/secure_fs.rs") (line 463) (scheme-callers 2 (caller (file "lib/std/os/secure-output.ss") (line 33)) (caller (file "lib/std/os/secure-output.ss") (line 53))))
-    (export (symbol "jerboa_secure_dir_list") (file "jerboa-native-rs/src/secure_fs.rs") (line 633) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 62))))
-    (export (symbol "jerboa_secure_dir_mkdirs") (file "jerboa-native-rs/src/secure_fs.rs") (line 472) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 56))))
-    (export (symbol "jerboa_secure_dir_open") (file "jerboa-native-rs/src/secure_fs.rs") (line 400) (scheme-callers 2 (caller (file "lib/std/os/secure-output.ss") (line 49)) (caller (file "lib/std/os/secure-output.ss") (line 51))))
-    (export (symbol "jerboa_secure_dir_open_strict") (file "jerboa-native-rs/src/secure_fs.rs") (line 432) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 51))))
-    (export (symbol "jerboa_secure_entry_info") (file "jerboa-native-rs/src/secure_fs.rs") (line 502) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 59))))
-    (export (symbol "jerboa_secure_fd_close") (file "jerboa-native-rs/src/secure_fs.rs") (line 916) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 77))))
+    (export (symbol "jerboa_secure_dir_close") (file "jerboa-native-rs/src/secure_fs.rs") (line 497) (scheme-callers 2 (caller (file "lib/std/os/secure-output.ss") (line 33)) (caller (file "lib/std/os/secure-output.ss") (line 53))))
+    (export (symbol "jerboa_secure_dir_list") (file "jerboa-native-rs/src/secure_fs.rs") (line 681) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 62))))
+    (export (symbol "jerboa_secure_dir_mkdirs") (file "jerboa-native-rs/src/secure_fs.rs") (line 506) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 56))))
+    (export (symbol "jerboa_secure_dir_open") (file "jerboa-native-rs/src/secure_fs.rs") (line 430) (scheme-callers 2 (caller (file "lib/std/os/secure-output.ss") (line 49)) (caller (file "lib/std/os/secure-output.ss") (line 51))))
+    (export (symbol "jerboa_secure_dir_open_strict") (file "jerboa-native-rs/src/secure_fs.rs") (line 464) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 51))))
+    (export (symbol "jerboa_secure_entry_info") (file "jerboa-native-rs/src/secure_fs.rs") (line 536) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 59))))
+    (export (symbol "jerboa_secure_fd_close") (file "jerboa-native-rs/src/secure_fs.rs") (line 987) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 77))))
     (export (symbol "jerboa_secure_free") (file "jerboa-native-rs/src/secure_mem.rs") (line 132) (scheme-callers 1 (caller (file "lib/std/crypto/secure-mem.ss") (line 38))))
-    (export (symbol "jerboa_secure_output_abort") (file "jerboa-native-rs/src/secure_fs.rs") (line 907) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 75))))
-    (export (symbol "jerboa_secure_output_begin") (file "jerboa-native-rs/src/secure_fs.rs") (line 760) (scheme-callers 2 (caller (file "lib/std/os/secure-output.ss") (line 68)) (caller (file "lib/std/os/secure-output.ss") (line 71))))
-    (export (symbol "jerboa_secure_output_begin_checked") (file "jerboa-native-rs/src/secure_fs.rs") (line 816) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 71))))
-    (export (symbol "jerboa_secure_output_commit") (file "jerboa-native-rs/src/secure_fs.rs") (line 867) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 73))))
+    (export (symbol "jerboa_secure_output_abort") (file "jerboa-native-rs/src/secure_fs.rs") (line 978) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 75))))
+    (export (symbol "jerboa_secure_output_begin") (file "jerboa-native-rs/src/secure_fs.rs") (line 818) (scheme-callers 2 (caller (file "lib/std/os/secure-output.ss") (line 68)) (caller (file "lib/std/os/secure-output.ss") (line 71))))
+    (export (symbol "jerboa_secure_output_begin_checked") (file "jerboa-native-rs/src/secure_fs.rs") (line 878) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 71))))
+    (export (symbol "jerboa_secure_output_commit") (file "jerboa-native-rs/src/secure_fs.rs") (line 933) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 73))))
     (export (symbol "jerboa_secure_random_fill") (file "jerboa-native-rs/src/secure_mem.rs") (line 198) (scheme-callers 1 (caller (file "lib/std/crypto/secure-mem.ss") (line 44))))
-    (export (symbol "jerboa_secure_read_file") (file "jerboa-native-rs/src/secure_fs.rs") (line 673) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 65))))
+    (export (symbol "jerboa_secure_read_file") (file "jerboa-native-rs/src/secure_fs.rs") (line 723) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 65))))
     (export (symbol "jerboa_secure_wipe") (file "jerboa-native-rs/src/secure_mem.rs") (line 183) (scheme-callers 1 (caller (file "lib/std/crypto/secure-mem.ss") (line 41))))
     (export (symbol "jerboa_sha1") (file "jerboa-native-rs/src/crypto.rs") (line 71) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 60))))
     (export (symbol "jerboa_sha256") (file "jerboa-native-rs/src/crypto.rs") (line 87) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 69))))
@@ -140,28 +140,28 @@
     (export (symbol "jerboa_sm_instance_new_hosted") (file "jerboa-native-rs/src/wasm_sm.rs") (line 989) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 174))))
     (export (symbol "jerboa_sm_module_free") (file "jerboa-native-rs/src/wasm_sm.rs") (line 934) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 150))))
     (export (symbol "jerboa_sm_module_new") (file "jerboa-native-rs/src/wasm_sm.rs") (line 847) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 144))))
-    (export (symbol "jerboa_sqlite_bind_blob") (file "jerboa-native-rs/src/sqlite.rs") (line 445) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 63))))
-    (export (symbol "jerboa_sqlite_bind_double") (file "jerboa-native-rs/src/sqlite.rs") (line 374) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 59))))
-    (export (symbol "jerboa_sqlite_bind_int") (file "jerboa-native-rs/src/sqlite.rs") (line 350) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 57))))
-    (export (symbol "jerboa_sqlite_bind_null") (file "jerboa-native-rs/src/sqlite.rs") (line 491) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 65))))
-    (export (symbol "jerboa_sqlite_bind_text") (file "jerboa-native-rs/src/sqlite.rs") (line 398) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 61))))
-    (export (symbol "jerboa_sqlite_changes") (file "jerboa-native-rs/src/sqlite.rs") (line 781) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 91))))
+    (export (symbol "jerboa_sqlite_bind_blob") (file "jerboa-native-rs/src/sqlite.rs") (line 442) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 63))))
+    (export (symbol "jerboa_sqlite_bind_double") (file "jerboa-native-rs/src/sqlite.rs") (line 372) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 59))))
+    (export (symbol "jerboa_sqlite_bind_int") (file "jerboa-native-rs/src/sqlite.rs") (line 348) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 57))))
+    (export (symbol "jerboa_sqlite_bind_null") (file "jerboa-native-rs/src/sqlite.rs") (line 487) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 65))))
+    (export (symbol "jerboa_sqlite_bind_text") (file "jerboa-native-rs/src/sqlite.rs") (line 396) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 61))))
+    (export (symbol "jerboa_sqlite_changes") (file "jerboa-native-rs/src/sqlite.rs") (line 773) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 91))))
     (export (symbol "jerboa_sqlite_close") (file "jerboa-native-rs/src/sqlite.rs") (line 194) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 46))))
-    (export (symbol "jerboa_sqlite_column_blob") (file "jerboa-native-rs/src/sqlite.rs") (line 644) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 82))))
-    (export (symbol "jerboa_sqlite_column_count") (file "jerboa-native-rs/src/sqlite.rs") (line 547) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 71))))
-    (export (symbol "jerboa_sqlite_column_double") (file "jerboa-native-rs/src/sqlite.rs") (line 596) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 77))))
-    (export (symbol "jerboa_sqlite_column_int") (file "jerboa-native-rs/src/sqlite.rs") (line 580) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 75))))
-    (export (symbol "jerboa_sqlite_column_name") (file "jerboa-native-rs/src/sqlite.rs") (line 686) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 85))))
-    (export (symbol "jerboa_sqlite_column_text") (file "jerboa-native-rs/src/sqlite.rs") (line 613) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 79))))
-    (export (symbol "jerboa_sqlite_column_type") (file "jerboa-native-rs/src/sqlite.rs") (line 564) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 73))))
-    (export (symbol "jerboa_sqlite_errmsg") (file "jerboa-native-rs/src/sqlite.rs") (line 797) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 93))))
-    (export (symbol "jerboa_sqlite_exec") (file "jerboa-native-rs/src/sqlite.rs") (line 233) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 48))))
-    (export (symbol "jerboa_sqlite_finalize") (file "jerboa-native-rs/src/sqlite.rs") (line 742) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 52))))
-    (export (symbol "jerboa_sqlite_last_insert_rowid") (file "jerboa-native-rs/src/sqlite.rs") (line 768) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 89))))
+    (export (symbol "jerboa_sqlite_column_blob") (file "jerboa-native-rs/src/sqlite.rs") (line 639) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 82))))
+    (export (symbol "jerboa_sqlite_column_count") (file "jerboa-native-rs/src/sqlite.rs") (line 543) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 71))))
+    (export (symbol "jerboa_sqlite_column_double") (file "jerboa-native-rs/src/sqlite.rs") (line 592) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 77))))
+    (export (symbol "jerboa_sqlite_column_int") (file "jerboa-native-rs/src/sqlite.rs") (line 576) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 75))))
+    (export (symbol "jerboa_sqlite_column_name") (file "jerboa-native-rs/src/sqlite.rs") (line 680) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 85))))
+    (export (symbol "jerboa_sqlite_column_text") (file "jerboa-native-rs/src/sqlite.rs") (line 609) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 79))))
+    (export (symbol "jerboa_sqlite_column_type") (file "jerboa-native-rs/src/sqlite.rs") (line 560) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 73))))
+    (export (symbol "jerboa_sqlite_errmsg") (file "jerboa-native-rs/src/sqlite.rs") (line 789) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 93))))
+    (export (symbol "jerboa_sqlite_exec") (file "jerboa-native-rs/src/sqlite.rs") (line 232) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 48))))
+    (export (symbol "jerboa_sqlite_finalize") (file "jerboa-native-rs/src/sqlite.rs") (line 735) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 52))))
+    (export (symbol "jerboa_sqlite_last_insert_rowid") (file "jerboa-native-rs/src/sqlite.rs") (line 760) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 89))))
     (export (symbol "jerboa_sqlite_open") (file "jerboa-native-rs/src/sqlite.rs") (line 153) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 44))))
-    (export (symbol "jerboa_sqlite_prepare") (file "jerboa-native-rs/src/sqlite.rs") (line 262) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 50))))
-    (export (symbol "jerboa_sqlite_reset") (file "jerboa-native-rs/src/sqlite.rs") (line 718) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 54))))
-    (export (symbol "jerboa_sqlite_step") (file "jerboa-native-rs/src/sqlite.rs") (line 518) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 68))))
+    (export (symbol "jerboa_sqlite_prepare") (file "jerboa-native-rs/src/sqlite.rs") (line 261) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 50))))
+    (export (symbol "jerboa_sqlite_reset") (file "jerboa-native-rs/src/sqlite.rs") (line 711) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 54))))
+    (export (symbol "jerboa_sqlite_step") (file "jerboa-native-rs/src/sqlite.rs") (line 514) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 68))))
     (export (symbol "jerboa_timing_safe_equal") (file "jerboa-native-rs/src/crypto.rs") (line 310) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 131))))
     (export (symbol "jerboa_tls_accept") (file "jerboa-native-rs/src/tls.rs") (line 807) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 86))))
     (export (symbol "jerboa_tls_close") (file "jerboa-native-rs/src/tls.rs") (line 1675) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 144))))
diff --git a/docs/status.md b/docs/status.md
index 457254a..feb2d06 100644
--- 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`, 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 154. | 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`, 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 96. | 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. |
diff --git a/jerboa-native-rs/src/secure_fs.rs b/jerboa-native-rs/src/secure_fs.rs
index 22532f5..dc46218 100644
--- a/jerboa-native-rs/src/secure_fs.rs
+++ b/jerboa-native-rs/src/secure_fs.rs
@@ -66,6 +66,8 @@ fn input_bytes<'a>(ptr: *const u8, len: usize, what: &str) -> Result<&'a [u8], S
     if len == 0 || len > MAX_PATH_BYTES || ptr.is_null() {
         return Err(format!("{what} must contain 1..={MAX_PATH_BYTES} bytes"));
     }
+    // SAFETY: callers must provide a readable buffer for `len` bytes; this
+    // wrapper rejects null, empty, and oversized path inputs before slicing.
     let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
     if bytes.contains(&0) {
         return Err(format!("{what} contains NUL"));
@@ -82,7 +84,7 @@ fn validate_component(component: &[u8]) -> Result<CString, String> {
         || component.contains(&b'\\')
         || component.contains(&0)
     {
-        return Err("unsafe filesystem path component".to_string());
+        return Err("invalid filesystem path component".to_string());
     }
     CString::new(component).map_err(|_| "filesystem path component contains NUL".to_string())
 }
@@ -104,12 +106,17 @@ fn relative_components(path: &[u8]) -> Result<Vec<CString>, String> {
 
 fn open_directory_at(parent: RawFd, name: &CString, create: bool) -> Result<OwnedFd, String> {
     let flags = libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_DIRECTORY;
+    // SAFETY: `parent` is an open directory fd and `name` is a NUL-terminated
+    // single path component validated by `validate_component`.
     let mut fd = unsafe { libc::openat(parent, name.as_ptr(), flags) };
     if fd < 0 && create && io::Error::last_os_error().raw_os_error() == Some(libc::ENOENT) {
+        // SAFETY: same descriptor/name invariant as the surrounding `openat`.
         let mkdir_result = unsafe { libc::mkdirat(parent, name.as_ptr(), DIRECTORY_MODE) };
         if mkdir_result < 0 && io::Error::last_os_error().raw_os_error() != Some(libc::EEXIST) {
             return Err(format!("mkdirat failed: {}", io::Error::last_os_error()));
         }
+        // Directory creation has completed or raced with another creator.
+        // SAFETY: reopen the same validated component without following symlinks.
         fd = unsafe { libc::openat(parent, name.as_ptr(), flags) };
     }
     if fd < 0 {
@@ -118,10 +125,14 @@ fn open_directory_at(parent: RawFd, name: &CString, create: bool) -> Result<Owne
             io::Error::last_os_error()
         ));
     }
+    // SAFETY: `fd` is a fresh successful `openat` result and ownership moves
+    // into `OwnedFd` exactly once.
     Ok(unsafe { OwnedFd::from_raw_fd(fd) })
 }
 
 fn duplicate_fd(fd: RawFd) -> Result<OwnedFd, String> {
+    // SAFETY: `fd` is supplied by an `OwnedFd` or retained valid descriptor;
+    // `F_DUPFD_CLOEXEC` returns a new owned descriptor on success.
     let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
     if duplicate < 0 {
         Err(format!(
@@ -129,6 +140,8 @@ fn duplicate_fd(fd: RawFd) -> Result<OwnedFd, String> {
             io::Error::last_os_error()
         ))
     } else {
+        // SAFETY: `duplicate` is a fresh successful `fcntl` result and
+        // ownership moves into `OwnedFd` exactly once.
         Ok(unsafe { OwnedFd::from_raw_fd(duplicate) })
     }
 }
@@ -150,7 +163,7 @@ fn canonical_components(path: &Path) -> Result<Vec<CString>, String> {
         match component {
             Component::RootDir => {}
             Component::Normal(name) => out.push(validate_component(name.as_bytes())?),
-            _ => return Err("canonical destination root contains an unsafe component".to_string()),
+            _ => return Err("canonical destination root contains an invalid component".to_string()),
         }
     }
     Ok(out)
@@ -200,6 +213,8 @@ fn open_root(path: &Path, create: bool) -> Result<OwnedFd, String> {
     };
     let root_name = CString::new("/").expect("literal has no NUL");
     let flags = libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_DIRECTORY;
+    // SAFETY: `root_name` is a static NUL-terminated path, and successful fd
+    // ownership is transferred to `OwnedFd` below.
     let root_fd = unsafe { libc::open(root_name.as_ptr(), flags) };
     if root_fd < 0 {
         return Err(format!(
@@ -207,6 +222,8 @@ fn open_root(path: &Path, create: bool) -> Result<OwnedFd, String> {
             io::Error::last_os_error()
         ));
     }
+    // SAFETY: `root_fd` is a fresh successful `open` result and ownership
+    // moves into `OwnedFd` exactly once.
     let mut current = unsafe { OwnedFd::from_raw_fd(root_fd) };
     for component in canonical_components(&existing)? {
         current = open_directory_at(current.as_raw_fd(), &component, false)?;
@@ -264,6 +281,8 @@ struct OutputTransaction {
 impl Drop for OutputTransaction {
     fn drop(&mut self) {
         if !self.published {
+            // SAFETY: `parent` is an owned directory fd and `temporary_name` is
+            // the validated generated name created by this transaction.
             unsafe {
                 libc::unlinkat(self.parent.as_raw_fd(), self.temporary_name.as_ptr(), 0);
             }
@@ -289,6 +308,8 @@ fn create_output(
         let temporary_name = CString::new(name).expect("generated name has no NUL");
         let flags =
             libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW;
+        // SAFETY: `parent` is an open directory fd and `temporary_name` is a
+        // generated NUL-terminated component without path separators.
         let fd = unsafe {
             libc::openat(
                 parent.as_raw_fd(),
@@ -306,6 +327,8 @@ fn create_output(
                 io::Error::last_os_error()
             ));
         }
+        // SAFETY: `fd` is a fresh successful `openat` result and ownership
+        // moves into `OwnedFd` exactly once.
         let file = unsafe { OwnedFd::from_raw_fd(fd) };
         let writer = duplicate_fd(file.as_raw_fd())?;
         let writer_fd = writer.as_raw_fd();
@@ -350,7 +373,10 @@ fn split_parent(root_handle: u64, path: &[u8], create: bool) -> Result<(OwnedFd,
 }
 
 fn stat_fd(fd: RawFd) -> Result<libc::stat, String> {
+    // SAFETY: zeroed is valid for `libc::stat`, which is immediately filled by
+    // `fstat` before any field is read.
     let mut stat: libc::stat = unsafe { std::mem::zeroed() };
+    // SAFETY: `fd` is a live descriptor and `stat` points to writable storage.
     if unsafe { libc::fstat(fd, &mut stat) } < 0 {
         Err(format!("fstat failed: {}", io::Error::last_os_error()))
     } else {
@@ -359,7 +385,11 @@ fn stat_fd(fd: RawFd) -> Result<libc::stat, String> {
 }
 
 fn stat_at(parent: RawFd, name: &CString) -> Result<libc::stat, String> {
+    // SAFETY: zeroed is valid for `libc::stat`, which is immediately filled by
+    // `fstatat` before any field is read.
     let mut stat: libc::stat = unsafe { std::mem::zeroed() };
+    // SAFETY: `parent` is a live directory fd, `name` is NUL-terminated, and
+    // `stat` points to writable storage.
     if unsafe { libc::fstatat(parent, name.as_ptr(), &mut stat, libc::AT_SYMLINK_NOFOLLOW) } < 0 {
         Err(format!("fstatat failed: {}", io::Error::last_os_error()))
     } else {
@@ -422,6 +452,8 @@ pub extern "C" fn jerboa_secure_dir_open(
         }
         let handle = next_handle();
         store.insert(handle, fd);
+        // SAFETY: `handle_out` was checked non-null and points to caller-owned
+        // storage for one `u64`.
         unsafe { *handle_out = handle };
         0
     })
@@ -453,6 +485,8 @@ pub extern "C" fn jerboa_secure_dir_open_strict(
         }
         let handle = next_handle();
         store.insert(handle, fd);
+        // SAFETY: `handle_out` was checked non-null and points to caller-owned
+        // storage for one `u64`.
         unsafe { *handle_out = handle };
         0
     })
@@ -542,6 +576,8 @@ pub extern "C" fn jerboa_secure_entry_info(
                 Err(error) => return fail(error),
             }
         };
+        // SAFETY: all output pointers were checked non-null and each points to
+        // caller-owned storage for one scalar result.
         unsafe {
             *kind_out = file_kind(&stat);
             *device_out = stat.st_dev as u64;
@@ -565,6 +601,8 @@ fn read_directory_names(
     struct DirectoryStream(*mut libc::DIR);
     impl Drop for DirectoryStream {
         fn drop(&mut self) {
+            // SAFETY: `DirectoryStream` owns the DIR pointer returned by
+            // `fdopendir` and closes it exactly once in Drop.
             unsafe { libc::closedir(self.0) };
         }
     }
@@ -573,8 +611,11 @@ fn read_directory_names(
         return Err("directory entry limit must be 1..=1000000".to_string());
     }
     let raw_fd = directory.into_raw_fd();
+    // SAFETY: `raw_fd` is transferred out of `OwnedFd`; `fdopendir` takes
+    // ownership on success and `DirectoryStream` closes it.
     let stream_ptr = unsafe { libc::fdopendir(raw_fd) };
     if stream_ptr.is_null() {
+        // SAFETY: `fdopendir` failed, so ownership of `raw_fd` remains here.
         unsafe { libc::close(raw_fd) };
         return Err(format!("fdopendir failed: {}", io::Error::last_os_error()));
     }
@@ -584,12 +625,17 @@ fn read_directory_names(
     let mut count = 0usize;
     loop {
         #[cfg(any(target_os = "macos", target_os = "freebsd"))]
+        // SAFETY: libc exposes the thread-local errno slot for this platform.
         let errno = unsafe { libc::__error() };
         #[cfg(any(target_os = "linux", target_os = "android"))]
+        // SAFETY: libc exposes the thread-local errno slot for this platform.
         let errno = unsafe { libc::__errno_location() };
+        // SAFETY: `errno` points to the current thread's errno storage.
         unsafe { *errno = 0 };
+        // SAFETY: `stream.0` is a live DIR pointer owned by `DirectoryStream`.
         let entry = unsafe { libc::readdir(stream.0) };
         if entry.is_null() {
+            // SAFETY: `errno` still points to the current thread's errno storage.
             let value = unsafe { *errno };
             if value != 0 {
                 return Err(format!(
@@ -599,6 +645,8 @@ fn read_directory_names(
             }
             break;
         }
+        // SAFETY: a non-null `dirent` from `readdir` contains a NUL-terminated
+        // `d_name` valid until the next call on the same stream.
         let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
         if name == b"." || name == b".." {
             continue;
@@ -659,6 +707,8 @@ pub extern "C" fn jerboa_secure_dir_list(
             Ok(names) => names,
             Err(error) => return fail(error),
         };
+        // SAFETY: `output` and `output_len` were checked non-null, and
+        // `read_directory_names` bounded `names.len()` by `output_capacity`.
         unsafe {
             std::ptr::copy_nonoverlapping(names.as_ptr(), output, names.len());
             *output_len = names.len();
@@ -698,10 +748,14 @@ pub extern "C" fn jerboa_secure_read_file(
             Err(error) => return fail(error),
         };
         let flags = libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW;
+        // SAFETY: `parent` is an open directory fd and `name` is a validated
+        // NUL-terminated single path component.
         let fd = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), flags) };
         if fd < 0 {
             return os_error("open input");
         }
+        // SAFETY: `fd` is a fresh successful `openat` result and ownership
+        // moves into `OwnedFd` exactly once.
         let owned = unsafe { OwnedFd::from_raw_fd(fd) };
         let initial = match stat_fd(owned.as_raw_fd()) {
             Ok(stat) => stat,
@@ -717,6 +771,8 @@ pub extern "C" fn jerboa_secure_read_file(
             return fail("secure input exceeds its byte limit");
         }
         let mut file: fs::File = owned.into();
+        // SAFETY: `output` is non-null and writable for `output_capacity`
+        // bytes, checked at the ABI boundary above.
         let buffer = unsafe { std::slice::from_raw_parts_mut(output, output_capacity) };
         let mut used = 0usize;
         while used < output_capacity {
@@ -745,6 +801,8 @@ pub extern "C" fn jerboa_secure_read_file(
         {
             return fail("secure input identity changed while it was read");
         }
+        // SAFETY: all output pointers were checked non-null and each points to
+        // caller-owned storage for one scalar result.
         unsafe {
             *output_len = used;
             *device_out = initial.st_dev as u64;
@@ -797,11 +855,15 @@ pub extern "C" fn jerboa_secure_output_begin(
         };
         let mut store = outputs();
         if store.len() >= MAX_OPEN_HANDLES {
+            // SAFETY: `writer_fd` is a duplicate descriptor returned by
+            // `create_output` and has not been handed to the caller.
             unsafe { libc::close(writer_fd) };
             return fail("too many secure output transactions");
         }
         let handle = next_handle();
         store.insert(handle, transaction);
+        // SAFETY: output pointers were checked non-null and point to
+        // caller-owned scalar storage.
         unsafe {
             *transaction_out = handle;
             *fd_out = writer_fd;
@@ -849,11 +911,15 @@ pub extern "C" fn jerboa_secure_output_begin_checked(
             };
         let mut store = outputs();
         if store.len() >= MAX_OPEN_HANDLES {
+            // SAFETY: `writer_fd` is a duplicate descriptor returned by
+            // `create_output` and has not been handed to the caller.
             unsafe { libc::close(writer_fd) };
             return fail("too many secure output transactions");
         }
         let handle = next_handle();
         store.insert(handle, transaction);
+        // SAFETY: output pointers were checked non-null and point to
+        // caller-owned scalar storage.
         unsafe {
             *transaction_out = handle;
             *fd_out = writer_fd;
@@ -870,6 +936,8 @@ pub extern "C" fn jerboa_secure_output_commit(handle: u64) -> i32 {
             Some(transaction) => transaction,
             None => return fail("invalid secure output transaction"),
         };
+        // SAFETY: transaction file fd is owned and live until this function
+        // returns or drops the transaction.
         if unsafe { libc::fsync(transaction.file.as_raw_fd()) } < 0 {
             return os_error("fsync temporary output");
         }
@@ -883,6 +951,8 @@ pub extern "C" fn jerboa_secure_output_commit(handle: u64) -> i32 {
                 return fail(error);
             }
         }
+        // SAFETY: both directory fd arguments are the live transaction parent,
+        // and temporary/final names are validated NUL-terminated components.
         if unsafe {
             libc::renameat(
                 transaction.parent.as_raw_fd(),
@@ -895,6 +965,7 @@ pub extern "C" fn jerboa_secure_output_commit(handle: u64) -> i32 {
             return os_error("publish output");
         }
         transaction.published = true;
+        // SAFETY: transaction parent directory fd is owned and live here.
         if unsafe { libc::fsync(transaction.parent.as_raw_fd()) } < 0 {
             return os_error("fsync output directory");
         }
@@ -918,6 +989,8 @@ pub extern "C" fn jerboa_secure_fd_close(fd: i32) -> i32 {
         if fd < 0 {
             return 0;
         }
+        // SAFETY: caller transfers ownership of descriptors returned by
+        // `jerboa_secure_output_begin`; invalid close errors are reported.
         if unsafe { libc::close(fd) } < 0 {
             os_error("close output descriptor")
         } else {
@@ -1120,6 +1193,7 @@ mod tests {
 
         fs::rename(root_dir.0.join("safe"), root_dir.0.join("retained")).unwrap();
         symlink(&outside.0, root_dir.0.join("safe")).unwrap();
+        // SAFETY: test owns `fd` returned by `begin` and converts it once.
         let mut writer = unsafe { fs::File::from_raw_fd(fd) };
         writer.write_all(b"pinned parent").unwrap();
         writer.flush().unwrap();
@@ -1143,6 +1217,7 @@ mod tests {
         symlink(&target, root_dir.0.join("result")).unwrap();
         let root = open_test_root(&root_dir.0);
         let (transaction, fd) = begin(root, b"result");
+        // SAFETY: test owns `fd` returned by `begin` and converts it once.
         let mut writer = unsafe { fs::File::from_raw_fd(fd) };
         writer.write_all(b"inside").unwrap();
         drop(writer);
@@ -1196,6 +1271,7 @@ mod tests {
         let root = open_test_root(&root_dir.0);
         let (_, device, inode) = read_file(root, b"target.txt", 64).unwrap();
         let (transaction, fd) = begin_checked(root, b"target.txt", device, inode);
+        // SAFETY: test owns `fd` returned by `begin_checked` and converts it once.
         let mut writer = unsafe { fs::File::from_raw_fd(fd) };
         writer.write_all(b"replacement").unwrap();
         drop(writer);
@@ -1216,6 +1292,7 @@ mod tests {
         let root = open_test_root(&root_dir.0);
         let (_, device, inode) = read_file(root, b"target.txt", 64).unwrap();
         let (transaction, fd) = begin_checked(root, b"target.txt", device, inode);
+        // SAFETY: test owns `fd` returned by `begin_checked` and converts it once.
         let mut writer = unsafe { fs::File::from_raw_fd(fd) };
         writer.write_all(b"replacement").unwrap();
         drop(writer);
diff --git a/jerboa-native-rs/src/sqlite.rs b/jerboa-native-rs/src/sqlite.rs
index bdd6d30..44b32cd 100644
--- a/jerboa-native-rs/src/sqlite.rs
+++ b/jerboa-native-rs/src/sqlite.rs
@@ -36,6 +36,7 @@ struct StmtEntry {
 // SAFETY: We protect all access with Mutex, and sqlite3/sqlite3_stmt are
 // thread-safe when accessed serially (which the mutex ensures).
 unsafe impl Send for DbEntry {}
+// SAFETY: same mutex-serialized access invariant as `DbEntry`.
 unsafe impl Send for StmtEntry {}
 
 fn next_id() -> u64 {
@@ -74,9 +75,9 @@ fn ffi_bytes<'a>(ptr: *const u8, len: usize, label: &str) -> Result<&'a [u8], ()
         set_last_error(format!("{label} length exceeds Rust slice limit"));
         return Err(());
     }
-    // SAFETY: The C ABI caller supplies `ptr`/`len`. We reject null nonempty
-    // buffers and lengths above `isize::MAX`; the caller remains responsible
-    // for providing a readable buffer for the duration of this call.
+    // The C ABI caller supplies `ptr`/`len`. We reject null nonempty buffers
+    // and lengths above `isize::MAX`.
+    // SAFETY: caller provides a readable buffer for the duration of this call.
     Ok(unsafe { std::slice::from_raw_parts(ptr, len) })
 }
 
@@ -116,14 +117,13 @@ fn copy_to_output(bytes: &[u8], output: *mut u8, output_max: usize, output_len: 
     }
     let copy_len = bytes.len().min(output_max);
     if copy_len != 0 {
-        // SAFETY: `output` is non-null when `output_max` is nonzero, and the
-        // caller promises it is writable for `output_max` bytes. We copy only
-        // `copy_len <= output_max` bytes from a Rust-owned source slice.
+        // `output` is non-null when `output_max` is nonzero, and the caller
+        // promises it is writable for `output_max` bytes.
+        // SAFETY: copy length is bounded by the caller-provided capacity.
         let out = unsafe { std::slice::from_raw_parts_mut(output, output_max) };
         out[..copy_len].copy_from_slice(&bytes[..copy_len]);
     }
-    // SAFETY: `output_len` was checked non-null and points to caller-owned
-    // storage for one `usize`.
+    // SAFETY: `output_len` was checked non-null for one caller-owned `usize`.
     unsafe {
         *output_len = bytes.len();
     }
@@ -204,9 +204,8 @@ pub extern "C" fn jerboa_sqlite_close(handle: u64) -> i32 {
             .collect();
         for k in to_remove {
             if let Some(entry) = stmts.remove(&k) {
-                // SAFETY: statement handles are inserted only after successful
-                // sqlite3_prepare_v2 and removed at most once while the store
-                // mutex serializes access.
+                // Statement handles are inserted only after successful prepare.
+                // SAFETY: removal/finalization is serialized by the store lock.
                 unsafe {
                     rusqlite::ffi::sqlite3_finalize(entry.raw);
                 }
@@ -290,10 +289,9 @@ pub extern "C" fn jerboa_sqlite_prepare(
         };
 
         let mut raw_stmt: RawStmt = std::ptr::null_mut();
-        // SAFETY: `entry.raw` is a live sqlite3 pointer kept alive by
-        // `DbEntry` while the store lock is held. `sql_str.as_ptr()` is valid
-        // for `sql_len_i32` bytes until this call returns; SQLite prepares the
-        // statement synchronously and stores its own compiled representation.
+        // `entry.raw` is live while the store lock is held; SQL bytes remain
+        // valid until SQLite returns from synchronous preparation.
+        // SAFETY: SQLite stores its own compiled representation before return.
         let rc = unsafe {
             rusqlite::ffi::sqlite3_prepare_v2(
                 entry.raw,
@@ -420,9 +418,8 @@ pub extern "C" fn jerboa_sqlite_bind_text(
                 return -1;
             }
         };
-        // SAFETY: `entry.raw` is live while the statement-store lock is held.
-        // `text_str.as_ptr()` is readable for `text_len_i32` bytes, and
-        // SQLITE_TRANSIENT tells SQLite to copy the bytes before returning.
+        // `entry.raw` is live while the statement-store lock is held.
+        // SAFETY: SQLITE_TRANSIENT copies readable text bytes before return.
         let rc = unsafe {
             rusqlite::ffi::sqlite3_bind_text64(
                 entry.raw,
@@ -467,9 +464,8 @@ pub extern "C" fn jerboa_sqlite_bind_blob(
                 return -1;
             }
         };
-        // SAFETY: `entry.raw` is live while the statement-store lock is held.
-        // `data.as_ptr()` is readable for `data_len_i32` bytes, and
-        // SQLITE_TRANSIENT tells SQLite to copy the bytes before returning.
+        // `entry.raw` is live while the statement-store lock is held.
+        // SAFETY: SQLITE_TRANSIENT copies readable blob bytes before return.
         let rc = unsafe {
             rusqlite::ffi::sqlite3_bind_blob64(
                 entry.raw,
@@ -625,9 +621,8 @@ pub extern "C" fn jerboa_sqlite_column_text(
             Some(e) => e,
             None => return -1,
         };
-        // SAFETY: statement handles are live while the store lock is held. The
-        // returned SQLite text pointer is valid until the next statement step,
-        // reset, finalize, or type conversion; we copy it before returning.
+        // SQLite text pointers are valid until the next statement change.
+        // SAFETY: statement is live under lock and bytes are copied immediately.
         let ptr = unsafe { rusqlite::ffi::sqlite3_column_text(entry.raw, col) };
         if ptr.is_null() {
             return copy_to_output(&[], output, output_max, output_len);
@@ -656,9 +651,8 @@ pub extern "C" fn jerboa_sqlite_column_blob(
             Some(e) => e,
             None => return -1,
         };
-        // SAFETY: statement handles are live while the store lock is held. The
-        // returned blob pointer is valid until the next statement step/reset or
-        // finalize; we copy it before returning.
+        // SQLite blob pointers are valid until the next statement change.
+        // SAFETY: statement is live under lock and bytes are copied immediately.
         let blob_ptr = unsafe { rusqlite::ffi::sqlite3_column_blob(entry.raw, col) };
         // SAFETY: same live statement invariant as above; SQLite reports the
         // byte count for the current column value.
@@ -698,9 +692,8 @@ pub extern "C" fn jerboa_sqlite_column_name(
             Some(e) => e,
             None => return -1,
         };
-        // SAFETY: statement handles are live while the store lock is held. The
-        // returned static/statement-owned column-name pointer is copied before
-        // returning.
+        // SQLite column-name pointers are statement-owned.
+        // SAFETY: statement is live under lock and bytes are copied immediately.
         let ptr = unsafe { rusqlite::ffi::sqlite3_column_name(entry.raw, col) };
         if ptr.is_null() {
             return copy_to_output(&[], output, output_max, output_len);
@@ -746,9 +739,8 @@ pub extern "C" fn jerboa_sqlite_finalize(stmt_handle: u64) -> i32 {
         };
         match store.remove(&stmt_handle) {
             Some(entry) => {
-                // SAFETY: statement handles are inserted only after successful
-                // sqlite3_prepare_v2 and removed at most once while the store
-                // mutex serializes access.
+                // Statement handles are inserted only after successful prepare.
+                // SAFETY: removal/finalization is serialized by the store lock.
                 unsafe {
                     rusqlite::ffi::sqlite3_finalize(entry.raw);
                 }