Skip to content

Commit 01dd8a8

Browse files
fix: bound binary and drawlist capacities (#411)
1 parent 94cbadd commit 01dd8a8

6 files changed

Lines changed: 103 additions & 8 deletions

File tree

docs/backend/engine-config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ backend or extremely large paste events.
8282

8383
| Detail | Value |
8484
|----------|-------|
85-
| Type | `number` (positive integer) |
85+
| Type | `number` (positive integer, `<= 8 << 20`) |
8686
| Default | `2 << 20` (2 MiB) |
8787

8888
Maximum byte size of a single rendered drawlist frame. If the builder exceeds
@@ -99,7 +99,7 @@ config: {
9999

100100
| Detail | Value |
101101
|----------|-------|
102-
| Type | `number` (positive integer) |
102+
| Type | `number` (positive integer, `<= 4 << 20`) |
103103
| Default | `512 << 10` (512 KiB) |
104104

105105
Maximum total byte size of the drawlist blob pool for a frame. If binary payloads

docs/protocol/safety.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class BinaryWriter {
4545
}
4646
```
4747

48-
The writer additionally enforces that `writeU32` and `writeI32` are called only at 4-byte aligned offsets. Calling at a misaligned offset throws `ZR_MISALIGNED`.
48+
The writer additionally enforces that constructor capacity is bounded and that `writeU32` and `writeI32` are called only at 4-byte aligned offsets. Calling at a misaligned offset throws `ZR_MISALIGNED`.
4949

5050
## ZrBinaryError
5151

packages/core/src/app/__tests__/config.bounds.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,35 @@ test("config bounds: maxEventBytes must be <= 4 MiB", () => {
3434
err.message.includes("maxEventBytes must be <= 4194304"),
3535
);
3636
});
37+
38+
test("config bounds: maxDrawlistBytes must be <= 8 MiB", () => {
39+
const backend = new StubBackend();
40+
assert.throws(
41+
() =>
42+
createApp({
43+
backend,
44+
initialState: { value: 0 },
45+
config: { maxDrawlistBytes: (8 << 20) + 1 },
46+
}),
47+
(err: unknown) =>
48+
err instanceof ZrUiError &&
49+
err.code === "ZRUI_INVALID_PROPS" &&
50+
err.message.includes("maxDrawlistBytes must be <= 8388608"),
51+
);
52+
});
53+
54+
test("config bounds: maxBlobBytes must be <= 4 MiB", () => {
55+
const backend = new StubBackend();
56+
assert.throws(
57+
() =>
58+
createApp({
59+
backend,
60+
initialState: { value: 0 },
61+
config: { maxBlobBytes: (4 << 20) + 1 },
62+
}),
63+
(err: unknown) =>
64+
err instanceof ZrUiError &&
65+
err.code === "ZRUI_INVALID_PROPS" &&
66+
err.message.includes("maxBlobBytes must be <= 4194304"),
67+
);
68+
});

packages/core/src/app/createApp/config.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const DEFAULT_CONFIG: ResolvedAppConfig = Object.freeze({
5050

5151
const MAX_SAFE_FPS_CAP = 1000;
5252
const MAX_SAFE_EVENT_BYTES = 4 << 20; /* 4 MiB */
53+
const MAX_SAFE_DRAWLIST_BYTES = 8 << 20; /* 8 MiB */
54+
const MAX_SAFE_BLOB_BYTES = 4 << 20; /* 4 MiB */
5355

5456
function invalidProps(detail: string): never {
5557
throw new ZrUiError("ZRUI_INVALID_PROPS", detail);
@@ -84,11 +86,15 @@ export function resolveAppConfig(config: AppConfig | undefined): ResolvedAppConf
8486
const maxDrawlistBytes =
8587
config.maxDrawlistBytes === undefined
8688
? DEFAULT_CONFIG.maxDrawlistBytes
87-
: requirePositiveInt("maxDrawlistBytes", config.maxDrawlistBytes);
89+
: requirePositiveIntAtMost(
90+
"maxDrawlistBytes",
91+
config.maxDrawlistBytes,
92+
MAX_SAFE_DRAWLIST_BYTES,
93+
);
8894
const maxBlobBytes =
8995
config.maxBlobBytes === undefined
9096
? DEFAULT_CONFIG.maxBlobBytes
91-
: requirePositiveInt("maxBlobBytes", config.maxBlobBytes);
97+
: requirePositiveIntAtMost("maxBlobBytes", config.maxBlobBytes, MAX_SAFE_BLOB_BYTES);
9298
const rootPadding =
9399
config.rootPadding === undefined
94100
? DEFAULT_CONFIG.rootPadding

packages/core/src/binary/__tests__/writer.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,52 @@ import { ZrBinaryError } from "../parseError.js";
33
import { BinaryWriter } from "../writer.js";
44

55
describe("BinaryWriter", () => {
6+
test("constructor rejects invalid capacity with ZrBinaryError", () => {
7+
assert.throws(
8+
() => new BinaryWriter(-1),
9+
(err: unknown) => {
10+
assert.ok(err instanceof ZrBinaryError);
11+
assert.equal(err.code, "ZR_LIMIT");
12+
assert.equal(err.offset, 0);
13+
assert.equal(
14+
err.message,
15+
"ZR_LIMIT at 0: capacity must be a non-negative integer (got -1)",
16+
);
17+
return true;
18+
},
19+
);
20+
21+
assert.throws(
22+
() => new BinaryWriter(1.5),
23+
(err: unknown) => {
24+
assert.ok(err instanceof ZrBinaryError);
25+
assert.equal(err.code, "ZR_LIMIT");
26+
assert.equal(err.offset, 0);
27+
assert.equal(
28+
err.message,
29+
"ZR_LIMIT at 0: capacity must be a non-negative integer (got 1.5)",
30+
);
31+
return true;
32+
},
33+
);
34+
});
35+
36+
test("constructor rejects excessive capacity before allocation", () => {
37+
assert.throws(
38+
() => new BinaryWriter(Number.MAX_SAFE_INTEGER),
39+
(err: unknown) => {
40+
assert.ok(err instanceof ZrBinaryError);
41+
assert.equal(err.code, "ZR_LIMIT");
42+
assert.equal(err.offset, 0);
43+
assert.equal(
44+
err.message,
45+
"ZR_LIMIT at 0: capacity 9007199254740991 exceeds maximum 16777216",
46+
);
47+
return true;
48+
},
49+
);
50+
});
51+
652
test("padTo4 writes 0x00 bytes only", () => {
753
const w = new BinaryWriter(16);
854
w.writeU8(0xff);

packages/core/src/binary/writer.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import { ZrBinaryError } from "./parseError.js";
1919

20+
const MAX_WRITER_CAPACITY_BYTES = 16 << 20; /* 16 MiB */
21+
2022
/**
2123
* Bounds-checked binary writer with cursor tracking.
2224
*
@@ -30,9 +32,18 @@ export class BinaryWriter {
3032

3133
constructor(capacity: number) {
3234
if (!Number.isInteger(capacity) || capacity < 0) {
33-
throw new Error(
34-
`BinaryWriter: capacity must be a non-negative integer (got ${String(capacity)})`,
35-
);
35+
throw new ZrBinaryError({
36+
code: "ZR_LIMIT",
37+
offset: 0,
38+
detail: `capacity must be a non-negative integer (got ${String(capacity)})`,
39+
});
40+
}
41+
if (capacity > MAX_WRITER_CAPACITY_BYTES) {
42+
throw new ZrBinaryError({
43+
code: "ZR_LIMIT",
44+
offset: 0,
45+
detail: `capacity ${String(capacity)} exceeds maximum ${String(MAX_WRITER_CAPACITY_BYTES)}`,
46+
});
3647
}
3748
this.buf = new Uint8Array(capacity);
3849
this.dv = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);

0 commit comments

Comments
 (0)