Files
reverse-engineering-anything/tests/boundary/providers/native/nativeCallObservation.test.ts
T
morluto 5044e20171 test: focus diagnostics and evidence checks on behavior (#9303)
* test: focus configuration diagnostics on their owners

Preserve shape errors, selected paths, recovery guidance, and unchanged-file behavior across parser, setup, and uninstall boundaries.

* test: focus artifact and evidence checks on behavior

Retain format, identity, malformed-input, and cleanup coverage while removing fixed prose and closing the admitted test source.

* refactor(debugger): use configured session admission limits

* test: focus evidence and diagnostics on behavior

* fix(test): restore verifier export and formatting

* test(config): verify debugger limit ranges

* test(rizin): keep fixture executable authoritative

* test(rizin): isolate debugger fixture environment
2026-11-08 16:39:20 +00:00

626 lines
20 KiB
TypeScript

import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisInputError,
} from "../../../../src/domain/analysisErrorCore.js";
import type { AnalysisError } from "../../../../src/domain/analysisErrorBase.js";
import { EvidenceIntegrityError } from "../../../../src/domain/evidenceErrors.js";
import { nativeCallPartialObservationSchema } from "../../../../src/domain/native/nativeCallPartialObservation.js";
import { nativeCallObservationResultSchema } from "../../../../src/domain/native/nativeCallObservation.js";
import {
NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION,
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
} from "../../../../src/native/NativeCallObservation.js";
import { err, ok, type Result } from "../../../../src/domain/result.js";
import type {
NativeCallTrace,
NativeCallTracer,
} from "../../../../src/native/LldbCallTracer.js";
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
import {
NativeFixtureRunner,
nativeMachoTarget,
} from "../../../fixtures/nativeCommands.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
const LOCATION = {
load_address: "0x100000928",
file_address: "0x100000928",
module: "Fixture",
module_path: "/tmp/Fixture",
symbol: "-[Greeter greet:times:]",
source_location: {
file_path: "/not-acquired/é space/fixture.pas",
line: 23,
column: null,
},
};
const run = (
overrides: Partial<NativeCallTrace["run"]> = {},
): NativeCallTrace["run"] => ({
status: "traced",
version: "lldb-2100.0.17.203",
pid: 4242,
outcome: "exited",
resource_limit_reached: false,
target_output: {
stdout_bytes: 0,
stderr_bytes: 0,
stdout_truncated: false,
stderr_truncated: false,
stdout_complete: true,
stderr_complete: true,
},
exit_status: 0,
exit_description: null,
killed: false,
terminated: true,
target_identity: {
loaded_image_sha256: null,
selected_file_sha256: "a".repeat(64),
file_device: "1",
file_inode: "2",
module_path: "/tmp/Fixture",
module_uuid: "fixture-uuid",
stable: true,
},
elapsed_ms: 12.5,
breakpoints: [
{ index: 0, location_count: 1, locations: [LOCATION] },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
events: [
{
sequence: 0,
elapsed_ms: 3,
thread_id: 7,
breakpoint_index: 0,
...LOCATION,
receiver_class: "Greeter",
selector: "greet:times:",
registers: [
{ name: "x0", value: "0x6000" },
{ name: "x1", value: "0x7000" },
],
backtrace: [{ ...LOCATION, source_location: null }],
},
],
other_stops: [],
...overrides,
});
/** Records the request and returns a canned run or failure. */
class FixtureTracer implements NativeCallTracer {
readonly requests: Parameters<NativeCallTracer["trace"]>[0][] = [];
constructor(
private readonly outcome: Result<NativeCallTrace, AnalysisError> = ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: "lldb-2100.0.17.203",
},
run: run(),
stdout: { text: "hello\n", bytes: 6, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
) {}
trace(request: Parameters<NativeCallTracer["trace"]>[0]) {
this.requests.push(request);
return Promise.resolve(this.outcome);
}
close() {
return Promise.resolve(ok(null));
}
}
const BREAKPOINTS = [
{ kind: "objc-method", class_name: "Greeter", selector: "greet:times:" },
{ kind: "function", name: "rea_add" },
];
const fixtureTarget = async () => {
const directory = await createTestTempDirectory("rea-native-calls-");
const path = join(directory, "Fixture");
await writeFile(path, "fixture executable");
return {
...nativeMachoTarget(path),
sha256: createHash("sha256").update("fixture executable").digest("hex"),
};
};
const observe = async (
tracer: NativeCallTracer,
parameters: Record<string, unknown>,
target?: Awaited<ReturnType<typeof fixtureTarget>>,
signal?: AbortSignal,
) =>
new NativeMacOSProvider({}, new NativeFixtureRunner(), "darwin", () => tracer)
.createClient(target ?? (await fixtureTarget()))
.execute(
"observe_native_calls",
// The provider parses JSON-like parameters itself.
JSON.parse(JSON.stringify(parameters)),
signal === undefined ? undefined : { signal },
);
describe("observe_native_calls projection: projects completed observations", () => {
it("launches the active Mach-O through the tracer and projects its run", async () => {
const tracer = new FixtureTracer();
const target = await fixtureTarget();
const observed = await observe(
tracer,
{
breakpoints: BREAKPOINTS,
arguments: ["--flag"],
environment: { REA_CASE: "selected" },
},
target,
);
if (!observed.ok) throw observed.error;
expect(tracer.requests[0]).toMatchObject({
executable: target.path,
architecture: "arm64",
input: {
arguments: ["--flag"],
environment: { REA_CASE: "selected" },
duration_ms: 10_000,
max_events: 1_000,
argument_registers: 4,
backtrace_frames: 0,
},
});
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.target.environment).toEqual({ REA_CASE: "selected" });
expect(result.breakpoints.map(({ request }) => request)).toEqual([
{
kind: "objc-method",
class_name: "Greeter",
selector: "greet:times:",
method_type: "any",
},
{ kind: "function", name: "rea_add", module: null },
]);
expect(result.process).toMatchObject({
pid: 4242,
outcome: "exited",
exit_status: 0,
terminated: true,
stdout: { text: "hello\n" },
});
expect(result.target.launch_identity).toEqual({
file_device: "1",
file_inode: "2",
loaded_image_sha256: null,
selected_file_sha256: "a".repeat(64),
module_path: "/tmp/Fixture",
module_uuid: "fixture-uuid",
stable: true,
});
expect(result.events[0]).toMatchObject({
receiver_class: "Greeter",
selector: "greet:times:",
source_location: LOCATION.source_location,
backtrace: [{ ...LOCATION, source_location: null }],
});
expect(result.breakpoints[0]?.locations[0]?.source_location).toEqual(
LOCATION.source_location,
);
expect(result.coverage).toEqual({
status: "complete",
event_limit_reached: false,
resource_limit_reached: false,
unresolved_breakpoints: [],
});
expect(observed.value.locations).toEqual([
{ kind: "artifact-path", path: target.path },
]);
});
});
describe("observe_native_calls projection: keeps locations within the metadata budget", () => {
it("projects every breakpoint location when the aggregate metadata budget allows it", async () => {
const locations = Array.from({ length: 65 }, (_, index) => ({
...LOCATION,
load_address: `0x${(0x100000928 + index * 4).toString(16)}`,
file_address: `0x${(0x100000928 + index * 4).toString(16)}`,
}));
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoints: [
{ index: 0, location_count: locations.length, locations },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.breakpoints[0]?.location_count).toBe(65);
expect(result.breakpoints[0]?.locations).toHaveLength(65);
expect(result.coverage).toEqual({
status: "complete",
event_limit_reached: false,
resource_limit_reached: false,
unresolved_breakpoints: [],
});
});
it("uses a generic warning when locations are missing without a budget signal", async () => {
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoints: [
{ index: 0, location_count: 2, locations: [LOCATION] },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.limitations).toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
);
});
});
describe("observe_native_calls projection: reports partial observation coverage", () => {
it("retains normal exit and location counts when breakpoint metadata is bounded", async () => {
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoint_locations_truncated: true,
breakpoints: [
{ index: 0, location_count: 64, locations: [LOCATION] },
{ index: 1, location_count: 64, locations: [] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.process).toMatchObject({
outcome: "exited",
exit_status: 0,
terminated: true,
});
expect(
result.breakpoints.map(({ location_count }) => location_count),
).toEqual([64, 64]);
expect(result.coverage).toMatchObject({
status: "partial",
breakpoint_locations_truncated: true,
resource_limit_reached: false,
unresolved_breakpoints: [],
});
expect(result.limitations).toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION,
);
expect(result.limitations).not.toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
);
expect(result.limitations.join(" ")).not.toContain("process was killed");
});
it("marks event limits, unresolved breakpoints and unconfirmed exits", async () => {
const tracer = new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
outcome: "event-limit",
exit_status: null,
killed: true,
terminated: false,
breakpoints: [
{ index: 0, location_count: 1, locations: [LOCATION] },
{ index: 1, location_count: 0, locations: [] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: false },
stderr: {
text: "x".repeat(16),
bytes: 2_000_000,
truncated: true,
complete: true,
},
terminated: true,
}),
);
const observed = await observe(tracer, {
breakpoints: BREAKPOINTS,
max_events: 1,
});
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.coverage).toEqual({
status: "partial",
event_limit_reached: true,
resource_limit_reached: false,
unresolved_breakpoints: [1],
});
expect(result.process.terminated).toBe(false);
expect(result.limitations.join(" ")).not.toContain("was killed");
expect(result.limitations).toContainEqual(
expect.stringMatching(
/output draining did not complete.*byte counts.*lower bounds.*not stream totals/iu,
),
);
expect(result.limitations).toEqual(
expect.arrayContaining([
expect.stringMatching(
/max_events \(1\).*later calls.*not recorded.*termination.*could not be confirmed/iu,
),
expect.stringMatching(
/Breakpoints 1.*matched no code.*calls could not be observed/iu,
),
expect.stringMatching(
/1048576 bytes.*per stream.*counted but not kept.*no runtime override/iu,
),
expect.stringMatching(
/could not confirm.*process 4242 exited.*check for it/iu,
),
]),
);
});
it("marks duration and aggregate resource limits as partial coverage", async () => {
for (const outcome of ["duration-elapsed", "resource-limit"] as const) {
const tracer = new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
outcome,
resource_limit_reached: outcome === "resource-limit",
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
);
const observed = await observe(tracer, { breakpoints: BREAKPOINTS });
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.coverage).toMatchObject({
status: "partial",
resource_limit_reached: outcome === "resource-limit",
});
}
});
});
describe("observe_native_calls projection: preserves unknown launch identity", () => {
it("preserves unknown launch identity without claiming digest binding", async () => {
const tracer = new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
target_identity: {
loaded_image_sha256: null,
selected_file_sha256: null,
file_device: null,
file_inode: null,
module_path: null,
module_uuid: null,
stable: false,
},
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
);
const observed = await observe(tracer, { breakpoints: BREAKPOINTS });
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.target.launch_identity).toEqual({
file_device: null,
file_inode: null,
loaded_image_sha256: null,
selected_file_sha256: null,
module_path: null,
module_uuid: null,
stable: false,
});
expect(result.limitations).toContainEqual(
expect.stringMatching(
/not provide enough module identity evidence.*pathname and module continuity.*launch/iu,
),
);
});
});
describe("observe_native_calls failures", () => {
it("rejects malformed input before launching anything", async () => {
for (const parameters of [
{ breakpoints: [] },
{ breakpoints: [{ kind: "objc-method", selector: "-[A b]" }] },
{ breakpoints: BREAKPOINTS, environment: { "A=B": "x" } },
{ breakpoints: BREAKPOINTS, argument_registers: 9 },
]) {
const tracer = new FixtureTracer();
const observed = await observe(tracer, parameters);
expect(observed.ok).toBe(false);
if (!observed.ok)
expect(observed.error).toBeInstanceOf(AnalysisInputError);
expect(tracer.requests).toEqual([]);
}
});
it("refuses a changed or non-Mach-O target", async () => {
const tracer = new FixtureTracer();
const target = await fixtureTarget();
const changed = await observe(
tracer,
{ breakpoints: BREAKPOINTS },
{ ...target, sha256: "f".repeat(64) },
);
expect(!changed.ok && changed.error).toBeInstanceOf(EvidenceIntegrityError);
const database = await new NativeMacOSProvider(
{},
new NativeFixtureRunner(),
"darwin",
() => tracer,
)
.createClient({
path: target.path,
sha256: target.sha256,
kind: "database",
format: "analysis-database",
})
.execute("observe_native_calls", { breakpoints: [] });
expect(!database.ok && database.error).toBeInstanceOf(
AnalysisCapabilityUnavailableError,
);
expect(tracer.requests).toEqual([]);
});
it("keeps the tracer's failure reason and caller cancellation", async () => {
const denied = new AnalysisCapabilityUnavailableError(
"native-macos",
"observe_native_calls",
"debugger-attach-denied: attach failed",
);
const failed = await observe(new FixtureTracer(err(denied)), {
breakpoints: BREAKPOINTS,
});
expect(!failed.ok && failed.error).toBe(denied);
const controller = new AbortController();
controller.abort();
const cancelled = await observe(
new FixtureTracer(),
{ breakpoints: BREAKPOINTS },
undefined,
controller.signal,
);
expect(!cancelled.ok && cancelled.error).toBeInstanceOf(
AnalysisCancelledError,
);
});
});
it("retains completed call facts when cancellation arrives with the tracer result", async () => {
const controller = new AbortController();
const reason = { caller: "completed-trace-consumption" };
const recorded = new FixtureTracer();
const result = await observe(
{
trace: async (request) => {
const result = await recorded.trace(request);
controller.abort(reason);
return result;
},
close: () => recorded.close(),
},
{ breakpoints: BREAKPOINTS },
undefined,
controller.signal,
);
if (result.ok) throw new Error("Expected completion cancellation");
expect(result.error).toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
});
const partial = nativeCallPartialObservationSchema.parse(
result.error.partialObservation,
);
expect(partial.events).toHaveLength(1);
expect(partial.process.stdout?.text).toBe("hello\n");
expect(partial.process.terminated).toBe(true);
expect(partial.coverage).toMatchObject({
status: "partial",
reason: "cancelled",
});
});
it("grows repeated source-location JSON by the location object, not by a token estimate", () => {
const path = "/not-acquired/é space/fixture.pas";
const located = {
...LOCATION,
source_location: { file_path: path, line: 23, column: null },
};
const absent = { ...located, source_location: null };
const copies = 3;
const withPath = Buffer.byteLength(
JSON.stringify(Array.from({ length: copies }, () => located)),
);
const withoutPath = Buffer.byteLength(
JSON.stringify(Array.from({ length: copies }, () => absent)),
);
const perCopy =
Buffer.byteLength(JSON.stringify(located.source_location)) -
Buffer.byteLength("null");
expect(withPath - withoutPath).toBe(copies * perCopy);
expect(perCopy).toBeGreaterThan(Buffer.byteLength(path));
});