Skip to content

Buffer: a dynamic (any-typed) string key silently writes a byte instead of an own property #6412

Description

@proggeramlug

Storing to a Buffer/Uint8Array through a computed key that is a string at runtime but any at compile time silently writes a byte instead of an own property, and the value reads back undefined.

Repro

const buf = Buffer.alloc(4);

const keys: any[] = ["dyn"];
const k: any = keys[0];        // a string at runtime, `any` statically
(buf as any)[k] = "D";
console.log((buf as any)[k]);  // node: "D"   perry: undefined

Static string keys are fine (buf["lit"], or a local declared : string) — #6406 fixed those. This is the dynamic half, and it's the common shape: a key off an untyped array, out of Object.keys(), or returned from a call.

Root cause

The byte-path guard (expr_assign.rs, and mirrored in expr_member/member_tail.rs) folds to Uint8ArraySet / Uint8ArrayGet whenever the key is not provably a string:

let key_is_string = matches!(index.as_ref(), Expr::String(_))
    || matches!(index.as_ref(), Expr::LocalGet(kid) if /* declared Type::String */);
if !key_is_string && matches!(ty, Type::Named(n) if n == "Uint8Array" || n == "Buffer") { /* fold to bytes */ }

An any-typed key is not provably a string, so it folds — and the byte path coerces it to an index, dropping the property store.

Why the obvious fix doesn't work

Inverting the guard to require a provably numeric key is unsound-free but destroys the buffer fast path, because a for loop counter is not typed Number:

Let { id: 2, name: "i", ty: Any, mutable: true, init: Some(Integer(0)) }

A loop counter and a string-holding any key are the same type to this guard. Requiring Type::Number therefore un-folds every buf[i] in a loop. Measured on a plain for (let i…) buf[i] = i & 0xff kernel:

guard Uint8Array folds
!key_is_string (today) 1
key_is_numeric 0

The native-region-proof compiler-output gate catches this immediately (its fixtures — h1_buffer_alias_negative, image_convolution — are exactly this shape). I tried it on #6406 and reverted.

Two real fixes

  1. Infer Number for numeric induction variables. for (let i = 0; …; i++) with no non-numeric assignment in the body is a number. Then a sound key_is_numeric guard keeps the fast path and fixes this bug. Worth doing carefully — widening type visibility can wake latent fast paths that were only "correct" because unreachable (that's how perf(codegen): keep the numeric-array specialization when the array is captured (#6369) #6377 revived runtime: await Promise.all([...]) never resumes when inputs settle via executor-captured resolvers — program silently exits 0 #6328).
  2. Discriminate at runtime. Always fold, and have the byte-path helper route a non-index key to the property path. Costs a check on the hot path but is robust to any key shape.

(1) is the cleaner win if the induction-variable analysis is cheap, since it also improves typing generally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions