From 35fbec9df555e8f4ac955f0fc0bd3c5861e62a4c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:56:01 +0800 Subject: [PATCH] fix(artifacts): validate Godot row extent before budget (#9638) --- src/artifacts/GodotPckArtifactReader.test.ts | 12 +++++++++++- src/artifacts/GodotPckArtifactReader.ts | 6 +++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/artifacts/GodotPckArtifactReader.test.ts b/src/artifacts/GodotPckArtifactReader.test.ts index 2d5e78906a..4b40dd5dee 100644 --- a/src/artifacts/GodotPckArtifactReader.test.ts +++ b/src/artifacts/GodotPckArtifactReader.test.ts @@ -234,9 +234,19 @@ describe("GodotPckArtifactReader", () => { }); }); - it("rejects oversized encoded path metadata before allocating its declared bytes", async () => { + it("reports an oversized path that cannot fit in the pack as malformed", async () => { const bytes = buildGodotPck(2, members); bytes.writeUInt32LE(0xffff_ffff, 100); + await withReader(bytes, async (reader) => { + await expect(list(reader)).rejects.toMatchObject({ + reason: "format", + }); + }); + }); + + it("refuses a fully framed path over the metadata budget", async () => { + const path = "a".repeat(Math.floor((64 * 1024 * 1024) / 18) + 1); + const bytes = buildGodotPck(2, [{ path, data: "" }]); await withReader(bytes, async (reader) => { await expect(list(reader)).rejects.toMatchObject({ reason: "limit", diff --git a/src/artifacts/GodotPckArtifactReader.ts b/src/artifacts/GodotPckArtifactReader.ts index 920066a48d..438c7ae8ad 100644 --- a/src/artifacts/GodotPckArtifactReader.ts +++ b/src/artifacts/GodotPckArtifactReader.ts @@ -91,6 +91,7 @@ export class GodotPckArtifactReader extends PositionalArtifactReader { if (index % 256 === 0) await setImmediate(); assertArtifactReadActive(signal); const pathBytes = await cursor.uint32(); + cursor.requireBytes(pathBytes + (header.format_version === 1 ? 32 : 36)); // Check worst-case JSON escaping before decoding or allocating the path. // The logical path, raw source path, and metadata source path can each // need six JSON bytes per input byte before retained facts are charged. @@ -274,9 +275,12 @@ class PckCursor { private readonly end: number, private readonly signal?: AbortSignal, ) {} - async bytes(length: number): Promise { + requireBytes(length: number): void { if (length > this.end - this.position) throw formatFailure("directory entry exceeds the pack"); + } + async bytes(length: number): Promise { + this.requireBytes(length); if (length > MAX_DIRECTORY_METADATA_BYTES / 18) throw metadataLimit(); if (length > DIRECTORY_CHUNK_BYTES) { // A large valid virtual path is bounded by retained representation, not