fix(pyinstaller): account for one-byte name expansion (#10058)

This commit is contained in:
morluto authored and GitHub committed 2026-11-11 17:13:54 +08:00
1 parent 9cd1eadfb7
commit 4de8020745
2 files changed
+82 -41

No files matched your search

+76 -37
View File
@@ -94,44 +94,83 @@ it.each(["PYZ\0", "BAD\0"])(
},
);
it("rejects a PYZ table whose back-references repeat one long row", async () => {
// One namespace row with a 16 KiB name, flagged for reference, then 999
// back-references to it. The table is about 21 KiB but would expand to
// 16 MiB of entry names.
const name = Buffer.alloc(16 * 1024, 0x61);
const refs = 1_000;
const row = Buffer.concat([
Buffer.from([0x28 | 0x80]),
int32(2),
Buffer.from([0x61]),
int32(name.length),
name,
Buffer.from([0x29, 3, 0x69]),
int32(3),
Buffer.from([0x69]),
int32(0),
Buffer.from([0x69]),
int32(0),
]);
const reference = Buffer.concat([Buffer.from([0x72]), int32(0)]);
const table = Buffer.concat([
Buffer.from([0x5b]),
int32(refs),
row,
...Array.from({ length: refs - 1 }, () => reference),
]);
const header = Buffer.alloc(12);
header.write("PYZ\0");
PYTHON_39_MAGIC.copy(header, 4);
header.writeUInt32BE(12, 8);
const image = Buffer.concat([header, table]);
expect(image.length).toBeLessThan(32 * 1024);
it.each([0x61, 0xe9])(
"rejects a PYZ table whose back-references repeat one long row (byte %i)",
async (byte) => {
// One namespace row with a 16 KiB name, flagged for reference, then 999
// back-references to it. The table is about 21 KiB but would expand to
// 16 or 32 MiB of UTF-8 entry names.
const name = Buffer.alloc(16 * 1024, byte);
const refs = 1_000;
const row = Buffer.concat([
Buffer.from([0x28 | 0x80]),
int32(2),
Buffer.from([0x61]),
int32(name.length),
name,
Buffer.from([0x29, 3, 0x69]),
int32(3),
Buffer.from([0x69]),
int32(0),
Buffer.from([0x69]),
int32(0),
]);
const reference = Buffer.concat([Buffer.from([0x72]), int32(0)]);
const table = Buffer.concat([
Buffer.from([0x5b]),
int32(refs),
row,
...Array.from({ length: refs - 1 }, () => reference),
]);
const header = Buffer.alloc(12);
header.write("PYZ\0");
PYTHON_39_MAGIC.copy(header, 4);
header.writeUInt32BE(12, 8);
const image = Buffer.concat([header, table]);
expect(image.length).toBeLessThan(32 * 1024);
const parsed = await readImage(image);
expect(parsed).toMatchObject({ ok: false, error: { reason: "limit" } });
if (parsed.ok) return;
expect(parsed.error.message).toContain("expand past");
});
const parsed = await readImage(image);
expect(parsed).toMatchObject({ ok: false, error: { reason: "limit" } });
if (parsed.ok) return;
expect(parsed.error.message).toContain("expand past");
},
);
it.each([0x61, 0x41, 0x7a, 0x5a, 0x75])(
"preserves namespace text from one-byte and UTF-8 marshal encodings (tag %i)",
async (tag) => {
const name = "é".repeat(64);
const encoded = Buffer.from(name, tag === 0x75 ? "utf8" : "latin1");
const table = Buffer.concat([
Buffer.from([0x5b]),
int32(1),
Buffer.from([0x29, 2, tag]),
tag === 0x7a || tag === 0x5a
? Buffer.from([encoded.length])
: int32(encoded.length),
encoded,
Buffer.from([0x29, 3, 0x69]),
int32(3),
Buffer.from([0x69]),
int32(0),
Buffer.from([0x69]),
int32(0),
]);
const image = pyzArchive({
magic: PYTHON_39_MAGIC,
modules: [],
toc: table,
});
expect(await readImage(image)).toMatchObject({
ok: true,
value: {
kind: "entries",
sha256: createHash("sha256").update(image).digest("hex"),
entries: [{ name, role: "namespace", typeCode: 3, length: 0 }],
},
});
},
);
const int32 = (value: number): Buffer => {
const bytes = Buffer.alloc(4);
+6 -4
View File
@@ -180,8 +180,9 @@ const parsePyzParts = (
/**
* Marshal back-references let one stored row appear many times, so a small
* table can name far more bytes than it holds. Each genuine entry stores its
* own name inline, so the expanded names can never exceed the table itself.
* table can name far more bytes than it holds. Inline one-byte Unicode names
* can double in UTF-8; UTF-8 marshal names do not expand. Allow that decoding
* expansion while bounding the total names repeated by back-references.
*/
const pyzEntries = (
value: MarshalValue,
@@ -193,16 +194,17 @@ const pyzEntries = (
pyInstallerFailure("format", "PYZ table of contents is not a list"),
);
const entries: PyzEntry[] = [];
const nameByteBudget = tableBytes * 2;
let nameBytes = 0;
for (const item of value) {
const entry = onePyzEntry(item, payloadEnd, entries.length);
if (!entry.ok) return entry;
nameBytes += Buffer.byteLength(entry.value.name, "utf8");
if (nameBytes > tableBytes)
if (nameBytes > nameByteBudget)
return err(
pyInstallerFailure(
"limit",
`PYZ table entry names expand past the ${String(tableBytes)}-byte table that stores them`,
`PYZ table entry names expand past the ${String(nameByteBudget)}-byte decoded-name budget`,
),
);
entries.push(entry.value);