diff --git a/CLAUDE.md b/CLAUDE.md index 1d656ac0aa..24019c23c5 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.1321 +**Current Version:** 0.5.1322 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index d06062577b..170d2dc93e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1321" +version = "0.5.1322" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1321" +version = "0.5.1322" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1321" +version = "0.5.1322" [[package]] name = "perry-ui-tvos" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1321" +version = "0.5.1322" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index f18a05e9ef..1696c6fba4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1321" +version = "0.5.1322" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7569-array-iterator-class-id-confusion.md b/changelog.d/7569-array-iterator-class-id-confusion.md new file mode 100644 index 0000000000..b63f1313fe --- /dev/null +++ b/changelog.d/7569-array-iterator-class-id-confusion.md @@ -0,0 +1,86 @@ +### Bug fixes + +**`arr[Symbol.iterator]` read an array's `capacity` as a `class_id`, so an +array literal could resolve its iterator to an unrelated user class's `values` +method — self-recursively, and fatally (#7563).** + +Reported as "a `class X extends Map` that overrides `values()` SIGSEGVs when +the override is iterated". `Map` turns out to be incidental, and so does the +iteration: the same crash reproduces with **no `Map` anywhere in the program** +and with **no `for-of` on the path**. + +```ts +class Plain { + values(): IterableIterator { + return [777][Symbol.iterator](); // <-- SIGSEGV + } +} +new Plain().values(); +``` + +#### Root cause + +`ObjectHeader` is `{ object_type: u32, class_id: u32, … }`; `ArrayHeader` is +`{ length: u32, capacity: u32 }`. The two `u32`s at offset 4 alias, so an array +pointer read as an `ObjectHeader` reports its **capacity** as a `class_id`. + +`arr[Symbol.iterator]` resolves through `js_class_method_bind(arr, "values")` +(`symbol/get.rs`, the #321 arm that makes `typeof arr[Symbol.iterator] === +"function"` hold). That builder's receiver→class step, +`class_id_from_method_receiver` in `object/native_module.rs`, read the field +with a bare `(*obj).class_id` — guarded only against closures and against the +handle band, never against the allocation's actual *type*. So whenever the class +whose id equalled the array's capacity happened to own a method named `values`, +`method_owner_class_id` found it and the canonical class method was returned as +the array's iterator. + +The default array capacity is `MIN_ARRAY_CAPACITY`-clamped, and class ids are +handed out from 1 in declaration order, so the collision is not exotic — it is +the common case for small programs. When the colliding class was the *calling* +class, `values` re-entered `values` once per step until the stack guard page: + +``` +perry_method_repro_ts__MyMap__values + → js_native_call_method_value → js_object_get_symbol_property [the array read] + → js_native_call_value → dispatch_bound_method → call_vtable_method + → perry_method_repro_ts__MyMap__values [× ~26 000] +``` + +`EXC_BAD_ACCESS (code=2)` at `str xzr, [sp], #-0x50` — a stack-overflow guard +fault, not a stale or null pointer. + +#### Fix + +`class_id_from_method_receiver` now uses `js_object_get_class_id`, the guarded +accessor that already existed for exactly this read: it rejects the handle band, +the `std::alloc`'d `Map`/`Set`/`Regex` headers (which have no `GcHeader` to +probe), and any allocation whose `GcHeader.obj_type` is not `GC_TYPE_OBJECT`. +The bare read bypassed all three. The sibling symbol-method arm in +`object/native_call_method.rs` already routed through that accessor and was +never affected — verified, not assumed. + +One line of behaviour change; the guard is deliberately not narrower than the +invariant it protects, so a genuine class instance still resolves to its id +(asserted in the same test). + +#### Not #7561 + +The #7561 `for (… of m.values())` rewrite is **not** implicated, and neither is +any Map fast path. `rewrite_collection_view_for_of` declines a subclass receiver +exactly as its doc comment claims, and the crash needs neither a `for-of` nor a +`Map`: calling `m.values()` and discarding the result is enough, and a plain +class reproduces it. The offending line predates #7561 by hundreds of commits +(it traces back through #5631's file split to #4630), matching the issue's +report that it reproduces at `969b447cc`. + +#### Also fixed by the same line + +The related shapes in the issue's table now match node as well — a `values` / +`keys` / `entries` / `[Symbol.iterator]` override on a `Map`, `Set` or `Array` +subclass, an indirect subclass, and a class-expression subclass, with +`super.values()` from inside an override still reaching the native base. + +Coverage: `test-files/test_gap_7563_array_iterator_class_id_confusion.ts` +(byte-compared against node; SIGSEGVs at the parent commit) and +`object::tests::array_receiver_is_never_read_as_a_class_id` (fails with +`Some(16)` — the array's capacity — before the fix). diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 8b52dd9b48..fac79dc8a3 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -1301,7 +1301,12 @@ pub(crate) fn canonical_bound_method_receiver(captured: f64) -> f64 { captured } -fn class_id_from_method_receiver(instance: f64) -> Option { +/// The `class_id` of `instance`, when `instance` really is a class instance. +/// +/// `pub(super)` so `object::tests` can assert the #7563 invariant directly: a +/// non-object allocation (an array, above all) must resolve to `None` rather +/// than to whatever its bytes happen to hold at the `class_id` offset. +pub(super) fn class_id_from_method_receiver(instance: f64) -> Option { if let Some(cid) = class_ref_id(instance) { return Some(cid); } @@ -1323,7 +1328,27 @@ fn class_id_from_method_receiver(instance: f64) -> Option { if crate::closure::is_closure_ptr(obj as usize) { return None; } - let cid = unsafe { (*obj).class_id }; + // #7563: the closure guard above fixed ONE instance of that type + // confusion; a bare `(*obj).class_id` read has it for every other + // non-object allocation too. `ObjectHeader` is `{ object_type: u32, + // class_id: u32, … }` while `ArrayHeader` is `{ length: u32, + // capacity: u32 }`, so the `class_id` slot of an ARRAY overlays its + // **capacity** — an N-capacity array literal was read back as + // "class id N". Reached from `arr[Symbol.iterator]`, which resolves via + // `js_class_method_bind(arr, "values")` (`symbol/get.rs`): whenever + // class id N happened to own a `values` method, the array's + // iterator resolved to THAT class's method. With `class Plain { + // values() { return [777][Symbol.iterator](); } }` the one-element + // literal read back as class id 1 — `Plain` itself — so `values` + // called `values` until the stack guard page: a SIGSEGV with no + // `Map` anywhere in the program. + // + // `js_object_get_class_id` is the guarded accessor for exactly this + // read: it rejects the handle band, the std::alloc'd Map/Set/Regex + // headers (which have no `GcHeader` to probe), and — the part that + // matters here — any allocation whose `GcHeader.obj_type` is not + // `GC_TYPE_OBJECT`. Reading the field directly bypassed all three. + let cid = crate::object::js_object_get_class_id(obj); if cid != 0 { return Some(cid); } diff --git a/crates/perry-runtime/src/object/tests.rs b/crates/perry-runtime/src/object/tests.rs index 0b2a424b1c..f5ea104a9a 100644 --- a/crates/perry-runtime/src/object/tests.rs +++ b/crates/perry-runtime/src/object/tests.rs @@ -1379,3 +1379,48 @@ fn stale_pre_grow_array_pointer_reads_the_real_length_in_object_ops() { "the freeze walk must not run past the array's real length" ); } + +/// #7563: an ARRAY receiver must never be read back as a class instance. +/// +/// `ObjectHeader` is `{ object_type: u32, class_id: u32, … }` and `ArrayHeader` +/// is `{ length: u32, capacity: u32 }`, so the two u32s at offset 4 alias — an +/// array read as an `ObjectHeader` reports its **capacity** as a `class_id`. +/// +/// That mattered because `arr[Symbol.iterator]` resolves through +/// `js_class_method_bind(arr, "values")`, whose receiver→class step used a bare +/// `(*obj).class_id` read instead of the guarded `js_object_get_class_id`. Any +/// class whose id equalled the array's capacity and which owned a `values` +/// method therefore captured the array's iterator. When that class was the +/// *calling* class — `class C { values() { return [x][Symbol.iterator](); } }` +/// — `values` re-entered `values` until the stack guard page, i.e. a SIGSEGV +/// with no `Map` anywhere in the program. +#[test] +fn array_receiver_is_never_read_as_a_class_id() { + let arr = crate::array::js_array_alloc(3); + assert!(!arr.is_null()); + // Impersonate exactly the class id this array's bytes would have yielded. + let impersonated = unsafe { (*arr).capacity }; + assert_ne!( + impersonated, 0, + "the test is vacuous unless the capacity is a non-zero (i.e. lookup-able) class id" + ); + + let arr_value = crate::value::js_nanbox_pointer(arr as i64); + assert_eq!( + super::native_module::class_id_from_method_receiver(arr_value), + None, + "an array is not a class instance: its capacity must not be read as a class id" + ); + + // The guard must not over-narrow. A genuine class instance carrying the + // very same id still resolves, so the bound-method identity path (#446) + // keeps working. + let obj = js_object_alloc(impersonated, 0); + assert!(!obj.is_null()); + let obj_value = crate::value::js_nanbox_pointer(obj as i64); + assert_eq!( + super::native_module::class_id_from_method_receiver(obj_value), + Some(impersonated), + "a real class instance must still resolve to its class id" + ); +} diff --git a/test-files/test_gap_7563_array_iterator_class_id_confusion.ts b/test-files/test_gap_7563_array_iterator_class_id_confusion.ts new file mode 100644 index 0000000000..b26d838cd6 --- /dev/null +++ b/test-files/test_gap_7563_array_iterator_class_id_confusion.ts @@ -0,0 +1,180 @@ +// #7563: `arr[Symbol.iterator]` read an ARRAY's `capacity` field as a `class_id`. +// +// `ObjectHeader` is `{ object_type: u32, class_id: u32, … }` and `ArrayHeader` +// is `{ length: u32, capacity: u32 }`, so the two u32s at offset 4 alias: an +// N-capacity array read as an `ObjectHeader` reports "class id N". +// +// `arr[Symbol.iterator]` resolves through `js_class_method_bind(arr, "values")` +// (`symbol/get.rs`), and that bound-method builder read `class_id` off the +// receiver with a BARE `(*obj).class_id` instead of the guarded +// `js_object_get_class_id` accessor (which rejects any allocation whose +// `GcHeader.obj_type` is not `GC_TYPE_OBJECT`). So whenever the class whose id +// equalled the array's capacity happened to own a `values` method, the array's +// iterator resolved to THAT class's method. +// +// The issue was reported as a `class X extends Map` bug, but Map is incidental: +// the only thing that matters is a class owning a method named `values`. When +// that class is also the one whose `values` body builds the array literal, the +// method calls itself until the stack guard page — a SIGSEGV. + +// ── the issue's exact reproducer ── +class MyMap extends Map { + values(): IterableIterator { + return [777 as unknown as V][Symbol.iterator](); + } +} + +const m = new MyMap(); +m.set("q", 5); +console.log("size:", m.size); +console.log("get:", m.get("q")); +const out: number[] = []; +for (const v of m.values()) out.push(v); +console.log("values:", out.join(",")); + +// ── the same crash with NO `Map` anywhere: a plain class whose method is named +// `values` and whose body iterates an array literal. Before the fix the +// one-element literal read back as class id 1 — the class itself — so +// `values` called `values` until the stack overflowed. ── +class Plain { + values(): IterableIterator { + return [777][Symbol.iterator](); + } +} +console.log("plain:", [...new Plain().values()].join(",")); + +// ── the non-recursive form of the same mis-dispatch: the array's capacity +// selects a DIFFERENT class that owns `values`. Pre-fix the 2-element +// literal resolved to `B.values` (a number), and the spread threw +// "value is not iterable". ── +class A { + one(): IterableIterator { + return [1][Symbol.iterator](); + } + two(): IterableIterator { + return [1, 2][Symbol.iterator](); + } + three(): IterableIterator { + return [1, 2, 3][Symbol.iterator](); + } +} +class B { + values(): number { + return 42; + } +} +const a = new A(); +console.log("cap1:", [...a.one()].join(",")); +console.log("cap2:", [...a.two()].join(",")); +console.log("cap3:", [...a.three()].join(",")); +console.log("B.values:", new B().values()); + +// ── a free function (no class in scope) always worked; keep it covered so a +// future narrowing of the guard cannot silently break the ordinary path. ── +function free(): IterableIterator { + return [888][Symbol.iterator](); +} +console.log("free:", [...free()].join(",")); + +// ── the array `values`/`keys`/`entries` surface itself must stay intact ── +const plainArr = [10, 20, 30]; +console.log("arr values:", [...plainArr.values()].join(",")); +console.log("arr keys:", [...plainArr.keys()].join(",")); +console.log("arr entries:", JSON.stringify([...plainArr.entries()])); +console.log("arr @@iterator:", [...plainArr[Symbol.iterator]()].join(",")); + +// ── native-base subclass overrides, the family the issue reported against ── +class MapKeysOverride extends Map { + keys(): IterableIterator { + return ["kk"][Symbol.iterator](); + } +} +class MapEntriesOverride extends Map { + entries(): IterableIterator<[string, number]> { + return ([["ee", 1]] as [string, number][])[Symbol.iterator](); + } +} +class MapIterOverride extends Map { + *[Symbol.iterator](): IterableIterator<[string, number]> { + yield ["ii", 9]; + } +} +const mk = new MapKeysOverride(); +mk.set("q", 5); +console.log("map keys override:", [...mk.keys()].join(",")); +const me = new MapEntriesOverride(); +me.set("q", 5); +console.log("map entries override:", JSON.stringify([...me.entries()])); +const mi = new MapIterOverride(); +mi.set("q", 5); +console.log("map @@iterator override:", JSON.stringify([...mi])); + +class SetValuesOverride extends Set { + values(): IterableIterator { + return [111][Symbol.iterator](); + } +} +class SetIterOverride extends Set { + *[Symbol.iterator](): IterableIterator { + yield 444; + } +} +const sv = new SetValuesOverride(); +sv.add(1); +console.log("set values override:", [...sv.values()].join(",")); +const si = new SetIterOverride(); +si.add(1); +console.log("set @@iterator override:", [...si].join(",")); + +class ArrValuesOverride extends Array { + values(): IterableIterator { + return [555][Symbol.iterator](); + } +} +const av = new ArrValuesOverride(); +av.push(1); +console.log("array values override:", [...av.values()].join(",")); + +// ── INDIRECT subclass and a class EXPRESSION: the two shapes CLAUDE.md's +// "native base-class subclassing" note calls out as historically lossy. ── +class MidMap extends Map {} +class LeafMap extends MidMap { + values(): IterableIterator { + return [888][Symbol.iterator](); + } +} +const lm = new LeafMap(); +lm.set("z", 3); +console.log("indirect override:", [...lm.values()].join(",")); + +const ExprMap = class extends Map { + values(): IterableIterator { + return [999][Symbol.iterator](); + } +}; +const em = new ExprMap(); +em.set("z", 3); +console.log("class-expression override:", [...em.values()].join(",")); + +// ── non-overriding subclasses keep the built-in surface ── +class PlainMap extends Map {} +const pm = new PlainMap(); +pm.set("a", 1); +pm.set("b", 2); +console.log("no-override values:", [...pm.values()].join(",")); +console.log("no-override keys:", [...pm.keys()].join(",")); +console.log("no-override entries:", JSON.stringify([...pm.entries()])); +console.log("no-override spread:", JSON.stringify([...pm])); + +// ── `super.()` from inside an override still reaches the native base ── +class SuperMap extends Map { + values(): IterableIterator { + return super.values(); + } +} +const sm = new SuperMap(); +sm.set("a", 1); +sm.set("b", 2); +console.log("super.values():", [...sm.values()].join(",")); + +console.log("done");