Description
BinaryWriter accepts any non-negative integer as capacity with no upper bound, which can lead to a generic RangeError from the Uint8Array constructor rather than the expected ZrBinaryError. This breaks the documented invariant that all binary module errors are ZrBinaryError instances.
Context
- File:
packages/core/src/binary/writer.ts:29-34
- Component: Binary protocol / memory allocation
Current Behavior
constructor(capacity: number) {
if (!Number.isInteger(capacity) || capacity < 0) {
throw new Error(
`BinaryWriter: capacity must be a non-negative integer (got ${String(capacity)})`,
);
}
this.buf = new Uint8Array(capacity); // RangeError if capacity > ArrayBuffer limit
// ...
}
- Passing
new BinaryWriter(Number.MAX_SAFE_INTEGER) throws a native RangeError: Invalid array length — not a ZrBinaryError. This violates the invariant documented in parseError.ts and reader.ts that all binary module errors are ZrBinaryError.
- There is no reasonable upper bound. Unlike
config.ts where maxEventBytes is capped at 4 MiB and maxDrawlistBytes is at least validated as positive, a caller could pass 2 ** 31 (2 GiB) which would attempt a massive allocation.
- The initial validation throws a plain
Error instead of ZrBinaryError, inconsistent with the rest of the binary module.
Expected Behavior
- The constructor should cap capacity at a reasonable maximum (e.g.,
ZR_MAX_DRAWSIZE_BYTES or config.maxDrawlistBytes).
- Any capacity violation should throw
ZrBinaryError with code ZR_LIMIT, consistent with the rest of the module.
- The initial
!Number.isInteger || < 0 check should also throw ZrBinaryError for consistency.
Suggested Fix
import { ZrBinaryError } from "./parseError.js";
+/** Maximum capacity for BinaryWriter (16 MiB). */
+const ZR_MAX_WRITER_CAPACITY = 16 << 20;
+
export class BinaryWriter {
private readonly buf: Uint8Array;
private readonly dv: DataView;
private off = 0;
constructor(capacity: number) {
if (!Number.isInteger(capacity) || capacity < 0) {
- throw new Error(
- `BinaryWriter: capacity must be a non-negative integer (got ${String(capacity)})`,
- );
+ throw new ZrBinaryError({
+ code: "ZR_LIMIT",
+ offset: 0,
+ detail: `capacity must be a non-negative integer (got ${String(capacity)})`,
+ });
+ }
+ if (capacity > ZR_MAX_WRITER_CAPACITY) {
+ throw new ZrBinaryError({
+ code: "ZR_LIMIT",
+ offset: 0,
+ detail: `capacity ${capacity} exceeds maximum ${ZR_MAX_WRITER_CAPACITY}`,
+ });
}
this.buf = new Uint8Array(capacity);
Impact
- Any code path that passes user-controlled or config-derived values to
BinaryWriter could trigger unexpected OOM / RangeError
- Severity: medium — unlikely in normal usage, but breaks error-handling contracts for library consumers who catch
ZrBinaryError
Positively — happy to submit a PR if this is welcome.
Description
BinaryWriteraccepts any non-negative integer as capacity with no upper bound, which can lead to a genericRangeErrorfrom theUint8Arrayconstructor rather than the expectedZrBinaryError. This breaks the documented invariant that all binary module errors areZrBinaryErrorinstances.Context
packages/core/src/binary/writer.ts:29-34Current Behavior
new BinaryWriter(Number.MAX_SAFE_INTEGER)throws a nativeRangeError: Invalid array length— not aZrBinaryError. This violates the invariant documented inparseError.tsandreader.tsthat all binary module errors areZrBinaryError.config.tswheremaxEventBytesis capped at 4 MiB andmaxDrawlistBytesis at least validated as positive, a caller could pass2 ** 31(2 GiB) which would attempt a massive allocation.Errorinstead ofZrBinaryError, inconsistent with the rest of the binary module.Expected Behavior
ZR_MAX_DRAWSIZE_BYTESorconfig.maxDrawlistBytes).ZrBinaryErrorwith codeZR_LIMIT, consistent with the rest of the module.!Number.isInteger || < 0check should also throwZrBinaryErrorfor consistency.Suggested Fix
import { ZrBinaryError } from "./parseError.js"; +/** Maximum capacity for BinaryWriter (16 MiB). */ +const ZR_MAX_WRITER_CAPACITY = 16 << 20; + export class BinaryWriter { private readonly buf: Uint8Array; private readonly dv: DataView; private off = 0; constructor(capacity: number) { if (!Number.isInteger(capacity) || capacity < 0) { - throw new Error( - `BinaryWriter: capacity must be a non-negative integer (got ${String(capacity)})`, - ); + throw new ZrBinaryError({ + code: "ZR_LIMIT", + offset: 0, + detail: `capacity must be a non-negative integer (got ${String(capacity)})`, + }); + } + if (capacity > ZR_MAX_WRITER_CAPACITY) { + throw new ZrBinaryError({ + code: "ZR_LIMIT", + offset: 0, + detail: `capacity ${capacity} exceeds maximum ${ZR_MAX_WRITER_CAPACITY}`, + }); } this.buf = new Uint8Array(capacity);Impact
BinaryWritercould trigger unexpected OOM /RangeErrorZrBinaryErrorPositively — happy to submit a PR if this is welcome.