security: document wasm landlock aproc unsafe invariants

ober

6be6f8aa7ea01b50b02684b1332643a0270a7e38

diff --git a/docs/ffi-audit.md b/docs/ffi-audit.md
index 1d78a91..8e75b75 100644
--- a/docs/ffi-audit.md
+++ b/docs/ffi-audit.md
@@ -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 321)
-    (unsafe-sites-without-nearby-safety-comment 48)
-    (unsafe-comment-review-sites 48)
+    (unsafe-sites-with-nearby-safety-comment 335)
+    (unsafe-sites-without-nearby-safety-comment 34)
+    (unsafe-comment-review-sites 34)
     (export-review-sites 196))
   (vendor
     (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated)))
@@ -127,7 +127,11 @@ has Linux-only regression tests for syscall-free input rejection. `wasm.rs`
 now bounds module/function/path/log buffers, converts poisoned
 handle-store locks into normal FFI errors, rejects null argument/result buffers
 with nonzero lengths, checks guest-memory offsets with overflow-safe helpers,
-and has Rust regressions for invalid C ABI inputs. `duckdb_native.rs` now
+documents socket-fd adoption and caller-buffer 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
 funnels path, SQL, text, and blob inputs through checked C ABI slice helpers,
 converts poisoned database/statement/result store locks into normal FFI errors,
 rejects null result and output slots, checks result indexes before converting
@@ -155,7 +159,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 321 annotated native unsafe sites and 48 remaining unsafe review sites.
+reports 335 annotated native unsafe sites and 34 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 53a2cb2..e1baa8f 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -678,8 +678,11 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit
   input rejection. `wasm.rs` now bounds module/function/path/log buffers,
   converts poisoned handle-store locks into normal FFI errors, rejects null
   argument/result buffers with nonzero lengths, checks guest-memory offsets
-  with overflow-safe helpers, and has Rust regressions for invalid C ABI
-  inputs. `duckdb_native.rs` now validates path, SQL, text, blob, result-slot,
+  with overflow-safe helpers, documents socket-fd adoption and caller-buffer
+  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
   SAN/path/PEM inputs through checked UTF-8 helpers, centralizes PEM and
@@ -697,8 +700,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 321
-  annotated native unsafe sites and 48 remaining unsafe review sites.
+  shutdown invariants for blocked TLS I/O. The generated report now shows 335
+  annotated native unsafe sites and 34 remaining unsafe review sites.
   Remaining work: continue unsafe invariant comments across the rest of
   `jerboa-native-rs`.
 
@@ -1188,7 +1191,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 | 48 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 |
+| Un-annotated Rust `unsafe` blocks | 34 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) |
diff --git a/docs/reviews/2026-07-27-native-export-review.sexp b/docs/reviews/2026-07-27-native-export-review.sexp
index d1af475..e4648d9 100644
--- a/docs/reviews/2026-07-27-native-export-review.sexp
+++ b/docs/reviews/2026-07-27-native-export-review.sexp
@@ -19,11 +19,11 @@
     (export (symbol "jerboa_antidebug_check_tracer") (file "jerboa-native-rs/src/antidebug.rs") (line 37) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 38))))
     (export (symbol "jerboa_antidebug_ptrace") (file "jerboa-native-rs/src/antidebug.rs") (line 8) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 36))))
     (export (symbol "jerboa_antidebug_timing_check") (file "jerboa-native-rs/src/antidebug.rs") (line 130) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 44))))
-    (export (symbol "jerboa_aproc_killpg") (file "jerboa-native-rs/src/aproc.rs") (line 675) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 155))))
-    (export (symbol "jerboa_aproc_set_nonblock") (file "jerboa-native-rs/src/aproc.rs") (line 645) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 151))))
+    (export (symbol "jerboa_aproc_killpg") (file "jerboa-native-rs/src/aproc.rs") (line 674) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 155))))
+    (export (symbol "jerboa_aproc_set_nonblock") (file "jerboa-native-rs/src/aproc.rs") (line 644) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 151))))
     (export (symbol "jerboa_aproc_spawn") (file "jerboa-native-rs/src/aproc.rs") (line 152) (scheme-callers 2 (caller (file "lib/std/os/aproc.ss") (line 127)) (caller (file "lib/std/os/aproc.ss") (line 142))))
     (export (symbol "jerboa_aproc_spawn_pty") (file "jerboa-native-rs/src/aproc.rs") (line 374) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 142))))
-    (export (symbol "jerboa_aproc_wait4") (file "jerboa-native-rs/src/aproc.rs") (line 691) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 159))))
+    (export (symbol "jerboa_aproc_wait4") (file "jerboa-native-rs/src/aproc.rs") (line 690) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 159))))
     (export (symbol "jerboa_argon2id_hash") (file "jerboa-native-rs/src/crypto.rs") (line 814) (scheme-callers 2 (caller (file "lib/std/crypto/native-rust.ss") (line 276)) (caller (file "lib/std/crypto/password.ss") (line 35))))
     (export (symbol "jerboa_argon2id_verify") (file "jerboa-native-rs/src/crypto.rs") (line 867) (scheme-callers 2 (caller (file "lib/std/crypto/native-rust.ss") (line 296)) (caller (file "lib/std/crypto/password.ss") (line 42))))
     (export (symbol "jerboa_chacha20_open") (file "jerboa-native-rs/src/crypto.rs") (line 606) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 204))))
@@ -78,10 +78,10 @@
     (export (symbol "jerboa_integrity_sign_verify") (file "jerboa-native-rs/src/integrity.rs") (line 141) (scheme-callers 1 (caller (file "lib/std/os/integrity.ss") (line 57))))
     (export (symbol "jerboa_integrity_verify_hash") (file "jerboa-native-rs/src/integrity.rs") (line 101) (scheme-callers 1 (caller (file "lib/std/os/integrity.ss") (line 55))))
     (export (symbol "jerboa_landlock_abi_version") (file "jerboa-native-rs/src/landlock.rs") (line 68) (scheme-callers 6 (caller (file "lib/std/os/landlock-native.ss") (line 78)) (caller (file "lib/std/os/landlock.ss") (line 12)) (caller (file "lib/std/os/landlock.ss") (line 56)) (caller (file "lib/std/os/landlock.ss") (line 57)) (caller (file "lib/std/os/limits/sandbox.ss") (line 455)) (caller (file "lib/std/os/sandbox.ss") (line 75))))
-    (export (symbol "jerboa_landlock_add_net_rule") (file "jerboa-native-rs/src/landlock.rs") (line 235) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 86))))
+    (export (symbol "jerboa_landlock_add_net_rule") (file "jerboa-native-rs/src/landlock.rs") (line 234) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 86))))
     (export (symbol "jerboa_landlock_add_path_rule") (file "jerboa-native-rs/src/landlock.rs") (line 129) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 83))))
     (export (symbol "jerboa_landlock_create_ruleset") (file "jerboa-native-rs/src/landlock.rs") (line 97) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 80))))
-    (export (symbol "jerboa_landlock_enforce") (file "jerboa-native-rs/src/landlock.rs") (line 277) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 89))))
+    (export (symbol "jerboa_landlock_enforce") (file "jerboa-native-rs/src/landlock.rs") (line 276) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 89))))
     (export (symbol "jerboa_last_error") (file "jerboa-native-rs/src/panic.rs") (line 9) (scheme-callers 11 (caller (file "lib/std/compress/native-rust.ss") (line 42)) (caller (file "lib/std/crypto/native-rust.ss") (line 48)) (caller (file "lib/std/crypto/x509.ss") (line 24)) (caller (file "lib/std/native.ss") (line 56)) (caller (file "lib/std/net/tls-rustls.ss") (line 167)) (caller (file "lib/std/os/aproc.ss") (line 164)) (caller (file "lib/std/os/integrity.ss") (line 42)) (caller (file "lib/std/os/secure-output.ss") (line 47)) (caller (file "lib/std/pcap.ss") (line 66)) (caller (file "lib/std/regex-native.ss") (line 51)) (caller (file "lib/std/wasm/sandbox.ss") (line 196))))
     (export (symbol "jerboa_pbkdf2_derive") (file "jerboa-native-rs/src/crypto.rs") (line 740) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 246))))
     (export (symbol "jerboa_pbkdf2_verify") (file "jerboa-native-rs/src/crypto.rs") (line 773) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 261))))
diff --git a/docs/status.md b/docs/status.md
index 9aa2e4b..ffb8db7 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`, 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 48. | 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 34. | 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/aproc.rs b/jerboa-native-rs/src/aproc.rs
index 20925ca..d4c9bd2 100644
--- a/jerboa-native-rs/src/aproc.rs
+++ b/jerboa-native-rs/src/aproc.rs
@@ -265,9 +265,9 @@ pub extern "C" fn jerboa_aproc_spawn(
         // — the raw pointer + count would not satisfy `'static` bounds.
         let inh_pairs: Vec<(i32, i32)> = if inherit_fd_count > 0 && !inherit_fds.is_null() {
             let mut v = Vec::with_capacity(inherit_fd_count);
-            // SAFETY: `inherit_fd_count` is a pair count from the Scheme
-            // caller; the ABI requires exactly two i32 values per pair, and the
-            // pairs are copied into an owned Vec for pre_exec.
+            // `inherit_fd_count` is a pair count from the Scheme caller; the
+            // ABI requires exactly two i32 values per pair.
+            // SAFETY: inherited fd pairs are copied into an owned Vec.
             let slice = unsafe { std::slice::from_raw_parts(inherit_fds, inherit_fd_count * 2) };
             for i in 0..inherit_fd_count {
                 v.push((slice[i * 2], slice[i * 2 + 1]));
@@ -284,9 +284,9 @@ pub extern "C" fn jerboa_aproc_spawn(
             }
         };
 
-        // SAFETY: CommandExt::pre_exec runs this closure in the child after
-        // fork and before exec. The closure only performs fd/syscall setup
-        // using async-signal-safe libc calls and data copied into owned Vecs.
+        // CommandExt::pre_exec runs this closure in the child after fork and
+        // before exec.
+        // SAFETY: only async-signal-safe libc calls use copied owned data.
         unsafe {
             cmd.pre_exec(move || {
                 if new_pgroup {
@@ -404,9 +404,9 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
         // openpty(3) gives us master + slave file descriptors.
         let mut master_fd: libc::c_int = -1;
         let mut slave_fd: libc::c_int = -1;
-        // SAFETY: openpty writes two c_int outputs and the optional name,
-        // termios, and winsize pointers are null. On success both fds are
-        // closed or handed to Scheme exactly once along every path below.
+        // openpty writes two c_int outputs; optional name, termios, and winsize
+        // pointers are null.
+        // SAFETY: on success both fds are closed or handed off exactly once.
         let rc = unsafe {
             libc::openpty(
                 &mut master_fd,
@@ -513,9 +513,8 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
         let slave = slave_fd;
         let stderr_w = stderr_write;
         let merge_stderr = stderr_mode == STDIO_MERGE_STDOUT;
-        // SAFETY: CommandExt::pre_exec runs in the child before exec. The
-        // closure uses only async-signal-safe fd/session operations on copied
-        // integers and does not allocate or call back into Rust-owned state.
+        // CommandExt::pre_exec runs in the child before exec.
+        // SAFETY: closure uses only async-signal-safe operations on copied fds.
         unsafe {
             cmd.pre_exec(move || {
                 // Become session leader so we can acquire the pty as controlling tty.
diff --git a/jerboa-native-rs/src/landlock.rs b/jerboa-native-rs/src/landlock.rs
index a13d3a9..faeb0ad 100644
--- a/jerboa-native-rs/src/landlock.rs
+++ b/jerboa-native-rs/src/landlock.rs
@@ -67,9 +67,9 @@ struct LandlockNetPortAttr {
 #[no_mangle]
 pub extern "C" fn jerboa_landlock_abi_version() -> i32 {
     ffi_wrap(|| {
-        // SAFETY: This is the Landlock version-probe ABI: null attr pointer,
-        // zero size, and LANDLOCK_CREATE_RULESET_VERSION in flags. No user
-        // memory is dereferenced by the kernel for this form.
+        // This is the Landlock version-probe ABI: null attr pointer and zero
+        // size with LANDLOCK_CREATE_RULESET_VERSION in flags.
+        // SAFETY: no user memory is dereferenced by the kernel for this form.
         let version = unsafe {
             libc::syscall(
                 SYS_LANDLOCK_CREATE_RULESET,
@@ -100,9 +100,9 @@ pub extern "C" fn jerboa_landlock_create_ruleset(fs_access_mask: u64, net_access
             handled_access_fs: fs_access_mask,
             handled_access_net: net_access_mask,
         };
-        // SAFETY: `attr` is a stack-allocated C-compatible ruleset attribute
-        // with the exact size passed to the syscall and remains live for the
-        // duration of the call.
+        // `attr` is a stack-allocated C-compatible ruleset attribute with the
+        // exact size passed to the syscall.
+        // SAFETY: `attr` remains live for the duration of this kernel call.
         let fd = unsafe {
             libc::syscall(
                 SYS_LANDLOCK_CREATE_RULESET,
@@ -141,9 +141,8 @@ pub extern "C" fn jerboa_landlock_add_path_rule(
             set_last_error("invalid landlock path length".into());
             return -1;
         }
-        // SAFETY: `path` is non-null and `path_len` is bounded to
-        // `isize::MAX`; the slice is immediately copied into an owned
-        // CString before any syscall can retain or outlive it.
+        // `path` is non-null and `path_len` is bounded to `isize::MAX`.
+        // SAFETY: the borrowed path slice is immediately copied into a CString.
         let path_bytes = unsafe { std::slice::from_raw_parts(path, path_len) };
         let path_buf = match CString::new(path_bytes) {
             Ok(value) => value,
@@ -153,9 +152,9 @@ pub extern "C" fn jerboa_landlock_add_path_rule(
             }
         };
 
-        // SAFETY: `path_buf` is NUL-terminated and remains live for the
-        // duration of `open`; flags request an O_PATH descriptor without
-        // following it into owned Rust file abstractions.
+        // `path_buf` is NUL-terminated and remains live for the duration of
+        // `open`.
+        // SAFETY: flags request an O_PATH descriptor owned as a raw fd.
         let parent_fd = unsafe {
             libc::open(
                 path_buf.as_ptr() as *const _,
@@ -201,9 +200,9 @@ pub extern "C" fn jerboa_landlock_add_path_rule(
             allowed_access: effective_access,
             parent_fd,
         };
-        // SAFETY: `attr` is C-compatible and live for the syscall, and its
-        // `parent_fd` field names the open O_PATH descriptor. Landlock copies
-        // the rule data during the syscall.
+        // `attr` is C-compatible and live for the syscall; `parent_fd` names
+        // the open O_PATH descriptor.
+        // SAFETY: Landlock copies the rule data during the syscall.
         let rc = unsafe {
             libc::syscall(
                 SYS_LANDLOCK_ADD_RULE,
diff --git a/jerboa-native-rs/src/wasm.rs b/jerboa-native-rs/src/wasm.rs
index 59a3336..44ceabe 100644
--- a/jerboa-native-rs/src/wasm.rs
+++ b/jerboa-native-rs/src/wasm.rs
@@ -127,9 +127,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 with
-    // nonzero length were rejected above. Callers consume the borrowed slice
-    // entirely within the enclosing C ABI call.
+    // `len` is bounded to `isize::MAX`, and null pointers with nonzero length
+    // were rejected above.
+    // SAFETY: callers consume the borrowed slice within the C ABI call.
     Ok(unsafe { std::slice::from_raw_parts(ptr, len) })
 }
 
@@ -326,9 +326,9 @@ pub extern "C" fn jerboa_wasm_set_socket(instance_handle: u64, fd: i32) -> i32 {
                 return -1;
             }
             use std::os::unix::io::FromRawFd;
-            // SAFETY: caller must ensure fd is a valid, owned UDP socket fd;
-            // this wrapper rejects negative fds and takes ownership exactly
-            // once by storing the resulting UdpSocket in the instance state.
+            // The caller must ensure fd is a valid, owned UDP socket fd; this
+            // wrapper rejects negative fds.
+            // SAFETY: ownership is taken exactly once into instance state.
             let socket = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
             inst.store.data_mut().udp_socket = Some(socket);
             inst.store.data_mut().peer_addr = None;
@@ -509,9 +509,9 @@ pub extern "C" fn jerboa_wasm_call(
                 set_last_error("WASM argument array too large".to_string());
                 return -1;
             }
-            // SAFETY: `args` is non-null, the element count is bounded to keep
-            // the byte length within `isize::MAX`, and the slice is consumed
-            // before this C ABI call returns.
+            // `args` is non-null, and the element count is bounded to keep the
+            // byte length within `isize::MAX`.
+            // SAFETY: the borrowed argument slice is consumed before return.
             let arg_slice = unsafe { std::slice::from_raw_parts(args, nargs) };
             arg_slice
                 .iter()
@@ -562,9 +562,9 @@ pub extern "C" fn jerboa_wasm_call(
                 set_last_error("WASM results array too large".to_string());
                 return -1;
             }
-            // SAFETY: `results` is non-null, the element count is bounded to
-            // keep the byte length within `isize::MAX`, and writes are capped
-            // to the caller-provided result slot count.
+            // `results` is non-null, and the element count is bounded to keep
+            // the byte length within `isize::MAX`.
+            // SAFETY: writes are capped to caller-provided result slots.
             let out = unsafe { std::slice::from_raw_parts_mut(results, nresults) };
             for (i, val) in wasm_results.into_iter().enumerate() {
                 if i >= nresults {
@@ -754,9 +754,9 @@ pub extern "C" fn jerboa_wasm_get_log(handle: u64, buf_ptr: *mut u8, buf_max: us
         let bytes = full.as_bytes();
         if !buf_ptr.is_null() && buf_max > 0 {
             let copy_len = bytes.len().min(buf_max);
-            // SAFETY: `buf_ptr` is non-null for a nonzero buffer, `buf_max`
-            // is bounded to `isize::MAX`, and `copy_len` is capped to both
-            // source and destination lengths.
+            // `buf_ptr` is non-null for a nonzero buffer, and `buf_max` is
+            // bounded to `isize::MAX`.
+            // SAFETY: `copy_len` is capped to both source and destination.
             unsafe {
                 std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf_ptr, copy_len);
             }