mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(paths): select caller paths through symlinks and .. as the filesystem does (#4067)
* fix: select caller paths through symlinks and .. as the filesystem does CLI adapters, binary target resolution, the reverse-engineering service and the objdump and Rizin command builders made caller-selected paths absolute with path.resolve, which collapses `link/..` lexically. On POSIX the filesystem follows `link` first, so these commands inspected, hashed or launched a different file and returned internally consistent Evidence for it. absoluteCallerPath now owns that step: on POSIX it prefixes a relative path with the working directory and keeps an absolute path as spelled; on Windows, where `..` resolves lexically, it keeps win32.resolve. Fixes #3864 Fixes #3911 Fixes #3912 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep firmware, WASM list and output-root paths on the selected file Review follow-up. The CLI already passed firmware and WASM paths through absoluteCallerPath, but snapshotFirmware and SafeOutputTree.create re-resolved them lexically, and the WASM glue and candidate lists still used resolve, so those inputs could still select a sibling file or create output beside the intended directory. absoluteCallerPath now also drops `.` segments and repeated or trailing separators on POSIX, which never change the named file, so selected paths such as `./module.wasm` keep matching their glue references exactly. Every `..` is still left for the filesystem. SafeOutputTree keeps host path syntax; its platform argument selects only the Windows name policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep directory requirements and relative cwd in caller paths Review follow-up. absoluteCallerPath stripped a trailing `/` or `/.`, so `file/` selected the regular file instead of failing with ENOTDIR; it now keeps one trailing separator. A relative cwd option was appended to the root; it is now made absolute without collapsing `..`. WASM glue matching resolved local literals lexically, so a module selected through `link/..` matched only by basename; the adapter now compares both sides through absoluteCallerPath and still reports each candidate as given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
stat,
|
||||
type FileHandle,
|
||||
} from "node:fs/promises";
|
||||
import { extname, isAbsolute, resolve } from "node:path";
|
||||
import { extname } from "node:path";
|
||||
|
||||
import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import {
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
type ExecutableMetadata,
|
||||
} from "../domain/binaryTarget.js";
|
||||
import type { PeCodeViewRecord } from "../domain/binaryTargetTypes.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
const XML_PLIST_PROBE_CHUNK_BYTES = 64 * 1024;
|
||||
|
||||
@@ -80,7 +81,7 @@ export const parseBinaryTarget = async (
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const hostArchitecture = options.hostArchitecture ?? process.arch;
|
||||
const { targetKind, formatHint, signal, architecture, entryPoint } = options;
|
||||
const candidate = isAbsolute(input) ? input : resolve(cwd, input);
|
||||
const candidate = absoluteCallerPath(input, { cwd });
|
||||
try {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
await access(candidate, constants.R_OK);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat as statPath } from "node:fs/promises";
|
||||
import type { Stats } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
runOwnedCommand,
|
||||
} from "../../process/OwnedCommand.js";
|
||||
import type { ProviderProcessSupervisor } from "../../process/ProviderProcess.js";
|
||||
import { absoluteCallerPath } from "../../filesystem/callerPath.js";
|
||||
|
||||
export type ReverseEngineeringOperation =
|
||||
| "inspect_with_objdump"
|
||||
@@ -291,7 +291,8 @@ export class ReverseEngineeringService {
|
||||
if (signal?.aborted) return err(new AnalysisCancelledError(operation));
|
||||
const cleanup = await this.retryCleanup(operation);
|
||||
if (!cleanup.ok) return cleanup;
|
||||
const absolutePath = path === undefined ? undefined : resolve(path);
|
||||
const absolutePath =
|
||||
path === undefined ? undefined : absoluteCallerPath(path);
|
||||
let digest: string | undefined;
|
||||
try {
|
||||
if (absolutePath !== undefined)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
realpath,
|
||||
type FileHandle,
|
||||
} from "node:fs/promises";
|
||||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { basename, dirname, isAbsolute, join } from "node:path";
|
||||
import type { Readable } from "node:stream";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
import { streamChunkToBuffer } from "./StreamBytes.js";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
destinationCaseCollisionMessage,
|
||||
normalizeArtifactPath,
|
||||
} from "./ArtifactPaths.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import { SafeOutputTreeCreationFailure } from "./SafeOutputTreeCreationFailure.js";
|
||||
import { hashOutputFile } from "./SafeOutputTreeReadback.js";
|
||||
@@ -92,7 +93,8 @@ export class SafeOutputTree {
|
||||
"path",
|
||||
"Extraction output root must be absolute",
|
||||
);
|
||||
const requested = resolve(outputRoot);
|
||||
// Path syntax follows the host; `platform` selects only the name policy.
|
||||
const requested = absoluteCallerPath(outputRoot);
|
||||
const name = basename(requested);
|
||||
if (name === "." || name === "..")
|
||||
throw new ArtifactReaderFailure("path", "Invalid extraction output root");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
createRecordedCrashService,
|
||||
createBinaryLayoutService,
|
||||
@@ -13,6 +12,7 @@ import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Inspect operator-selected files through the same shared workflow as MCP. */
|
||||
export const registerBinaryDiagnosticsCommands = (
|
||||
@@ -39,7 +39,7 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
() =>
|
||||
pe.inspect(
|
||||
{
|
||||
path: resolve(args.path),
|
||||
path: absoluteCallerPath(args.path),
|
||||
max_file_bytes: options.maxFileBytes,
|
||||
max_entries: options.maxEntries,
|
||||
},
|
||||
@@ -63,7 +63,7 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectBinaryLayout, async () => {
|
||||
const result = await service.inspectAndClose(
|
||||
{ path: resolve(args.path) },
|
||||
{ path: absoluteCallerPath(args.path) },
|
||||
{ signal },
|
||||
);
|
||||
return result.ok ? result.value : projectAnalysisError(result.error);
|
||||
@@ -90,7 +90,7 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectRecordedCrash, async () => {
|
||||
const result = await recorded.inspectAndClose(
|
||||
{
|
||||
path: resolve(args.path),
|
||||
path: absoluteCallerPath(args.path),
|
||||
include_debugger_context: options.debuggerContext,
|
||||
},
|
||||
{ signal },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { inspectPacketFieldsInputSchema } from "../domain/captures/packetFields.js";
|
||||
import { z } from "incur";
|
||||
import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js";
|
||||
import { resolve } from "node:path";
|
||||
import { createPacketCaptureService } from "../composition/captures.js";
|
||||
import {
|
||||
packetCaptureOptionsSchema,
|
||||
@@ -14,6 +13,7 @@ import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
export const registerCaptureCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
@@ -105,7 +105,7 @@ export const registerCaptureCommands = (
|
||||
{
|
||||
...options,
|
||||
...(all_matches ? { match_limit: null } : {}),
|
||||
path: resolve(args.path),
|
||||
path: absoluteCallerPath(args.path),
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "incur";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { resolveCliAnalysisSelector } from "./analysisSelector.js";
|
||||
import type { DirectAnalysis } from "../composition/directAnalysis.js";
|
||||
@@ -16,6 +15,7 @@ import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnal
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { javascriptApplicationOptions } from "../cliObservationOptions.js";
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Register provider-neutral binary overview and procedure CLI commands. */
|
||||
export const registerCoreBinaryCommands = (
|
||||
@@ -84,7 +84,7 @@ const registerOverviewCommands = (
|
||||
logCliCommand(logger, "analyze", () =>
|
||||
runCliJavaScriptApplicationAnalysis(
|
||||
{
|
||||
input_path: resolve(args.path),
|
||||
input_path: absoluteCallerPath(args.path),
|
||||
integrity_policy:
|
||||
options.integrityPolicy ?? defaultIntegrityPolicy,
|
||||
...(options.sourcePath === undefined
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { evmInterfaceModule } from "../composition/evm.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Resolve operator paths and preserve explicit encoding through the shared workflow. */
|
||||
export const registerEvmCommands = (
|
||||
@@ -30,7 +30,10 @@ export const registerEvmCommands = (
|
||||
evmInterfaceModule.cliCommand,
|
||||
async () => {
|
||||
const result = await service.inspect(
|
||||
{ path: resolve(args.path), encoding: args.encoding },
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
encoding: args.encoding,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
return result.ok
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
|
||||
import { createFirmwareAnalysisProvider } from "../composition/firmware.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
@@ -11,6 +10,7 @@ import type { FirmwareOperation } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** One-shot firmware CLI commands backed by the same workflows as MCP. */
|
||||
export const registerFirmwareCommands = (
|
||||
@@ -50,7 +50,7 @@ export const registerFirmwareCommands = (
|
||||
CLI_COMMANDS.inspectFirmwareRegions,
|
||||
"inspect_firmware_regions",
|
||||
// CLI paths are operator-relative; resolve before shared validation.
|
||||
{ path: resolve(args.path) },
|
||||
{ path: absoluteCallerPath(args.path) },
|
||||
),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.extractFirmware, {
|
||||
@@ -75,8 +75,8 @@ export const registerFirmwareCommands = (
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
execute(CLI_COMMANDS.extractFirmware, "extract_firmware", {
|
||||
path: resolve(args.path),
|
||||
output_directory: resolve(args.outputDirectory),
|
||||
path: absoluteCallerPath(args.path),
|
||||
output_directory: absoluteCallerPath(args.outputDirectory),
|
||||
...(options.offset === undefined && options.length === undefined
|
||||
? {}
|
||||
: { range: { offset: options.offset, length: options.length } }),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { createGoBinaryService } from "../composition/go.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
@@ -7,6 +6,7 @@ import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Resolve caller paths and inspect static build metadata through the shared workflow. */
|
||||
export const registerGoCommands = (cli: CliInstance, logger: Logger): void => {
|
||||
@@ -24,7 +24,11 @@ export const registerGoCommands = (cli: CliInstance, logger: Logger): void => {
|
||||
CLI_COMMANDS.inspectGoBinary,
|
||||
async () =>
|
||||
finishAnalysisCommand(
|
||||
() => service.inspect({ path: resolve(args.path) }, { signal }),
|
||||
() =>
|
||||
service.inspect(
|
||||
{ path: absoluteCallerPath(args.path) },
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
"go-build-info",
|
||||
"inspect_go_binary",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { createRustBinaryService } from "../composition/rust.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
@@ -7,6 +6,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Resolve caller paths and inspect embedded Rust dependencies through the shared workflow. */
|
||||
export const registerRustCommands = (
|
||||
@@ -24,7 +24,7 @@ export const registerRustCommands = (
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, async () => {
|
||||
const result = await service.inspect(
|
||||
{ path: resolve(args.path) },
|
||||
{ path: absoluteCallerPath(args.path) },
|
||||
{ signal },
|
||||
);
|
||||
return result.ok ? result.value : projectAnalysisError(result.error);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { inspectSignalDecodersInputSchema } from "../domain/signals/signalDecoders.js";
|
||||
import { z } from "incur";
|
||||
import type { SignalRecordingService } from "../application/signals/SignalRecordingService.js";
|
||||
import { resolve } from "node:path";
|
||||
import { createSignalRecordingService } from "../composition/signals.js";
|
||||
import {
|
||||
signalRecordingOptionsSchema,
|
||||
@@ -14,6 +13,7 @@ import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
export const registerSignalCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
@@ -107,7 +107,7 @@ export const registerSignalCommands = (
|
||||
{
|
||||
...options,
|
||||
...(all_lines ? { line_limit: null } : {}),
|
||||
path: resolve(args.path),
|
||||
path: absoluteCallerPath(args.path),
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { createSqliteDatabaseService } from "../composition/sqlite.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
@@ -9,6 +8,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Resolve an operator-selected SQLite snapshot through the shared inspection workflow. */
|
||||
export const registerSqliteCommands = (
|
||||
@@ -55,7 +55,7 @@ export const registerSqliteCommands = (
|
||||
const inspection = await service
|
||||
.inspect(
|
||||
{
|
||||
path: resolve(args.path),
|
||||
path: absoluteCallerPath(args.path),
|
||||
...(options.table === undefined
|
||||
? {}
|
||||
: { table: options.table }),
|
||||
|
||||
@@ -2,13 +2,13 @@ import { compareWasmInterfaces } from "../application/wasm/WasmInterfaceComparis
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { wasmArtifactModule } from "../composition/wasm.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Resolve all caller-selected artifact paths through the shared workflow. */
|
||||
export const registerWasmCommands = (
|
||||
@@ -64,10 +64,12 @@ export const registerWasmCommands = (
|
||||
async () => {
|
||||
const result = await service.inspect(
|
||||
{
|
||||
path: resolve(args.path),
|
||||
glue_paths: options.glue_paths?.map((path) => resolve(path)),
|
||||
path: absoluteCallerPath(args.path),
|
||||
glue_paths: options.glue_paths?.map((path) =>
|
||||
absoluteCallerPath(path),
|
||||
),
|
||||
candidate_paths: options.candidate_paths?.map((path) =>
|
||||
resolve(path),
|
||||
absoluteCallerPath(path),
|
||||
),
|
||||
},
|
||||
{ signal },
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { posix, win32 } from "node:path";
|
||||
|
||||
/**
|
||||
* Make a caller-selected local path absolute without changing which file it
|
||||
* names. POSIX resolves `..` after following the preceding symlink, so lexical
|
||||
* normalization of `link/../file` can select a different file. On POSIX, only
|
||||
* interior `.` segments and repeated separators are removed, which never
|
||||
* change the named file; every `..` is kept for the filesystem to resolve, and
|
||||
* a trailing `/` or `/.` keeps its directory requirement as one trailing `/`.
|
||||
* A relative `cwd` is made absolute the same way. Win32 resolves `..`
|
||||
* lexically, so normalizing there is exact.
|
||||
*/
|
||||
export const absoluteCallerPath = (
|
||||
path: string,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
platform = process.platform,
|
||||
}: { readonly cwd?: string; readonly platform?: NodeJS.Platform } = {},
|
||||
): string => {
|
||||
if (platform === "win32") return win32.resolve(cwd, path);
|
||||
if (!posix.isAbsolute(path)) {
|
||||
const base = posix.isAbsolute(cwd)
|
||||
? cwd
|
||||
: absoluteCallerPath(cwd, { platform });
|
||||
return absoluteCallerPath(`${base}/${path}`, { platform });
|
||||
}
|
||||
const segments = path
|
||||
.split("/")
|
||||
.filter((segment) => segment !== "" && segment !== ".");
|
||||
const directory = /\/\.?$/u.test(path) && segments.length > 0;
|
||||
return `/${segments.join("/")}${directory ? "/" : ""}`;
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
readlink,
|
||||
realpath,
|
||||
} from "node:fs/promises";
|
||||
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { isAbsolute, join, relative, sep } from "node:path";
|
||||
import { Transform } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import {
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { FirmwareRequest } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
|
||||
import { FIRMWARE_LIMITS } from "./FirmwareRelease.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
@@ -63,7 +64,7 @@ export const snapshotFirmware = async (
|
||||
root: string,
|
||||
signal: AbortSignal,
|
||||
) => {
|
||||
const sourcePath = resolve(request.input.path);
|
||||
const sourcePath = absoluteCallerPath(request.input.path);
|
||||
const source = await realpath(sourcePath);
|
||||
const handle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
const fullPath = join(root, "firmware.bin");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { ObjdumpInput } from "../domain/reverseEngineering.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
export const OBJDUMP_PROVIDER_IDENTITY = {
|
||||
id: "gnu.objdump",
|
||||
@@ -20,5 +19,8 @@ export const objdumpCommand = (input: ObjdumpInput): readonly string[] => {
|
||||
disassemble: ["-d"],
|
||||
disassemble_all: ["-D"],
|
||||
} satisfies Record<typeof input.operation, readonly string[]>;
|
||||
return [...argumentsByOperation[input.operation], resolve(input.path)];
|
||||
return [
|
||||
...argumentsByOperation[input.operation],
|
||||
absoluteCallerPath(input.path),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { RizinInput } from "../domain/reverseEngineering.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
export const RIZIN_PROVIDER_IDENTITY = {
|
||||
id: "rizin",
|
||||
@@ -14,5 +13,5 @@ export const rizinCommand = (input: RizinInput): readonly string[] => [
|
||||
"-q",
|
||||
"-c",
|
||||
input.command,
|
||||
resolve(input.path),
|
||||
absoluteCallerPath(input.path),
|
||||
];
|
||||
|
||||
@@ -3,7 +3,6 @@ import { isUtf8 } from "node:buffer";
|
||||
import { open, readFile, readlink, realpath, stat } from "node:fs/promises";
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import { isatty } from "node:tty";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
type OwnedInferiorIdentity,
|
||||
type OwnedDebuggerDetachResult,
|
||||
} from "../process/ownedDebuggerSession.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
const MAX_SESSIONS = 8;
|
||||
const MAX_PENDING_COMMANDS = 16;
|
||||
@@ -378,7 +378,7 @@ export class RizinDebugSessionManager {
|
||||
if (signal?.aborted)
|
||||
return err(new AnalysisCancelledError("start_rizin_debug_session"));
|
||||
const id = randomUUID();
|
||||
const resolvedPath = resolve(input.path);
|
||||
const resolvedPath = absoluteCallerPath(input.path);
|
||||
const launchProvenance: RizinLaunchProvenance = {
|
||||
caller_selected_path: input.path,
|
||||
resolved_path: resolvedPath,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { expect, it } from "vitest";
|
||||
import { parseWabtOutput } from "./WabtOutput.js";
|
||||
import { parseWabtWat } from "./WabtWat.js";
|
||||
import { associateWasmGlue } from "./WasmGlueAdapter.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
const golden = () =>
|
||||
readFile(
|
||||
new URL("../../tests/fixtures/wasm/objdump.txt", import.meta.url),
|
||||
@@ -282,3 +283,22 @@ it("keeps exact local, ambiguous URL, computed and unparseable glue distinct", (
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("matches a CLI-selected ./ module exactly against its glue reference", () => {
|
||||
// The CLI makes `./module.wasm` absolute from /app with absoluteCallerPath.
|
||||
const selected = absoluteCallerPath("./module.wasm", {
|
||||
cwd: "/app",
|
||||
platform: "linux",
|
||||
});
|
||||
expect(
|
||||
associateWasmGlue('fetch("./module.wasm");', "/app/glue.js", [
|
||||
selected,
|
||||
"/other/module.wasm",
|
||||
]).references,
|
||||
).toMatchObject([
|
||||
{
|
||||
association: "local-path-candidate",
|
||||
candidate_paths: ["/app/module.wasm"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { basename, dirname } from "node:path";
|
||||
import * as t from "@babel/types";
|
||||
import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js";
|
||||
import { traverseJavaScriptAst } from "../domain/javascript/javascriptSemanticTraversal.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
/** Associate visible literals with selected candidates without asserting data flow or URL identity. */
|
||||
export const associateWasmGlue = (
|
||||
source: string,
|
||||
@@ -41,11 +42,17 @@ export const associateWasmGlue = (
|
||||
!/^[a-z][a-z\d+.-]*:|^\/\//iu.test(value) &&
|
||||
!value.startsWith("/") &&
|
||||
!/[?#]/u.test(value);
|
||||
const exact = local
|
||||
? candidates.filter(
|
||||
(candidate) => candidate === resolve(dirname(path), value),
|
||||
)
|
||||
: [];
|
||||
// Compare through the caller-path owner so `.` spellings match and a
|
||||
// symlink followed by `..` is never collapsed onto a sibling.
|
||||
const target = local
|
||||
? absoluteCallerPath(value, { cwd: dirname(path) })
|
||||
: undefined;
|
||||
const exact =
|
||||
target === undefined
|
||||
? []
|
||||
: candidates.filter(
|
||||
(candidate) => absoluteCallerPath(candidate) === target,
|
||||
);
|
||||
const matches =
|
||||
exact.length > 0
|
||||
? exact
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { peResourceFixture } from "../../../src/native/pe/PeResources.fixture.js";
|
||||
import { createGoBinaryFixture } from "../../fixtures/go/image.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
// POSIX follows `link` before `..`, so `link/../sample` names target/sample.
|
||||
describe.skipIf(process.platform === "win32")(
|
||||
"caller paths through a directory symlink and ..",
|
||||
() => {
|
||||
cliTest(
|
||||
"inspects the file the filesystem selects, not its lexical sibling",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-symlink-parent-");
|
||||
await mkdir(join(root, "target/child"), { recursive: true });
|
||||
await symlink("target/child", join(root, "link"));
|
||||
const selected = createGoBinaryFixture({ goVersion: "go1.26.0" }).bytes;
|
||||
const sibling = createGoBinaryFixture({ goVersion: "go1.25.0" }).bytes;
|
||||
await writeFile(join(root, "target/sample"), selected);
|
||||
await writeFile(join(root, "sample"), sibling);
|
||||
const selectedDigest = createHash("sha256")
|
||||
.update(selected)
|
||||
.digest("hex");
|
||||
for (const [command, operation] of [
|
||||
["inspect-go-binary", "inspect_go_binary"],
|
||||
["inspect-rust-binary", "inspect_rust_binary"],
|
||||
] as const)
|
||||
// Concatenate: path.join would collapse `link/..` itself.
|
||||
for (const path of ["link/../sample", `${root}/link/../sample`]) {
|
||||
const result = await cli.run({
|
||||
arguments: [command, path, "--json"],
|
||||
cwd: root,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract(operation).outputSchema.parse(
|
||||
result.json,
|
||||
);
|
||||
expect(evidence.subject?.digest.sha256, `${command} ${path}`).toBe(
|
||||
selectedDigest,
|
||||
);
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"reads PE resources from the file the filesystem selects",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-symlink-parent-pe-");
|
||||
await mkdir(join(root, "target/child"), { recursive: true });
|
||||
await symlink("target/child", join(root, "link"));
|
||||
const image = (payload: string) =>
|
||||
peResourceFixture(true, [
|
||||
{
|
||||
type: 10,
|
||||
name: 1,
|
||||
language: 1033,
|
||||
payload: Buffer.from(payload),
|
||||
},
|
||||
]).bytes;
|
||||
await writeFile(join(root, "target/sample.exe"), image("SELECTED"));
|
||||
await writeFile(join(root, "sample.exe"), image("OTHER"));
|
||||
const selectedDigest = createHash("sha256")
|
||||
.update(image("SELECTED"))
|
||||
.digest("hex");
|
||||
for (const path of [
|
||||
"link/../sample.exe",
|
||||
`${root}/link/../sample.exe`,
|
||||
]) {
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-pe-resources", path, "--json"],
|
||||
cwd: root,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract(
|
||||
"inspect_pe_resources",
|
||||
).outputSchema.parse(result.json);
|
||||
expect(evidence.subject?.digest.sha256, path).toBe(selectedDigest);
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { absoluteCallerPath } from "../../../src/filesystem/callerPath.js";
|
||||
|
||||
it("keeps POSIX parent components for the filesystem to resolve", () => {
|
||||
const posix = { cwd: "/work", platform: "linux" } as const;
|
||||
expect(absoluteCallerPath("link/../sample", posix)).toBe(
|
||||
"/work/link/../sample",
|
||||
);
|
||||
expect(absoluteCallerPath("/abs/link/../sample", posix)).toBe(
|
||||
"/abs/link/../sample",
|
||||
);
|
||||
expect(absoluteCallerPath("../sample", posix)).toBe("/work/../sample");
|
||||
});
|
||||
|
||||
it("removes only POSIX spellings that cannot change the named file", () => {
|
||||
const posix = { cwd: "/work/", platform: "darwin" } as const;
|
||||
expect(absoluteCallerPath("./module.wasm", posix)).toBe("/work/module.wasm");
|
||||
expect(absoluteCallerPath("a//./b", posix)).toBe("/work/a/b");
|
||||
expect(absoluteCallerPath("/x/./y", posix)).toBe("/x/y");
|
||||
expect(absoluteCallerPath("sample", { ...posix, cwd: "/" })).toBe("/sample");
|
||||
expect(absoluteCallerPath("/", posix)).toBe("/");
|
||||
// A trailing separator or `.` still requires a directory.
|
||||
expect(absoluteCallerPath("sample/", posix)).toBe("/work/sample/");
|
||||
expect(absoluteCallerPath("sample/.", posix)).toBe("/work/sample/");
|
||||
expect(absoluteCallerPath("a//b//", posix)).toBe("/work/a/b/");
|
||||
});
|
||||
|
||||
it("makes a relative POSIX cwd absolute before appending the path", () => {
|
||||
expect(
|
||||
absoluteCallerPath("sample", { cwd: "fixtures/../x", platform: "linux" }),
|
||||
).toBe(`${process.cwd()}/fixtures/../x/sample`);
|
||||
});
|
||||
|
||||
it("uses lexical resolution on Windows, where it is exact", () => {
|
||||
const win32 = { cwd: "C:\\work", platform: "win32" } as const;
|
||||
expect(absoluteCallerPath("link\\..\\sample", win32)).toBe(
|
||||
"C:\\work\\sample",
|
||||
);
|
||||
expect(absoluteCallerPath("D:\\x\\..\\y", win32)).toBe("D:\\y");
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
copyFile,
|
||||
mkdir,
|
||||
realpath,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { SafeOutputTree } from "../../../src/artifacts/SafeOutputTree.js";
|
||||
import { absoluteCallerPath } from "../../../src/filesystem/callerPath.js";
|
||||
import { snapshotFirmware } from "../../../src/firmware/FirmwareFiles.js";
|
||||
import { associateWasmGlue } from "../../../src/wasm/WasmGlueAdapter.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
// POSIX follows `link` before `..`, so `link/../x` names target/x, not ./x.
|
||||
const layout = async (prefix: string) => {
|
||||
const root = await realpath(await createTestTempDirectory(prefix));
|
||||
await mkdir(join(root, "target/child"), { recursive: true });
|
||||
await symlink("target/child", join(root, "link"));
|
||||
return root;
|
||||
};
|
||||
|
||||
describe.skipIf(process.platform === "win32")(
|
||||
"file owners given a directory symlink followed by ..",
|
||||
() => {
|
||||
it("snapshots the firmware image the filesystem selects", async () => {
|
||||
const root = await layout("rea-symlink-parent-firmware-");
|
||||
const selected = Buffer.from("selected firmware");
|
||||
await writeFile(join(root, "target/image.bin"), selected);
|
||||
await writeFile(join(root, "image.bin"), "sibling firmware");
|
||||
const work = await createTestTempDirectory("rea-firmware-snapshot-");
|
||||
// Concatenate: path.join would collapse `link/..` itself.
|
||||
const path = `${root}/link/../image.bin`;
|
||||
const snapshot = await snapshotFirmware(
|
||||
{ operation: "inspect_firmware_regions", input: { path } },
|
||||
work,
|
||||
new AbortController().signal,
|
||||
);
|
||||
expect(snapshot.subject).toMatchObject({
|
||||
path,
|
||||
sha256: createHash("sha256").update(selected).digest("hex"),
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a trailing separator after a regular file", async () => {
|
||||
const root = await layout("rea-trailing-separator-");
|
||||
// A real executable, so only the trailing separator can reject it.
|
||||
await copyFile(process.execPath, join(root, "sample"));
|
||||
expect((await parseBinaryTarget(`${root}/sample`)).ok).toBe(true);
|
||||
for (const path of [`${root}/sample/`, `${root}/sample/.`]) {
|
||||
const target = await parseBinaryTarget(path);
|
||||
expect(target.ok, path).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("matches WASM glue to a module selected through link/..", () => {
|
||||
const root = "/app";
|
||||
const selected = absoluteCallerPath("link/../module.wasm", {
|
||||
cwd: root,
|
||||
});
|
||||
expect(
|
||||
associateWasmGlue(
|
||||
'fetch("./module.wasm");',
|
||||
absoluteCallerPath("link/../glue.js", { cwd: root }),
|
||||
[selected, "/app/module.wasm"],
|
||||
).references,
|
||||
).toMatchObject([
|
||||
{ association: "local-path-candidate", candidate_paths: [selected] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates the output directory the filesystem selects", async () => {
|
||||
const root = await layout("rea-symlink-parent-output-");
|
||||
const tree = await SafeOutputTree.create(`${root}/link/../out`);
|
||||
try {
|
||||
expect(tree.outputRoot).toBe(join(root, "target/out"));
|
||||
expect((await stat(join(root, "target/out"))).isDirectory()).toBe(true);
|
||||
await expect(stat(join(root, "out"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
} finally {
|
||||
await tree.rollback();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
@@ -10,6 +10,7 @@ import { z } from "zod";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
|
||||
import { evidenceSchema } from "../../../src/domain/evidence.js";
|
||||
import { absoluteCallerPath } from "../../../src/filesystem/callerPath.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
import {
|
||||
createRizinTestManager,
|
||||
@@ -92,7 +93,7 @@ process.stdin.on("data", (chunk) => {
|
||||
const evidence = evidenceSchema.parse(command.structuredContent);
|
||||
expect(evidence.parameters.launch_provenance).toEqual({
|
||||
caller_selected_path: callerPath,
|
||||
resolved_path: resolve(callerPath),
|
||||
resolved_path: absoluteCallerPath(callerPath),
|
||||
});
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
output_invalid_utf8: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
type ProviderProcessHandle,
|
||||
} from "../../../src/process/ProviderProcess.js";
|
||||
import { evidenceSchema } from "../../../src/domain/evidence.js";
|
||||
import { absoluteCallerPath } from "../../../src/filesystem/callerPath.js";
|
||||
import {
|
||||
createRizinTestManager,
|
||||
rizinTestInferiorTty,
|
||||
@@ -422,7 +422,7 @@ process.stdin.on("data", (chunk) => {
|
||||
const manager = createRizinTestManager({ REA_RIZIN_COMMAND: provider });
|
||||
managers.push(manager);
|
||||
const callerSelectedPath = "./relative/../fixture.bin";
|
||||
const resolvedPath = resolve(callerSelectedPath);
|
||||
const resolvedPath = absoluteCallerPath(callerSelectedPath);
|
||||
const started = await manager.start({
|
||||
path: callerSelectedPath,
|
||||
inferior_tty: inferiorTty,
|
||||
@@ -670,7 +670,7 @@ process.stdin.on("data", (chunk) => {
|
||||
);
|
||||
managers.push(manager);
|
||||
const callerSelectedPath = "./cancelled/../fixture.bin";
|
||||
const resolvedPath = resolve(callerSelectedPath);
|
||||
const resolvedPath = absoluteCallerPath(callerSelectedPath);
|
||||
const started = await manager.start({
|
||||
path: callerSelectedPath,
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
@@ -849,7 +849,7 @@ process.stdin.on("data", (chunk) => {
|
||||
);
|
||||
managers.push(manager);
|
||||
const callerSelectedPath = "./exit/../fixture.bin";
|
||||
const resolvedPath = resolve(callerSelectedPath);
|
||||
const resolvedPath = absoluteCallerPath(callerSelectedPath);
|
||||
const started = await manager.start({
|
||||
path: callerSelectedPath,
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
|
||||
Reference in New Issue
Block a user