fix(artifacts): validate cpio alignment padding
This commit is contained in:
1 parent
c9aa907495
commit
0ade49ae90
2 files changed
+46
-7
No files matched your search
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user