Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ pub fn gc_init() {
gc_register_mutable_root_scanner(crate::v8::scan_v8_promise_hook_roots_mut);
gc_register_mutable_root_scanner(crate::typed_feedback::scan_typed_feedback_roots_mut);
gc_register_mutable_root_scanner(crate::typedarray_props::scan_typed_array_own_props_roots_mut);
// A typed array's materialized backing ArrayBuffer lives only as a raw
// address in TYPED_ARRAY_VIEW_META — collectable/stale under a live typed
// array, which made `subarray` hand back a garbage-length view.
gc_register_mutable_root_scanner(crate::typedarray_view::scan_typed_array_view_meta_roots_mut);
gc_register_mutable_root_scanner(transition_cache_mutable_root_scanner);
gc_register_mutable_root_scanner(crate::object::scan_object_cache_roots_mut);
gc_register_mutable_root_scanner(crate::object::scan_arguments_object_roots_mut);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/gc/tests/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ fn test_gc_type_metadata_covers_all_declared_types() {
pointer_free: true,
move_hook_kind: GcMoveHookKind::None,
rewrite_hook_kind: GcRewriteHookKind::None,
finalize_hook_kind: GcFinalizeHookKind::None,
finalize_hook_kind: GcFinalizeHookKind::TypedArrayViewMeta,
},
GcTypeInfo {
type_id: GC_TYPE_SET,
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-runtime/src/gc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ pub(crate) enum GcFinalizeHookKind {
NativeTypedView,
NativeHandle,
NativePodView,
/// Drop a dead typed array's `TYPED_ARRAY_VIEW_META` entry. The table is
/// keyed by the header address and records the array's materialized backing
/// ArrayBuffer; leaving the entry behind both keeps that buffer rooted
/// forever and lets whatever is allocated at the reused address inherit a
/// backing that is not its own.
TypedArrayViewMeta,
/// Drop the embedded `temporal_rs` value in a `GC_TYPE_TEMPORAL` cell so a
/// heap-owning variant (e.g. a `ZonedDateTime` IANA timezone string) is
/// released when the cell is swept. POD variants drop to a no-op.
Expand Down Expand Up @@ -388,7 +394,7 @@ pub(super) static GC_TYPE_INFO_BY_ID: [Option<GcTypeInfo>; MALLOC_KIND_BUCKET_CO
true,
GcMoveHookKind::None,
GcRewriteHookKind::None,
GcFinalizeHookKind::None,
GcFinalizeHookKind::TypedArrayViewMeta,
)),
Some(gc_type_info_entry(
GC_TYPE_SET,
Expand Down Expand Up @@ -699,6 +705,9 @@ pub(crate) unsafe fn gc_type_finalize_unmarked_payload(obj_type: u8, user_ptr: *
GcFinalizeHookKind::ErrorSideTables => {
crate::node_submodules::diagnostics_gc::error_side_tables_clear_dead(user_ptr as usize);
}
GcFinalizeHookKind::TypedArrayViewMeta => {
crate::typedarray_view::clear_view_meta(user_ptr as usize);
}
}
}

Expand Down
27 changes: 27 additions & 0 deletions crates/perry-runtime/src/typedarray_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,33 @@ pub(crate) fn view_backing_data_ptr(addr: usize) -> Option<*mut u8> {
})
}

/// A typed array's backing `ArrayBuffer` is reachable only through
/// `TYPED_ARRAY_VIEW_META`, as a raw address the collector cannot see. Without
/// this scanner the buffer is swept (or left stale after evacuation) while the
/// typed array still points at it, and `js_typed_array_backing_buffer` then
/// hands `js_typed_array_view` a dead pointer. That path does not fail loudly:
/// it falls back to `js_typed_array_new`, which reinterprets the dead buffer's
/// *byte* length as an element count — so `subarray(0, 11)` on an
/// `Int32Array(17)` returned a 68-element array (17 × 4 bytes) over a fresh
/// store, and the `set` that followed threw "offset is out of bounds".
///
/// Root the backing and let the visitor rewrite it, the way every other
/// object-keyed side table already does.
pub(crate) fn scan_typed_array_view_meta_roots_mut(
visitor: &mut crate::gc::RuntimeRootVisitor<'_>,
) {
if !any_view_meta() {
return;
}
TYPED_ARRAY_VIEW_META.with(|r| {
for meta in r.borrow_mut().values_mut() {
let mut backing = meta.backing as *mut crate::buffer::BufferHeader;
visitor.visit_raw_mut_ptr_slot(&mut backing);
meta.backing = backing as usize;
}
});
}

/// Drop any recorded view metadata for `addr` (called from
/// `unregister_typed_array` when the typed array is collected).
pub(crate) fn clear_view_meta(addr: usize) {
Expand Down
46 changes: 46 additions & 0 deletions tests/issue_typed_array_backing_gc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// A typed array's backing ArrayBuffer is materialized lazily and recorded only
// in `TYPED_ARRAY_VIEW_META`, a side table the collector did not trace. The
// buffer was therefore swept out from under a live typed array, and `subarray`
// — which builds its view from that backing — silently fell back to
// `js_typed_array_new`, reinterpreting the dead buffer's *byte* length as an
// element count: `subarray(0, 11)` on an `Int32Array(17)` returned a 68-element
// array (17 x 4), and a subsequent `set` threw "offset is out of bounds".

const a = new Int32Array(17);

if (a.subarray(0, 11).length !== 11) {
throw new Error("subarray is broken before any collection");
}

for (let round = 0; round < 5; round++) {
if (typeof gc === "function") {
gc();
gc();
}
for (let i = 0; i < 5000; i++) {
const junk = { x: i, y: [i] };
}
if (typeof gc === "function") gc();

const len = a.subarray(0, 11).length;
if (len !== 11) {
throw new Error(`round ${round}: subarray(0, 11).length = ${len}, expected 11`);
}

const buffer = a.buffer;
if (!buffer || buffer.byteLength !== 68) {
throw new Error(
`round ${round}: a.buffer.byteLength = ${buffer && buffer.byteLength}, expected 68`,
);
}

// The view must still alias the array, not a fresh copy.
const view = a.subarray(0, 4);
view[0] = 4242;
if (a[0] !== 4242) {
throw new Error(`round ${round}: subarray stopped aliasing (a[0] = ${a[0]})`);
}
a[0] = 0;
}

console.log("typed-array backing survived GC");
51 changes: 51 additions & 0 deletions tests/test_typed_array_backing_gc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PERRY="${PERRY_BIN:-${PERRY:-$ROOT/target/release/perry}}"
RUNTIME_DIR="${PERRY_RUNTIME_DIR:-$ROOT/target/release}"
FIXTURE="$ROOT/tests/issue_typed_array_backing_gc.js"
WORKDIR="${TMPDIR:-/tmp}/perry-ta-backing-gc-$$"
BIN="$WORKDIR/perry-ta-backing-gc"
COMPILE_LOG="$WORKDIR/compile.log"
OUT_LOG="$WORKDIR/out.log"

mkdir -p "$WORKDIR"
trap 'rm -rf "$WORKDIR"' EXIT

if [[ ! -x "$PERRY" ]]; then
PERRY="$ROOT/target/debug/perry"
fi
if [[ ! -x "$PERRY" ]]; then
echo "SKIP: perry binary not found (build with cargo build --release -p perry)"
exit 0
fi

env PERRY_ALLOW_UNIMPLEMENTED=1 PERRY_RUNTIME_DIR="$RUNTIME_DIR" "$PERRY" compile --no-cache --no-auto-optimize "$FIXTURE" -o "$BIN" \
>"$COMPILE_LOG" 2>&1 || {
cat "$COMPILE_LOG" >&2
exit 1
}

# Run under the default collector and again with evacuation forced, so both the
# sweep and the relocation path are covered.
for mode in default evacuate; do
set +e
if [[ "$mode" == "evacuate" ]]; then
PERRY_GC_FORCE_EVACUATE=1 "$BIN" >"$OUT_LOG" 2>&1
else
"$BIN" >"$OUT_LOG" 2>&1
fi
rc=$?
set -e
if [[ "$rc" -ne 0 ]]; then
echo "typed-array backing fixture failed ($mode GC, exit $rc):" >&2
cat "$OUT_LOG" >&2
exit 1
fi
grep -qx "typed-array backing survived GC" "$OUT_LOG" || {
echo "unexpected output ($mode GC):" >&2
cat "$OUT_LOG" >&2
exit 1
}
done
Loading