You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
crates/perry-runtime/src/fs/dirent.rs's options_with_file_types decodes a raw ObjectHeader pointer out of a NaN-boxed f64, then allocates, then dereferences the pointer it computed before the allocation:
pub(crate)unsafefnoptions_with_file_types(options_value:f64) -> bool{let bits = options_value.to_bits();let value = crate::value::JSValue::from_bits(bits);let raw_ptr = /* decode from options_value */;ifcrate::value::addr_class::is_handle_band(raw_ptr){returnfalse;}let obj_ptr = raw_ptr as*constcrate::object::ObjectHeader;let key = crate::string::js_string_from_bytes(b"withFileTypes".as_ptr(),13);// <-- ALLOCATESlet val = crate::object::js_object_get_field_by_name(obj_ptr, key);// <-- stale obj_ptrcrate::value::js_is_truthy(f64::from_bits(val.bits())) != 0}
js_string_from_bytes can trigger a collection. If that cycle evacuates the options object, obj_ptr names a from-space address that has already been swept, and js_object_get_field_by_name reads an unrelated live object — or garbage. options_value is a plain Rust f64 local, not a GC root, so it is stale by the same argument; re-deriving the pointer after the allocation would not help on its own.
The file already knows how to do this
options_field_value, 30 lines below in the same file, has the same signature and handles it correctly:
So the fix is self-contained — no signature change, no caller churn: give options_with_file_types a RuntimeHandleScope, root options_value, and re-read the handle after the js_string_from_bytes call, exactly as its sibling does.
Reachability
One caller, dirent.rs:382, on the fs.readdir / fs.readdirSync path:
let with_file_types = options_with_file_types(options_value);
{ withFileTypes: true } is an object literal freshly allocated by the caller, so it is a young-generation object — precisely the kind a minor collection triggered by the very next allocation is most likely to move.
No reproducer yet — this is a read of the code, not an observed crash. It would present as a nondeterministic wrong withFileTypes result or a segfault under GC pressure, and evacuation makes it far more likely than the non-moving default did.
crates/perry-runtime/src/fs/dirent.rs'soptions_with_file_typesdecodes a rawObjectHeaderpointer out of a NaN-boxedf64, then allocates, then dereferences the pointer it computed before the allocation:js_string_from_bytescan trigger a collection. If that cycle evacuates the options object,obj_ptrnames a from-space address that has already been swept, andjs_object_get_field_by_namereads an unrelated live object — or garbage.options_valueis a plain Rustf64local, not a GC root, so it is stale by the same argument; re-deriving the pointer after the allocation would not help on its own.The file already knows how to do this
options_field_value, 30 lines below in the same file, has the same signature and handles it correctly:and it re-reads through the handle after its own allocating
js_string_from_bytescall:So the fix is self-contained — no signature change, no caller churn: give
options_with_file_typesaRuntimeHandleScope, rootoptions_value, and re-read the handle after thejs_string_from_bytescall, exactly as its sibling does.Reachability
One caller,
dirent.rs:382, on thefs.readdir/fs.readdirSyncpath:{ withFileTypes: true }is an object literal freshly allocated by the caller, so it is a young-generation object — precisely the kind a minor collection triggered by the very next allocation is most likely to move.Notes
< 0x1000handle floor toaddr_class::is_handle_band). It is pre-existing and unrelated to that change, so it was not folded in: fix(lint): clear the GC store-site and address-classification gates (#7258, #7259) #7273 is a lint-gate fix and a GC-rooting change belongs on its own reviewable/revertable commit.withFileTypesresult or a segfault under GC pressure, and evacuation makes it far more likely than the non-moving default did.