test(native): preserve admitted identity and partial boundary facts

This commit is contained in:
morluto
2026-10-24 07:28:13 +08:00
parent 48821e82a1
commit 64f261bc6f
6 changed files with 122 additions and 65 deletions
@@ -422,30 +422,18 @@ describe.skipIf(process.platform === "win32")(
"PSP inspected ABI declaration boundary",
() => {
it("refuses a PSP image whose section headers lie beyond the captured file", async () => {
const { directory, provider } = await installation();
const { directory } = await installation();
const path = join(directory, "psp-truncated-sections.elf");
await writeFile(path, pspElfBytes({ sectionHeadersBeyondEof: true }));
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
if (
parsed.value.kind !== "executable" ||
parsed.value.architecture !== "mips"
)
throw new Error("PSP ELF was not recognized as MIPS");
expect(parsed.value.mips.abiFlags).toEqual(pspAbiFlags);
expect(parsed.value.mips.limitations).toContainEqual(
expect.stringContaining("ABI declarations in unavailable headers"),
);
expect(provider.inspectTargetSupport(parsed.value).status).toBe(
"unsupported",
);
const result = await provider.resolveAnalysisProfile(parsed.value);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Incomplete ABI inspection was admitted");
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
expect(result.error.message).toContain(
"MIPS ABI declaration inspection is incomplete",
);
expect(parsed).toMatchObject({
ok: false,
error: {
_tag: "BinaryTargetError",
constraint: "file_format_rejected",
reason: expect.stringContaining("truncated ELF section header table"),
},
});
});
it("retains independently inspected PSP ABI bytes through the real target reader", async () => {
@@ -559,10 +547,16 @@ describe.skipIf(process.platform === "win32")(
expect(result.ok).toBe(false);
if (result.ok)
throw new Error("Conflicting language override was accepted");
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
expect(result.error.message).toContain(
"does not accept REA_GHIDRA_LANGUAGE_ID overrides",
);
expect(result.error).toMatchObject({
_tag: "ConfigurationError",
settings: [
{
setting: "REA_GHIDRA_LANGUAGE_ID",
constraint:
"The bounded PSP Allegrex profile does not accept processor or compiler specification overrides.",
},
],
});
});
it("keeps PSP and generic language-override launch paths mutually exclusive", () => {
@@ -67,7 +67,7 @@ describe("Hopper analysis profiles", () => {
expect("loaderArgs" in input).toBe(false);
});
it("reports an unresolved profile when the launcher version is unavailable", async () => {
it("refuses a profile when the selected launcher cannot be fingerprinted", async () => {
const resolved = await resolveHopperAnalysisProfile(
target("elf", "x86_64"),
{
@@ -77,10 +77,15 @@ describe("Hopper analysis profiles", () => {
resourceScope: inspectionResources.owner,
},
);
expect(resolved).toEqual({
ok: true,
value: {
profile: null,
expect(resolved).toMatchObject({
ok: false,
error: {
_tag: "ProviderAdapterError",
operation: "resolve_analysis_profile",
diagnostics: {
launcher_path: "/missing/hopper",
system_code: "ENOENT",
},
},
});
});
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { OFFICIAL_TOOL_CONTRACTS } from "../../../../src/contracts/officialToolContracts.js";
import { nativeSymbolMetadataSchema } from "../../../../src/domain/native/nativeSymbol.js";
import { functionDossierSchema } from "../../../../src/domain/hopperValues.js";
const execute = promisify(execFile);
@@ -30,6 +31,7 @@ const probeResultSchema = z.strictObject({
procedure: z.strictObject({
address: z.literal("0x401000"),
name: z.literal("fixture-procedure"),
symbol: nativeSymbolMetadataSchema.nullable(),
classification: z.null(),
body: z.strictObject({
available: z.literal(false),
@@ -44,6 +46,7 @@ const probeResultSchema = z.strictObject({
length: z.number(),
signature: z.literal("int fixture-procedure()"),
locals: z.array(z.unknown()),
symbol: nativeSymbolMetadataSchema.nullable(),
classification: z.null(),
body: z.strictObject({
available: z.literal(false),
@@ -55,6 +58,7 @@ const probeResultSchema = z.strictObject({
procedure: z.strictObject({
address: z.literal("0x401000"),
name: z.literal("fixture-procedure"),
symbol: nativeSymbolMetadataSchema.nullable(),
classification: z.null(),
body: z.strictObject({
available: z.literal(false),
@@ -5,13 +5,13 @@ import { describe, expect, it } from "vitest";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
import { thinMach } from "../../../../src/domain/binaryTarget.fixture.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
describe("artifact directory diagnostics", () => {
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"preserves the inaccessible member path and errno through provider projection",
"retains an inaccessible member and its errno alongside readable inventory facts",
async () => {
const root = await createTestTempDirectory("rea-directory-eacces-");
const appPath = join(root, "Denied.app");
@@ -32,20 +32,31 @@ describe("artifact directory diagnostics", () => {
const result = await new ArtifactProvider(process.env)
.createClient(target.value)
.execute("inventory_artifact", {});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(projectAnalysisError(result.error)).toMatchObject({
code: "artifact_operation_failed",
details: {
operation: "inventory_artifact",
reason: "io",
},
if (!result.ok) throw result.error;
const inventory = artifactInventoryResultSchema.parse(
result.value.result,
);
expect(inventory.coverage.status).toBe("unknown");
const blocked = inventory.occurrences.find(
({ logical_path }) => logical_path === "Contents/blocked.txt",
);
expect(blocked).toMatchObject({
artifact_id: null,
hash_status: "unavailable",
});
const projected = projectAnalysisError(result.error);
const detail = projected.details?.detail;
expect(typeof detail).toBe("string");
const detail = blocked?.limitations.join("\n");
expect(detail).toContain("Denied.app/Contents/blocked.txt");
expect(detail).toContain("EACCES");
expect(inventory.limitations).toContainEqual(
expect.stringContaining("EACCES"),
);
expect(inventory.occurrences).toContainEqual(
expect.objectContaining({
logical_path: "Contents/MacOS/Fixture",
hash_status: "verified",
artifact_id: expect.any(String),
}),
);
} finally {
await chmod(blockedPath, 0o600);
}
@@ -610,6 +610,7 @@ const completedOutputCloseFixture = async () => {
join(tmpdir(), "rea-lldb-complete-output-"),
);
const fakeLldb = join(fixtureRoot, "fake-lldb");
const completedPidPath = join(fixtureRoot, "completed-target.pid");
const completed = {
...successfulTracerOutput,
target_output: {
@@ -628,6 +629,7 @@ const config = JSON.parse(fs.readFileSync(command.slice("rea_trace ".length), "u
const child = spawnSync(process.execPath, ["-e", ""]);
const result = ${JSON.stringify(completed)};
result.pid = child.pid;
fs.writeFileSync(${JSON.stringify(completedPidPath)}, String(child.pid));
fs.writeFileSync(config.pid_path, String(child.pid));
fs.writeFileSync(config.stdout_capture_path, "ok");
fs.writeFileSync(config.stderr_capture_path, "");
@@ -637,7 +639,18 @@ process.exit(0);
`,
{ mode: 0o700 },
);
return { fixtureRoot, fakeLldb, completed };
return {
fixtureRoot,
fakeLldb,
readCompleted: async () => ({
...completed,
pid: z.coerce
.number()
.int()
.positive()
.parse(await readFile(completedPidPath, "utf8")),
}),
};
};
describe("LLDB process exit at output-close loop completion", () => {
@@ -645,8 +658,7 @@ describe("LLDB process exit at output-close loop completion", () => {
it.skipIf(process.platform === "win32")(
`retains completed trace facts after output closure (cancelled=${String(abort)})`,
async () => {
const { fixtureRoot, fakeLldb, completed } =
await completedOutputCloseFixture();
const fixture = await completedOutputCloseFixture();
const controller = new AbortController();
const reason = { requested: "stop after completed output closure" };
let interrupted = false;
@@ -672,10 +684,11 @@ describe("LLDB process exit at output-close loop completion", () => {
return closed;
});
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fakeLldb, sha256: "e".repeat(64) }),
ok({ path: fixture.fakeLldb, sha256: "e".repeat(64) }),
);
try {
const result = await tracer.trace(request, controller.signal);
const completed = await fixture.readCompleted();
expect(interrupted).toBe(abort);
if (result.ok) {
expect(abort).toBe(false);
@@ -698,6 +711,7 @@ describe("LLDB process exit at output-close loop completion", () => {
coverage: { status: "partial", reason: "cancelled" },
debugger: { version: completed.version },
process: {
pid: completed.pid,
outcome: "exited",
exit_status: 0,
terminated: true,
@@ -712,7 +726,7 @@ describe("LLDB process exit at output-close loop completion", () => {
} finally {
waitSpy.mockRestore();
expect((await tracer.close()).ok).toBe(true);
await rm(fixtureRoot, { recursive: true, force: true });
await rm(fixture.fixtureRoot, { recursive: true, force: true });
}
},
);
@@ -16,6 +16,7 @@ import { dirname, join } from "node:path";
import { describe, expect, it, onTestFinished } from "vitest";
import { signatureInspectionFixture } from "../../../fixtures/nativeCommands.js";
import { resolvedAnalysisError } from "../../../../src/domain/analysisErrorBase.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import {
@@ -77,19 +78,20 @@ const registerSignatureObservationCases = () => {
.createClient(target)
.execute("inspect_signature", {});
if (!result.ok) throw result.error;
expect(captures).toHaveLength(3);
expect(
captures.every(
(args) => args[args.indexOf("-a") + 1] === target.architecture,
),
).toBe(true);
expect(captures.map((args) => args[args.indexOf("-a") + 1])).toEqual([
target.architecture,
target.architecture,
target.architecture,
...target.availableArchitectures,
]);
expect(result.value.result).toMatchObject({
signed: true,
identifier: "com.example.fixture",
designated_requirement: "identifier com.example.fixture",
limitations: [
limitations: expect.arrayContaining([
"Inspected a digest-verified Mach-O byte snapshot; filesystem-bound signing context and trust validation are not examined.",
],
expect.stringContaining("Partial signature coverage:"),
]),
});
});
@@ -271,9 +273,11 @@ it.each([
},
};
let failed = false;
let displayCaptures = 0;
const runner: NativeCommandRunner = {
async run(tool, args) {
const slice = args.includes("-a");
const display = args.includes("--verbose=4");
const slice = display && displayCaptures++ > 0;
const selected =
stage === "slice"
? slice
@@ -281,9 +285,7 @@ it.each([
? args.includes("-r-")
: stage === "entitlements"
? args.includes("--entitlements")
: !args.includes("-r-") &&
!args.includes("--entitlements") &&
!slice;
: display && !slice;
if (selected) {
failed = true;
await rm(path);
@@ -443,7 +445,11 @@ it.skipIf(process.platform !== "darwin")(
);
const path = join(directory, "source\ncode object is not signed\noperand");
await copyFile("/usr/bin/true", path);
const target = await nativeMachoTargetForFile(path);
const admitted = await parseBinaryTarget(path);
if (!admitted.ok) throw admitted.error;
const target = admitted.value;
if (target.kind !== "executable")
throw new Error("Expected the copied system executable");
const originalBytes = await readFile(path);
const real = new XcrunCommandRunner({});
let snapshotPath: string | undefined;
@@ -487,7 +493,11 @@ it.skipIf(process.platform !== "darwin")(
const path = join(directory, "source\ncode object is not signed\noperand");
await copyFile("/usr/bin/true", path);
await runFile("/usr/bin/codesign", ["--remove-signature", path]);
const target = await nativeMachoTargetForFile(path);
const admitted = await parseBinaryTarget(path);
if (!admitted.ok) throw admitted.error;
const target = admitted.value;
if (target.kind !== "executable")
throw new Error("Expected the copied system executable");
const result = await new NativeMacOSProvider(
{},
new XcrunCommandRunner({}),
@@ -498,7 +508,22 @@ it.skipIf(process.platform !== "darwin")(
if (!result.ok) throw result.error;
const parsed = inspectSignatureSchema.parse(result.value.result);
expect(parsed).toMatchObject({ signed: false });
expect(parsed.provenance).toHaveLength(3);
const selectedArchitecture =
target.architecture === "x86" ? "i386" : target.architecture;
expect(
parsed.provenance.map(
({ command }) => command[command.indexOf("-a") + 1],
),
).toEqual([
selectedArchitecture,
selectedArchitecture,
selectedArchitecture,
...(target.availableArchitectures.length > 1
? target.availableArchitectures.map((architecture) =>
architecture === "x86" ? "i386" : architecture,
)
: []),
]);
expect(parsed.provenance.every(({ exit }) => exit.code === 1)).toBe(true);
},
);
@@ -620,7 +645,11 @@ it.skipIf(process.platform !== "darwin")(
program,
]);
}
const target = await nativeMachoTargetForFile(path);
const admitted = await parseBinaryTarget(path);
if (!admitted.ok) throw admitted.error;
const target = admitted.value;
if (target.kind !== "executable")
throw new Error("Expected the copied system executable");
const real = new XcrunCommandRunner({});
let capturedPath: string | undefined;
const runner: NativeCommandRunner = {
@@ -702,7 +731,7 @@ for (const cleanupFails of [false, true])
expect(result.error.cleanupIncomplete).toBe(cleanupFails);
const facts = inspectSignatureSchema.parse(result.error.partialObservation);
expect(facts.signed).toBe(true);
expect(facts.provenance).toHaveLength(3);
expect(facts.provenance).toHaveLength(5);
expect(
projectAnalysisError(result.error).details?.partial_observation,
).toEqual(facts);