Skip to content

Uint8Array<ArrayBuffer>-annotated receiver: indexOf / lastIndexOf silently return -1 (plain Uint8Array, any and Float32Array<ArrayBuffer> are correct) #10894

Closed

Description

@proggeramlug

Summary

A receiver whose static type is written Uint8Array<ArrayBuffer> (the generic form TypeScript ≥ 5.7's lib types use, and what tsc infers for new Uint8Array(n)) gets the wrong indexOf / lastIndexOf: they return -1 for a value that is present. No error — a silent wrong value. The same call through Uint8Array, through any, or through Float32Array<ArrayBuffer> is correct, so this is the static dispatch chosen for that one annotation, not the runtime method.

Repro

const u = new Uint8Array(100); u[50] = 44; u[90] = 44;
function plain(m: Uint8Array): string { return [m.indexOf(44), m.indexOf(44, 51), m.lastIndexOf(44, 60), m.includes(44, 91), m.length, m[50]].join(" "); }
function generic(m: Uint8Array<ArrayBuffer>): string { return [m.indexOf(44), m.indexOf(44, 51), m.lastIndexOf(44, 60), m.includes(44, 91), m.length, m[50]].join(" "); }
function untyped(m: any): string { return [m.indexOf(44), m.indexOf(44, 51), m.lastIndexOf(44, 60), m.includes(44, 91), m.length, m[50]].join(" "); }
function f32(m: Float32Array<ArrayBuffer>): string { return [m.indexOf(44), m.indexOf(44, 51)].join(" "); }
console.log("Uint8Array              ", plain(u));
console.log("Uint8Array<ArrayBuffer> ", generic(u));
console.log("any                     ", untyped(u));
const f = new Float32Array(100); f[50] = 44; f[90] = 44;
console.log("Float32Array<ArrayBuffer>", f32(f));
receiver annotation node 24 perry 0.5.1618 (b9ba951ff8, macOS arm64)
Uint8Array 50 90 50 false 100 44 50 90 50 false 100 44
Uint8Array<ArrayBuffer> 50 90 50 false 100 44 -1 -1 -1 false 100 44
any 50 90 50 false 100 44 50 90 50 false 100 44
Float32Array<ArrayBuffer> 50 90 50 90

length and indexed reads are right on the same receiver; only the search methods are wrong (includes not distinguishable in this fixture — the expected answer is false).

A binding whose type is only inferred through the generic is fine — inside for await (const m of gen()) with gen(): AsyncGenerator<Uint8Array<ArrayBuffer>>, m.indexOf(44, from) is correct, while passing that same m to a parameter annotated Uint8Array<ArrayBuffer> returns -1.

Impact

Found in a real program: the TypeScript Native Messaging host at https://github.com/guest271314/NativeMessagingHosts (nm_typescript.ts). sendMessage(message: Uint8Array<ArrayBuffer>) splits replies larger than 1 MiB at a comma found with message.indexOf(COMMA, searchStart). With -1 it never splits, so a 1,048,581-byte message goes out as one frame over the 1 MiB host→browser limit (node: 2 frames, max 1,048,571).

Related: #10873 (same program). Context: jlucaso1/js-compiled#2

Activity

  1. proggeramlug commented on Sep 21, 2026

    @proggeramlug
    ContributorAuthor

    Fixed in v0.5.1633 (merge train 253, 0fa3915293) by #10912 — the generic spelling Uint8Array<ArrayBuffer> silently got the wrong indexOf / lastIndexOf / includes / join. Covered by buffer_backed_generic_tests.rs (perry-hir) and typed_array_receiver_tests.rs (perry-runtime), both inside suites run to completion on the landed tree: perry-hir 471/0 and perry-runtime 4233/0/0.

  2. proggeramlug commented on Sep 21, 2026

    @proggeramlug
    ContributorAuthor

    Severity bump: this is an out-of-bounds read, not just a wrong value — it SIGSEGVs. (Found while chasing the "64 MiB SIGSEGV" noted at the bottom of #10895; that crash is this bug.)

    macOS crash report for the Native Messaging host (Uint8Array<ArrayBuffer>-annotated message, 64 MiB message, main 841b605c97):

    EXC_BAD_ACCESS / SIGSEGV, KERN_INVALID_ADDRESS
      js_array_indexOf_jsvalue + 2264
      perry_fn_…__sendMessage + 3232          <- message.indexOf(COMMA, searchStart)
      perry_closure_…__11
      perry_runtime::promise::async_step::call_async_step_body
    

    So the generic annotation dispatches indexOf to the plain-Array js_array_indexOf_jsvalue, which walks the typed array's byte storage as 8-byte JSValue slots: -1 when the garbage never equals the needle and the walk stays inside mapped memory, SIGSEGV when it runs off the end.

    Six-line crash repro — rc=139 on Linux x86_64 main (v0.5.1632) at n = 2 MiB, 16 MiB and 64 MiB; the published 0.5.1520 returns -1 without crashing:

    function g(m: Uint8Array<ArrayBuffer>, from: number): number { return m.indexOf(44, from); }
    const n = Number(process.argv[2]);
    const u = new Uint8Array(n); u.fill(110);   // no match anywhere -> scans to "length"
    console.log("generic", g(u, 1048568));

    With the receiver annotated plain Uint8Array the same host round-trips 64 MiB 100/100.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions