mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Check the identity of the open file a runtime pack artifact is read from
Staging stat'ed an artifact's path, read the path, and stat'ed it again. When the path was a symbolic link switched to another file of the same size during the read and back before the second stat, both identities were those of the verified file, so the bytes of the other file were staged without hashing. Staging now opens the artifact once and takes both identities from the open file with fstat, so they describe the file the bytes come from. A test switches the artifact's link to a same-sized file during the read and checks that the staged copy has the verified bytes.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, readFileSync, renameSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -21,6 +30,10 @@ vi.mock("node:crypto", async (original) => {
|
||||
const crypto = await original<typeof import("node:crypto")>();
|
||||
return { ...crypto, createHash: vi.fn(crypto.createHash) };
|
||||
});
|
||||
vi.mock("node:fs", async (original) => {
|
||||
const fs = await original<typeof import("node:fs")>();
|
||||
return { ...fs, readFileSync: vi.fn(fs.readFileSync) };
|
||||
});
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterEach(async () =>
|
||||
@@ -192,3 +205,38 @@ test("staging does not hash an unchanged artifact again", async () => {
|
||||
expect(hashes() - start).toBe(firstStage - 1);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"staging reads an artifact's bytes from the file whose identity it checks",
|
||||
async () => {
|
||||
const f = await packFixture();
|
||||
// The artifact path is a symbolic link to the verified file.
|
||||
const real = f.object + ".real";
|
||||
renameSync(f.object, real);
|
||||
symlinkSync(real, f.object);
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
// During the next read the link names another file of the same size,
|
||||
// and it names the verified file again before the read ends.
|
||||
const other = f.object + ".other";
|
||||
writeFileSync(other, "x".repeat(statSync(real).size));
|
||||
const read = vi.mocked(readFileSync);
|
||||
const original = read.getMockImplementation()!;
|
||||
read.mockImplementation(((source: Parameters<typeof readFileSync>[0], ...rest: unknown[]) => {
|
||||
if (typeof source === "string" && !source.endsWith("base.o"))
|
||||
return (original as (...args: unknown[]) => unknown)(source, ...rest);
|
||||
read.mockImplementation(original);
|
||||
unlinkSync(f.object);
|
||||
symlinkSync(other, f.object);
|
||||
try {
|
||||
return (original as (...args: unknown[]) => unknown)(source, ...rest);
|
||||
} finally {
|
||||
unlinkSync(f.object);
|
||||
symlinkSync(real, f.object);
|
||||
}
|
||||
}) as typeof readFileSync);
|
||||
const staged = f.stage(selection);
|
||||
expect(read.getMockImplementation()).toBe(original);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
closeSync,
|
||||
constants,
|
||||
existsSync,
|
||||
fstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
writeSync,
|
||||
type Stats,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
@@ -62,7 +64,10 @@ interface FileIdentity {
|
||||
}
|
||||
|
||||
function fileIdentity(path: string): FileIdentity {
|
||||
const info = statSync(path);
|
||||
return statsIdentity(statSync(path));
|
||||
}
|
||||
|
||||
function statsIdentity(info: Stats): FileIdentity {
|
||||
return {
|
||||
dev: info.dev,
|
||||
ino: info.ino,
|
||||
@@ -256,9 +261,16 @@ function stageArtifact(
|
||||
let before: FileIdentity;
|
||||
let after: FileIdentity;
|
||||
try {
|
||||
before = fileIdentity(source);
|
||||
bytes = readFileSync(source);
|
||||
after = fileIdentity(source);
|
||||
// The identities are those of the open file the bytes come from, also
|
||||
// when the path is a symbolic link that changes during the read.
|
||||
const input = openSync(source, constants.O_RDONLY);
|
||||
try {
|
||||
before = statsIdentity(fstatSync(input));
|
||||
bytes = readFileSync(input);
|
||||
after = statsIdentity(fstatSync(input));
|
||||
} finally {
|
||||
closeSync(input);
|
||||
}
|
||||
} catch {
|
||||
throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user