mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
test(native): preserve admitted identity and partial boundary facts
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user