security: harden spidermonkey wasm ffi
ober
0d6ee9f3473b86604a4580d69d79f192c0036db8
--- 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 400) - (unsafe-sites-with-nearby-safety-comment 211) - (unsafe-sites-without-nearby-safety-comment 189) - (unsafe-comment-review-sites 189) + (unsafe-sites 409) + (unsafe-sites-with-nearby-safety-comment 221) + (unsafe-sites-without-nearby-safety-comment 188) + (unsafe-comment-review-sites 188) (export-review-sites 196)) (vendor (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated))) @@ -135,9 +135,13 @@ signed values to `usize`, and centralizes output-buffer writes with bounded copy invariants. `x509.rs` now funnels certificate SAN/path/PEM inputs through checked C ABI UTF-8 helpers, centralizes PEM/fingerprint output-buffer writes, documents atomic file-publication syscall invariants, and has regressions for -null/nonzero input and output-slot failures. The generated inventory now -reports 211 annotated native unsafe sites and 189 remaining unsafe review -sites. +null/nonzero input and output-slot failures. `wasm_sm.rs` now compiles under +the crate-wide `unsafe_op_in_unsafe_fn` denial for the SpiderMonkey feature, +checks host-memory offset arithmetic, bounds C ABI module/function/argument/log +buffers, converts poisoned handle-store locks into normal FFI errors, and has +helper regressions for null/nonzero inputs and signed offset conversion. The +generated inventory now reports 221 annotated native unsafe sites and 188 +remaining unsafe review sites. Remaining work before closing K3-P1-01: --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -678,7 +678,10 @@ not started." poisoned handle-store locks into normal FFI errors. `x509.rs` now validates SAN/path/PEM inputs through checked UTF-8 helpers, centralizes PEM and fingerprint output writes, and documents atomic file-publication syscalls. - The generated report now shows 211 annotated native unsafe sites and 189 + `wasm_sm.rs` now compiles under `unsafe_op_in_unsafe_fn` for the + SpiderMonkey feature, bounds C ABI buffers, validates host-memory offsets, + and converts poisoned handle-store locks into normal FFI errors. The + generated report now shows 221 annotated native unsafe sites and 188 remaining unsafe review sites. Remaining work: continue unsafe invariant comments across the rest of `jerboa-native-rs`. --- a/docs/reviews/2026-07-27-native-export-review.sexp +++ b/docs/reviews/2026-07-27-native-export-review.sexp @@ -132,14 +132,14 @@ (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)))) (export (symbol "jerboa_sha384") (file "jerboa-native-rs/src/crypto.rs") (line 184) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 78)))) (export (symbol "jerboa_sha512") (file "jerboa-native-rs/src/crypto.rs") (line 194) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 87)))) - (export (symbol "jerboa_sm_add_fuel") (file "jerboa-native-rs/src/wasm_sm.rs") (line 964) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 186)))) - (export (symbol "jerboa_sm_call") (file "jerboa-native-rs/src/wasm_sm.rs") (line 837) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 168)))) - (export (symbol "jerboa_sm_get_log") (file "jerboa-native-rs/src/wasm_sm.rs") (line 977) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 180)))) - (export (symbol "jerboa_sm_instance_free") (file "jerboa-native-rs/src/wasm_sm.rs") (line 828) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 162)))) - (export (symbol "jerboa_sm_instance_new") (file "jerboa-native-rs/src/wasm_sm.rs") (line 818) (scheme-callers 2 (caller (file "lib/std/wasm/sandbox.ss") (line 156)) (caller (file "lib/std/wasm/sandbox.ss") (line 174)))) - (export (symbol "jerboa_sm_instance_new_hosted") (file "jerboa-native-rs/src/wasm_sm.rs") (line 823) (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 777) (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 697) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 144)))) + (export (symbol "jerboa_sm_add_fuel") (file "jerboa-native-rs/src/wasm_sm.rs") (line 1149) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 186)))) + (export (symbol "jerboa_sm_call") (file "jerboa-native-rs/src/wasm_sm.rs") (line 1005) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 168)))) + (export (symbol "jerboa_sm_get_log") (file "jerboa-native-rs/src/wasm_sm.rs") (line 1162) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 180)))) + (export (symbol "jerboa_sm_instance_free") (file "jerboa-native-rs/src/wasm_sm.rs") (line 994) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 162)))) + (export (symbol "jerboa_sm_instance_new") (file "jerboa-native-rs/src/wasm_sm.rs") (line 984) (scheme-callers 2 (caller (file "lib/std/wasm/sandbox.ss") (line 156)) (caller (file "lib/std/wasm/sandbox.ss") (line 174)))) + (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 318) (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 247) (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 227) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 57)))) --- 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, DuckDB native ABI, and X.509 native ABI now have nearby `SAFETY:` comments or equivalent checked FFI invariants, with the generated unannotated unsafe-site count at 189. | 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`, 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, and X.509 native ABI now have nearby `SAFETY:` comments or equivalent checked FFI invariants, with the generated unannotated unsafe-site count at 188. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. | | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. | | Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup for requested axes, explicit sandbox-axis refusal, and egress proxy env wiring. | Install native Landlock path/net rules in the worker pre-exec path and keep Linux/macOS/FreeBSD parity tests current. | | Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. | Continue moving risky APIs behind explicit unsafe imports as new modules land. | --- a/jerboa-native-rs/src/wasm_sm.rs +++ b/jerboa-native-rs/src/wasm_sm.rs @@ -8,8 +8,8 @@ use ::std::collections::HashMap; use ::std::ptr; use ::std::sync::atomic::{AtomicU64, Ordering}; -use ::std::sync::Mutex; use ::std::sync::OnceLock; +use ::std::sync::{Mutex, MutexGuard}; use mozjs::jsapi::*; use mozjs::jsval::JSVal; @@ -31,6 +31,9 @@ use crate::panic::set_last_error; // ============================================================ const LOG_BUFFER_CAP: usize = 10_000; +const MAX_C_ABI_SLICE_LEN: usize = isize::MAX as usize; +const MAX_WASM_COPY_LEN: usize = 16 * 1024 * 1024; +const MAX_WASI_IOVS: usize = 1024; // ============================================================ // Thread-local host state for import callbacks @@ -73,6 +76,102 @@ fn sm_instances() -> &'static Mutex<HashMap<u64, SmWasmInstance>> { I.get_or_init(|| Mutex::new(HashMap::new())) } +fn lock_sm_modules() -> Option<MutexGuard<'static, HashMap<u64, SmWasmModule>>> { + match sm_modules().lock() { + Ok(guard) => Some(guard), + Err(_) => { + set_last_error("spidermonkey module store lock poisoned".into()); + None + } + } +} + +fn lock_sm_instances() -> Option<MutexGuard<'static, HashMap<u64, SmWasmInstance>>> { + match sm_instances().lock() { + Ok(guard) => Some(guard), + Err(_) => { + set_last_error("spidermonkey instance store lock poisoned".into()); + None + } + } +} + +fn ffi_bytes<'a>(ptr: *const u8, len: usize, label: &str) -> ::std::result::Result<&'a [u8], ()> { + if len > MAX_C_ABI_SLICE_LEN { + set_last_error(format!("{label} too large")); + return Err(()); + } + if ptr.is_null() && len != 0 { + set_last_error(format!("null {label}")); + return Err(()); + } + if len == 0 { + return Ok(&[]); + } + // SAFETY: `len` is bounded to `isize::MAX`, and null pointers paired with + // nonzero length were rejected above. The returned slice is copied or + // decoded before control returns across the C ABI. + Ok(unsafe { ::std::slice::from_raw_parts(ptr, len) }) +} + +fn ffi_utf8(ptr: *const u8, len: usize, label: &str) -> ::std::result::Result<String, ()> { + let bytes = ffi_bytes(ptr, len, label)?; + ::std::str::from_utf8(bytes) + .map(str::to_owned) + .map_err(|_| { + set_last_error(format!("invalid UTF-8 {label}")); + }) +} + +fn ffi_i64_args<'a>( + ptr: *const i64, + len: usize, + label: &str, +) -> ::std::result::Result<&'a [i64], ()> { + if len > MAX_C_ABI_SLICE_LEN / ::std::mem::size_of::<i64>() { + set_last_error(format!("{label} too large")); + return Err(()); + } + if ptr.is_null() && len != 0 { + set_last_error(format!("null {label}")); + return Err(()); + } + if len == 0 { + return Ok(&[]); + } + // SAFETY: `len * size_of::<i64>()` is bounded below `isize::MAX`, and + // null pointers paired with nonzero length were rejected above. + Ok(unsafe { ::std::slice::from_raw_parts(ptr, len) }) +} + +fn js_i32_arg_to_usize(value: i32) -> Option<usize> { + usize::try_from(value).ok() +} + +fn copy_log_prefix( + bytes: &[u8], + output: *mut u8, + output_max: usize, +) -> ::std::result::Result<(), ()> { + if output_max > MAX_C_ABI_SLICE_LEN { + set_last_error("spidermonkey log output too large".into()); + return Err(()); + } + if output.is_null() && output_max != 0 { + set_last_error("null spidermonkey log output".into()); + return Err(()); + } + let n = bytes.len().min(output_max); + if n != 0 { + // SAFETY: null output with nonzero capacity was rejected, `output_max` + // is bounded to `isize::MAX`, and `n` is capped to both buffers. + unsafe { + ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), output, n); + } + } + Ok(()) +} + /// Global JS engine — initialized once, never dropped. /// We leak it intentionally to avoid lifecycle issues with Runtime handles. fn sm_engine() -> &'static mozjs::rust::JSEngineHandle { @@ -127,10 +226,14 @@ fn read_wasm_memory(offset: usize, len: usize) -> Option<Vec<u8>> { CALL_CTX.with(|ctx| { let borrow = ctx.borrow(); let call_ctx = borrow.as_ref()?; - if call_ctx.memory_ptr.is_null() || offset + len > call_ctx.memory_len { + let end = offset.checked_add(len)?; + if call_ctx.memory_ptr.is_null() || len > MAX_WASM_COPY_LEN || end > call_ctx.memory_len { return None; } let mut buf = vec![0u8; len]; + // SAFETY: guest-memory bounds are checked with overflow-safe addition, + // the source pointer is non-null, and the destination Vec owns `len` + // initialized bytes. unsafe { ::std::ptr::copy_nonoverlapping(call_ctx.memory_ptr.add(offset), buf.as_mut_ptr(), len); } @@ -147,9 +250,18 @@ fn write_wasm_memory(offset: usize, data: &[u8]) -> bool { Some(c) => c, None => return false, }; - if call_ctx.memory_ptr.is_null() || offset + data.len() > call_ctx.memory_len { + let Some(end) = offset.checked_add(data.len()) else { + return false; + }; + if call_ctx.memory_ptr.is_null() + || data.len() > MAX_WASM_COPY_LEN + || end > call_ctx.memory_len + { return false; } + // SAFETY: guest-memory bounds are checked with overflow-safe addition, + // the destination pointer is non-null, and `data` provides at least + // `data.len()` initialized source bytes. unsafe { ::std::ptr::copy_nonoverlapping( data.as_ptr(), @@ -163,7 +275,9 @@ fn write_wasm_memory(offset: usize, data: &[u8]) -> bool { // ---- log_message(level, msg_ptr, msg_len) -> 0 ---- unsafe extern "C" fn host_log_message(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; let level = if argc > 0 && args.get(0).get().is_int32() { args.get(0).get().to_int32() } else { @@ -188,9 +302,12 @@ unsafe extern "C" fn host_log_message(_cx: *mut JSContext, argc: u32, vp: *mut V }; let msg = if msg_len > 0 { - match read_wasm_memory(msg_ptr as usize, msg_len as usize) { - Some(bytes) => String::from_utf8_lossy(&bytes).to_string(), - None => format!("[ptr={},len={}]", msg_ptr, msg_len), + match (js_i32_arg_to_usize(msg_ptr), js_i32_arg_to_usize(msg_len)) { + (Some(ptr), Some(len)) => match read_wasm_memory(ptr, len) { + Some(bytes) => String::from_utf8_lossy(&bytes).to_string(), + None => format!("[ptr={},len={}]", msg_ptr, msg_len), + }, + _ => format!("[ptr={},len={}]", msg_ptr, msg_len), } } else { String::new() @@ -214,7 +331,9 @@ unsafe extern "C" fn host_log_message(_cx: *mut JSContext, argc: u32, vp: *mut V // ---- get_time_ms() -> i32 ---- unsafe extern "C" fn host_get_time_ms(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; let ms = CALL_CTX.with(|ctx| { if let Some(ref call_ctx) = *ctx.borrow() { if let Ok(instances) = sm_instances().try_lock() { @@ -231,19 +350,25 @@ unsafe extern "C" fn host_get_time_ms(_cx: *mut JSContext, argc: u32, vp: *mut V // ---- random_get(buf_ptr, buf_len) -> errno ---- unsafe extern "C" fn host_random_get(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; let buf_ptr = if argc > 0 && args.get(0).get().is_int32() { - args.get(0).get().to_int32() as usize + js_i32_arg_to_usize(args.get(0).get().to_int32()).unwrap_or(0) } else { 0 }; let buf_len = if argc > 1 && args.get(1).get().is_int32() { - args.get(1).get().to_int32() as usize + js_i32_arg_to_usize(args.get(1).get().to_int32()).unwrap_or(0) } else { 0 }; if buf_len > 0 { + if buf_len > MAX_WASM_COPY_LEN { + args.rval().set(Int32Value(28)); + return true; + } let mut rand_buf = vec![0u8; buf_len]; // Simple PRNG: fill with pseudo-random bytes based on time let seed = ::std::time::SystemTime::now() @@ -266,24 +391,28 @@ unsafe extern "C" fn host_random_get(_cx: *mut JSContext, argc: u32, vp: *mut Va // ---- fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr) -> errno ---- unsafe extern "C" fn host_fd_write(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; let _fd = if argc > 0 && args.get(0).get().is_int32() { args.get(0).get().to_int32() } else { 1 }; let iovs_ptr = if argc > 1 && args.get(1).get().is_int32() { - args.get(1).get().to_int32() as usize + js_i32_arg_to_usize(args.get(1).get().to_int32()).unwrap_or(0) } else { 0 }; let iovs_len = if argc > 2 && args.get(2).get().is_int32() { - args.get(2).get().to_int32() as usize + js_i32_arg_to_usize(args.get(2).get().to_int32()) + .unwrap_or(0) + .min(MAX_WASI_IOVS) } else { 0 }; let nwritten_ptr = if argc > 3 && args.get(3).get().is_int32() { - args.get(3).get().to_int32() as usize + js_i32_arg_to_usize(args.get(3).get().to_int32()).unwrap_or(0) } else { 0 }; @@ -292,7 +421,9 @@ unsafe extern "C" fn host_fd_write(_cx: *mut JSContext, argc: u32, vp: *mut Valu // Read iov entries: each is (ptr: i32, len: i32) = 8 bytes for i in 0..iovs_len { - let iov_offset = iovs_ptr + i * 8; + let Some(iov_offset) = i.checked_mul(8).and_then(|n| iovs_ptr.checked_add(n)) else { + break; + }; if let Some(iov_bytes) = read_wasm_memory(iov_offset, 8) { let data_ptr = u32::from_le_bytes([iov_bytes[0], iov_bytes[1], iov_bytes[2], iov_bytes[3]]) @@ -303,7 +434,8 @@ unsafe extern "C" fn host_fd_write(_cx: *mut JSContext, argc: u32, vp: *mut Valu if let Some(data) = read_wasm_memory(data_ptr, data_len) { // Write to stderr (fd 1 = stdout, fd 2 = stderr) let _ = ::std::io::Write::write_all(&mut ::std::io::stderr(), &data); - total_written += data_len as u32; + total_written = + total_written.saturating_add(data_len.min(u32::MAX as usize) as u32); } } } @@ -319,21 +451,25 @@ unsafe extern "C" fn host_fd_write(_cx: *mut JSContext, argc: u32, vp: *mut Valu // ---- fd_read(fd, iovs_ptr, iovs_len, nread_ptr) -> errno ---- unsafe extern "C" fn host_fd_read(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; args.rval().set(Int32Value(8)); // EBADF — stdin blocked true } // ---- clock_time_get(clock_id, precision, time_ptr) -> errno ---- unsafe extern "C" fn host_clock_time_get(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; let _clock_id = if argc > 0 && args.get(0).get().is_int32() { args.get(0).get().to_int32() } else { 0 }; let time_ptr = if argc > 2 && args.get(2).get().is_int32() { - args.get(2).get().to_int32() as usize + js_i32_arg_to_usize(args.get(2).get().to_int32()).unwrap_or(0) } else { 0 }; @@ -352,14 +488,18 @@ unsafe extern "C" fn host_clock_time_get(_cx: *mut JSContext, argc: u32, vp: *mu // ---- proc_exit(code) ---- unsafe extern "C" fn host_proc_exit(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; args.rval().set(UndefinedValue()); true } // ---- recv_packet / send_packet / cdb_open / cdb_find / cdb_close stubs ---- unsafe extern "C" fn host_stub_i32(_cx: *mut JSContext, argc: u32, vp: *mut Value) -> bool { - let args = CallArgs::from_vp(vp, argc); + // SAFETY: SpiderMonkey invokes native callbacks with a valid `(argc, vp)` + // pair for the duration of the callback. + let args = unsafe { CallArgs::from_vp(vp, argc) }; args.rval().set(Int32Value(-1)); true } @@ -370,133 +510,138 @@ unsafe extern "C" fn host_stub_i32(_cx: *mut JSContext, argc: u32, vp: *mut Valu /// Build the JS import object: { wasi_snapshot_preview1: {...}, dns: {...} } unsafe fn build_hosted_imports(cx: &mut AutoRealm) -> *mut JSObject { - rooted!(&in(cx) let mut imports = JS_NewPlainObject(cx)); - if imports.is_null() { - return ptr::null_mut(); - } + // SAFETY: `cx` is an active SpiderMonkey realm supplied by the caller; all + // created objects and values are rooted for the duration of this helper, + // and native callback pointers have the ABI SpiderMonkey expects. + unsafe { + rooted!(&in(cx) let mut imports = JS_NewPlainObject(cx)); + if imports.is_null() { + return ptr::null_mut(); + } - // ---- wasi_snapshot_preview1 namespace ---- - rooted!(&in(cx) let mut wasi = JS_NewPlainObject(cx)); - if wasi.is_null() { - return ptr::null_mut(); - } + // ---- wasi_snapshot_preview1 namespace ---- + rooted!(&in(cx) let mut wasi = JS_NewPlainObject(cx)); + if wasi.is_null() { + return ptr::null_mut(); + } - JS_DefineFunction( - cx, - wasi.handle().into(), - c"fd_write".as_ptr(), - Some(host_fd_write), - 4, - 0, - ); - JS_DefineFunction( - cx, - wasi.handle().into(), - c"fd_read".as_ptr(), - Some(host_fd_read), - 4, - 0, - ); - JS_DefineFunction( - cx, - wasi.handle().into(), - c"clock_time_get".as_ptr(), - Some(host_clock_time_get), - 3, - 0, - ); - JS_DefineFunction( - cx, - wasi.handle().into(), - c"random_get".as_ptr(), - Some(host_random_get), - 2, - 0, - ); - JS_DefineFunction( - cx, - wasi.handle().into(), - c"proc_exit".as_ptr(), - Some(host_proc_exit), - 1, - 0, - ); - - rooted!(&in(cx) let mut wasi_val = ObjectValue(wasi.get())); - JS_SetProperty( - cx, - imports.handle(), - c"wasi_snapshot_preview1".as_ptr(), - wasi_val.handle(), - ); - - // ---- dns namespace ---- - rooted!(&in(cx) let mut dns = JS_NewPlainObject(cx)); - if dns.is_null() { - return ptr::null_mut(); - } + JS_DefineFunction( + cx, + wasi.handle().into(), + c"fd_write".as_ptr(), + Some(host_fd_write), + 4, + 0, + ); + JS_DefineFunction( + cx, + wasi.handle().into(), + c"fd_read".as_ptr(), + Some(host_fd_read), + 4, + 0, + ); + JS_DefineFunction( + cx, + wasi.handle().into(), + c"clock_time_get".as_ptr(), + Some(host_clock_time_get), + 3, + 0, + ); + JS_DefineFunction( + cx, + wasi.handle().into(), + c"random_get".as_ptr(), + Some(host_random_get), + 2, + 0, + ); + JS_DefineFunction( + cx, + wasi.handle().into(), + c"proc_exit".as_ptr(), + Some(host_proc_exit), + 1, + 0, + ); + + rooted!(&in(cx) let mut wasi_val = ObjectValue(wasi.get())); + JS_SetProperty( + cx, + imports.handle(), + c"wasi_snapshot_preview1".as_ptr(), + wasi_val.handle(), + ); + + // ---- dns namespace ---- + rooted!(&in(cx) let mut dns = JS_NewPlainObject(cx)); + if dns.is_null() { + return ptr::null_mut(); + } + + JS_DefineFunction( + cx, + dns.handle().into(), + c"log_message".as_ptr(), + Some(host_log_message), + 3, + 0, + ); + JS_DefineFunction( + cx, + dns.handle().into(), + c"get_time_ms".as_ptr(), + Some(host_get_time_ms), + 0, + 0, + ); + JS_DefineFunction( + cx, + dns.handle().into(), + c"recv_packet".as_ptr(), + Some(host_stub_i32), + 2, + 0, + ); + JS_DefineFunction( + cx, + dns.handle().into(), + c"send_packet".as_ptr(), + Some(host_stub_i32), + 4, + 0, + ); + JS_DefineFunction( + cx, + dns.handle().into(), + c"cdb_open".as_ptr(), + Some(host_stub_i32), + 2, + 0, + ); + JS_DefineFunction( + cx, + dns.handle().into(), + c"cdb_find".as_ptr(), + Some(host_stub_i32), + 5, + 0, + ); + JS_DefineFunction( + cx, + dns.handle().into(), + c"cdb_close".as_ptr(), + Some(host_stub_i32), + 1, + 0, + ); + + rooted!(&in(cx) let mut dns_val = ObjectValue(dns.get())); + JS_SetProperty(cx, imports.handle(), c"dns".as_ptr(), dns_val.handle()); - JS_DefineFunction( - cx, - dns.handle().into(), - c"log_message".as_ptr(), - Some(host_log_message), - 3, - 0, - ); - JS_DefineFunction( - cx, - dns.handle().into(), - c"get_time_ms".as_ptr(), - Some(host_get_time_ms), - 0, - 0, - ); - JS_DefineFunction( - cx, - dns.handle().into(), - c"recv_packet".as_ptr(), - Some(host_stub_i32), - 2, - 0, - ); - JS_DefineFunction( - cx, - dns.handle().into(), - c"send_packet".as_ptr(), - Some(host_stub_i32), - 4, - 0, - ); - JS_DefineFunction( - cx, - dns.handle().into(), - c"cdb_open".as_ptr(), - Some(host_stub_i32), - 2, - 0, - ); - JS_DefineFunction( - cx, - dns.handle().into(), - c"cdb_find".as_ptr(), - Some(host_stub_i32), - 5, - 0, - ); - JS_DefineFunction( - cx, - dns.handle().into(), - c"cdb_close".as_ptr(), - Some(host_stub_i32), - 1, - 0, - ); - - rooted!(&in(cx) let mut dns_val = ObjectValue(dns.get())); - JS_SetProperty(cx, imports.handle(), c"dns".as_ptr(), dns_val.handle()); - - imports.get() + imports.get() + } } // ============================================================ @@ -513,180 +658,185 @@ unsafe fn sm_compile_and_call( func_name: &str, args: &[i32], ) -> ::std::result::Result<i32, String> { - // Re-root the global in this scope - rooted!(&in(cx) let global = global_ptr); - - // Get WebAssembly constructors - rooted!(&in(cx) let mut wasm_val = UndefinedValue()); - if !JS_GetProperty( - cx, - global.handle(), - c"WebAssembly".as_ptr(), - wasm_val.handle_mut(), - ) { - return Err("WebAssembly not available".into()); - } - rooted!(&in(cx) let wasm_obj = wasm_val.to_object()); - rooted!(&in(cx) let mut module_ctor = UndefinedValue()); - rooted!(&in(cx) let mut instance_ctor = UndefinedValue()); - JS_GetProperty( - cx, - wasm_obj.handle(), - c"Module".as_ptr(), - module_ctor.handle_mut(), - ); - JS_GetProperty( - cx, - wasm_obj.handle(), - c"Instance".as_ptr(), - instance_ctor.handle_mut(), - ); - - // Compile: aligned buffer → ArrayBuffer → WebAssembly.Module - let mut aligned = vec![0u8; wasm_bytes.len() + 8]; - let off = aligned.as_ptr() as usize % 8; - let start = if off == 0 { 0 } else { 8 - off }; - aligned[start..start + wasm_bytes.len()].copy_from_slice(wasm_bytes); - - let ab = NewArrayBufferWithUserOwnedContents( - cx, - wasm_bytes.len(), - aligned[start..].as_ptr() as *mut _, - ); - if ab.is_null() { - return Err("ArrayBuffer creation failed".into()); - } + // SAFETY: `cx` is an active SpiderMonkey realm, `global_ptr` belongs to + // that realm, all JS values are rooted, and borrowed slices live through + // the synchronous SpiderMonkey compile/instantiate/call sequence. + unsafe { + // Re-root the global in this scope + rooted!(&in(cx) let global = global_ptr); + + // Get WebAssembly constructors + rooted!(&in(cx) let mut wasm_val = UndefinedValue()); + if !JS_GetProperty( + cx, + global.handle(), + c"WebAssembly".as_ptr(), + wasm_val.handle_mut(), + ) { + return Err("WebAssembly not available".into()); + } + rooted!(&in(cx) let wasm_obj = wasm_val.to_object()); + rooted!(&in(cx) let mut module_ctor = UndefinedValue()); + rooted!(&in(cx) let mut instance_ctor = UndefinedValue()); + JS_GetProperty( + cx, + wasm_obj.handle(), + c"Module".as_ptr(), + module_ctor.handle_mut(), + ); + JS_GetProperty( + cx, + wasm_obj.handle(), + c"Instance".as_ptr(), + instance_ctor.handle_mut(), + ); - rooted!(&in(cx) let buf_val = ObjectValue(ab)); - let compile_args = HandleValueArray::from(buf_val.handle().into_handle()); - rooted!(&in(cx) let mut module_obj = ptr::null_mut::<JSObject>()); - if !Construct1( - cx, - module_ctor.handle(), - &compile_args, - module_obj.handle_mut(), - ) { - return Err("WebAssembly.Module compilation failed".into()); - } + // Compile: aligned buffer → ArrayBuffer → WebAssembly.Module + let mut aligned = vec![0u8; wasm_bytes.len() + 8]; + let off = aligned.as_ptr() as usize % 8; + let start = if off == 0 { 0 } else { 8 - off }; + aligned[start..start + wasm_bytes.len()].copy_from_slice(wasm_bytes); - // Build imports - let imports_obj = if hosted { - build_hosted_imports(cx) - } else { - JS_NewPlainObject(cx) - }; - if imports_obj.is_null() { - return Err("failed to build imports".into()); - } - rooted!(&in(cx) let imports = imports_obj); - - // Instantiate: new WebAssembly.Instance(module, imports) - rooted!(&in(cx) let mut inst_args = ValueArray::new([ - ObjectValue(module_obj.get()), - ObjectValue(imports.get()), - ])); - rooted!(&in(cx) let mut instance_obj = ptr::null_mut::<JSObject>()); - if !Construct1( - cx, - instance_ctor.handle(), - &HandleValueArray::from(&inst_args), - instance_obj.handle_mut(), - ) { - return Err("WebAssembly.Instance creation failed".into()); - } + let ab = NewArrayBufferWithUserOwnedContents( + cx, + wasm_bytes.len(), + aligned[start..].as_ptr() as *mut _, + ); + if ab.is_null() { + return Err("ArrayBuffer creation failed".into()); + } + + rooted!(&in(cx) let buf_val = ObjectValue(ab)); + let compile_args = HandleValueArray::from(buf_val.handle().into_handle()); + rooted!(&in(cx) let mut module_obj = ptr::null_mut::<JSObject>()); + if !Construct1( + cx, + module_ctor.handle(), + &compile_args, + module_obj.handle_mut(), + ) { + return Err("WebAssembly.Module compilation failed".into()); + } + + // Build imports + let imports_obj = if hosted { + build_hosted_imports(cx) + } else { + JS_NewPlainObject(cx) + }; + if imports_obj.is_null() { + return Err("failed to build imports".into()); + } + rooted!(&in(cx) let imports = imports_obj); + + // Instantiate: new WebAssembly.Instance(module, imports) + rooted!(&in(cx) let mut inst_args = ValueArray::new([ + ObjectValue(module_obj.get()), + ObjectValue(imports.get()), + ])); + rooted!(&in(cx) let mut instance_obj = ptr::null_mut::<JSObject>()); + if !Construct1( + cx, + instance_ctor.handle(), + &HandleValueArray::from(&inst_args), + instance_obj.handle_mut(), + ) { + return Err("WebAssembly.Instance creation failed".into()); + } - // Get exports object - rooted!(&in(cx) let mut exports_val = UndefinedValue()); - JS_GetProperty( - cx, - instance_obj.handle(), - c"exports".as_ptr(), - exports_val.handle_mut(), - ); - rooted!(&in(cx) let exports_obj = exports_val.to_object()); - - // Extract WASM linear memory pointer for host imports - { - rooted!(&in(cx) let mut mem_val = UndefinedValue()); + // Get exports object + rooted!(&in(cx) let mut exports_val = UndefinedValue()); JS_GetProperty( cx, - exports_obj.handle(), - c"memory".as_ptr(), - mem_val.handle_mut(), + instance_obj.handle(), + c"exports".as_ptr(), + exports_val.handle_mut(), ); - if !mem_val.get().is_undefined() && mem_val.get().is_object() { - // memory is a WebAssembly.Memory — get its .buffer (ArrayBuffer) - rooted!(&in(cx) let mem_obj = mem_val.to_object()); - rooted!(&in(cx) let mut buf_val = UndefinedValue()); + rooted!(&in(cx) let exports_obj = exports_val.to_object()); + + // Extract WASM linear memory pointer for host imports + { + rooted!(&in(cx) let mut mem_val = UndefinedValue()); JS_GetProperty( cx, - mem_obj.handle(), - c"buffer".as_ptr(), - buf_val.handle_mut(), + exports_obj.handle(), + c"memory".as_ptr(), + mem_val.handle_mut(), ); - if !buf_val.get().is_undefined() && buf_val.get().is_object() { - let ab = buf_val.to_object(); - let mut len: usize = 0; - let mut is_shared = false; - let mut data: *mut u8 = ptr::null_mut(); - GetArrayBufferLengthAndData(ab, &mut len, &mut is_shared, &mut data); - if !data.is_null() { - CALL_CTX.with(|ctx| { - if let Some(ref mut call_ctx) = *ctx.borrow_mut() { - call_ctx.memory_ptr = data; - call_ctx.memory_len = len; - } - }); + if !mem_val.get().is_undefined() && mem_val.get().is_object() { + // memory is a WebAssembly.Memory — get its .buffer (ArrayBuffer) + rooted!(&in(cx) let mem_obj = mem_val.to_object()); + rooted!(&in(cx) let mut buf_val = UndefinedValue()); + JS_GetProperty( + cx, + mem_obj.handle(), + c"buffer".as_ptr(), + buf_val.handle_mut(), + ); + if !buf_val.get().is_undefined() && buf_val.get().is_object() { + let ab = buf_val.to_object(); + let mut len: usize = 0; + let mut is_shared = false; + let mut data: *mut u8 = ptr::null_mut(); + GetArrayBufferLengthAndData(ab, &mut len, &mut is_shared, &mut data); + if !data.is_null() { + CALL_CTX.with(|ctx| { + if let Some(ref mut call_ctx) = *ctx.borrow_mut() { + call_ctx.memory_ptr = data; + call_ctx.memory_len = len; + } + }); + } } } } - } - // Get exported function - let c_name = - ::std::ffi::CString::new(func_name).map_err(|_| "invalid function name".to_string())?; - rooted!(&in(cx) let mut func_val = UndefinedValue()); - JS_GetProperty( - cx, - exports_obj.handle(), - c_name.as_ptr(), - func_val.handle_mut(), - ); - if func_val.get().is_undefined() { - return Err(format!("export '{}' not found", func_name)); - } + // Get exported function + let c_name = + ::std::ffi::CString::new(func_name).map_err(|_| "invalid function name".to_string())?; + rooted!(&in(cx) let mut func_val = UndefinedValue()); + JS_GetProperty( + cx, + exports_obj.handle(), + c_name.as_ptr(), + func_val.handle_mut(), + ); + if func_val.get().is_undefined() { + return Err(format!("export '{}' not found", func_name)); + } - // Build arguments - let js_args: Vec<JSVal> = args.iter().map(|a: &i32| Int32Value(*a)).collect(); - let call_args = HandleValueArray { - length_: js_args.len(), - elements_: if js_args.is_empty() { - ptr::null() - } else { - js_args.as_ptr() - }, - }; + // Build arguments + let js_args: Vec<JSVal> = args.iter().map(|a: &i32| Int32Value(*a)).collect(); + let call_args = HandleValueArray { + length_: js_args.len(), + elements_: if js_args.is_empty() { + ptr::null() + } else { + js_args.as_ptr() + }, + }; - // Call - rooted!(&in(cx) let mut rval = UndefinedValue()); - if !Call( - cx, - HandleValue::undefined(), - func_val.handle().into(), - &call_args, - rval.handle_mut().into(), - ) { - return Err("WASM function call failed (exception in WASM)".into()); - } + // Call + rooted!(&in(cx) let mut rval = UndefinedValue()); + if !Call( + cx, + HandleValue::undefined(), + func_val.handle().into(), + &call_args, + rval.handle_mut().into(), + ) { + return Err("WASM function call failed (exception in WASM)".into()); + } - // Extract i32 result