security: close blocking ffi collect-safe audit

ober

6240d9da1217a96e51d322f86257504445e41525

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