Files
rea/tests/boundary/process/packedBridgeVerification.test.ts
T

270 lines
9.9 KiB
TypeScript

import { execFile } from "node:child_process";
import {
access,
copyFile,
mkdir,
readFile,
readdir,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, it, onTestFinished } from "vitest";
import { z } from "zod";
import {
createPackageVerifierCommands,
type PackageVerifierCommands,
} from "../../../scripts/lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
import { verifyPackedBridge } from "../../../scripts/verify-packed-bridge.mjs";
import {
PrivateRuntimeRoot,
privateRuntimeRootCapability,
} from "../../../src/process/PrivateRuntimeRoot.js";
import { GHIDRA_BRIDGE_SOURCES } from "../../../src/ghidra/GhidraBridgeSources.js";
import {
createTestWorkspace,
removeTestWorkspace,
} from "../../support/workspace/workspaceFixture.js";
const exec = promisify(execFile);
const packedFiles = [
"package/bridge/hopper_bridge.py",
"package/bridge/cutter/rea_bridge.py",
...Object.values(GHIDRA_BRIDGE_SOURCES).map(
(name) => `package/bridge/ghidra/${name}`,
),
];
const hopper = packedFiles[0]!;
const validSource =
'# Unicode source: 雨\nraise RuntimeError("Packed source must never execute")\n';
const expectedPythonCommandFailure = (diagnostic: string | RegExp) => ({
name: "VerifierCommandExitError",
code: 1,
snapshot: {
stderr: {
text:
typeof diagnostic === "string"
? expect.stringContaining(diagnostic)
: expect.stringMatching(diagnostic),
},
},
});
async function fixture(source: string = validSource, omitHopper = false) {
const scratch = await createTestWorkspace("rea-packed-bridge-");
const directory = scratch.root;
let commands: PackageVerifierCommands | undefined;
onTestFinished(() =>
commands === undefined ? removeTestWorkspace(directory) : commands.close(),
);
commands = await createPackageVerifierCommands({
workspace: { path: directory, close: () => removeTestWorkspace(directory) },
});
const staging = join(directory, "source");
const workspace = join(directory, "workspace");
await mkdir(workspace);
for (const member of packedFiles) {
if (omitHopper && member === hopper) continue;
const path = join(staging, member);
await mkdir(dirname(path), { recursive: true });
await writeFile(path, member === hopper ? source : "fixture\n");
}
const tarball = join(directory, "packed bridges.tgz");
await exec("tar", ["-czf", tarball, "-C", staging, "package"]);
return { workspace, tarball, packedFiles, commands };
}
it("parses packed Python without executing it or extracting archive members", async () => {
const input = await fixture();
const sentinel = join(input.workspace, "caller-owned.txt");
await writeFile(sentinel, "preserved\n");
await verifyPackedBridge(input);
expect(await readdir(input.workspace)).toEqual(["caller-owned.txt"]);
expect(await readFile(sentinel, "utf8")).toBe("preserved\n");
});
it.each(["cwd", "pythonpath", "sitecustomize"] as const)(
"rejects invalid packed Python without importing ambient %s code",
async (kind) => {
const input = await fixture("def broken(:\n");
const modules = join(input.workspace, "local modules");
await mkdir(modules);
const marker = join(input.workspace, "ambient-import-executed.txt");
const moduleSource = [
"import os",
"from pathlib import Path",
"Path(os.environ['REA_PACKED_IMPORT_MARKER']).write_text('executed')",
...(kind === "sitecustomize"
? ["import ast", "ast.parse = lambda *args, **kwargs: None"]
: ["def parse(*args, **kwargs): return None"]),
].join("\n");
await writeFile(
join(modules, kind === "sitecustomize" ? "sitecustomize.py" : "ast.py"),
moduleSource,
);
const verifier = new URL(
"../../../scripts/verify-packed-bridge.mjs",
import.meta.url,
).href;
// A separate Node process owns cwd/environment changes; the Vitest worker
// and its other subprocess fixtures retain their original environment.
const result = await exec(
process.execPath,
[
"--input-type=module",
"--eval",
`import { verifyPackedBridge } from ${JSON.stringify(verifier)};
import { createPackageVerifierCommands } from ${JSON.stringify(new URL("../../../scripts/lib/verify-package-commands.mjs", import.meta.url).href)};
import { withVerifierCleanup } from ${JSON.stringify(new URL("../../../scripts/lib/verifier-cleanup.mjs", import.meta.url).href)};
import { PrivateRuntimeRoot } from ${JSON.stringify(new URL("../../../dist/process/PrivateRuntimeRoot.js", import.meta.url).href)};
const root = await PrivateRuntimeRoot.create({ prefix: "rea-packed-python-command-" });
let commands;
await withVerifierCleanup(async () => {
commands = await createPackageVerifierCommands({ workspace: root });
try {
await verifyPackedBridge({ ...JSON.parse(process.env.REA_PACKED_VERIFY_INPUT), commands });
console.log(JSON.stringify({ accepted: true }));
} catch (error) {
console.log(JSON.stringify({ accepted: false, name: error.name, code: error.code, stderr: error.snapshot?.stderr?.text }));
}
}, [() => commands === undefined ? root.close() : commands.close()]);`,
],
{
cwd: kind === "cwd" ? modules : input.workspace,
env: {
...Object.fromEntries(
Object.entries(process.env).filter(
([name]) => name.toLowerCase() !== "pythonpath",
),
),
PYTHONPATH: kind === "cwd" ? "" : modules,
REA_PACKED_IMPORT_MARKER: marker,
REA_PACKED_VERIFY_INPUT: JSON.stringify({
tarball: input.tarball,
packedFiles: input.packedFiles,
}),
},
},
);
const observation = z
.object({
accepted: z.boolean(),
name: z.string().optional(),
code: z.number().optional(),
stderr: z.string().optional(),
})
.parse(JSON.parse(result.stdout));
expect(observation).toMatchObject({
accepted: false,
name: "VerifierCommandExitError",
code: 1,
stderr: expect.stringContaining("SyntaxError"),
});
await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.each(packedFiles)(
"rejects an unadvertised required member: %s",
async (member) => {
const input = await fixture();
await expect(
verifyPackedBridge({
...input,
packedFiles: packedFiles.filter((path) => path !== member),
}),
).rejects.toThrow(`package omitted ${member}`);
expect(await readdir(input.workspace)).toEqual([]);
},
);
it("rejects a missing actual Python member even when the listing advertises it", async () => {
const input = await fixture(validSource, true);
await expect(verifyPackedBridge(input)).rejects.toMatchObject(
expectedPythonCommandFailure(hopper),
);
expect(await readdir(input.workspace)).toEqual([]);
});
it("reports invalid packed Python syntax with the member filename", async () => {
const input = await fixture("def broken(:\n");
await expect(verifyPackedBridge(input)).rejects.toMatchObject(
expectedPythonCommandFailure(/hopper_bridge\.py[\s\S]*SyntaxError/u),
);
expect(await readdir(input.workspace)).toEqual([]);
});
it.each(["valid", "invalid"] as const)(
"parses %s source under an owned private root and preserves owner cleanup",
async (kind) => {
expect(privateRuntimeRootCapability().available).toBe(true);
const input = await fixture(
kind === "valid" ? validSource : "def broken(:\n",
);
const root = await PrivateRuntimeRoot.create({
prefix: "rea-packed-bridge-lease-",
});
let commands: PackageVerifierCommands | undefined;
await withVerifierCleanup(async () => {
commands = await createPackageVerifierCommands({ workspace: root });
const pack = join(root.path, "pack");
await mkdir(pack);
const tarball = join(pack, "packed bridges.tgz");
await copyFile(input.tarball, tarball);
const parsed = verifyPackedBridge({ tarball, packedFiles, commands });
if (kind === "valid") await parsed;
else
await expect(parsed).rejects.toMatchObject(
expectedPythonCommandFailure("SyntaxError"),
);
expect(await readdir(root.path)).toEqual(["pack"]);
expect(
Buffer.compare(await readFile(tarball), await readFile(input.tarball)),
).toBe(0);
}, [() => (commands === undefined ? root.close() : commands.close())]);
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.each(["symlink", "hardlink", "directory"])(
"rejects a %s Python member without following it or extracting files",
async (kind) => {
const input = await fixture();
await exec("python3", [
"-I",
"-S",
"-c",
[
"import io, sys, tarfile",
"with tarfile.open(sys.argv[1], 'w:gz') as archive:",
" target = tarfile.TarInfo('package/bridge/target.py')",
" target.size = 5",
" archive.addfile(target, io.BytesIO(b'pass\\n'))",
" member = tarfile.TarInfo(sys.argv[2])",
" member.type = {'symlink': tarfile.SYMTYPE, 'hardlink': tarfile.LNKTYPE, 'directory': tarfile.DIRTYPE}[sys.argv[3]]",
" member.linkname = 'target.py' if sys.argv[3] == 'symlink' else target.name",
" archive.addfile(member)",
].join("\n"),
input.tarball,
hopper,
kind,
]);
await expect(verifyPackedBridge(input)).rejects.toMatchObject(
expectedPythonCommandFailure(`${hopper} is not a regular file`),
);
expect(await readdir(input.workspace)).toEqual([]);
},
);
it("rejects malformed archives without writing to the caller's workspace", async () => {
const input = await fixture();
await writeFile(input.tarball, "not a tar archive\n");
await expect(verifyPackedBridge(input)).rejects.toMatchObject(
expectedPythonCommandFailure("ReadError"),
);
expect(await readdir(input.workspace)).toEqual([]);
});