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
↓ peach-browser-8f54f349.tar.gzatlas at this save
files changed2 files
src/crypto/peachPayload.binaryreader-bounds.test.ts+67 −0src/crypto/peachPayload.ts+12 −1src/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+});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;