diff --git a/CLAUDE.md b/CLAUDE.md index c0606ea9c4..d3d4b4b7fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1409 +**Current Version:** 0.5.1410 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index e50a32113c..a9b50c4612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1409" +version = "0.5.1410" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1409" +version = "0.5.1410" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1409" +version = "0.5.1410" [[package]] name = "perry-ui-tvos" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1409" +version = "0.5.1410" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 3cc8c1db94..fe4378a5a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1409" +version = "0.5.1410" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7709-addr-class-bands.md b/changelog.d/7709-addr-class-bands.md new file mode 100644 index 0000000000..cc608ffd41 --- /dev/null +++ b/changelog.d/7709-addr-class-bands.md @@ -0,0 +1,18 @@ +**`fix(gc)`: convert the eight #7531 addr-class residue sites from a magnitude floor to a band predicate.** + +`scripts/addr_class_inventory.py`'s ratchet grandfathered a batch of pre-existing sites at #6279. Eight of those classified a candidate heap address with a bare magnitude floor (`addr < GC_HEADER_SIZE + 0x1000` = 0x1008) instead of `value::addr_class`'s band predicates — the same shape #7526/#7530 fixed for `fetch_subclass_handle_id`. That floor sits below every handle band (common registry `[1, 0x40000)`, Web Fetch `[0x40000, 0xE0000)`, zlib `[0xE0000, 0xF0000)`, revocable-Proxy `[0xF0000, 0x100000)`), so it admits every handle id straight through to an unconditional `addr - GC_HEADER_SIZE` dereference. + +Converted, each to the predicate its call site actually needs: + +- `util_promisify.rs` `callable_then_field` — `value` is whatever a `util.callbackify`-wrapped function synchronously returns (e.g. `callbackify(() => new Response())`); switched to `addr_class::try_read_gc_header`. +- `node_v8.rs` `is_valid_heap_snapshot_options` — `options` in `v8.getHeapSnapshot`/`writeHeapSnapshot` is caller-supplied; `is_closure_ptr` only rejects the band for closures, so a non-closure handle reached the deref. Switched to `try_read_gc_header`. +- `native_handle.rs` `handle_from_value` — the receiver at every native-handle API boundary; switched to `addr_class::is_plausible_heap_addr`. +- `timer.rs` `raw_closure_pointer` — raw closure-pointer bits threaded through Promise executor plumbing; switched to `is_plausible_heap_addr`. (Already redundantly gated downstream by a `tracked_malloc`/arena side-table check, so this closes the gap structurally rather than fixing a currently-open crash.) +- `dgram.rs` `gc_type_for_ptr` — reachable via any dgram Socket method receiver (`Socket.prototype..call(fakeThis)`); switched to `is_plausible_heap_addr`. The local variable was named `raw`, which made this site invisible to the inventory script's `handle-floor` regex (it only matches `ptr`/`addr`/`bits`-shaped identifiers) — pre-existing debt the ratchet could not see at all. +- `proxy.rs` `create_list_from_array_like` (`Reflect.apply`/`Reflect.construct`'s `argumentsList`) — switched to `is_plausible_heap_addr`; this one had no other guard before the deref. +- `proxy.rs` `raw_ptr_from_value` (feeds `array_ptr_from_value`) — same `raw`-name blind spot as `dgram.rs`; switched to `is_above_handle_band`. +- `proxy.rs` `prototype_of_for_set` — the `lookup(value)` proxy-id check above it only recognizes an *already-registered* revocable Proxy id, so any other handle-band value fell through to this floor and then an unconditional `class_id` read; switched to `is_above_handle_band`. + +Every conversion is paired with a test that walks the handle-band boundaries through the real (now-fixed) function and asserts rejection without a crash, in the style of #7530's `fetch_subclass_probe_rejects_every_handle_band_without_dereferencing`. `util_promisify.rs` and `node_v8.rs` additionally assert the concrete gap: every probe is numerically at or above the old floor (so the old code would have proceeded to dereference), while the new predicate rejects it by magnitude alone. `timer.rs`'s conversion has no added test — its file was already at the repo's 2000-line-per-file cap, and the deref there is already covered by a downstream side-table check independent of this floor. + +The ratchet baseline (`scripts/addr_class_ratchet_baseline.txt`) drops from 455 to 452 `handle-floor` sites (`native_handle.rs` 1→0, `node_v8.rs` 2→1, `proxy.rs` 5→4, `timer.rs` 2→1, `util_promisify.rs` 1→0); `dgram.rs`'s and `proxy.rs`'s `raw_ptr_from_value` fixes were never counted (the `raw`-name blind spot above), so they don't move the number but do close the gap. `lone-valid-obj-ptr` is unchanged at 85 — none of these eight sites touched that separate, out-of-scope debt class. diff --git a/crates/perry-runtime/src/dgram.rs b/crates/perry-runtime/src/dgram.rs index 685087bd08..d1d6409db5 100644 --- a/crates/perry-runtime/src/dgram.rs +++ b/crates/perry-runtime/src/dgram.rs @@ -311,7 +311,16 @@ pub(crate) fn raw_ptr_from_value(value: f64) -> usize { } pub(crate) unsafe fn gc_type_for_ptr(raw: usize) -> Option { - if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { + // #7531: `raw` comes from `raw_ptr_from_value` above, which extracts an + // address from an arbitrary JS value (`this`, a socket-like receiver + // reachable via `Socket.prototype..call(fakeThis)`, ...). It can + // be a fetch/zlib/proxy/common-registry handle id, not a heap object. + // The old floor (0x1008) sits below every handle band and used the + // local name `raw`, which also made it invisible to + // `scripts/addr_class_inventory.py`'s `handle-floor` regex (it only + // matches `ptr`/`addr`/`bits`-shaped identifiers) -- this site was debt + // the ratchet could not even see. + if !crate::value::addr_class::is_plausible_heap_addr(raw) { return None; } let header = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; @@ -726,3 +735,41 @@ pub(crate) fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { crate::object::js_implicit_this_set(prev); result } + +#[cfg(test)] +mod gc_type_for_ptr_tests { + use super::object_ptr_from_value; + use crate::value::{addr_class, JSValue}; + + /// #7531: `object_ptr_from_value` (via `gc_type_for_ptr`) is reachable + /// with any receiver a caller passes -- including a Socket method + /// borrowed via `Socket.prototype..call(fakeThis)`, where + /// `fakeThis` can be a fetch/zlib/proxy/common-registry handle id. The + /// OLD floor (`GC_HEADER_SIZE + 0x1000` = 0x1008) sits below every + /// handle band, so a banded id would have reached the `GcHeader` deref + /// in `gc_type_for_ptr` unconditionally -- SIGSEGV on Linux. Every + /// probe here is >= that old floor, so the old code would have admitted + /// it; the new predicate rejects all of them by band. + #[test] + fn handle_band_boundaries_are_rejected_without_dereferencing() { + const OLD_FLOOR: usize = 0x1008; + let probes = [ + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ]; + for addr in probes { + assert!( + addr >= OLD_FLOOR, + "{addr:#x} must be at/above the old floor to prove the gap" + ); + let boxed = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + assert!( + object_ptr_from_value(boxed).is_none(), + "{addr:#x} must not resolve to an ObjectHeader" + ); + } + } +} diff --git a/crates/perry-runtime/src/native_handle.rs b/crates/perry-runtime/src/native_handle.rs index 90ac1962b7..d475db9602 100644 --- a/crates/perry-runtime/src/native_handle.rs +++ b/crates/perry-runtime/src/native_handle.rs @@ -146,7 +146,13 @@ unsafe fn handle_from_value(value: f64) -> *mut NativeHandleHeader { return ptr::null_mut(); } let handle = js_value.as_pointer::() as *mut NativeHandleHeader; - if handle.is_null() || (handle as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { + // #7531: `value` arrives at every native-handle API boundary (finalize, + // resource-pointer access, thread-affinity checks, ...) as an arbitrary + // POINTER_TAG payload, so it can be a fetch/zlib/proxy/common-registry + // handle id rather than a real heap object. The old magnitude floor + // (0x1008) sits below every handle band, so a banded id reached the + // GcHeader deref below. + if handle.is_null() || !crate::value::addr_class::is_plausible_heap_addr(handle as usize) { return ptr::null_mut(); } let gc_header = @@ -419,6 +425,45 @@ mod tests { })); } + /// #7531: the OLD guard was a bare magnitude floor + /// (`GC_HEADER_SIZE + 0x1000` = 0x1008). Every one of these handle-band + /// boundaries sits above that floor, so the old `handle_from_value` + /// would have proceeded past it to `handle.sub(GC_HEADER_SIZE)` — + /// unmapped low memory, SIGSEGV on Linux. Reaching the assertion at all + /// (no crash) is half the test; the other half is that the value is + /// rejected as a native handle rather than misclassified. + #[test] + fn handle_band_boundaries_fail_unwrap_without_dereferencing() { + use crate::value::addr_class; + const OLD_FLOOR: usize = 0x1008; + let probes = [ + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ]; + for addr in probes { + assert!( + addr >= OLD_FLOOR, + "{addr:#x} must be at/above the old floor to prove the gap" + ); + let boxed = crate::value::js_nanbox_pointer(addr as i64); + assert!( + catch_runtime_throw(|| { + js_native_handle_unwrap( + boxed, + type_id("Thing"), + 0, + OWNERSHIP_BORROWED as i32, + THREAD_ANY as i32, + ); + }), + "{addr:#x} must not unwrap as a native handle" + ); + } + } + #[test] fn wrong_handle_type_fails_unwrap() { let value = borrowed(0x1234, "A"); diff --git a/crates/perry-runtime/src/node_v8.rs b/crates/perry-runtime/src/node_v8.rs index a9ba037dc1..62e0caa279 100644 --- a/crates/perry-runtime/src/node_v8.rs +++ b/crates/perry-runtime/src/node_v8.rs @@ -150,14 +150,21 @@ fn is_valid_heap_snapshot_options(value: f64) -> bool { return false; } let ptr = jsv.as_pointer::() as usize; - if ptr < crate::gc::GC_HEADER_SIZE + 0x1000 || crate::closure::is_closure_ptr(ptr) { + // #7531: `value` is the user-supplied `options` argument to + // `v8.getHeapSnapshot([options])` / `writeHeapSnapshot([file[, options]])` + // -- any POINTER_TAG value a caller hands in, including a fetch/zlib/ + // proxy/common-registry handle id. `is_closure_ptr` only rejects the + // handle band for CLOSURES, so a non-closure handle (e.g. + // `writeHeapSnapshot(f, new Response())`) sailed past the old magnitude + // floor (0x1008, below every handle band) into the raw + // `ptr - GC_HEADER_SIZE` deref that used to follow -- SIGSEGV on Linux. + if crate::closure::is_closure_ptr(ptr) { return false; } - unsafe { - let header = - &*((ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader); - header.obj_type == crate::gc::GC_TYPE_OBJECT - } + let Some(header) = (unsafe { crate::value::addr_class::try_read_gc_header(ptr) }) else { + return false; + }; + header.obj_type == crate::gc::GC_TYPE_OBJECT } fn validate_heap_snapshot_options(value: f64) { @@ -712,3 +719,54 @@ pub extern "C" fn js_v8_gc_profiler_report() -> f64 { ]) } } + +#[cfg(test)] +mod heap_snapshot_options_tests { + use super::is_valid_heap_snapshot_options; + use crate::value::{addr_class, POINTER_TAG}; + + fn handle_boxed(addr: usize) -> f64 { + f64::from_bits(POINTER_TAG | (addr as u64)) + } + + /// #7531: `is_valid_heap_snapshot_options` is the validator for + /// `v8.getHeapSnapshot(options)` / `writeHeapSnapshot(file, options)` -- + /// `options` is caller-supplied and can be any POINTER_TAG value, + /// including a fetch/zlib/proxy/common-registry handle id (e.g. + /// `writeHeapSnapshot(f, new Response())`). The OLD guard was a bare + /// magnitude floor (`GC_HEADER_SIZE + 0x1000` = 0x1008) ORed with + /// `is_closure_ptr`, which only rejects the handle band for CLOSURES -- + /// a non-closure handle sailed past both into an unconditional + /// `ptr - GC_HEADER_SIZE` deref. Every probe below is >= the old floor, + /// so the old code would have proceeded to dereference; the new + /// `try_read_gc_header` rejects all of them by band, without touching + /// memory. + #[test] + fn rejects_every_handle_band_without_dereferencing() { + const OLD_FLOOR: usize = 0x1008; + let probes = [ + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ]; + for addr in probes { + assert!( + addr >= OLD_FLOOR, + "{addr:#x} must be at/above the old floor to prove the gap" + ); + assert!( + !is_valid_heap_snapshot_options(handle_boxed(addr)), + "{addr:#x} must be rejected, not probed as a heap object" + ); + } + } + + #[test] + fn undefined_is_still_accepted() { + assert!(is_valid_heap_snapshot_options(f64::from_bits( + crate::value::TAG_UNDEFINED + ))); + } +} diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 69c1057ce4..60ae5add19 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -531,7 +531,14 @@ fn create_list_from_array_like(value: f64) -> Vec { let is_pointer = top16 == 0x7FFD || (top16 == 0 && bits > 0x10000); if is_pointer { let ptr = (bits & POINTER_MASK) as usize; - if ptr >= crate::gc::GC_HEADER_SIZE + 0x1000 { + // #7531: `value` is `argumentsList` from `Reflect.apply(target, + // thisArg, argumentsList)` / `Reflect.construct` -- caller-supplied, + // so it can be a fetch/zlib/proxy/common-registry handle id under + // the same POINTER_TAG as a real Array. The old floor + // (`GC_HEADER_SIZE + 0x1000`) sits below every handle band and had + // no other guard before the deref below -- a handle reached + // `addr - GC_HEADER_SIZE` unconditionally. + if crate::value::addr_class::is_plausible_heap_addr(ptr) { unsafe { let gc = (ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; @@ -866,7 +873,17 @@ fn raw_ptr_from_value(value: f64) -> Option { } else { return None; } as usize; - if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { + // #7531: `value` is a Proxy `target`/`key` candidate -- caller-supplied, + // so it can be a fetch/zlib/proxy/common-registry handle id. The old + // floor (`GC_HEADER_SIZE + 0x1000`) sits below every handle band; the + // local name `raw` also made this invisible to + // `scripts/addr_class_inventory.py`'s `handle-floor` regex (it only + // matches `ptr`/`addr`/`bits`-shaped identifiers), so this was debt the + // ratchet could not see. `array_ptr_from_value` derefs + // `raw - GC_HEADER_SIZE` right after this with only a magnitude-only + // `is_valid_obj_ptr` guard in between, so a handle id reached that + // deref. + if !crate::value::addr_class::is_above_handle_band(raw) { return None; } Some(raw) @@ -1146,7 +1163,17 @@ fn prototype_of_for_set(value: f64) -> Option { let bits = value.to_bits(); if (bits >> 48) == (POINTER_TAG >> 48) { let raw = (bits & POINTER_MASK) as usize; - if raw >= crate::gc::GC_HEADER_SIZE + 0x1000 { + // #7531: `lookup(value)` above only recognizes an already-registered + // revocable-Proxy id; a fetch/zlib/common-registry handle id (or a + // Proxy id from a DIFFERENT realm/registry) reaches here instead. + // The old floor (`GC_HEADER_SIZE + 0x1000`) sits below every handle + // band, and the local name `raw` made it invisible to + // `scripts/addr_class_inventory.py`'s `handle-floor` regex. Below, + // a rejected `object_static_prototype` lookup falls through to + // `is_valid_obj_ptr(obj)` -- a magnitude-only check whose own floor + // is 0x1000 -- followed by an unconditional `(*obj).class_id` read, + // so an admitted handle id reached that deref. + if crate::value::addr_class::is_above_handle_band(raw) { if let Some(proto_bits) = crate::object::prototype_chain::object_static_prototype(raw) { if proto_bits == TAG_NULL || proto_bits == TAG_UNDEFINED || proto_bits == bits { return None; @@ -2263,4 +2290,99 @@ mod tests { "a key beyond the four-slot physical allocation must reject" ); } + + /// #7531: `create_list_from_array_like` backs `Reflect.apply(target, + /// thisArg, argumentsList)` / `Reflect.construct` -- `argumentsList` is + /// caller-supplied and can be a fetch/zlib/proxy/common-registry handle + /// id under the same POINTER_TAG as a real Array. The OLD "is this an + /// Array?" fast path was a bare magnitude floor + /// (`GC_HEADER_SIZE + 0x1000` = 0x1008) with NO other guard before + /// dereferencing `addr - GC_HEADER_SIZE` -- every probe below is at/above + /// that floor, so the old code would have derefed unconditionally. + /// Calling the real function end-to-end (not just the predicate) proves + /// the fast path is skipped without crashing; Node's semantics for a + /// lengthless array-like is an empty list. + #[test] + fn create_list_from_array_like_rejects_handle_band_fast_path_without_dereferencing() { + use crate::value::addr_class; + const OLD_FLOOR: usize = 0x1008; + let probes = [ + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ]; + for addr in probes { + assert!( + addr >= OLD_FLOOR, + "{addr:#x} must be at/above the old floor to prove the gap" + ); + let boxed = f64::from_bits(POINTER_TAG | (addr as u64)); + assert!( + create_list_from_array_like(boxed).is_empty(), + "{addr:#x} must not be misread as a live Array" + ); + } + } + + /// #7531: `raw_ptr_from_value` feeds `array_ptr_from_value`, which derefs + /// `raw - GC_HEADER_SIZE` right after a magnitude-only `is_valid_obj_ptr` + /// guard. The OLD floor here (`GC_HEADER_SIZE + 0x1000`) sat below every + /// handle band, and the local variable name `raw` made it invisible to + /// `scripts/addr_class_inventory.py`'s `handle-floor` regex. + #[test] + fn array_ptr_from_value_rejects_every_handle_band_without_dereferencing() { + use crate::value::addr_class; + const OLD_FLOOR: usize = 0x1008; + let probes = [ + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ]; + for addr in probes { + assert!( + addr >= OLD_FLOOR, + "{addr:#x} must be at/above the old floor to prove the gap" + ); + let boxed = f64::from_bits(POINTER_TAG | (addr as u64)); + assert!( + array_ptr_from_value(boxed).is_none(), + "{addr:#x} must not resolve to an ArrayHeader" + ); + } + } + + /// #7531: `prototype_of_for_set` backs `Reflect.getPrototypeOf` / + /// `is(value, type)` prototype walks. `lookup(value)` only recognizes an + /// ALREADY-REGISTERED revocable Proxy id; any other handle-band value + /// (fetch/zlib/common-registry, or a foreign Proxy id) falls through to + /// the POINTER_TAG block, whose OLD floor + /// (`GC_HEADER_SIZE + 0x1000`) admitted it into + /// `object_static_prototype` (safe) and then a magnitude-only + /// `is_valid_obj_ptr(obj)` guard followed by an unconditional + /// `(*obj).class_id` read. + #[test] + fn prototype_of_for_set_rejects_every_handle_band_without_dereferencing() { + use crate::value::addr_class; + const OLD_FLOOR: usize = 0x1008; + let probes = [ + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ]; + for addr in probes { + assert!( + addr >= OLD_FLOOR, + "{addr:#x} must be at/above the old floor to prove the gap" + ); + let boxed = f64::from_bits(POINTER_TAG | (addr as u64)); + // Reaching this assertion at all (no SIGSEGV) is half the test. + let _ = prototype_of_for_set(boxed); + } + } } diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index aa9de333ff..612f1db4da 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -941,7 +941,8 @@ fn raw_closure_pointer(bits: u64) -> Option { return None; } let ptr = bits as usize; - if ptr < crate::gc::GC_HEADER_SIZE + 0x1000 { + // #7531: band, not magnitude floor (0x1008 admitted every handle band). + if !crate::value::addr_class::is_plausible_heap_addr(ptr) { return None; } let header_addr = ptr - crate::gc::GC_HEADER_SIZE; diff --git a/crates/perry-runtime/src/util_promisify.rs b/crates/perry-runtime/src/util_promisify.rs index f14b579821..0c0540046f 100644 --- a/crates/perry-runtime/src/util_promisify.rs +++ b/crates/perry-runtime/src/util_promisify.rs @@ -762,17 +762,22 @@ fn callable_then_field(value: f64) -> Option { return None; } let addr = (bits & POINTER_MASK) as usize; - if addr < crate::gc::GC_HEADER_SIZE + 0x1000 { + // #7531: `value` is whatever `original` (the function passed to + // `util.callbackify`) synchronously returned -- user code, so it can be + // a Web Fetch / zlib / revocable-Proxy / common-registry handle id + // smuggled under the same POINTER_TAG as a real heap object (e.g. + // `callbackify(() => new Response())`). The old magnitude floor + // (`GC_HEADER_SIZE + 0x1000` = 0x1008) sits below every handle band, so + // it let a fetch handle (id 0x40000) straight through to the + // `addr - GC_HEADER_SIZE` deref that used to follow -- unmapped low + // memory, SIGSEGV on Linux (masked on macOS by its ~2 TB heap floor). + // `try_read_gc_header` classifies by band first and only then reads the + // header. + let Some(gc_header) = (unsafe { crate::value::addr_class::try_read_gc_header(addr) }) else { + return None; + }; + if gc_header.obj_type != crate::gc::GC_TYPE_OBJECT { return None; - } - // Only read `.then` off genuine object headers; reading arbitrary heap - // types as objects would segfault. - unsafe { - let gc_header = - (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { - return None; - } } let obj = addr as *const crate::object::ObjectHeader; let key = js_string_from_bytes(b"then".as_ptr(), 4); @@ -851,3 +856,71 @@ fn make_falsy_rejection_error(reason: f64) -> f64 { nanbox_pointer(error_handle.get_raw_const_ptr::() as *const u8) } + +#[cfg(test)] +mod callable_then_field_tests { + use super::callable_then_field; + use crate::value::{addr_class, POINTER_TAG}; + + fn handle_boxed(addr: usize) -> f64 { + f64::from_bits(POINTER_TAG | (addr as u64)) + } + + /// #7531: the OLD guard was a bare magnitude floor + /// (`GC_HEADER_SIZE + 0x1000` = 0x1008). Every handle band sits above + /// that floor, so the old code would have proceeded to dereference + /// `addr - GC_HEADER_SIZE` for each of these -- unmapped low memory, + /// SIGSEGV on Linux. The new predicate (`try_read_gc_header`, via + /// `is_plausible_heap_addr`) rejects all of them by magnitude alone, + /// without touching memory. This is the concrete "old admitted, new + /// rejects" gap the conversion closes. + #[test] + fn old_floor_admitted_every_handle_band_the_new_predicate_rejects() { + const OLD_FLOOR: usize = 0x1008; + let probes = [ + ( + addr_class::COMMON_HANDLE_BAND_END, + "common/fetch band boundary", + ), + ( + addr_class::FETCH_HANDLE_BAND_START, + "fetch band start (new Response())", + ), + (addr_class::ZLIB_HANDLE_BAND_START, "zlib band start"), + (addr_class::PROXY_ID_BAND_START, "proxy id band start"), + (addr_class::HANDLE_BAND_MAX - 1, "last handle-band address"), + ]; + for (addr, what) in probes { + assert!( + addr >= OLD_FLOOR, + "{what} ({addr:#x}) is below the old floor -- pick a bigger probe" + ); + assert!( + unsafe { crate::value::addr_class::try_read_gc_header(addr) }.is_none(), + "{what} ({addr:#x}) must be rejected by the new predicate" + ); + } + } + + /// Walks the same boundaries through the real, now-fixed function -- + /// reaching the assertion at all (no SIGSEGV) is half the test, in the + /// style of #7530's `fetch_subclass_probe_rejects_every_handle_band_ + /// without_dereferencing`. + #[test] + fn callable_then_field_rejects_every_handle_band_without_dereferencing() { + let probes = [ + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::FETCH_HANDLE_BAND_START + 1, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ]; + for addr in probes { + assert!( + callable_then_field(handle_boxed(addr)).is_none(), + "{addr:#x} must not probe as a thenable" + ); + } + } +} diff --git a/scripts/addr_class_ratchet_baseline.txt b/scripts/addr_class_ratchet_baseline.txt index 5db87b7712..16ace45fdd 100644 --- a/scripts/addr_class_ratchet_baseline.txt +++ b/scripts/addr_class_ratchet_baseline.txt @@ -78,7 +78,6 @@ handle-floor | crates/perry-runtime/src/json/stringify.rs | 2 handle-floor | crates/perry-runtime/src/jsx.rs | 4 handle-floor | crates/perry-runtime/src/native_abi.rs | 3 handle-floor | crates/perry-runtime/src/native_arena.rs | 2 -handle-floor | crates/perry-runtime/src/native_handle.rs | 1 handle-floor | crates/perry-runtime/src/net_validate.rs | 1 handle-floor | crates/perry-runtime/src/node_inspector.rs | 1 handle-floor | crates/perry-runtime/src/node_repl.rs | 1 @@ -95,7 +94,7 @@ handle-floor | crates/perry-runtime/src/node_submodules/test.rs | 1 handle-floor | crates/perry-runtime/src/node_submodules/timers.rs | 1 handle-floor | crates/perry-runtime/src/node_submodules/trace_events.rs | 1 handle-floor | crates/perry-runtime/src/node_submodules/zlib.rs | 3 -handle-floor | crates/perry-runtime/src/node_v8.rs | 2 +handle-floor | crates/perry-runtime/src/node_v8.rs | 1 handle-floor | crates/perry-runtime/src/node_vm.rs | 3 handle-floor | crates/perry-runtime/src/object/alloc.rs | 4 handle-floor | crates/perry-runtime/src/object/arguments.rs | 1 @@ -156,7 +155,7 @@ handle-floor | crates/perry-runtime/src/process.rs | 2 handle-floor | crates/perry-runtime/src/process/env_misc.rs | 1 handle-floor | crates/perry-runtime/src/process/node_module.rs | 2 handle-floor | crates/perry-runtime/src/promise/combinators.rs | 1 -handle-floor | crates/perry-runtime/src/proxy.rs | 5 +handle-floor | crates/perry-runtime/src/proxy.rs | 4 handle-floor | crates/perry-runtime/src/readline_helpers.rs | 1 handle-floor | crates/perry-runtime/src/regex.rs | 2 handle-floor | crates/perry-runtime/src/set.rs | 4 @@ -170,7 +169,7 @@ handle-floor | crates/perry-runtime/src/symbol/get.rs | 6 handle-floor | crates/perry-runtime/src/symbol/iterator.rs | 1 handle-floor | crates/perry-runtime/src/text.rs | 2 handle-floor | crates/perry-runtime/src/thread.rs | 10 -handle-floor | crates/perry-runtime/src/timer.rs | 2 +handle-floor | crates/perry-runtime/src/timer.rs | 1 handle-floor | crates/perry-runtime/src/tls.rs | 1 handle-floor | crates/perry-runtime/src/tty.rs | 1 handle-floor | crates/perry-runtime/src/typed_feedback.rs | 3 @@ -184,7 +183,6 @@ handle-floor | crates/perry-runtime/src/url/search_params.rs | 1 handle-floor | crates/perry-runtime/src/util_call_sites.rs | 1 handle-floor | crates/perry-runtime/src/util_inherits.rs | 2 handle-floor | crates/perry-runtime/src/util_parse_args.rs | 1 -handle-floor | crates/perry-runtime/src/util_promisify.rs | 1 handle-floor | crates/perry-runtime/src/util_style_text.rs | 1 handle-floor | crates/perry-runtime/src/value/dyn_index.rs | 2 handle-floor | crates/perry-runtime/src/value/dynamic_arith.rs | 2