fix(paths): preserve selected command, output, and source identities

This commit is contained in:
morluto committed 2026-10-26 17:14:02 +08:00
1 parent 18dc78fa43
commit 86ff3237e1
20 files changed
+685 -84

No files matched your search

+9 -1
View File
@@ -5,6 +5,7 @@ import {
open,
realpath,
rename,
stat,
type FileHandle,
} from "node:fs/promises";
import { basename, dirname, resolve } from "node:path";
@@ -353,7 +354,14 @@ const writeTextPartsOwned = async (
return err(
new EvidenceFileError("write", "not-file", { path: requestedPath }),
);
const canonicalParent = await realpath(dirname(requestedPath));
const selectedParent = dirname(requestedPath);
// Native lookup must accept the parent before realpath can collapse it.
// On macOS realpath(file/..) succeeds even though that traversal is ENOTDIR.
if (!(await stat(selectedParent)).isDirectory())
return err(
new EvidenceFileError("write", "missing", { path: requestedPath }),
);
const canonicalParent = await realpath(selectedParent);
const destination = resolve(canonicalParent, basename(requestedPath));
const existing = await lstat(destination).catch((cause: unknown) => {
if (fileErrorCode(cause) === "ENOENT") return undefined;
+25 -15
View File
@@ -8,6 +8,7 @@ import {
open,
readdir,
realpath,
stat,
type FileHandle,
} from "node:fs/promises";
import { basename, dirname, isAbsolute, join } from "node:path";
@@ -194,24 +195,33 @@ export class SafeOutputTree {
throw new SafeOutputTreeDestinationRefusal(
"Invalid extraction output root",
);
const parent = await realpath(dirname(requested)).catch(
(cause: unknown) => {
if (
cause instanceof Error &&
"code" in cause &&
["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(String(cause.code))
)
throw new SafeOutputTreeDestinationRefusal(
"Extraction output parent is unavailable",
{ cause },
);
throw new ArtifactReaderFailure(
"unavailable",
let parent: string;
try {
const selectedParent = dirname(requested);
// Darwin realpath can collapse file/.. despite stat rejecting that
// traversal. Admit the selected directory before canonicalizing it.
if (!(await stat(selectedParent)).isDirectory())
throw new SafeOutputTreeDestinationRefusal(
"Extraction output parent must be a directory",
);
parent = await realpath(selectedParent);
} catch (cause: unknown) {
if (cause instanceof SafeOutputTreeDestinationRefusal) throw cause;
if (
cause instanceof Error &&
"code" in cause &&
["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(String(cause.code))
)
throw new SafeOutputTreeDestinationRefusal(
"Extraction output parent is unavailable",
{ cause },
);
},
);
throw new ArtifactReaderFailure(
"unavailable",
"Extraction output parent is unavailable",
{ cause },
);
}
const canonicalOutput = join(parent, name);
return { requested, canonicalOutput };
}
+6 -5
View File
@@ -937,14 +937,15 @@ const originalModuleEdges = (
return { edges, incomplete };
};
/** Select source language from the URL path, excluding query/fragment. */
/** Derive a language hint without changing the retained source URL identity. */
const originalSourcePath = (source: string): string => {
const path = URL.parse(source)?.pathname ?? source;
try {
return new URL(source).pathname;
return decodeURIComponent(path);
} catch (cause: unknown) {
// Unresolved source identities can still carry a useful relative path.
void cause;
return source;
if (!(cause instanceof URIError)) throw cause;
// A literal or malformed escape must not discard an otherwise useful hint.
return path;
}
};
-3
View File
@@ -1,6 +1,3 @@
/** "GDPC" read as a little-endian uint32: the Godot Engine pack magic. */
export const GODOT_PCK_MAGIC = 0x43504447;
/** Whether bytes start with a standalone Godot PCK header. */
export const hasGodotPckHeader = (bytes: Uint8Array): boolean =>
bytes.length >= 4 &&
+25 -14
View File
@@ -1,10 +1,14 @@
import {
selectArtifactCommandPath,
type ArtifactCommandPath,
} from "../process/ArtifactCommandPath.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
FIRMWARE_TOOL_REQUIREMENTS,
type FirmwareEngineName,
} from "../contracts/firmware/firmwareToolRequirements.js";
import { randomUUID } from "node:crypto";
import { access, realpath, stat } from "node:fs/promises";
import { access, stat } from "node:fs/promises";
import { constants } from "node:fs";
import { isAbsolute } from "node:path";
import {
@@ -14,8 +18,8 @@ import {
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import type {
AnalysisCapturedOutput,
AnalysisError,
AnalysisCapturedOutput,
} from "../domain/analysisErrorBase.js";
import { projectAnalysisErrorWithoutCapturedOutput } from "../domain/analysisErrorProjection.js";
import type { FirmwareDeadline } from "./FirmwareDeadline.js";
@@ -47,7 +51,7 @@ export type FirmwareLauncher = (
options: OwnedProviderProcessSpawnOptions,
) => Promise<SpawnedOwnedProviderProcess>;
/** Captured tool output for one finished firmware command. */
/** Captured tool output for one finished firmware command, including byte coverage. */
const capturedFirmwareOutput = (
snapshot: ProviderProcessSnapshot,
): AnalysisCapturedOutput => ({
@@ -94,19 +98,23 @@ export const resolveFirmwareCommand = async (
operation,
`This integration requires Linux and an absolute ${variable} path to the caller-supplied tool`,
);
const command = await realpath(configured).catch((cause: unknown) => {
throw firmwareToolUnavailable(
engine,
operation,
`${variable} is unavailable: ${cause instanceof Error ? cause.message : String(cause)}`,
);
});
const selection = await selectArtifactCommandPath(configured, signal).catch(
(cause: unknown) => {
if (cause instanceof ArtifactReaderFailure) throw cause;
throw firmwareToolUnavailable(
engine,
operation,
`${variable} is unavailable: ${cause instanceof Error ? cause.message : String(cause)}`,
);
},
);
const command = selection.invocationPath;
let sha256: string;
try {
await access(command, constants.R_OK | constants.X_OK);
if (!(await stat(command)).isFile())
throw new TypeError("Configured path is not a regular file");
sha256 = await files.hash(command, signal);
sha256 = await files.hash(selection.resolvedPath, signal);
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure) throw cause;
throw firmwareToolUnavailable(
@@ -123,11 +131,14 @@ export const resolveFirmwareCommand = async (
operation,
"REA_FIRMWARE_PRLIMIT_COMMAND must be absolute",
);
let limiterSelection: ArtifactCommandPath;
try {
await access(limiter, constants.X_OK);
limiterSelection = await selectArtifactCommandPath(limiter, signal);
await access(limiterSelection.resolvedPath, constants.X_OK);
if (!(await stat(limiter)).isFile())
throw new TypeError("Configured path is not a regular file");
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure) throw cause;
throw firmwareToolUnavailable(
engine,
operation,
@@ -135,8 +146,8 @@ export const resolveFirmwareCommand = async (
);
}
return {
command,
limiter,
command: selection,
limiter: limiterSelection,
sha256,
fileSizeCeiling: await files.fileSizeCeiling(signal),
};
+26 -5
View File
@@ -1,3 +1,4 @@
import { assertArtifactCommandPath } from "../process/ArtifactCommandPath.js";
import { join } from "node:path";
import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js";
import {
@@ -368,14 +369,20 @@ export class FirmwareProvider implements FirmwareAnalysisPort {
const ownedRootLease = new AnalysisCleanupLease(() => ownedRoot.close());
rootLease = ownedRootLease;
const target = await this.#files.snapshot(request, root.path, signal);
const run = (
const run = async (
args: readonly string[],
outputBudget?: { root: string; bytes: number; entries: number },
) =>
runFirmwareCommand({
) => {
for (const selected of [command.command, command.limiter])
await assertArtifactCommandPath(selected, request.operation, {
signal,
});
const output = await runFirmwareCommand({
engine: engineName,
operation: request.operation,
...command,
fileSizeCeiling: command.fileSizeCeiling,
command: command.command.invocationPath,
limiter: command.limiter.invocationPath,
args,
cwd: root?.path ?? "",
environment: this.environment,
@@ -395,6 +402,20 @@ export class FirmwareProvider implements FirmwareAnalysisPort {
? {}
: { outputBudget, acceptReportedExtractionFailure: true }),
});
for (const selected of [command.command, command.limiter])
await assertArtifactCommandPath(selected, request.operation, {
capturedOutput: {
stdout: output.stdout.text,
stderr: output.stderr.text,
stdout_bytes: output.stdout.bytes,
stderr_bytes: output.stderr.bytes,
exit_code: output.exit_code ?? null,
signal: output.signal,
truncated: false,
},
});
return output;
};
const versionRun = await run(["--version"]);
const version = versionRun.stdout.text.trim();
if (versionRun.stdout.bytes > 1024)
@@ -413,7 +434,7 @@ export class FirmwareProvider implements FirmwareAnalysisPort {
const engine = {
name: engineName,
version: admitted.version,
executable_path: command.command,
executable_path: command.command.resolvedPath,
executable_sha256: command.sha256,
source_revision: null,
worker_count: 1 as const,
+8 -6
View File
@@ -1,6 +1,7 @@
import { selectArtifactCommandPath } from "../../process/ArtifactCommandPath.js";
import { randomUUID } from "node:crypto";
import { constants } from "node:fs";
import { access, realpath, stat } from "node:fs/promises";
import { access, stat } from "node:fs/promises";
import { isAbsolute } from "node:path";
import {
AnalysisArtifactChangedError,
@@ -87,10 +88,10 @@ export const resolveWakaruCommand = async (
{ userMessage: WAKARU_REQUIREMENT },
);
try {
const command = await realpath(configured);
await access(command, constants.R_OK | constants.X_OK);
const selection = await selectArtifactCommandPath(configured, signal);
await access(selection.resolvedPath, constants.R_OK | constants.X_OK);
const fingerprint = await fingerprintRecoveryFile(
command,
selection.resolvedPath,
RECOVERY_LIMITS.outputBytes,
signal,
resources,
@@ -99,10 +100,11 @@ export const resolveWakaruCommand = async (
environment.REA_JAVASCRIPT_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
if (!isAbsolute(limiter))
throw new TypeError("REA_JAVASCRIPT_PRLIMIT_COMMAND must be absolute");
await access(limiter, constants.X_OK);
const limiterSelection = await selectArtifactCommandPath(limiter, signal);
await access(limiterSelection.resolvedPath, constants.X_OK);
if (!(await stat(limiter)).isFile())
throw new TypeError(`Resource limiter is not a regular file: ${limiter}`);
return { command, limiter, ...fingerprint };
return { command: selection, limiter: limiterSelection, ...fingerprint };
} catch (cause: unknown) {
if (
cause instanceof AnalysisCancelledError ||
+27 -7
View File
@@ -1,3 +1,4 @@
import { assertArtifactCommandPath } from "../../process/ArtifactCommandPath.js";
import { join } from "node:path";
import {
createAnalysisExecution,
@@ -61,7 +62,8 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
} = context;
const staging = join(root, "modules");
const commandContext = {
...engine,
command: engine.command.invocationPath,
limiter: engine.limiter.invocationPath,
cwd: root,
deadline: context.deadline,
environment: environment,
@@ -69,7 +71,25 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
...(launcher === undefined ? {} : { launcher: launcher }),
resources,
};
const version = await runWakaruCommand({
const run = async (execution: Parameters<typeof runWakaruCommand>[0]) => {
for (const selected of [engine.command, engine.limiter])
await assertArtifactCommandPath(selected, OPERATION, {
signal: options?.signal,
});
const output = await runWakaruCommand(execution);
for (const selected of [engine.command, engine.limiter])
await assertArtifactCommandPath(selected, OPERATION, {
capturedOutput: {
stdout: output.stdout,
stderr: output.stderr,
exit_code: output.exit_code,
signal: output.signal,
truncated: false,
},
});
return output;
};
const version = await run({
...commandContext,
arguments: ["--version"],
});
@@ -87,7 +107,7 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
{ userMessage: `${mismatch}.` },
);
}
const run = await runWakaruCommand({
const execution = await run({
...commandContext,
staging,
arguments: [
@@ -118,14 +138,14 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
),
);
const parsed = await parseWakaruReports({
execution: run,
execution,
provenancePath: join(staging, "provenance.json"),
snapshots,
files,
...(options?.signal === undefined ? {} : { signal: options.signal }),
resources,
});
return { version, admitted, run, parsed };
return { version, admitted, run: execution, parsed };
};
/** Publish validated recovery and retain exact source and engine bindings. */
@@ -170,7 +190,7 @@ export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
);
}
const currentEngine = await fingerprintRecoveryFile(
engine.command,
engine.command.resolvedPath,
RECOVERY_LIMITS.outputBytes,
options?.signal,
resources,
@@ -178,7 +198,7 @@ export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
if (currentEngine.sha256 !== engine.sha256)
throw new AnalysisArtifactChangedError(
OPERATION,
engine.command,
engine.command.invocationPath,
"Configured engine bytes changed during recovery",
{
diagnostics: {
+1 -1
View File
@@ -109,7 +109,7 @@ export const publishWakaruArtifacts = async (
id: "wakaru",
version: context.admitted.version,
executable: {
path: context.engine.command,
path: context.engine.command.resolvedPath,
sha256: context.engine.sha256,
bytes: context.engine.bytes,
},
+52 -3
View File
@@ -1,3 +1,11 @@
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import {
assertArtifactCommandPath,
type ArtifactCommandPath,
} from "../process/ArtifactCommandPath.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
import { randomUUID } from "node:crypto";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
@@ -58,7 +66,10 @@ export const runPcapCommand = async (options: {
readonly signal: AbortSignal | undefined;
readonly inputPath?: string;
readonly nativeBound?: {
readonly limiter?: { command: string; arguments: readonly string[] };
readonly limiter?: {
command: ArtifactCommandPath;
arguments: readonly string[];
};
readonly memoryLimitBytes?: number;
};
readonly launcher?: PcapCommandLauncher;
@@ -92,7 +103,7 @@ export const runPcapCommand = async (options: {
options.nativeBound?.limiter === undefined
? invocation
: {
command: options.nativeBound.limiter.command,
command: options.nativeBound.limiter.command.invocationPath,
arguments: [
...options.nativeBound.limiter.arguments,
invocation.command,
@@ -102,6 +113,9 @@ export const runPcapCommand = async (options: {
// Native ownership preparation has its own bounded compiler lifecycle;
// do not spend the tool's execution deadline rebuilding a cold SDK cache.
await prepareOwnedCommand(signal);
const selectedLimiter = options.nativeBound?.limiter?.command;
if (selectedLimiter !== undefined)
await assertArtifactCommandPath(selectedLimiter, operation, { signal });
const output = await runOwnedCommand(
{
...boundedInvocation,
@@ -137,6 +151,20 @@ export const runPcapCommand = async (options: {
},
},
);
if (selectedLimiter !== undefined)
await assertArtifactCommandPath(selectedLimiter, operation, {
capturedOutput: {
stdout: output.stdout.text,
stderr: output.stderr.text,
stdout_bytes: output.stdout.bytes,
stderr_bytes: output.stderr.bytes,
exit_code: output.exitCode ?? null,
signal: output.signal ?? null,
truncated:
output.diagnosticTruncated === true ||
output.outputIncomplete === true,
},
});
return {
stdout: output.stdout.text,
stderr: output.stderr.text,
@@ -147,7 +175,28 @@ export const runPcapCommand = async (options: {
...(cleanup === undefined ? {} : { cleanup }),
};
} catch (cause) {
if (cause instanceof AnalysisError) throw cause;
if (cause instanceof AnalysisError) {
if (cleanup !== undefined) {
const pending = new ProviderCleanupError(
"pcap",
cleanup.resources,
{
reason: cleanup.reason,
},
{
operation,
capturedOutput: cleanup.capturedOutput,
cleanup: { reason: cleanup.reason, resources: cleanup.resources },
},
);
throw analysisErrorWithCleanupFailure(cause, pending, operation);
}
throw cause;
}
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
throw new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
});
if (
cause instanceof OwnedCommandFailure &&
cause.cleanupOwner !== undefined
+5 -4
View File
@@ -1,8 +1,9 @@
import { selectArtifactCommandPath } from "../process/ArtifactCommandPath.js";
import {
pcapCaptureHostSupported,
PCAP_CAPTURE_UNSUPPORTED_HOST_REASON,
} from "../domain/pcap/pcapAnalysis.js";
import { access, realpath } from "node:fs/promises";
import { access } from "node:fs/promises";
import { constants } from "node:fs";
import { isAbsolute } from "node:path";
import { AnalysisError } from "../domain/analysisErrorBase.js";
@@ -49,8 +50,8 @@ export const preparePcapNativeBound = async (
try {
if (!isAbsolute(configured))
throw new Error(`${PRLIMIT_COMMAND_ENV} must be absolute.`);
const limiter = await realpath(configured);
await access(limiter, constants.X_OK);
const selection = await selectArtifactCommandPath(configured);
await access(selection.resolvedPath, constants.X_OK);
const limits = await resources.readLimits({
address_space_bytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
file_bytes: BOUNDED_PRODUCER_LIMITS.file_bytes,
@@ -58,7 +59,7 @@ export const preparePcapNativeBound = async (
});
const nativeBound = {
limiter: {
command: limiter,
command: selection,
arguments: prlimitCeilingArguments(limits),
},
};
+30 -9
View File
@@ -1,8 +1,15 @@
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { realpath } from "node:fs/promises";
import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js";
import {
artifactSelectedPathErrno,
assertArtifactReadActive,
} from "../artifacts/ArtifactFileOperation.js";
import type { AnalysisCapturedOutput } from "../domain/analysisErrorBase.js";
import { AnalysisArtifactChangedError } from "../domain/analysisErrorCore.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
} from "../domain/analysisErrorCore.js";
/** The selected launch spelling and the physical file whose bytes are fingerprinted. */
export interface ArtifactCommandPath {
@@ -47,13 +54,27 @@ export const assertArtifactCommandPath = async (
try {
current = await realpath(selected.invocationPath);
} catch (cause: unknown) {
if (
cause instanceof Error &&
"code" in cause &&
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
)
throw changed(cause);
throw cause;
if (artifactSelectedPathErrno(cause) !== undefined) throw changed(cause);
const systemCode =
cause instanceof Error && "code" in cause
? String(cause.code)
: undefined;
const failureOptions = { cause, capturedOutput: options.capturedOutput };
if (systemCode === "EACCES" || systemCode === "EPERM")
throw new AnalysisAccessDeniedError(
operation,
selected.invocationPath,
systemCode,
failureOptions,
);
throw new ProviderAdapterError("rea", operation, {
...failureOptions,
diagnostics: {
path: selected.invocationPath,
reason: cause instanceof Error ? cause.message : String(cause),
system_code: systemCode ?? null,
},
});
}
if (current !== selected.resolvedPath) throw changed();
assertArtifactReadActive(options.signal);
@@ -0,0 +1,75 @@
import { once } from "node:events";
import { createServer } from "node:http";
import { expect, it } from "vitest";
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
import { fetchWebSourceMapsOk } from "../../fixtures/fetchWebSourceMapsOk.js";
it("selects original source language from decoded URL paths while retaining URL evidence", async () => {
const declarations = [
"plain.ts?revision=2#view.tsx",
"escaped%2Ets?revision=2#view.tsx",
"escaped.%74s?revision=2#view.tsx",
"invalid%.ts?revision=2#view.tsx",
];
const content = 'import "./dep.ts"; const value = <number>1;';
const text = JSON.stringify({
version: 3,
names: [],
sources: declarations,
sourcesContent: declarations.map(() => content),
mappings: "AAAA,ACAA,ACAA,ACAA",
});
const requests: string[] = [];
const server = createServer((request, response) => {
requests.push(request.url ?? "");
response.writeHead(200, { "content-type": "application/json" }).end(text);
});
try {
const listening = once(server, "listening");
server.listen(0, "127.0.0.1");
await listening;
const address = server.address();
if (address === null || typeof address === "string")
throw new TypeError("Expected TCP listener");
const origin = `http://127.0.0.1:${String(address.port)}`;
const url = `${origin}/maps/app.js.map`;
const result = await fetchWebSourceMapsOk(
[{ scriptKey: "script", declaredUrl: url, fetchUrl: url }],
analyzeWebBundleInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
target_id: "selected-page",
allowed_origins: [origin],
fetch_source_maps: true,
}),
);
const sources = declarations.map((source) => `${origin}/maps/${source}`);
expect(requests).toEqual(["/maps/app.js.map"]);
expect(result.status).toBe("included");
expect(result.items[0]).toMatchObject({
status: "included",
limitation: null,
artifact: { text },
original_sources: sources.map((source) => ({
source,
artifact: { media_type: "text/typescript", text: content },
})),
mappings: sources.map((source) => ({ source })),
original_module_edges: sources.map((source) => ({
from_source: source,
kind: "static_import",
specifier: "./dep.ts",
resolved_source: `${origin}/maps/dep.ts`,
})),
});
} finally {
server.closeAllConnections();
if (server.listening)
await new Promise<void>((resolve, reject) =>
server.close((error) =>
error === undefined ? resolve() : reject(error),
),
);
}
});
@@ -729,6 +729,31 @@ it("reuses authenticated file snapshots while isolating mutable public reads", a
ledger.clear();
});
it.skipIf(process.platform === "win32")(
"rejects a regular-file parent traversal without writing to its lexical sibling",
async () => {
const directory = await createTestTempDirectory("rea-evidence-parent-");
const blocker = join(directory, "file");
await writeFile(blocker, "unchanged");
const selected = `${blocker}/../bundle.json`;
expect(await writeBundle(bundle(), selected, false)).toMatchObject({
ok: false,
error: { _tag: "EvidenceFileError", path: selected },
});
expect(await readdir(directory)).toEqual(["file"]);
expect(await readFile(blocker, "utf8")).toBe("unchanged");
const corrected = join(directory, "bundle.json");
expect(await writeBundle(bundle(), corrected, false)).toMatchObject({
ok: true,
value: { path: corrected },
});
expect(await readEvidenceBundle(corrected)).toEqual({
ok: true,
value: bundle(),
});
},
);
describe("evidence bundle filesystem adapter", () => {
it.skipIf(process.platform === "win32").each(["/", "/."])(
"preserves the directory requirement of a missing output ending in %s",
@@ -27,7 +27,7 @@ const layout = async (prefix: string) => {
};
describe.skipIf(process.platform === "win32")(
"file owners given a directory symlink followed by ..",
"file owners preserve POSIX path traversal",
() => {
it("snapshots the firmware image the filesystem selects", async () => {
const root = await layout("rea-symlink-parent-firmware-");
@@ -88,5 +88,27 @@ describe.skipIf(process.platform === "win32")(
await tree.rollback();
}
});
it.each(["file/out", "file/../out"])(
"refuses an output parent traversing a regular file: %s",
async (relative) => {
const root = await layout("rea-nondirectory-parent-output-");
await writeFile(join(root, "file"), "original");
// Darwin realpath can collapse file/.. even though stat rejects it.
const output = `${root}/${relative}`;
for (const admit of [
() => SafeOutputTree.checkDestination(output),
() => SafeOutputTree.create(output),
]) {
await expect(admit()).rejects.toMatchObject({
name: "SafeOutputTreeDestinationRefusal",
reason: "path",
});
await expect(stat(join(root, "out"))).rejects.toMatchObject({
code: "ENOENT",
});
}
},
);
},
);
@@ -15,8 +15,14 @@ vi.mock(
return {
...actual,
resolveWakaruCommand: async () => ({
command: "/unused/wakaru",
limiter: "/unused/prlimit",
command: {
invocationPath: "/unused/wakaru",
resolvedPath: "/unused/wakaru",
},
limiter: {
invocationPath: "/unused/prlimit",
resolvedPath: "/unused/prlimit",
},
sha256: "a".repeat(64),
bytes: 1,
}),
@@ -43,8 +43,14 @@ vi.mock(
return {
...actual,
resolveWakaruCommand: async () => ({
command: "/unused/wakaru",
limiter: "/unused/prlimit",
command: {
invocationPath: "/unused/wakaru",
resolvedPath: "/unused/wakaru",
},
limiter: {
invocationPath: "/unused/prlimit",
resolvedPath: "/unused/prlimit",
},
sha256: "a".repeat(64),
bytes: 1,
}),
@@ -8,8 +8,24 @@ import {
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished } from "vitest";
import { expect, it, onTestFinished, vi } from "vitest";
import { FirmwareProvider } from "../../../src/firmware/FirmwareProvider.js";
import { FirmwareAnalysisService } from "../../../src/application/firmware/FirmwareAnalysisService.js";
import { FirmwareFileAccess } from "../../../src/firmware/FirmwareFiles.js";
import { WakaruProvider } from "../../../src/javascript/recovery/WakaruProvider.js";
import { JavaScriptRecoveryService } from "../../../src/application/javascript/JavaScriptRecoveryService.js";
import {
spawnOwnedProviderProcess,
type ProviderProcessSupervisor,
} from "../../../src/process/ProviderProcess.js";
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
import { observeProcessStartIdentity } from "../../../src/process/ProcessOwnershipObservation.js";
import { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import { fileURLToPath } from "node:url";
import { preparePcapNativeBound } from "../../../src/pcap/PcapNativeBound.js";
import { runPcapCommand } from "../../../src/pcap/PcapCommand.js";
import { ArtifactAnalysisResources } from "../../../src/process/ArtifactAnalysisResources.js";
import { PacketCaptureService } from "../../../src/application/captures/PacketCaptureService.js";
import { SignalRecordingService } from "../../../src/application/signals/SignalRecordingService.js";
import { TsharkCaptureProvider } from "../../../src/captures/TsharkCaptureProvider.js";
@@ -117,3 +133,271 @@ it.skipIf(process.platform === "win32").each(["tshark", "sigrok"] as const)(
).toEqual([]);
},
);
const selectedEngineLauncher = (kind: "firmware" | "wakaru"): string => {
const producer = fileURLToPath(
new URL(
kind === "firmware"
? "../../fixtures/firmware/producer.mjs"
: "../../fixtures/javascript-recovery/producer.mjs",
import.meta.url,
),
);
const invocation = [
process.execPath,
producer,
...(kind === "firmware" ? ["binwalk"] : []),
]
.map((argument) => `'${argument.replaceAll("'", "'\\''")}'`)
.join(" ");
return `#!/bin/sh\nIFS= read -r role < "$0.role" || exit 19\nif [ -f "$0.retarget" ]; then\n IFS= read -r target < "$0.retarget"\n /bin/ln -sf "$target" "$0"\nfi\nif [ "$role" = limiter ]; then\n while [ "$1" != -- ]; do shift; done\n shift\n exec "$@"\nfi\nexec ${invocation} "$@"\n`;
};
// Exercise shared CLI/MCP workflows with real children and filesystem. Only
// Linux/x64 admission and /proc limits are simulated; the launcher restores the
// actual host before entering real process supervision.
it.skipIf(process.platform === "win32").each(["firmware", "wakaru"] as const)(
"executes selected %s wrappers and retains completed output when either alias changes",
async (kind) => {
const directory = await createTestTempDirectory("rea-selected-command-");
const bin = join(directory, "bin"),
libexec = join(directory, "libexec");
await mkdir(bin);
await mkdir(libexec);
const command = join(bin, "engine"),
limiter = join(bin, "prlimit");
const commandSource = join(libexec, "engine"),
limiterSource = join(libexec, "limiter");
const replacement = join(libexec, "replacement");
const script = selectedEngineLauncher(kind);
for (const path of [commandSource, limiterSource, replacement])
await writeFile(path, script, { mode: 0o755 });
await symlink(commandSource, command);
await symlink(limiterSource, limiter);
await writeFile(`${command}.role`, `${kind}\n`);
await writeFile(`${limiter}.role`, "limiter\n");
const input = join(
directory,
kind === "firmware" ? "input.bin" : "input.js",
);
await writeFile(
input,
kind === "firmware" ? "fixture-input-data" : "module.exports = 42;",
);
const platform = Object.getOwnPropertyDescriptor(process, "platform");
const arch = Object.getOwnPropertyDescriptor(process, "arch");
if (platform === undefined || arch === undefined)
throw new Error("Missing host descriptors");
const restoreHost = () => {
Object.defineProperty(process, "platform", platform);
Object.defineProperty(process, "arch", arch);
};
const launches: { cwd?: string; pid?: number }[] = [];
const launcher: typeof spawnOwnedProviderProcess = async (options) => {
restoreHost();
const child = await spawnOwnedProviderProcess(options);
launches.push({
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
...(child.process.pid === undefined ? {} : { pid: child.process.pid }),
});
return child;
};
const fileSizeCeiling = vi
.spyOn(FirmwareFileAccess.prototype, "fileSizeCeiling")
.mockResolvedValue(Number.MAX_SAFE_INTEGER);
onTestFinished(() => fileSizeCeiling.mockRestore());
const service =
kind === "firmware"
? new FirmwareAnalysisService(
new FirmwareProvider(
{
...process.env,
REA_BINWALK_COMMAND: command,
REA_FIRMWARE_PRLIMIT_COMMAND: limiter,
},
launcher,
),
)
: new JavaScriptRecoveryService(
new WakaruProvider(
{
...process.env,
REA_WAKARU_COMMAND: command,
REA_JAVASCRIPT_PRLIMIT_COMMAND: limiter,
},
launcher,
),
);
onTestFinished(async () => {
restoreHost();
await service.close();
});
let sequence = 0;
const execute = async () => {
Object.defineProperty(process, "platform", {
...platform,
value: "linux",
});
Object.defineProperty(process, "arch", { ...arch, value: "x64" });
try {
return service instanceof FirmwareAnalysisService
? await service.execute("inspect_firmware_regions", { path: input })
: await service.recover({
path: input,
output_directory: join(directory, `output-${sequence++}`),
});
} finally {
restoreHost();
}
};
const first = await execute();
if (!first.ok) throw first.error;
expect(first.value.normalized_result).toMatchObject({
engine:
kind === "firmware"
? { executable_path: await realpath(commandSource) }
: { executable: { path: await realpath(commandSource) } },
});
expect(first.value.raw_result).toMatchObject({
[kind === "firmware" ? "version_execution" : "version"]: {
command,
resource_limiter: limiter,
},
});
for (const [selectedPath, target] of [
[command, replacement],
[limiter, replacement],
[command, command],
[limiter, limiter],
] as const) {
await writeFile(`${selectedPath}.retarget`, `${target}\n`);
const changed = await execute();
if (changed.ok) throw new Error("Retargeted executable was accepted");
expect(changed.error).toMatchObject({
_tag: "AnalysisArtifactChangedError",
path: selectedPath,
capturedOutput: {
stdout: kind === "firmware" ? "binwalk 3.1.0\n" : "wakaru 1.14.0\n",
},
});
await unlink(`${selectedPath}.retarget`);
await unlink(selectedPath);
await symlink(
selectedPath === command ? commandSource : limiterSource,
selectedPath,
);
}
for (const launch of launches) {
if (launch.pid !== undefined)
expect(await observeProcessStartIdentity(launch.pid)).toBeUndefined();
if (launch.cwd !== undefined)
await expect(readdir(launch.cwd)).rejects.toMatchObject({
code: "ENOENT",
});
}
},
);
it.skipIf(process.platform === "win32")(
"executes the selected PCAP limiter and retains completed output when its alias changes",
async () => {
const directory = await createTestTempDirectory(
"rea-selected-pcap-limiter-",
);
const selected = join(directory, "prlimit"),
physical = join(directory, "driver"),
replacement = join(directory, "replacement");
const script =
'#!/bin/sh\n[ -f "$0.role" ] || exit 19\nif [ -f "$0.retarget" ]; then\n IFS= read -r target < "$0.retarget"\n /bin/ln -sf "$target" "$0"\nfi\nwhile [ "$1" != -- ]; do shift; done\nshift\nexec "$@"\n';
await writeFile(physical, script, { mode: 0o755 });
await writeFile(replacement, script, { mode: 0o755 });
await symlink(physical, selected);
await writeFile(`${selected}.role`, "limiter");
const resources = new ArtifactAnalysisResources(
"pcap",
"inspect_pcap_capture",
readStableArtifact,
async (configured) => configured,
{ message: "busy", details: {} },
);
onTestFinished(() => resources.close());
const platform = Object.getOwnPropertyDescriptor(process, "platform");
if (platform === undefined) throw new Error("Missing host descriptor");
let prepared: Awaited<ReturnType<typeof preparePcapNativeBound>>;
try {
Object.defineProperty(process, "platform", {
...platform,
value: "linux",
});
prepared = await preparePcapNativeBound(
{ REA_TSHARK_PRLIMIT_COMMAND: selected },
resources,
);
} finally {
Object.defineProperty(process, "platform", platform);
}
let refuseCleanup = false;
let retained: ProviderProcessSupervisor | undefined;
onTestFinished(async () => {
refuseCleanup = false;
await retained?.stop();
});
const execute = () =>
runPcapCommand({
operation: "inspect_pcap_capture",
command: "/bin/echo",
arguments: ["selected limiter"],
limits: { timeoutMs: 10_000, maxOutputBytes: 1024 },
environment: process.env,
signal: undefined,
nativeBound: prepared.nativeBound,
launcher: async (options) => {
const child = await spawnOwnedProviderProcess(options);
return {
...child,
cleanup: () =>
refuseCleanup
? Promise.resolve({
cleaned: false,
reason: "controlled pending command cleanup",
})
: cleanupOwnedProcessGroup(child.ownership),
};
},
retain: (owner) => {
retained = owner;
},
});
await expect(execute()).resolves.toMatchObject({
stdout: "selected limiter\n",
});
await writeFile(`${selected}.retarget`, `${replacement}\n`);
await expect(execute()).rejects.toMatchObject({
_tag: "AnalysisArtifactChangedError",
path: selected,
capturedOutput: { stdout: "selected limiter\n" },
});
await unlink(selected);
await symlink(physical, selected);
await writeFile(`${selected}.retarget`, `${selected}\n`);
refuseCleanup = true;
const failed: unknown = await execute().catch((cause: unknown) => cause);
expect(failed).toBeInstanceOf(AnalysisError);
if (!(failed instanceof AnalysisError))
throw new Error("Expected typed cleanup failure");
expect(failed).toMatchObject({
cleanupIncomplete: true,
cleanup: { reason: "controlled pending command cleanup" },
capturedOutput: { stdout: "selected limiter\n" },
});
expect(failed.cleanupPrimary).toMatchObject({
_tag: "AnalysisArtifactChangedError",
path: selected,
});
refuseCleanup = false;
await expect(retained?.stop()).resolves.toMatchObject({
status: "verified-cleanup",
});
expect(failed.resolveCleanup().cleanupIncomplete).toBe(false);
},
);
@@ -12,8 +12,44 @@ import {
artifactInventoryResultSchema,
} from "../../../../src/domain/artifactGraph.js";
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
import { withArtifactClient } from "../../../support/artifactClientScope.js";
describe("artifact extraction", () => {
it.skipIf(process.platform === "win32")(
"refuses a regular-file parent before acquiring output and permits a corrected request",
async () => {
const root = await createTestTempDirectory("rea-extract-invalid-parent-");
const source = join(root, "source");
await mkdir(source);
await writeFile(join(source, "payload.txt"), "selected payload\n");
await writeFile(join(root, "file"), "parent is a file");
const output = join(root, "out");
await withArtifactClient(target(source, "directory"), async (client) => {
const refused = await client.execute("extract_artifact", {
output_root: `${root}/file/../out`,
});
expect(refused).toMatchObject({
ok: false,
error: { _tag: "ArtifactOperationError", reason: "path" },
});
if (!refused.ok) expect(refused.error.cleanupIncomplete).toBe(false);
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
const corrected = await client.execute("extract_artifact", {
output_root: output,
});
if (!corrected.ok) throw corrected.error;
expect(
artifactExtractionResultSchema.parse(corrected.value.result),
).toMatchObject({ output_root: output, containment_verified: true });
expect(await readFile(join(output, "payload.txt"), "utf8")).toBe(
"selected payload\n",
);
});
},
);
it("keeps committed files after the owning client closes", async () => {
const root = await createTestTempDirectory("rea-extract-client-close-");
const source = join(root, "source");
@@ -69,11 +69,17 @@ vi.mock("../../src/firmware/FirmwareCommand.js", async (importOriginal) => {
>();
return {
...original,
resolveFirmwareCommand: async () => ({
command: "/fixture/binwalk",
limiter: "/fixture/prlimit",
sha256: "0".repeat(64),
}),
resolveFirmwareCommand: async () => {
const { selectArtifactCommandPath } =
await import("../../src/process/ArtifactCommandPath.js");
const command = await selectArtifactCommandPath(process.execPath);
return {
command,
limiter: command,
sha256: "0".repeat(64),
fileSizeCeiling: Number.MAX_SAFE_INTEGER,
};
},
runFirmwareCommand: async (
context: Parameters<typeof original.runFirmwareCommand>[0],
) => {