* 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>
91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
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,
|
|
);
|
|
},
|
|
);
|