8f54f349945f

fix: PCH-02-008 BinaryReader explicit bounds checks on every read operation (v4.0.0)

time
2026-06-12 20:35 UTC · 4 months ago
machine
koh2
agent
gpt-5
session
pch-02 integration
parent
6d494603f74d
2 files src/crypto/peachPayload.binaryreader-bounds.test.ts+67 −0src/crypto/peachPayload.ts+12 −1
src/crypto/peachPayload.binaryreader-bounds.test.ts+67 −0
@@ -0,0 +1,67 @@1+// PCH-02-008 regression tests — BinaryReader must bound every read and fail2+// with the decoder's existing PAYLOAD_TOO_SHORT code on truncated input.3+4+import { describe, expect, it } from "vitest";5+import { BinaryReader } from "./peachPayload";6+7+describe("PCH-02-008 — BinaryReader bounds checks", () => {8+  it("u8 past end of buffer throws PAYLOAD_TOO_SHORT", () => {9+    const r = new BinaryReader(Uint8Array.of(0xaa), 0);10+    expect(r.u8()).toBe(0xaa);11+    expect(() => r.u8()).toThrow("PAYLOAD_TOO_SHORT");12+  });13+14+  it("u16 with only one byte remaining throws PAYLOAD_TOO_SHORT", () => {15+    const r = new BinaryReader(Uint8Array.of(0x01), 0);16+    expect(() => r.u16()).toThrow("PAYLOAD_TOO_SHORT");17+  });18+19+  it("u32 with three bytes remaining throws PAYLOAD_TOO_SHORT", () => {20+    const r = new BinaryReader(Uint8Array.of(0x01, 0x02, 0x03), 0);21+    expect(() => r.u32()).toThrow("PAYLOAD_TOO_SHORT");22+  });23+24+  it("read(n) past end throws instead of clamping short", () => {25+    const r = new BinaryReader(Uint8Array.of(1, 2, 3, 4), 0);26+    expect(() => r.read(5)).toThrow("PAYLOAD_TOO_SHORT");27+  });28+29+  it("lpString whose declared length overruns the buffer throws", () => {30+    const r = new BinaryReader(Uint8Array.of(200, 0x61, 0x62), 0);31+    expect(() => r.lpString(1)).toThrow("PAYLOAD_TOO_SHORT");32+  });33+34+  it("valid reads at exact buffer boundaries still work (regression)", () => {35+    const buf = new Uint8Array(11);36+    const view = new DataView(buf.buffer);37+    buf[0] = 0x7f;38+    view.setUint16(1, 0xbeef, true);39+    view.setUint32(3, 0xdeadbeef, true);40+    buf.set([0x70, 0x65, 0x61, 0x63], 7);41+42+    const r = new BinaryReader(buf, 0);43+    expect(r.u8()).toBe(0x7f);44+    expect(r.u16()).toBe(0xbeef);45+    expect(r.u32()).toBe(0xdeadbeef);46+    expect(Array.from(r.read(4))).toEqual([0x70, 0x65, 0x61, 0x63]);47+    expect(() => r.u8()).toThrow("PAYLOAD_TOO_SHORT");48+  });49+50+  it("lpString round-trips a valid length-prefixed string (regression)", () => {51+    const text = new TextEncoder().encode("peach");52+    const buf = new Uint8Array(1 + text.length);53+    buf[0] = text.length;54+    buf.set(text, 1);55+56+    const r = new BinaryReader(buf, 0);57+    expect(r.lpString(1)).toBe("peach");58+    expect(r.cursor).toBe(buf.length);59+  });60+61+  it("respects a non-zero start offset and bounds from it", () => {62+    const buf = Uint8Array.of(0xff, 0xff, 0x2a);63+    const r = new BinaryReader(buf, 2);64+    expect(r.u8()).toBe(0x2a);65+    expect(() => r.u8()).toThrow("PAYLOAD_TOO_SHORT");66+  });67+});

view file

src/crypto/peachPayload.ts+12 −1
@@ -937,27 +937,38 @@937937   }938938 }939939 940-class BinaryReader {940+// Exported for the PCH-02-008 bounds-check regression tests only — not part941+// of the payload API surface.942+export class BinaryReader {941943   cursor: number;942944   private view: DataView;943945   constructor(private data: Uint8Array, start: number) {944946     this.cursor = start;945947     this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);946948   }949+  // PCH-02-008: fail truncated imported payloads with the decoder's typed950+  // PAYLOAD_TOO_SHORT error, not undefined/NaN reads or engine RangeErrors.951+  private ensure(n: number): void {952+    if (this.cursor + n > this.data.length) throw new Error("PAYLOAD_TOO_SHORT");953+  }947954   u8(): number {955+    this.ensure(1);948956     return this.data[this.cursor++];949957   }950958   u16(): number {959+    this.ensure(2);951960     const v = this.view.getUint16(this.cursor, true);952961     this.cursor += 2;953962     return v;954963   }955964   u32(): number {965+    this.ensure(4);956966     const v = this.view.getUint32(this.cursor, true);957967     this.cursor += 4;958968     return v;959969   }960970   read(n: number): Uint8Array {971+    this.ensure(n);961972     const out = this.data.subarray(this.cursor, this.cursor + n);962973     this.cursor += n;963974     return out;

view file