Found while migrating the computed read/write modules onto the Layer 1 rooting API (slice 4 of #7615).
The bug
Two array-element store arms in crates/perry-codegen/src/expr/index_set.rs
lower a key that is a heap string before the value, then use it after the
value's evaluation.
1. arr[k] = v, index not statically numeric
Gate: is_array_expr(ctx, object) && !is_numeric_expr(ctx, index). The arm
exists precisely for keys that are not proven numeric — its own comment names
forEach's (item, k) and for-in string keys as the motivating shapes.
let recv_guard = guard_store_operand_across(ctx, object, &arr_box, recv_collects);
let idx_double = lower_expr(ctx, index)?; // heap string, routinely
let val_double = lower_expr(ctx, value)?; // <-- arbitrary user code
...
call js_typed_feedback_array_set_index_or_string(site, arr_handle, idx_double, val_double)
The receiver is guarded (#7341). The key is not. js_typed_feedback_array_ set_index_or_string parses the key, so after an evacuating minor in the RHS the
element lands under a garbage key — or the parse reads retired memory.
2. arr[stringKey] = v
Gate: is_array_expr(ctx, object) && is_string_expr(ctx, index). Here the key is
a heap string by construction, and neither operand is guarded:
let arr_box = lower_expr(ctx, object)?;
let key_box = lower_expr(ctx, index)?;
let (val_double, val_bits) = lower_value_for_optional_barrier(ctx, value, ...)?; // <-- user code
let arr_handle = unbox_to_i64(blk, &arr_box);
let key_handle = unbox_str_handle(blk, &key_box); // pre-move StringHeader*
Why it was missed
The dynamic-string-key object arm further down the same file has pushed a
nested key_guard for exactly this since #7154. These two array arms were not
covered because their key is not statically a string on arm 1, and because
arm 2 predates the guard family.
Shapes
const out: unknown[] = [];
for (const i in src) out[i] = mk(); // arm 2
function put(k: any, i: number) { out[k] = { i }; } // arm 1
Status
Fixed in the slice-4 PR. Both arms become one operand group over
[object, index, value] (arm 1) / [object, index] across [value] (arm 2),
so the receiver's window and the key's window are derived from list order
rather than restated — the receiver's spans the key and the value, the key's
spans the value, and the value is lowered last so it takes nothing.
Found while migrating the computed read/write modules onto the Layer 1 rooting API (slice 4 of #7615).
The bug
Two array-element store arms in
crates/perry-codegen/src/expr/index_set.rslower a key that is a heap string before the value, then use it after the
value's evaluation.
1.
arr[k] = v, index not statically numericGate:
is_array_expr(ctx, object) && !is_numeric_expr(ctx, index). The armexists precisely for keys that are not proven numeric — its own comment names
forEach's(item, k)and for-in string keys as the motivating shapes.The receiver is guarded (#7341). The key is not.
js_typed_feedback_array_ set_index_or_stringparses the key, so after an evacuating minor in the RHS theelement lands under a garbage key — or the parse reads retired memory.
2.
arr[stringKey] = vGate:
is_array_expr(ctx, object) && is_string_expr(ctx, index). Here the key isa heap string by construction, and neither operand is guarded:
Why it was missed
The dynamic-string-key object arm further down the same file has pushed a
nested
key_guardfor exactly this since #7154. These two array arms were notcovered because their key is not statically a string on arm 1, and because
arm 2 predates the guard family.
Shapes
Status
Fixed in the slice-4 PR. Both arms become one operand group over
[object, index, value](arm 1) /[object, index]across[value](arm 2),so the receiver's window and the key's window are derived from list order
rather than restated — the receiver's spans the key and the value, the key's
spans the value, and the value is lowered last so it takes nothing.