Skip to content

BinaryWriter constructor allows unbounded capacity, throws generic RangeError instead of ZrBinaryError #408

@sulthonzh

Description

@sulthonzh

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
    // ...
}
  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions