diff --git a/crates/perry-stdlib/src/fetch/mod.rs b/crates/perry-stdlib/src/fetch/mod.rs index 61082b4ab3..b4ec5ad233 100644 --- a/crates/perry-stdlib/src/fetch/mod.rs +++ b/crates/perry-stdlib/src/fetch/mod.rs @@ -120,6 +120,32 @@ mod tests { assert_ne!(native_id as usize, id); crate::common::drop_handle(native_id); } + + /// `string_from_header` must treat a handle-band value (a Fetch / native + /// registry id, not a `StringHeader` pointer) as "not a string" and return + /// `None` WITHOUT dereferencing it. Regression for the doctor / mcp-list + /// startup SIGSEGV: `fetch()` called with a non-string first argument (a + /// `Request`/`Headers` object) passed the bare handle id into the + /// `url_ptr` `*StringHeader` slot, and reading `(*ptr).byte_len` at `id+4` + /// dereferenced an unmapped low address. + #[test] + fn string_from_header_rejects_handle_band_ids() { + use perry_runtime::value::addr_class; + for &id in &[ + 1usize, // common native handle + addr_class::FETCH_HANDLE_BAND_START, // 0x40000 + addr_class::FETCH_HANDLE_BAND_START + 2, // a fetch handle id + addr_class::HANDLE_BAND_MAX - 1, // 0xFFFFF + ] { + assert!(addr_class::is_handle_band(id)); + // Must return None without dereferencing the bogus pointer. + let r = unsafe { string_from_header(id as *const StringHeader) }; + assert!( + r.is_none(), + "handle-band id {id:#x} must be rejected, got {r:?}" + ); + } + } } struct StreamState { @@ -198,6 +224,20 @@ pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option()); let bytes = std::slice::from_raw_parts(data_ptr, len);