Skip to content

Commit 02be9dd

Browse files
proggeramlugRalph Küpper
andauthored
fix(runtime): fetch string_from_header must reject handle-band ids (doctor/mcp SIGSEGV) (#5560)
The doctor / mcp-list startup segfault (EXC_BAD_ACCESS at the fetch-handle address, e.g. 0x40005/0x40006) root-causes to perry_stdlib::fetch:: string_from_header dereferencing a native registry id as a *StringHeader. lldb on a debug bundle (doctor): frame #0 perry_stdlib::fetch::string_from_header ldr w9,[x1,#0x4] x1=0x40002 frame #1 js_fetch_with_options frame #2 global_this_fetch_thunk fetch() was called with a non-string first argument — a Request/Headers object (a Web Fetch handle, registry id in [0x40000, 0xE0000)). The codegen passes the bare handle id into the url_ptr *StringHeader slot of js_fetch_with_options(url_ptr, method_ptr, body_ptr, headers_json_ptr), and string_from_header reads (*ptr).byte_len at id+4 -> deref of unmapped low address -> SIGSEGV. Its only guard was `ptr < 0x1000` (the TAG_UNDEFINED 0x1 remnant); a 0x40002 handle clears that floor. Non-deterministic for mcp list because whether the id+4 page is mapped at the call depends on heap/page layout: when mapped, byte_len reads garbage and fetch proceeds to the intended error (clean rc=1); when unmapped, SIGSEGV. Fix: reject the whole handle band (is_handle_band, < 0x100000) in string_from_header so a native handle is treated as 'not a string' (None) instead of dereferenced — same robustness pattern as the inline .length / IC-miss / class-field-guard band gates. Adds a regression test. This is the doctor/mcp wall and is INDEPENDENT of the alias-new fix (a9e41e163) and of the inline-.length handle-band fix (175fda498) — both touch different code paths; a9e41e163 merely advanced doctor far enough to reach this fetch() call. Co-authored-by: Ralph Küpper <ralph2@skelpo.com>
1 parent e24949f commit 02be9dd

1 file changed

Lines changed: 40 additions & 0 deletions

File tree

  • crates/perry-stdlib/src/fetch

crates/perry-stdlib/src/fetch/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,32 @@ mod tests {
120120
assert_ne!(native_id as usize, id);
121121
crate::common::drop_handle(native_id);
122122
}
123+
124+
/// `string_from_header` must treat a handle-band value (a Fetch / native
125+
/// registry id, not a `StringHeader` pointer) as "not a string" and return
126+
/// `None` WITHOUT dereferencing it. Regression for the doctor / mcp-list
127+
/// startup SIGSEGV: `fetch()` called with a non-string first argument (a
128+
/// `Request`/`Headers` object) passed the bare handle id into the
129+
/// `url_ptr` `*StringHeader` slot, and reading `(*ptr).byte_len` at `id+4`
130+
/// dereferenced an unmapped low address.
131+
#[test]
132+
fn string_from_header_rejects_handle_band_ids() {
133+
use perry_runtime::value::addr_class;
134+
for &id in &[
135+
1usize, // common native handle
136+
addr_class::FETCH_HANDLE_BAND_START, // 0x40000
137+
addr_class::FETCH_HANDLE_BAND_START + 2, // a fetch handle id
138+
addr_class::HANDLE_BAND_MAX - 1, // 0xFFFFF
139+
] {
140+
assert!(addr_class::is_handle_band(id));
141+
// Must return None without dereferencing the bogus pointer.
142+
let r = unsafe { string_from_header(id as *const StringHeader) };
143+
assert!(
144+
r.is_none(),
145+
"handle-band id {id:#x} must be rejected, got {r:?}"
146+
);
147+
}
148+
}
123149
}
124150

125151
struct StreamState {
@@ -198,6 +224,20 @@ pub(crate) unsafe fn string_from_header(ptr: *const StringHeader) -> Option<Stri
198224
if ptr.is_null() || (ptr as usize) < 0x1000 {
199225
return None;
200226
}
227+
// A handle-band value (`< 0x100000`: Web Fetch Headers/Request/Response/Blob
228+
// ids, net/http small handles, zlib/proxy ids) is a registry id, NOT a
229+
// `StringHeader` pointer. It reaches here when `fetch()` is called with a
230+
// non-string first argument such as a `Request`/`Headers` object — the
231+
// codegen passes the bare handle id into the `url_ptr` `*StringHeader`
232+
// slot. Reading `(*ptr).byte_len` at `id + 4` then dereferences an
233+
// unmapped low address → SIGSEGV (the doctor / mcp-list startup crash at
234+
// the fetch-handle address). The `< 0x1000` floor above only catches the
235+
// TAG_UNDEFINED `0x1` remnant; widen it to the whole handle band so any
236+
// native handle is treated as "not a string" (`None`) rather than
237+
// dereferenced.
238+
if perry_runtime::value::addr_class::is_handle_band(ptr as usize) {
239+
return None;
240+
}
201241
let len = (*ptr).byte_len as usize;
202242
let data_ptr = (ptr as *const u8).add(std::mem::size_of::<StringHeader>());
203243
let bytes = std::slice::from_raw_parts(data_ptr, len);

0 commit comments

Comments
 (0)