Skip to content

Reassigning a new Uint8Array binding drops element writes and reads undefined (the Uint8-specialized twin of #8100) #8111

Description

@proggeramlug

Found while fixing #8100 (PR #8109), which is the same disease in the
js_typed_array_* element helpers. This one is in the Uint8Array-specialized
pair and is not fixed by that PR — measured pre-fix and post-fix on the
same probe, byte-identical output, so #8109 neither causes nor fixes it.

Reproducer

let Q: Uint8Array = new Uint8Array(2);
Q = [9, 10] as any;
Q[0] = 5;
console.log("store:", JSON.stringify(Q));
let k: any = 1;
console.log("dyn:", Q[k]);
console.log("read:", Q[0], Q[1], Q.length, Q.at(1));
let R: Uint8Array = new Uint8Array([3, 4]);
console.log("real u8:", R[0], R[1], R.length);
const B = Buffer.from([1, 2]);
console.log("buf:", B[0], B[1]);
node 26.5.1     perry
store: [5,10]   store: [9,10]                <- the write is SILENTLY DROPPED
dyn: 10         dyn: undefined
read: 5 10 2 10 read: undefined undefined 2 10
real u8: 3 4 2  real u8: 3 4 2               (control, OK)
buf: 1 2        buf: 1 2                     (control, OK)

Both sides exit 0. Q.length and Q.at(1) are already correct, so the binding
really does hold the plain array — only the specialized element accessors are
wrong.

Reproduced at b5ab0262e (#8109's branch) and at its parent, --profile perry-dev, --no-auto-optimize --no-cache, PERRY_RUNTIME_DIR pinned to a
freshly built archive pair.

Root cause

crates/perry-runtime/src/typedarray/access.rs:

// :605
pub extern "C" fn js_uint8array_index_get_value(target: *const TypedArrayHeader, index: i32) -> f64 {
    ...
    if let Some(kind) = lookup_typed_array_kind(addr) {
        if !matches!(kind, KIND_UINT8 | KIND_UINT8_CLAMPED) { return undefined; }
        js_typed_array_get(addr as *const TypedArrayHeader, index)
    } else if crate::buffer::is_registered_buffer(addr) {
        crate::buffer::js_buffer_index_get_value(...)
    } else {
        undefined          // <- a plain array / object lands here
    }
}

// :635  js_uint8array_set — same three-way shape, same trailing arm, silent no-op

A receiver that is neither a registered typed array nor a registered buffer
falls off the end. Codegen reaches these helpers through
is_uint8array_receiver (perry-codegen/src/expr/index_get.rs /
index_set.rs), which is a different predicate from the local_type_hint
one #8100 is about — it keys on receiver_class_name — but it still fires for
a reassigned Uint8Array local.

Note the asymmetry with #8100: these helpers are memory-SAFE (they validate
before dereferencing), they just answer undefined / drop the store instead of
falling back to the ordinary [[Get]] / [[Set]].

Suggested fix

The get side is the same shape as #8109's: replace the trailing undefined
with a dispatch through crate::typedarray::classify_element_read_receiver
(added in #8109) → js_dyn_index_get. The wrong-KIND arm above must stay as it
is — the classifier answers TypedArray for a registered typed array of any
kind, so only the final else changes.

The set side needs its own decision and is why this is a separate issue rather
than part of #8109, which is scoped to reads: a dropped store is not the same
call as a wrong read, js_dyn_index_set has its own return-value contract, and
js_uint8array_set takes an i32 value rather than a NaN-boxed f64.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions