From c124027630df8d370438f77188b108a7db70a877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 14:37:40 +0200 Subject: [PATCH 1/4] test(gc): make the POINTER_FREE misdeclaration hazard detectable (#7635) #7635 sabotaged `layout_finish_deferred_boxed_object(ptr, saw_pointer)` to `(ptr, false)` -- every JSON-parsed record claiming POINTER_FREE while holding heap strings -- and got byte-identical correct output under PERRY_GC_ZEAL + PERRY_GC_PROTECT_FROMSPACE and under PERRY_GC_FORCE_EVACUATE, with copying minors and retired quarantine sets observed live. Those knobs do not discriminate this hazard. Four tests that do, sabotage-verified in both directions: the child-slot enumerator (deterministic, no GC timing), relocation across a copying minor, the finalize's two exact outcomes, and the same invariant driven through the real js_json_parse entry point so the json/parser.rs call site is covered rather than only the helper. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- crates/perry-runtime/src/gc/tests/copying.rs | 1 + .../tests/copying/deferred_finalize_7635.rs | 356 ++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs diff --git a/crates/perry-runtime/src/gc/tests/copying.rs b/crates/perry-runtime/src/gc/tests/copying.rs index acce664f66..37cff3ceee 100644 --- a/crates/perry-runtime/src/gc/tests/copying.rs +++ b/crates/perry-runtime/src/gc/tests/copying.rs @@ -1,5 +1,6 @@ mod adaptive_tenuring; mod all_pointer_elements_7469; +mod deferred_finalize_7635; mod pointer_publish_7154; mod promise_side_tables; mod survival_and_malloc; diff --git a/crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs b/crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs new file mode 100644 index 0000000000..c98e18cdb0 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs @@ -0,0 +1,356 @@ +//! #7635 — is a `POINTER_FREE` MISDECLARATION detectable at all? +//! +//! Invariant under test: +//! +//! > `layout_finish_deferred_boxed_object(ptr, saw_pointer)` is the ONLY thing +//! > that moves a materialiser-built record off its `GC_LAYOUT_POINTER_FREE` +//! > birth state. When any pointer was stored, that call is load-bearing for GC +//! > correctness: `heap_payload_slot_selection` short-circuits on `POINTER_FREE` +//! > and skips the WHOLE payload without consulting any mask, so a record left +//! > in that state neither keeps its children alive nor has its slots rewritten +//! > when they move. +//! +//! # Why this file exists rather than an end-to-end probe +//! +//! #7633 deferred the JSON materialiser's per-slot layout notes to one +//! finalize. Auditing it, #7635 sabotaged that finalize to +//! `(ptr, /* saw_pointer */ false)` — every parsed record claiming +//! `POINTER_FREE` while holding heap strings — and got **byte-identical correct +//! output** from a 4,000-record Perry-compiled workload under `PERRY_GC_ZEAL=1 +//! PERRY_GC_PROTECT_FROMSPACE=1` and under `PERRY_GC_FORCE_EVACUATE=1`, with +//! copying minors and retired quarantine sets observed live. Those three knobs +//! do not discriminate this hazard, for a structural reason worth stating once: +//! +//! - `PERRY_GC_PROTECT_FROMSPACE` faults on a *deref of a retired page*. A +//! stranded child is only dereferenced if the mutator happens to read that +//! field again after the retirement, and only while the address is still +//! inside the bounded quarantine (`…_DEPTH`, default 4). +//! - `PERRY_GC_VERIFY_EVACUATION` walks the same enumeration the rewrite pass +//! walks — which is to say it asks this very layout state which slots exist. +//! It is blind to a misdeclaration by construction. (`gc/fromspace_scan.rs`'s +//! module header makes the same point about the verifier generally.) +//! - `PERRY_GC_FORCE_EVACUATE` only makes survivors MOVE; moving harder does +//! not make an un-enumerated slot enumerable. +//! +//! Two things do discriminate it, and both are used here: +//! +//! 1. **The child-slot enumerator itself** — `gc_child_slots` is the single +//! question every collector pass funnels through, so asking it directly is +//! deterministic regardless of GC timing, conservative-scan residue, or +//! whether a copying minor happened to run. +//! 2. **Relocation** — after a copying minor that actually moved things, a +//! traced child has a NEW address and the holding slot says so. A stranded +//! child's slot still holds its pre-cycle address. That comparison does not +//! depend on reading the stale memory, which is why it is stable where a +//! poison/deref instrument is not. +//! +//! The layout-independent end-to-end instrument is `PERRY_GC_FROMSPACE_SCAN=1` +//! (whole-payload word scan, no root enumeration), which reports a stranded +//! child as `dangling=`. It is the knob #7635's audit was missing. +//! +//! [`a_misdeclared_pointer_free_record_strands_its_child`] is the SABOTAGE ARM, +//! made permanent: it performs the identical construction with the finalize's +//! `saw_pointer` forced to `false` and asserts the child is stranded. A green +//! run of the positive test therefore means the finalize was load-bearing, not +//! that nothing was tried. + +use super::*; + +use crate::object::ObjectHeader; + +const FIELD_VALUES: [&[u8]; 2] = [b"value_alpha", b"value_bravo"]; + +fn fresh_string(bytes: &[u8]) -> usize { + crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) as usize +} + +unsafe fn field_bits(obj: *mut ObjectHeader, index: usize) -> u64 { + let fields = (obj as *const u8).add(std::mem::size_of::()) as *const u64; + *fields.add(index) +} + +unsafe fn layout_state_of(user_ptr: usize) -> u16 { + (*header_from_user_ptr(user_ptr as *const u8))._reserved & GC_LAYOUT_STATE_MASK +} + +/// Addresses of the slots the collector says it will visit inside `user_ptr`. +/// A `POINTER_FREE` payload contributes NOTHING here — that is the failure mode +/// this file guards. +unsafe fn enumerated_slot_addrs(user_ptr: usize) -> Vec { + test_heap_child_slots_for_user(user_ptr as *mut u8) + .into_iter() + .filter_map(|slot| match slot { + HeapChildSlot::Child(p, _) => Some(p as usize), + HeapChildSlot::PointerFreeRange(_) => None, + }) + .collect() +} + +unsafe fn field_slot_addr(obj: *mut ObjectHeader, index: usize) -> usize { + let fields = (obj as *const u8).add(std::mem::size_of::()) as *const u64; + fields.add(index) as usize +} + +/// The materialiser's construction loop, byte for byte: allocate the record, +/// store each field through the layout-deferred slot helper (no per-slot note), +/// accumulate the one fact the elided notes were computing, and settle the +/// layout state once. +/// +/// `honest_finalize == false` is #7635's sabotage — the exact +/// `(ptr, /* saw_pointer */ false)` mutation, expressed as an argument so both +/// arms run identical code up to that single boolean. +unsafe fn materialise_record(honest_finalize: bool) -> *mut ObjectHeader { + let obj = crate::object::js_object_alloc(0, FIELD_VALUES.len() as u32); + assert_eq!( + layout_state_of(obj as usize), + GC_LAYOUT_POINTER_FREE, + "test premise: a fresh record is born POINTER_FREE, which is why the \ + finalize is the only thing that can move it off that state" + ); + let mut saw_pointer = false; + for (index, bytes) in FIELD_VALUES.iter().enumerate() { + let child = fresh_string(bytes); + saw_pointer |= + crate::object::store_object_field_slot_layout_deferred(obj, index, string_bits(child)); + } + assert!( + saw_pointer, + "test premise: storing heap strings must be reported as pointer-bearing" + ); + layout_finish_deferred_boxed_object(obj as usize, saw_pointer && honest_finalize); + obj +} + +/// The finalize's two exact outcomes, pinned so a refactor cannot quietly widen +/// either one. A record with no pointer stored KEEPS its `POINTER_FREE` birth +/// state (that is the state's whole value); any pointer stored lands in +/// `GC_LAYOUT_UNKNOWN`, the tag-checked scan-all-slots state — never in a mask. +#[test] +fn finalize_settles_pointer_free_or_unknown_and_nothing_else() { + let _guard = CopyingNurseryTestGuard::new(1); + unsafe { + let numeric = crate::object::js_object_alloc(0, 2); + for index in 0..2usize { + assert!( + !crate::object::store_object_field_slot_layout_deferred( + numeric, + index, + crate::value::JSValue::number(index as f64 + 1.0).bits(), + ), + "a number store must not be reported as pointer-bearing" + ); + } + layout_finish_deferred_boxed_object(numeric as usize, false); + assert_eq!( + layout_state_of(numeric as usize), + GC_LAYOUT_POINTER_FREE, + "a record that stored no pointer keeps the birth state — the \ + tracer skips its whole payload, which is what #7630 bought" + ); + assert_eq!( + test_heap_child_slot_count(numeric as *mut u8), + 0, + "and the collector enumerates zero payload slots on it" + ); + + let pointered = materialise_record(/* honest_finalize = */ true); + assert_eq!( + layout_state_of(pointered as usize), + GC_LAYOUT_UNKNOWN, + "any pointer stored must settle in the conservative scan-all state" + ); + assert!( + !layout_has_typed_descriptor(pointered as usize), + "the finalize routes through `layout_mark_unknown`, so a mask a \ + slow-path by-name store created mid-construction is REMOVED, not \ + stranded" + ); + } +} + +/// The positive arm. A record built exactly as the JSON materialiser builds it, +/// holding the ONLY reference to each of its children, must have every field +/// enumerated as a child edge and must survive a copying minor with both +/// children relocated and both slots rewritten. +#[test] +fn a_materialised_record_keeps_its_children_traced_and_rewritten_7635() { + let _guard = CopyingNurseryTestGuard::new(1); + + let obj = unsafe { materialise_record(/* honest_finalize = */ true) }; + let before: Vec = (0..FIELD_VALUES.len()) + .map(|index| unsafe { (field_bits(obj, index) & POINTER_MASK) as usize }) + .collect(); + assert_eq!( + test_heap_child_slot_count(obj as *mut u8), + FIELD_VALUES.len(), + "the collector must enumerate every pointer-bearing field of a \ + finalized record; a POINTER_FREE record enumerates ZERO" + ); + + // The record's slots are now the sole path to each child. + js_shadow_slot_set(0, ptr_bits(obj as usize)); + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert!( + trace.copying_nursery.copied_objects >= FIELD_VALUES.len() + 1, + "this test proves nothing unless the cycle actually MOVED the record \ + and both children (copied_objects = {})", + trace.copying_nursery.copied_objects + ); + + let moved = (js_shadow_slot_get(0) & POINTER_MASK) as usize as *mut ObjectHeader; + assert_ne!(moved as usize, obj as usize, "the record itself must move"); + unsafe { + for (index, bytes) in FIELD_VALUES.iter().enumerate() { + let child = (field_bits(moved, index) & POINTER_MASK) as usize; + assert_ne!( + child, before[index], + "field {index} must have been relocated and its slot rewritten" + ); + assert!( + crate::arena::pointer_in_nursery(child) + || crate::arena::pointer_in_old_gen(child), + "field {index} must name a live heap object, not a stale address" + ); + assert_string_bytes(child as *const crate::StringHeader, bytes); + } + } + js_shadow_slot_set(0, crate::value::TAG_UNDEFINED); +} + +/// The same invariant driven through the REAL entry point, `js_json_parse`, so +/// the finalize CALL SITE in `json/parser.rs` is covered and not merely the +/// helper it calls. #7635's sabotage was applied at that call site, and the two +/// tests above would stay green through it. +/// +/// The parsed record holds the only reference to each of its string values — +/// only the KEYS are interned into the (rooted) parse-key cache — so a +/// misdeclared record strands them. +#[test] +fn json_parse_record_keeps_its_string_values_traced_and_rewritten_7635() { + let _guard = CopyingNurseryTestGuard::new(1); + + // Values are 11 bytes, well above `SHORT_STRING_MAX_LEN`, so they are real + // heap `StringHeader`s in the nursery — collectable and movable — rather + // than inline short strings that no layout state could strand. + let text = br#"{"alpha":"value_alpha","bravo":"value_bravo"}"#; + let parsed = + unsafe { crate::json::js_json_parse(crate::string::js_string_from_bytes( + text.as_ptr(), + text.len() as u32, + )) }; + js_shadow_slot_set(0, parsed.bits()); + + let obj = (js_shadow_slot_get(0) & POINTER_MASK) as usize as *mut ObjectHeader; + let before: Vec = (0..FIELD_VALUES.len()) + .map(|index| unsafe { (field_bits(obj, index) & POINTER_MASK) as usize }) + .collect(); + unsafe { + assert_ne!( + layout_state_of(obj as usize), + GC_LAYOUT_POINTER_FREE, + "a parsed record holding heap strings must NOT be left in the \ + birth state — that is #7635's sabotage" + ); + for (index, bytes) in FIELD_VALUES.iter().enumerate() { + assert_eq!( + field_bits(obj, index) & TAG_MASK, + STRING_TAG, + "test premise: field {index} must hold a HEAP string" + ); + assert_string_bytes(before[index] as *const crate::StringHeader, bytes); + } + let enumerated = enumerated_slot_addrs(obj as usize); + for index in 0..FIELD_VALUES.len() { + assert!( + enumerated.contains(&field_slot_addr(obj, index)), + "the collector must enumerate parsed field {index} as a child \ + edge; it reported {enumerated:?}" + ); + } + } + + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert!( + trace.copying_nursery.copied_objects >= FIELD_VALUES.len() + 1, + "this test proves nothing unless the cycle actually MOVED the record \ + and both values (copied_objects = {})", + trace.copying_nursery.copied_objects + ); + + let moved = (js_shadow_slot_get(0) & POINTER_MASK) as usize as *mut ObjectHeader; + unsafe { + for (index, bytes) in FIELD_VALUES.iter().enumerate() { + let child = (field_bits(moved, index) & POINTER_MASK) as usize; + assert_ne!( + child, before[index], + "parsed field {index} must have been relocated and its slot \ + rewritten" + ); + assert_string_bytes(child as *const crate::StringHeader, bytes); + } + } + js_shadow_slot_set(0, crate::value::TAG_UNDEFINED); +} + +/// SABOTAGE ARM, made permanent — #7635's exact mutation. +/// +/// The identical construction with the finalize's `saw_pointer` forced to +/// `false`: the record stays `POINTER_FREE`, `heap_payload_slot_selection` +/// skips the whole payload, and the collector enumerates NOTHING. That is the +/// stranded-live-child hazard, and asserting it here is what makes the positive +/// test above a detector rather than a formality. +/// +/// Asserted on the ENUMERATOR, not through a collection, so nothing here leaves +/// a stale pointer behind for a later cycle on this thread. +/// +/// If a future change makes the collector reach these children anyway — a +/// conservative payload sweep, a layout-independent rescue pass — this test +/// goes red. That is the intended signal, not a nuisance: it would mean the +/// `POINTER_FREE` trace-skip had stopped being load-bearing, and both this file +/// and the doc comment on `GC_LAYOUT_POINTER_FREE` would need rewriting. +#[test] +fn a_misdeclared_pointer_free_record_strands_its_child() { + let _guard = CopyingNurseryTestGuard::new(1); + + let honest = unsafe { materialise_record(/* honest_finalize = */ true) }; + let sabotaged = unsafe { materialise_record(/* honest_finalize = */ false) }; + + unsafe { + assert_eq!( + layout_state_of(sabotaged as usize), + GC_LAYOUT_POINTER_FREE, + "the sabotage must actually leave the record misdeclared, or this \ + arm tests nothing" + ); + assert_eq!( + test_heap_child_slot_count(honest as *mut u8), + FIELD_VALUES.len() + ); + assert_eq!( + test_heap_child_slot_count(sabotaged as *mut u8), + 0, + "a POINTER_FREE record skips its whole payload — the children are \ + invisible to marking, to the evacuation rewrite, and to the \ + remembered-set scan alike" + ); + + // Same fields, same bits, same stores: only the finalize differed. + for index in 0..FIELD_VALUES.len() { + assert_eq!( + field_bits(sabotaged, index) & TAG_MASK, + STRING_TAG, + "field {index} really does hold a heap string in both arms" + ); + } + + // Leave no stale-pointer landmine: put the misdeclared record back into + // the conservative state before the guard drops. + layout_mark_unknown(sabotaged as *mut u8); + assert_eq!( + test_heap_child_slot_count(sabotaged as *mut u8), + FIELD_VALUES.len(), + "and the very same record becomes fully enumerable the moment its \ + layout state is corrected — the state is the ONLY difference" + ); + } +} From c5b5c2a0d43ce1378822508bedbdef5a28287983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 14:57:20 +0200 Subject: [PATCH 2/4] docs(gc): record what can and cannot verify a POINTER_FREE claim (#7635) #7635's audit forced every JSON-parsed record to POINTER_FREE while it held heap strings and got byte-identical correct output under zeal + from-space protect and under force-evacuate. The instruments were not at fault: js_json_parse is LAZY for 1 KB-16 MB top-level arrays (json_tape), so parse_object runs when an element is first READ, and the probe read its records only after the last collection. A traced-object audit on the sabotaged build saw zero POINTER_FREE objects with pointer-bearing payload words -- nothing was stranded because nothing was there. Under PERRY_JSON_TAPE=0 the same sabotage SIGSEGVs, PERRY_GC_FROMSPACE_SCAN reports dangling=8000 owners=4000 (exactly 4000 records x 2 fields, vs 0 clean), and PERRY_GC_PROTECT_FROMSPACE names the faulting address. With the records merely touched before the churn, the default lazy path reads back 7,872 of 8,000 values wrong. Written onto the constant so the next PR in this family does not cite "clean under zeal + protect" as evidence without first showing the subject existed during a collection, and prefers the layout-independent PERRY_GC_FROMSPACE_SCAN. PERRY_GC_VERIFY_EVACUATION is genuinely blind here -- it walks the same enumeration the rewrite pass walks. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- crates/perry-runtime/src/gc/layout.rs | 29 +++++++ .../tests/copying/deferred_finalize_7635.rs | 82 +++++++++++-------- 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 5c50db7531..cc2f4894d4 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -18,6 +18,35 @@ pub(super) const GC_COPY_PROMOTION_SURVIVALS: u8 = 4; // Low bits remain object freeze/seal/preventExtensions flags. pub const GC_LAYOUT_STATE_MASK: u16 = 0xC000; pub(super) const GC_LAYOUT_UNKNOWN: u16 = 0x0000; +/// No payload slot holds a pointer, so `heap_payload_slot_selection` skips the +/// WHOLE payload without consulting any mask. This is the one layout state that +/// is not a precision hint: marking, the evacuation rewrite and the +/// remembered-set dirty scan all funnel through that same enumeration, so an +/// object left here while holding a heap pointer loses that child outright — it +/// is neither kept alive nor rewritten when it moves. +/// +/// **How to verify a change to this state (#7635).** The end-to-end knobs do +/// catch a misdeclaration, but only if the workload actually holds a misdeclared +/// object across a collection, and it is easy to build one that never does: +/// #7635 forced every JSON-parsed record to `POINTER_FREE` while it held heap +/// strings and got byte-identical correct output under `PERRY_GC_ZEAL=1 +/// PERRY_GC_PROTECT_FROMSPACE=1` and `PERRY_GC_FORCE_EVACUATE=1`, because +/// `js_json_parse` is LAZY for 1 KB–16 MB top-level arrays (`json_tape`) and the +/// probe read its records only after the last GC. Under `PERRY_JSON_TAPE=0` the +/// same sabotage SIGSEGVs. So: +/// +/// - "clean under zeal + from-space protect" is evidence only once you have +/// shown the misdeclared object EXISTED during a collection; +/// - `PERRY_GC_FROMSPACE_SCAN=1` is the instrument to prefer — its +/// whole-payload word scan consults no layout state, and it reported the +/// stranded children at exactly `dangling=8000 owners=4000`; +/// - `PERRY_GC_VERIFY_EVACUATION` is blind here by construction: it walks the +/// same enumeration the rewrite pass walks, which is to say it asks this +/// state which slots exist. +/// +/// The workload-free detectors are the child-slot enumerator and relocation +/// across a copying minor; worked example, sabotage-verified in both +/// directions: `gc/tests/copying/deferred_finalize_7635.rs`. pub const GC_LAYOUT_POINTER_FREE: u16 = 0x4000; pub(crate) const GC_LAYOUT_SIDE_MASK: u16 = 0x8000; // A side-layout payload whose entire live prefix contains pointers. Bit 13 is diff --git a/crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs b/crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs index c98e18cdb0..a0fd5f9393 100644 --- a/crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs +++ b/crates/perry-runtime/src/gc/tests/copying/deferred_finalize_7635.rs @@ -10,7 +10,7 @@ //! > in that state neither keeps its children alive nor has its slots rewritten //! > when they move. //! -//! # Why this file exists rather than an end-to-end probe +//! # Why a unit test, when the end-to-end probe reported clean //! //! #7633 deferred the JSON materialiser's per-slot layout notes to one //! finalize. Auditing it, #7635 sabotaged that finalize to @@ -18,40 +18,52 @@ //! `POINTER_FREE` while holding heap strings — and got **byte-identical correct //! output** from a 4,000-record Perry-compiled workload under `PERRY_GC_ZEAL=1 //! PERRY_GC_PROTECT_FROMSPACE=1` and under `PERRY_GC_FORCE_EVACUATE=1`, with -//! copying minors and retired quarantine sets observed live. Those three knobs -//! do not discriminate this hazard, for a structural reason worth stating once: +//! copying minors and retired quarantine sets observed live. //! -//! - `PERRY_GC_PROTECT_FROMSPACE` faults on a *deref of a retired page*. A -//! stranded child is only dereferenced if the mutator happens to read that -//! field again after the retirement, and only while the address is still -//! inside the bounded quarantine (`…_DEPTH`, default 4). -//! - `PERRY_GC_VERIFY_EVACUATION` walks the same enumeration the rewrite pass -//! walks — which is to say it asks this very layout state which slots exist. -//! It is blind to a misdeclaration by construction. (`gc/fromspace_scan.rs`'s -//! module header makes the same point about the verifier generally.) -//! - `PERRY_GC_FORCE_EVACUATE` only makes survivors MOVE; moving harder does -//! not make an un-enumerated slot enumerable. +//! **The instruments were not at fault; the probe's subject never existed.** +//! `js_json_parse` routes a top-level array of 1 KB–16 MB through the LAZY TAPE +//! (`json_tape`, default since #179), so `parse_object` does not run at +//! `JSON.parse` time — it runs when an element is first read. The probe read +//! its records only *after* the churn, so every misdeclared record was +//! materialised after the last collection. A traced-object audit added to +//! `heap_payload_slot_selection` on the sabotaged build confirms it: **zero** +//! objects in `POINTER_FREE` state with pointer-bearing payload words were ever +//! handed to the collector. Nothing was stranded because nothing was there. //! -//! Two things do discriminate it, and both are used here: +//! Re-run so the misdeclared records actually live across a collection and +//! every instrument fires (measured on the sabotaged build, this branch): +//! +//! | arm | clean | sabotaged | +//! |---|---|---| +//! | default (lazy), read after churn | exit 0 | exit 0, byte-identical, `dangling=0` | +//! | `PERRY_JSON_TAPE=0`, read after churn | exit 0 | **SIGSEGV**; `dangling=8000 owners=4000` on the first scanned cycle; `PERRY_GC_PROTECT_FROMSPACE=1` prints `FAULT: signal 10 at 0x…` | +//! | default (lazy), records touched BEFORE the churn | exit 0 | 7,872 of 8,000 values read back wrong | +//! +//! So the end-to-end lesson is about *probe construction*, not instrument +//! capability — with one real exception. `PERRY_GC_VERIFY_EVACUATION` walks the +//! same enumeration the rewrite pass walks, i.e. it asks this very layout state +//! which slots exist, and is blind to a misdeclaration by construction; +//! `gc/fromspace_scan.rs`'s module header makes the same point about the +//! verifier generally. **`PERRY_GC_FROMSPACE_SCAN=1` is the layout-independent +//! one** — a whole-payload word scan that consults no root enumeration and no +//! layout state — and it is the knob to reach for on this hazard class. +//! +//! What this file adds on top is a detector that needs no workload at all, so +//! it cannot be defeated by a lazy path, a GC that did not happen to run, or +//! conservative-scan residue: //! //! 1. **The child-slot enumerator itself** — `gc_child_slots` is the single //! question every collector pass funnels through, so asking it directly is -//! deterministic regardless of GC timing, conservative-scan residue, or -//! whether a copying minor happened to run. +//! deterministic regardless of GC timing. //! 2. **Relocation** — after a copying minor that actually moved things, a //! traced child has a NEW address and the holding slot says so. A stranded -//! child's slot still holds its pre-cycle address. That comparison does not -//! depend on reading the stale memory, which is why it is stable where a -//! poison/deref instrument is not. -//! -//! The layout-independent end-to-end instrument is `PERRY_GC_FROMSPACE_SCAN=1` -//! (whole-payload word scan, no root enumeration), which reports a stranded -//! child as `dangling=`. It is the knob #7635's audit was missing. +//! child's slot still holds its pre-cycle address, and that comparison never +//! reads the stale memory. //! //! [`a_misdeclared_pointer_free_record_strands_its_child`] is the SABOTAGE ARM, //! made permanent: it performs the identical construction with the finalize's //! `saw_pointer` forced to `false` and asserts the child is stranded. A green -//! run of the positive test therefore means the finalize was load-bearing, not +//! run of the positive tests therefore means the finalize was load-bearing, not //! that nothing was tried. use super::*; @@ -176,7 +188,9 @@ fn finalize_settles_pointer_free_or_unknown_and_nothing_else() { fn a_materialised_record_keeps_its_children_traced_and_rewritten_7635() { let _guard = CopyingNurseryTestGuard::new(1); - let obj = unsafe { materialise_record(/* honest_finalize = */ true) }; + let obj = unsafe { + materialise_record(/* honest_finalize = */ true) + }; let before: Vec = (0..FIELD_VALUES.len()) .map(|index| unsafe { (field_bits(obj, index) & POINTER_MASK) as usize }) .collect(); @@ -207,8 +221,7 @@ fn a_materialised_record_keeps_its_children_traced_and_rewritten_7635() { "field {index} must have been relocated and its slot rewritten" ); assert!( - crate::arena::pointer_in_nursery(child) - || crate::arena::pointer_in_old_gen(child), + crate::arena::pointer_in_nursery(child) || crate::arena::pointer_in_old_gen(child), "field {index} must name a live heap object, not a stale address" ); assert_string_bytes(child as *const crate::StringHeader, bytes); @@ -233,11 +246,12 @@ fn json_parse_record_keeps_its_string_values_traced_and_rewritten_7635() { // heap `StringHeader`s in the nursery — collectable and movable — rather // than inline short strings that no layout state could strand. let text = br#"{"alpha":"value_alpha","bravo":"value_bravo"}"#; - let parsed = - unsafe { crate::json::js_json_parse(crate::string::js_string_from_bytes( + let parsed = unsafe { + crate::json::js_json_parse(crate::string::js_string_from_bytes( text.as_ptr(), text.len() as u32, - )) }; + )) + }; js_shadow_slot_set(0, parsed.bits()); let obj = (js_shadow_slot_get(0) & POINTER_MASK) as usize as *mut ObjectHeader; @@ -312,8 +326,12 @@ fn json_parse_record_keeps_its_string_values_traced_and_rewritten_7635() { fn a_misdeclared_pointer_free_record_strands_its_child() { let _guard = CopyingNurseryTestGuard::new(1); - let honest = unsafe { materialise_record(/* honest_finalize = */ true) }; - let sabotaged = unsafe { materialise_record(/* honest_finalize = */ false) }; + let honest = unsafe { + materialise_record(/* honest_finalize = */ true) + }; + let sabotaged = unsafe { + materialise_record(/* honest_finalize = */ false) + }; unsafe { assert_eq!( From 05e5ef7dbd0bccc80615de94d8ffffefc9abb0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 14:59:51 +0200 Subject: [PATCH 3/4] docs(changelog): add the #7643 fragment and qualify #7633's Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- .../7633-json-materialiser-layout-deferred.md | 12 ++++++ ...-pointer-free-misdeclaration-detectable.md | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 changelog.d/7643-pointer-free-misdeclaration-detectable.md diff --git a/changelog.d/7633-json-materialiser-layout-deferred.md b/changelog.d/7633-json-materialiser-layout-deferred.md index 9b8fdb749a..94ac0ab32c 100644 --- a/changelog.d/7633-json-materialiser-layout-deferred.md +++ b/changelog.d/7633-json-materialiser-layout-deferred.md @@ -24,3 +24,15 @@ Pinned-host, interleaved, hash-identical: `json_pipeline` 200k `build_out` 451 → 422 MB; `layout_note_slot` / `layout_forget_object` vanish from the profile. Also splits `barrier.rs`'s slot-store helpers into `barrier_store.rs` for the 2000-line cap. + +Qualification added by #7643: this landed on the strength of its argument +(the materialiser owns each object end to end, finalize is reached on every +path, the pointer case is conservative) plus a clean gap suite — the GC +half of it had no regression test, and #7635 showed why that mattered by +forcing every parsed record to `POINTER_FREE` and getting byte-identical +correct output from every runtime instrument. The argument was right, but +the probe could not have shown it either way: `js_json_parse` is lazy for +1 KB–16 MB top-level arrays, so the records were materialised after the +last collection. #7643 adds the workload-free regression tests +(`gc/tests/copying/deferred_finalize_7635.rs`) and the verification note on +`GC_LAYOUT_POINTER_FREE`. diff --git a/changelog.d/7643-pointer-free-misdeclaration-detectable.md b/changelog.d/7643-pointer-free-misdeclaration-detectable.md new file mode 100644 index 0000000000..c01f7273b8 --- /dev/null +++ b/changelog.d/7643-pointer-free-misdeclaration-detectable.md @@ -0,0 +1,42 @@ +**test(gc): make the `POINTER_FREE` misdeclaration hazard detectable (#7635)** + +#7635 sabotaged #7633's `layout_finish_deferred_boxed_object(ptr, saw_pointer)` +to `(ptr, false)` — every JSON-parsed record claiming `POINTER_FREE` while +holding heap strings — and got byte-identical correct output under +`PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1` and under +`PERRY_GC_FORCE_EVACUATE=1`, with copying minors and retired quarantine sets +observed live. Every future change to layout-state bookkeeping was about to +inherit that sentence as evidence. + +**The instruments were never at fault; the probe's subject never existed.** +`js_json_parse` routes a 1 KB–16 MB top-level array through the lazy tape +(`json_tape`), so `parse_object` — the function carrying the sabotage — runs +when an element is first *read*, not at `JSON.parse` time. The probe read its +records only after the churn, so every misdeclared record was materialised after +the last collection. A temporary audit in `heap_payload_slot_selection` on the +sabotaged build counted zero `POINTER_FREE` objects with pointer-bearing payload +words ever handed to the collector. CLAUDE.md hazard 4, applied to a probe. + +Re-run so the records live across a collection and everything fires: under +`PERRY_JSON_TAPE=0` the same sabotage SIGSEGVs, `PERRY_GC_FROMSPACE_SCAN=1` +reports `dangling=8000 owners=4000` (exactly 4,000 records × 2 fields, against +`dangling=0` clean) and `PERRY_GC_PROTECT_FROMSPACE=1` names the faulting +address; with the records merely touched before the churn, the default lazy path +reads back 7,872 of 8,000 values wrong. + +Ships four workload-free regression tests +(`gc/tests/copying/deferred_finalize_7635.rs`) that cannot be defeated by a lazy +path or a GC that did not happen to run: the finalize's two exact outcomes, the +child-slot enumerator, relocation across a copying minor gated on +`copied_objects`, the same invariant through the real `js_json_parse` entry +point so the `json/parser.rs` call site is covered, and a permanent sabotage arm +asserting a misdeclared record enumerates ZERO slots. Sabotage-verified in both +directions: #7635's exact parser mutation reddens the parse test, neutering +`layout_finish_deferred_boxed_object` reddens all four. + +The doc comment on `GC_LAYOUT_POINTER_FREE` now records what can and cannot +verify a claim about this state — including that `PERRY_GC_VERIFY_EVACUATION` is +blind to a misdeclaration by construction (it walks the same enumeration the +rewrite pass walks) and that `PERRY_GC_FROMSPACE_SCAN` is the +layout-independent instrument. No GC behaviour change, no new knobs, no codegen +change. From f5875c5b2cb1d13fa9586ada41c9210159e37607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 15:11:08 +0200 Subject: [PATCH 4/4] chore: bump version to 0.5.1365 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 79558c4d96..eaa41ce9e5 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.1364 +**Current Version:** 0.5.1365 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index ba2dc58148..c886d14b64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1364" +version = "0.5.1365" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1364" +version = "0.5.1365" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1364" +version = "0.5.1365" [[package]] name = "perry-ui-tvos" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1364" +version = "0.5.1365" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index e4ef02e9d3..9243c940b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1364" +version = "0.5.1365" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"