security: deny implicit unsafe ops in native crate

Jaime Fournier

6e8fb5bd9ea4b872af3ab02c111702983bde768e

diff --git a/docs/ffi-audit.md b/docs/ffi-audit.md
index 93f9214..b3d0070 100644
--- a/docs/ffi-audit.md
+++ b/docs/ffi-audit.md
@@ -52,6 +52,10 @@ current release gate has a machine-readable count of Scheme FFI sites, native
 C/Rust files, Rust C ABI exports, pointer/width-sensitive bindings, blocking
 candidate calls, and unsafe Rust 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.
+
 Remaining work before closing K3-P1-01:
 
 - Review every `foreign-procedure`, `foreign-callable`, and `define-ftype`
@@ -60,8 +64,7 @@ Remaining work before closing K3-P1-01:
   nonblocking.
 - Add targeted scanner rules for null-return checks, integer width confusion,
   caller-supplied length trust, and bytevector pointer lifetime hazards.
-- Add `SAFETY:` invariant comments near Rust unsafe sites and enable
-  `#![deny(unsafe_op_in_unsafe_fn)]` where supported.
+- Add `SAFETY:` invariant comments near Rust unsafe sites.
 - Reconcile the 231 Rust `#[no_mangle]` exports with live Scheme callers and
   unexport or document unused exports.
 - 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 7939769..edbd219 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -597,9 +597,11 @@ not started."
 - **Status:** started. `docs/ffi-audit.md` and `tools/ffi-audit-report.ss`
   now provide a reproducible inventory for Scheme FFI sites, Rust C ABI
   exports, pointer/width-sensitive bindings, blocking candidates, and unsafe
-  Rust sites. Remaining work: per-binding verdicts, scanner rules for
-  null/width/bounds/GC-safety hazards, unsafe invariant comments, export
-  shrinking/justification, and the `vendor/jsqlite` CVE/replacement decision.
+  Rust sites. `jerboa-native-rs` now denies `unsafe_op_in_unsafe_fn`, so unsafe
+  function bodies do not implicitly permit unsafe operations. Remaining work:
+  per-binding verdicts, scanner rules for null/width/bounds/GC-safety hazards,
+  unsafe invariant comments, export shrinking/justification, and the
+  `vendor/jsqlite` CVE/replacement decision.
 
 ### K3-P1-02 — TOCTOU-safe filesystem capability checks
 **Serves:** G2. **Effort:** 1 week.
diff --git a/jerboa-native-rs/src/http_parse.rs b/jerboa-native-rs/src/http_parse.rs
index 52f6659..18bb065 100644
--- a/jerboa-native-rs/src/http_parse.rs
+++ b/jerboa-native-rs/src/http_parse.rs
@@ -27,8 +27,10 @@ pub unsafe extern "C" fn jerboa_http_parse(buf: *const u8, buf_len: usize, out: 
         return -1;
     }
 
-    let data = std::slice::from_raw_parts(buf, buf_len);
-    let out_slice = std::slice::from_raw_parts_mut(out, PARSE_OUT_SIZE);
+    // SAFETY: null pointers are rejected above; the FFI caller must pass
+    // readable input bytes and a writable PARSE_OUT_SIZE output buffer.
+    let data = unsafe { std::slice::from_raw_parts(buf, buf_len) };
+    let out_slice = unsafe { std::slice::from_raw_parts_mut(out, PARSE_OUT_SIZE) };
 
     let mut headers_storage = [httparse::EMPTY_HEADER; MAX_HEADERS];
     let mut req = httparse::Request::new(&mut headers_storage);
@@ -116,5 +118,7 @@ pub unsafe extern "C" fn jerboa_writev2(
         },
     ];
     let count = if use_two { 2 } else { 1 };
-    libc::writev(fd, iovs.as_ptr(), count)
+    // SAFETY: buf1 is non-null with len1 > 0; buf2 is included only when
+    // non-null with len2 > 0; iovs points to count initialized entries.
+    unsafe { libc::writev(fd, iovs.as_ptr(), count) }
 }
diff --git a/jerboa-native-rs/src/lib.rs b/jerboa-native-rs/src/lib.rs
index 766ab1f..4c507f2 100644
--- a/jerboa-native-rs/src/lib.rs
+++ b/jerboa-native-rs/src/lib.rs
@@ -1,3 +1,5 @@
+#![deny(unsafe_op_in_unsafe_fn)]
+
 mod antidebug;
 mod aproc;
 mod compress;
diff --git a/jerboa-native-rs/src/secure_mem.rs b/jerboa-native-rs/src/secure_mem.rs
index 00727f2..cc8439d 100644
--- a/jerboa-native-rs/src/secure_mem.rs
+++ b/jerboa-native-rs/src/secure_mem.rs
@@ -17,7 +17,9 @@ fn round_up_to_page(size: usize, page_size: usize) -> Option<usize> {
 
 unsafe fn secure_zero(ptr: *mut u8, size: usize) {
     for i in 0..size {
-        ptr.add(i).write_volatile(0);
+        // SAFETY: caller guarantees ptr points to a writable allocation of at
+        // least size bytes; i is bounded by 0..size.
+        unsafe { ptr.add(i).write_volatile(0) };
     }
     std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst);
 }