fix(artifacts): validate cpio alignment padding

This commit is contained in:
morluto committed 2026-10-08 23:48:07 +08:00
1 parent c9aa907495
commit 0ade49ae90
2 files changed
+46 -7

No files matched your search

+15 -7
View File
@@ -73,6 +73,15 @@ class ByteSource {
return bytes;
}
/** Consume the NUL alignment bytes required by newc and crc records. */
async padding(length: number, label: string): Promise<void> {
if ((await this.read(length, label)).some((byte) => byte !== 0))
throw new ArtifactReaderFailure(
"format",
`cpio ${label} contains nonzero bytes`,
);
}
/** Yield `length` bytes in chunks without buffering them together. */
async *take(length: number, label: string): AsyncGenerator<Buffer> {
let remaining = length;
@@ -325,11 +334,9 @@ export class CpioArtifactReader implements ArtifactReader {
"cpio member name size is invalid",
);
const nameBytes = await source.read(header.nameSize, "member name");
await drain(
source.take(
padding(header.format, 110 + header.nameSize),
"name padding",
),
await source.padding(
padding(header.format, 110 + header.nameSize),
"name padding",
);
// cpio names require exactly one terminal NUL: a missing terminator or
// an embedded NUL with trailing bytes is malformed. Decode without
@@ -637,8 +644,9 @@ export class CpioArtifactReader implements ArtifactReader {
for (const byte of chunk) sum.value = (sum.value + byte) >>> 0;
yield chunk;
}
await drain(
source.take(padding(header.format, header.fileSize), "data padding"),
await source.padding(
padding(header.format, header.fileSize),
"data padding",
);
this.#cancelled();
}
@@ -2,6 +2,7 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Readable } from "node:stream";
import { buffer } from "node:stream/consumers";
import { gunzipSync, gzipSync } from "node:zlib";
import { describe, expect, it } from "vitest";
@@ -411,3 +412,33 @@ describe("cpio hard links and CRC archives", () => {
await reader.close();
});
});
it.each(["newc", "crc"] as const)(
"rejects nonzero %s alignment under either integrity policy",
async (format) => {
for (const policy of ["fail", "record-and-continue"] as const) {
for (const [offset, label] of [
[113, "name padding"],
[117, "data padding"],
] as const) {
// 110-byte header, a three-byte NUL-terminated name, then one data byte.
const decoded = gunzipSync(
gzipCpio([{ name: "ab", mode: MODE.file, data: "x" }], format),
);
decoded[offset] = 1;
const reader = new CpioArtifactReader(
() => Promise.resolve(Readable.from([gzipSync(decoded)])),
policy,
);
try {
await expect(collect(reader)).rejects.toMatchObject({
reason: "format",
message: `cpio ${label} contains nonzero bytes`,
});
} finally {
await reader.close();
}
}
}
},
);