diff --git a/CLAUDE.md b/CLAUDE.md index 8ac7ced3ae..9402553dbd 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.1404 +**Current Version:** 0.5.1405 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 7d89eb61b4..ee979ff0b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1404" +version = "0.5.1405" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1404" +version = "0.5.1405" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1404" +version = "0.5.1405" [[package]] name = "perry-ui-tvos" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1404" +version = "0.5.1405" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 226aeab86e..40620a4630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1404" +version = "0.5.1405" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7164-ffi-by-index-field-count.md b/changelog.d/7164-ffi-by-index-field-count.md new file mode 100644 index 0000000000..46cf13dac5 --- /dev/null +++ b/changelog.d/7164-ffi-by-index-field-count.md @@ -0,0 +1,9 @@ +### Fixed + +- **`js_object_set_field` by index left the written slot outside the collector's view (#7164).** `perry_ffi::alloc_object()` allocates with `field_count = 0` and `INLINE_SLOT_FLOOR` physical slots. The documented by-index writer bounds-checks `field_index` against `max(field_count, INLINE_SLOT_FLOOR)` — so it *accepts* an index at or above `field_count` — but never widened `field_count` to cover it. + + `object::gc_field_slot_range` bounds the collector's view of an object's payload by `field_count`, and `heap_payload_slot_selection` short-circuits to `Empty` on an empty payload **before** consulting the per-object pointer mask. A pointer written through the by-index setter was therefore invisible to both tracing (never marked → swept while still live) and to evacuation rewriting (a stale from-space pointer left in a live slot). That is the crash in the issue: the bogus `0x9` slot the copying minor SIGSEGVs on is downstream wreckage, not the defect. + + This is the by-index counterpart of the "#7154 publication order" invariant that `field_set_by_name/tail.rs` already enforces at two sites; only the by-index path was missing it. Widening before the store is safe because the existing bounds check already holds `field_index` below the physical capacity, and every physical slot is `undefined`-initialized at allocation, so the widened range can only ever expose non-pointer sentinels. + + The regression test asserts the defect **deterministically**, without racing a collector: it checks the collector's own child-slot enumerator (`gc_child_slots` — the function every mark/scan/evacuation pass calls) reports the written slot at all. Before the fix it enumerated `[]`. It then runs a real copying minor gated on `copied_objects > 0`, so a run that collected nothing cannot report success. diff --git a/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs b/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs index 6fdd5f7a57..feadbd8b99 100644 --- a/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs +++ b/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs @@ -266,3 +266,100 @@ fn test_fresh_closure_capture_slots_are_initialized_7154() { } } } + +/// #7164 — `js_object_set_field` (the BY-INDEX setter on `perry-ffi`'s stable +/// surface, re-exported verbatim from `object/field_get_set/field_ops.rs`) +/// must widen `field_count` the same way the by-name path already does at +/// `object/field_set_by_name/tail.rs`'s two "#7154 publication order" sites. +/// +/// `perry_ffi::alloc_object()` calls `js_object_alloc(0, 0)`: `field_count = +/// 0` with `INLINE_SLOT_FLOOR` (4) physical slots undefined-initialized. +/// `js_object_set_field(obj, 0, pointer_value)` passes the bounds check +/// (`0 < max(field_count, 4)`) and stores the pointer, but — unlike +/// `tail.rs`'s by-name writer — never bumps `field_count`. The collector's +/// view of the payload (`object::gc_field_slot_range`, and downstream +/// `heap_payload_slot_selection`'s `payload.is_empty()` short-circuit) is +/// bounded by `field_count`, so the stored pointer is outside the traced +/// range: never marked (swept while live) and never rewritten by a copying +/// minor (stale from-space pointer left in a live slot). Reachable through +/// `perry-ffi`'s documented surface alone (`alloc_object` + `js_object_set_field`). +#[test] +fn test_ffi_index_field_set_widens_field_count_7164() { + let _guard = CopyingNurseryTestGuard::new(1); + + // Mirrors `perry_ffi::alloc_object()` exactly: class_id=0, field_count=0. + let obj = crate::object::js_object_alloc(0, 0); + assert_eq!( + unsafe { (*obj).field_count }, + 0, + "test setup: alloc_object()'s field_count starts at 0" + ); + + let child = crate::string::js_string_from_bytes( + b"ffi-index-set-field-7164".as_ptr(), + "ffi-index-set-field-7164".len() as u32, + ); + + // The exact documented-FFI call: `perry_ffi::js_object_set_field(obj, 0, + // value)` forwards verbatim to this runtime symbol. + crate::object::js_object_set_field( + obj, + 0, + crate::value::JSValue::from_bits(string_bits(child as usize)), + ); + + // Deterministic assertion, independent of GC timing (mirrors the two + // tests above): the slot the store just published must be enumerated as + // a child edge, or the collector's view of the payload never covers it. + let field0_slot = unsafe { + (obj as *mut u8).add(std::mem::size_of::()) as usize + }; + let enumerated = unsafe { enumerated_child_slots(obj as usize) }; + assert!( + enumerated.contains(&field0_slot), + "#7164: js_object_set_field(obj, 0, ..) on a field_count=0 object must \ + widen field_count so the collector's view covers the written slot; \ + the collector enumerated {enumerated:?} (a raw field_count=0 leaves \ + the whole payload range empty, so the mask is never consulted)" + ); + assert_eq!( + unsafe { (*obj).field_count }, + 1, + "#7164: js_object_set_field must widen field_count to cover the \ + written index, mirroring field_set_by_name/tail.rs's publication order" + ); + + // End to end: the object is the ONLY root — the string is reachable + // exclusively through the field this defect leaves untraced. It must + // survive a copying minor, and the slot must be rewritten to the + // relocated address (not left pointing at reclaimed from-space memory). + js_shadow_slot_set(0, ptr_bits(obj as usize)); + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert!( + trace.copying_nursery.copied_objects > 0, + "#7164 regression test requires a COPYING minor; a non-moving \ + collection cannot expose a missing mark/rewrite (copied_objects=0)" + ); + + let obj_after = (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::object::ObjectHeader; + let stored = crate::object::js_object_get_field(obj_after, 0); + assert!( + stored.is_string(), + "#7164: the field must still hold a live string after a copying \ + minor (got bits {:#x}) — an untraced slot is either collected out \ + from under a live reference or left as a stale from-space pointer", + stored.bits() + ); + let recovered = (stored.bits() & POINTER_MASK) as usize; + assert!( + crate::arena::classify_heap_generation(recovered) != crate::arena::HeapGeneration::Unknown, + "#7164: the string reached only through the object field must remain \ + a live heap pointer after the collection (recovered {recovered:#x})" + ); + unsafe { + assert_string_bytes( + recovered as *const crate::StringHeader, + b"ffi-index-set-field-7164", + ); + } +} diff --git a/crates/perry-runtime/src/object/field_get_set/field_ops.rs b/crates/perry-runtime/src/object/field_get_set/field_ops.rs index edb2770da1..cc5b647048 100644 --- a/crates/perry-runtime/src/object/field_get_set/field_ops.rs +++ b/crates/perry-runtime/src/object/field_get_set/field_ops.rs @@ -149,6 +149,22 @@ pub extern "C" fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, }; let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; let slot = fields_ptr.add(field_index as usize); + // #7164 publication order (same invariant as the two "#7154 + // publication order" sites in field_set_by_name/tail.rs): widen + // `field_count` FIRST, before the store. `object::gc_field_slot_range` + // bounds the collector's view of the payload by `field_count`, so a + // write at an index the count does not yet cover is invisible to BOTH + // tracing and evacuation rewriting -- a pointer stored there is never + // marked (swept while live) and never rewritten (stale from-space + // pointer left in a live slot) by a copying minor. This is the + // BY-INDEX counterpart of that bug: `alloc_limit` above already bounds + // `field_index` below the physical capacity, and every physical slot + // is undefined-initialized at allocation (`object/alloc.rs`), so + // widening here can only ever expose non-pointer sentinels ahead of + // the store that is about to fill this one in. + if field_index >= (*obj).field_count { + (*obj).field_count = field_index + 1; + } crate::gc::runtime_store_jsvalue_slot( obj as usize, slot as usize,