mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
ae09e3fe70
* fix(native): recover macOS command deadlines Promote native_command_timeout_ms (default 60000), native_ui_deadline_ms (default 180000), and pwntools_timeout_ms (default 30000) as validated per-call settings. Keep cpuSeconds, byte allowances, LLDB grace, and plist probes fixed, and say those caps have no runtime override. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(native): report the deadline that actually expired Pass the selected native command timeout through xcrun resolution, thread the pwntools deadline into pwndbg failures, and state the selected UI deadline apart from the fixed wait-step cap. SIGXFSZ and EFBIG recovery names the file-size allowance. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(native): honor the selected deadline on helper stages The UI helper compile and capture subprocesses used fixed 60s and 30s exec timeouts, so a longer native_ui_deadline_ms still failed as a generic provider error. Preparation timeouts reported the 60000 ms default instead of the selected native_command_timeout_ms. Memory resource-constraint recovery names memory again, and the Darwin plist CLI expectation matches the byte-counted input allowance. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
681 lines
22 KiB
TypeScript
681 lines
22 KiB
TypeScript
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
|
import type { RecordedCrash } from "../../../src/domain/native/recordedCrash.js";
|
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { join } from "node:path";
|
|
import { expect, it, onTestFinished } from "vitest";
|
|
import { z } from "zod";
|
|
import {
|
|
createTestWorkspace,
|
|
removeTestWorkspace,
|
|
} from "../../support/workspace/workspaceFixture.js";
|
|
import { PwntoolsRecordedCrashProvider } from "../../../src/native/pwntools/PwntoolsRecordedCrashProvider.js";
|
|
import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js";
|
|
import {
|
|
spawnOwnedProviderProcess,
|
|
type OwnedProviderProcessSpawnOptions,
|
|
type SpawnedOwnedProviderProcess,
|
|
} from "../../../src/process/ProviderProcess.js";
|
|
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
|
|
import {
|
|
stopProviderProcessFixture,
|
|
waitForProviderProcessReady,
|
|
} from "../../fixtures/providerProcess.js";
|
|
import {
|
|
recordedCrashFixture,
|
|
recordedCrashFileMappingFixture,
|
|
recordedCrashDebuggerFixture,
|
|
recordedCrashFixtureBytes,
|
|
recordedCrashSignalFixture,
|
|
} from "../../fixtures/binaryDiagnostics/recordedCrash.js";
|
|
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
|
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
|
|
|
const requestSchema = z.object({
|
|
snapshot_path: z.string(),
|
|
reply_path: z.string(),
|
|
});
|
|
const host = process.platform === "linux" && process.arch === "x64";
|
|
|
|
const withCleanupGate = (
|
|
launched: SpawnedOwnedProviderProcess,
|
|
cleanupAllowed: () => boolean,
|
|
): SpawnedOwnedProviderProcess => ({
|
|
...launched,
|
|
cleanup: async () => {
|
|
const result = await cleanupOwnedProcessGroup(launched.ownership);
|
|
if (!cleanupAllowed())
|
|
throw new Error("source-owned post-cleanup reporting failure");
|
|
return result;
|
|
},
|
|
});
|
|
|
|
const spawnDebuggerFixture = (
|
|
spawn: OwnedProviderProcessSpawnOptions,
|
|
scenario: string,
|
|
): Promise<SpawnedOwnedProviderProcess> => {
|
|
const script =
|
|
scenario === "process-failure"
|
|
? "process.stderr.write('debugger failed',()=>{process.exitCode=9;})"
|
|
: scenario.startsWith("cancelled")
|
|
? "process.stdout.write('ready\\n'); setInterval(() => {}, 1000)"
|
|
: "process.stdout.write('debugger stage diagnostic')";
|
|
const exec = scenario.startsWith("cancelled-exec");
|
|
return spawnOwnedProviderProcess({
|
|
...spawn,
|
|
command: exec ? "/bin/sh" : process.execPath,
|
|
arguments: exec
|
|
? [
|
|
"-c",
|
|
scenario === "cancelled-exec-foreign"
|
|
? 'REA_PROCESS_RUN_ID=source-owned-foreign exec "$1" -e "$2"'
|
|
: 'exec "$1" -e "$2"',
|
|
"recorded-debugger-launcher",
|
|
process.execPath,
|
|
script,
|
|
]
|
|
: ["-e", script],
|
|
});
|
|
};
|
|
|
|
const expectForeignDebuggerPreserved = async (
|
|
child: SpawnedOwnedProviderProcess["process"] | undefined,
|
|
): Promise<void> => {
|
|
if (child === undefined || child.pid === undefined)
|
|
throw new Error("Expected an owned fixture PID");
|
|
const environment = await readFile(`/proc/${child.pid}/environ`);
|
|
expect(
|
|
environment.includes(
|
|
Buffer.from("REA_PROCESS_RUN_ID=source-owned-foreign\0"),
|
|
),
|
|
).toBe(true);
|
|
expect(child.exitCode).toBeNull();
|
|
expect(child.signalCode).toBeNull();
|
|
};
|
|
|
|
it
|
|
.runIf(host)
|
|
.each([
|
|
"success",
|
|
"success-aarch64",
|
|
"wrong-register",
|
|
"wrong-register-source",
|
|
"missing-thread",
|
|
"duplicate-thread",
|
|
"missing-signal",
|
|
"duplicate-signal",
|
|
"missing-register",
|
|
"duplicated-physical-thread",
|
|
"duplicated-physical-signal",
|
|
"omitted-physical-note",
|
|
"hidden-segment",
|
|
"wrong-header-range",
|
|
"wrong-ei-version-zero",
|
|
"wrong-ei-version-two",
|
|
"wrong-e-version-zero",
|
|
"wrong-e-version-two",
|
|
"wrong-note-kind",
|
|
"wrong-owner-display",
|
|
"wrong-pid",
|
|
"wrong-current-signal",
|
|
"wrong-signal-number",
|
|
"wrong-signal-code",
|
|
"wrong-signal-errno",
|
|
"wrong-signal-address",
|
|
"wrong-raw-note",
|
|
"malformed-range",
|
|
] as const)(
|
|
"validates recorded source bytes and cleanup: %s",
|
|
async (scenario) => {
|
|
const architecture = scenario.endsWith("aarch64")
|
|
? "aarch64-little-endian"
|
|
: "x86_64-little-endian";
|
|
const { path, value, bytes } = await fixture((path) => {
|
|
const value = recordedCrashSignalFixture(path, architecture);
|
|
if (scenario === "wrong-owner-display") {
|
|
value.notes = value.notes.map((note) =>
|
|
note.index === 0
|
|
? {
|
|
...note,
|
|
owner_bytes_base64: Buffer.from("EVIL\0").toString("base64"),
|
|
owner_display: "EVIL",
|
|
}
|
|
: note,
|
|
);
|
|
value.threads = [];
|
|
}
|
|
return value;
|
|
});
|
|
if (scenario.startsWith("wrong-ei-version-"))
|
|
bytes[6] = Number(scenario.at(-1));
|
|
if (scenario.startsWith("wrong-e-version-"))
|
|
bytes.writeUInt32LE(Number(scenario.at(-1)), 20);
|
|
if (scenario.startsWith("wrong-e")) await writeFile(path, bytes);
|
|
let owned = "";
|
|
const provider = new PwntoolsRecordedCrashProvider(
|
|
{ REA_PWNTOOLS_PYTHON: process.execPath },
|
|
async (spawn) => {
|
|
owned = spawn.cwd ?? "";
|
|
const request = requestSchema.parse(
|
|
JSON.parse(await readFile(spawn.arguments.at(-1) ?? "", "utf8")),
|
|
);
|
|
expect(await readFile(request.snapshot_path)).toEqual(bytes);
|
|
const payload = structuredClone(value);
|
|
alterRecordedCrashReply(payload, scenario);
|
|
const {
|
|
artifact: _artifact,
|
|
diagnostics: _diagnostics,
|
|
debugger: _debugger,
|
|
decoder_diagnostics: _decoderDiagnostics,
|
|
...raw
|
|
} = payload;
|
|
await writeFile(
|
|
request.reply_path,
|
|
JSON.stringify({
|
|
ok: true,
|
|
profile: PWNTOOLS_PROVIDER_IDENTITY.version,
|
|
value: raw,
|
|
}),
|
|
);
|
|
return spawnOwnedProviderProcess({
|
|
...spawn,
|
|
command: process.execPath,
|
|
arguments: ["-e", "process.stdout.write('recorded warning')"],
|
|
});
|
|
},
|
|
);
|
|
const result = await provider.inspect({
|
|
path,
|
|
include_debugger_context: false,
|
|
});
|
|
if (scenario === "success" || scenario === "success-aarch64") {
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.architecture).toBe(architecture);
|
|
expect(result.value.threads).toEqual(value.threads);
|
|
expect(result.value.diagnostics.stdout).toBe("recorded warning");
|
|
} else
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisOutputError",
|
|
capturedOutput: { stdout: "recorded warning" },
|
|
},
|
|
});
|
|
expect(await readFile(path)).toEqual(bytes);
|
|
await expect(access(owned)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|
|
|
|
it
|
|
.runIf(host)
|
|
.each([
|
|
"unconfigured",
|
|
"unsafe-mapping",
|
|
"success",
|
|
"process-failure",
|
|
"invalid-context",
|
|
"cancelled",
|
|
"cancelled-exec",
|
|
"cancelled-exec-foreign",
|
|
"cleanup-failure",
|
|
"decoder-cleanup-failure",
|
|
] as const)(
|
|
"owns optional context from the same snapshot: %s",
|
|
async (scenario) => {
|
|
const { path, value, bytes, root } = await fixture(
|
|
scenario === "unsafe-mapping"
|
|
? (path) =>
|
|
recordedCrashFileMappingFixture(
|
|
[Buffer.from("../../outside-owned-root")],
|
|
path,
|
|
)
|
|
: recordedCrashFixture,
|
|
);
|
|
const controller = new AbortController();
|
|
let owned = "";
|
|
let snapshotPath = "";
|
|
let cleanupAllowed = false;
|
|
let launches = 0;
|
|
let debuggerProcess: SpawnedOwnedProviderProcess["process"] | undefined;
|
|
const venv = join(root, "venv");
|
|
const plugin = join(root, "gdbinit.py");
|
|
await mkdir(venv);
|
|
await writeFile(plugin, "");
|
|
const provider = new PwntoolsRecordedCrashProvider(
|
|
{
|
|
REA_PWNTOOLS_PYTHON: process.execPath,
|
|
...(scenario === "unconfigured"
|
|
? {}
|
|
: {
|
|
REA_PWNDBG_GDB: process.execPath,
|
|
REA_PWNDBG_GDBINIT: plugin,
|
|
REA_PWNDBG_VENV_PATH: venv,
|
|
}),
|
|
},
|
|
async (spawn) => {
|
|
launches++;
|
|
const request = requestSchema.parse(
|
|
JSON.parse(await readFile(spawn.arguments.at(-1) ?? "", "utf8")),
|
|
);
|
|
expect(await readFile(request.snapshot_path)).toEqual(bytes);
|
|
if (snapshotPath === "") {
|
|
snapshotPath = request.snapshot_path;
|
|
owned = spawn.cwd ?? "";
|
|
const {
|
|
artifact: _artifact,
|
|
diagnostics: _diagnostics,
|
|
debugger: _debugger,
|
|
decoder_diagnostics: _decoderDiagnostics,
|
|
...raw
|
|
} = value;
|
|
await writeFile(
|
|
request.reply_path,
|
|
JSON.stringify({
|
|
ok: true,
|
|
profile: PWNTOOLS_PROVIDER_IDENTITY.version,
|
|
value: raw,
|
|
}),
|
|
);
|
|
const launched = await spawnOwnedProviderProcess({
|
|
...spawn,
|
|
command: process.execPath,
|
|
arguments: ["-e", "process.stdout.write('basic stage diagnostic')"],
|
|
});
|
|
return scenario === "decoder-cleanup-failure"
|
|
? withCleanupGate(launched, () => cleanupAllowed)
|
|
: launched;
|
|
}
|
|
expect(request.snapshot_path).toBe(snapshotPath);
|
|
const context = recordedCrashDebuggerFixture();
|
|
if (scenario === "invalid-context")
|
|
context.maps = context.maps.map((map) => ({ ...map, end: "0x0" }));
|
|
await writeFile(
|
|
request.reply_path,
|
|
JSON.stringify({ ok: true, value: context }),
|
|
);
|
|
const launched = await spawnDebuggerFixture(spawn, scenario);
|
|
debuggerProcess = launched.process;
|
|
if (scenario.startsWith("cancelled")) {
|
|
await waitForProviderProcessReady(launched.process);
|
|
setImmediate(() => controller.abort());
|
|
}
|
|
return scenario === "cleanup-failure"
|
|
? withCleanupGate(launched, () => cleanupAllowed)
|
|
: launched;
|
|
},
|
|
);
|
|
onTestFinished(async () => {
|
|
cleanupAllowed = true;
|
|
// The test retains its creation handle even when the provider must refuse
|
|
// an exec'd worker with a foreign token, or a regression leaves it live.
|
|
if (debuggerProcess !== undefined)
|
|
await stopProviderProcessFixture(debuggerProcess);
|
|
await provider.close();
|
|
});
|
|
const result = await provider.inspect(
|
|
{
|
|
path,
|
|
include_debugger_context: true,
|
|
},
|
|
{ signal: controller.signal },
|
|
);
|
|
if (scenario === "success") {
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.debugger).toMatchObject({
|
|
status: "available",
|
|
maps: [{ permissions: null }],
|
|
diagnostics: { stdout: "debugger stage diagnostic" },
|
|
});
|
|
} else {
|
|
if (result.ok) throw new Error("Expected optional debugger failure");
|
|
expectOptionalContextFailure(scenario, result.error, value);
|
|
if (scenario === "unsafe-mapping") {
|
|
expect(launches).toBe(1);
|
|
await expect(access(owned)).rejects.toMatchObject({ code: "ENOENT" });
|
|
snapshotPath = "";
|
|
await expectBasicCoreFallback(provider, path, value.notes);
|
|
expect(launches).toBe(2);
|
|
}
|
|
if (scenario === "cancelled-exec-foreign")
|
|
await expectForeignDebuggerPreserved(debuggerProcess);
|
|
}
|
|
if (scenario === "cancelled" || scenario === "cancelled-exec")
|
|
expectDebuggerStopped(debuggerProcess);
|
|
expect(await readFile(path)).toEqual(bytes);
|
|
if (
|
|
scenario === "cleanup-failure" ||
|
|
scenario === "decoder-cleanup-failure" ||
|
|
scenario === "cancelled-exec-foreign"
|
|
) {
|
|
await expect(access(owned)).resolves.toBeUndefined();
|
|
expect(await readFile(snapshotPath)).toEqual(bytes);
|
|
const refused = await provider.inspect({
|
|
path,
|
|
include_debugger_context: true,
|
|
});
|
|
if (refused.ok)
|
|
throw new Error("Unreleased ownership must block a launch");
|
|
expect(refused.error.cleanupResources).toContain(owned);
|
|
expect(launches).toBe(scenario === "decoder-cleanup-failure" ? 1 : 2);
|
|
await expect(provider.close()).rejects.toMatchObject({
|
|
cleanupIncomplete: true,
|
|
});
|
|
await expect(access(owned)).resolves.toBeUndefined();
|
|
cleanupAllowed = true;
|
|
if (
|
|
scenario === "cancelled-exec-foreign" &&
|
|
debuggerProcess !== undefined
|
|
)
|
|
await stopProviderProcessFixture(debuggerProcess);
|
|
await provider.close();
|
|
}
|
|
await expect(access(owned)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|
|
|
|
const expectDebuggerStopped = (
|
|
child: SpawnedOwnedProviderProcess["process"] | undefined,
|
|
): void => {
|
|
if (child === undefined)
|
|
throw new Error("Expected an owned debugger process");
|
|
expect(child.exitCode !== null || child.signalCode !== null).toBe(true);
|
|
};
|
|
|
|
const expectBasicCoreFallback = async (
|
|
provider: PwntoolsRecordedCrashProvider,
|
|
path: string,
|
|
notes: RecordedCrash["notes"],
|
|
) => {
|
|
const basic = await provider.inspect({
|
|
path,
|
|
include_debugger_context: false,
|
|
});
|
|
if (!basic.ok) throw basic.error;
|
|
expect(basic.value.notes).toEqual(notes);
|
|
expect(basic.value.debugger).toEqual({ status: "not-requested" });
|
|
};
|
|
|
|
const expectOptionalContextFailure = (
|
|
scenario: string,
|
|
error: AnalysisError,
|
|
value: RecordedCrash,
|
|
) => {
|
|
const projected = projectAnalysisError(error);
|
|
if (scenario === "unsafe-mapping")
|
|
expect(error).toMatchObject({
|
|
_tag: "AnalysisCapabilityUnavailableError",
|
|
partialObservation: {
|
|
notes: value.notes,
|
|
debugger: { status: "unavailable", requested: true },
|
|
},
|
|
});
|
|
if (scenario === "cancelled-exec-foreign")
|
|
expect(projected.code).toBe("cleanup_incomplete");
|
|
if (scenario === "unconfigured" || scenario === "unsafe-mapping")
|
|
expect(projected).toMatchObject({
|
|
details: { captured_output: { stdout: "basic stage diagnostic" } },
|
|
});
|
|
else if (scenario === "decoder-cleanup-failure")
|
|
expect(projected).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: {
|
|
diagnostics: { captured_output: { stdout: "basic stage diagnostic" } },
|
|
},
|
|
});
|
|
else if (
|
|
scenario === "invalid-context" ||
|
|
scenario.startsWith("cancelled") ||
|
|
scenario === "cleanup-failure"
|
|
) {
|
|
const serialized = JSON.stringify(projected);
|
|
expect(serialized).toContain("basic stage diagnostic");
|
|
expect(serialized).toContain(
|
|
scenario.startsWith("cancelled") ? "ready" : "debugger stage diagnostic",
|
|
);
|
|
expect(error.partialObservation).toMatchObject({
|
|
artifact: {
|
|
...value.artifact,
|
|
sha256: createHash("sha256")
|
|
.update(recordedCrashFixtureBytes(value))
|
|
.digest("hex"),
|
|
},
|
|
threads: value.threads,
|
|
notes: value.notes,
|
|
});
|
|
expect(error._tag).toBe(
|
|
scenario === "invalid-context"
|
|
? "AnalysisOutputError"
|
|
: scenario.startsWith("cancelled") &&
|
|
scenario !== "cancelled-exec-foreign"
|
|
? "AnalysisCancelledError"
|
|
: "ProviderAdapterError",
|
|
);
|
|
if (scenario === "cleanup-failure" || scenario === "invalid-context") {
|
|
if (scenario === "cleanup-failure")
|
|
expect(error).toBeInstanceOf(ProviderCleanupError);
|
|
expect(projected).toMatchObject({
|
|
details: {
|
|
partial_observation: {
|
|
debugger: { status: "unavailable", requested: true },
|
|
},
|
|
},
|
|
});
|
|
}
|
|
} else
|
|
expect(projected).toMatchObject({
|
|
category: "execution_failure",
|
|
details: {
|
|
diagnostics: {
|
|
preceding_output: { stdout: "basic stage diagnostic" },
|
|
stderr: "debugger failed",
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
it.runIf(host)(
|
|
"reports a non-default debugger deadline when pwndbg times out",
|
|
async () => {
|
|
const { path, value, bytes, root } = await fixture();
|
|
const venv = join(root, "venv");
|
|
const plugin = join(root, "gdbinit.py");
|
|
await mkdir(venv);
|
|
await writeFile(plugin, "");
|
|
let stage = 0;
|
|
const provider = new PwntoolsRecordedCrashProvider(
|
|
{
|
|
REA_PWNTOOLS_PYTHON: process.execPath,
|
|
REA_PWNDBG_GDB: process.execPath,
|
|
REA_PWNDBG_GDBINIT: plugin,
|
|
REA_PWNDBG_VENV_PATH: venv,
|
|
},
|
|
async (spawn) => {
|
|
stage += 1;
|
|
const request = requestSchema.parse(
|
|
JSON.parse(await readFile(spawn.arguments.at(-1) ?? "", "utf8")),
|
|
);
|
|
if (stage === 1) {
|
|
const {
|
|
artifact: _artifact,
|
|
diagnostics: _diagnostics,
|
|
debugger: _debugger,
|
|
decoder_diagnostics: _decoderDiagnostics,
|
|
...raw
|
|
} = value;
|
|
await writeFile(
|
|
request.reply_path,
|
|
JSON.stringify({
|
|
ok: true,
|
|
profile: PWNTOOLS_PROVIDER_IDENTITY.version,
|
|
value: raw,
|
|
}),
|
|
);
|
|
return spawnOwnedProviderProcess({
|
|
...spawn,
|
|
command: process.execPath,
|
|
arguments: ["-e", "process.stdout.write('basic stage diagnostic')"],
|
|
});
|
|
}
|
|
await new Promise<void>((resolve) => {
|
|
if (spawn.signal?.aborted === true) {
|
|
resolve();
|
|
return;
|
|
}
|
|
spawn.signal?.addEventListener("abort", () => resolve(), {
|
|
once: true,
|
|
});
|
|
});
|
|
return spawnOwnedProviderProcess({
|
|
...spawn,
|
|
command: process.execPath,
|
|
arguments: ["-e", "process.exit(0)"],
|
|
});
|
|
},
|
|
);
|
|
onTestFinished(() => provider.close());
|
|
const result = await provider.inspect({
|
|
path,
|
|
include_debugger_context: true,
|
|
pwntools_timeout_ms: 1_500,
|
|
});
|
|
expect(await readFile(path)).toEqual(bytes);
|
|
if (result.ok) throw new Error("expected debugger timeout");
|
|
const projected = projectAnalysisError(result.error);
|
|
expect(projected.code).toBe("provider_timeout");
|
|
expect(projected.message).toContain("of 1500 ms");
|
|
expect(projected.remediation.action).toContain("above 1500");
|
|
expect(projected.remediation.action).not.toContain("above 30000");
|
|
},
|
|
10_000,
|
|
);
|
|
|
|
const fixture = async (
|
|
createReport: (path: string) => RecordedCrash = recordedCrashFixture,
|
|
) => {
|
|
const workspace = await createTestWorkspace("rea-recorded-core-seam-");
|
|
onTestFinished(() => removeTestWorkspace(workspace.root));
|
|
const path = join(workspace.root, "recorded.core");
|
|
const value = createReport(path);
|
|
const bytes = recordedCrashFixtureBytes(value);
|
|
await writeFile(path, bytes);
|
|
return { path, value, bytes, root: workspace.root };
|
|
};
|
|
|
|
const alterRecordedCrashReply = (
|
|
payload: RecordedCrash,
|
|
scenario: string,
|
|
): void => {
|
|
if (scenario === "wrong-owner-display")
|
|
payload.notes = payload.notes.map((note) =>
|
|
note.index === 0 ? { ...note, owner_display: "FORGED" } : note,
|
|
);
|
|
if (
|
|
scenario === "duplicated-physical-thread" ||
|
|
scenario === "duplicated-physical-signal"
|
|
) {
|
|
const noteIndex = scenario === "duplicated-physical-thread" ? 0 : 1;
|
|
const note = payload.notes[noteIndex];
|
|
if (note === undefined) throw new Error("missing fixture note");
|
|
payload.notes.push({ ...note, index: 2 });
|
|
if (noteIndex === 0)
|
|
payload.threads.push(
|
|
...payload.threads.map((thread) => ({
|
|
...thread,
|
|
note_index: 2,
|
|
})),
|
|
);
|
|
else
|
|
payload.signals.push(
|
|
...payload.signals.map((signal) => ({
|
|
...signal,
|
|
note_index: 2,
|
|
})),
|
|
);
|
|
}
|
|
if (scenario === "omitted-physical-note") {
|
|
payload.notes = payload.notes.filter((note) => note.index === 0);
|
|
payload.signals = [];
|
|
}
|
|
if (scenario === "hidden-segment") {
|
|
payload.segments = [];
|
|
payload.notes = [];
|
|
payload.threads = [];
|
|
payload.signals = [];
|
|
}
|
|
if (scenario === "wrong-header-range")
|
|
payload.segments = payload.segments.map((segment) => ({
|
|
...segment,
|
|
header_location: { offset: "0x0", bytes: "0x38" },
|
|
}));
|
|
if (scenario === "wrong-note-kind") {
|
|
payload.notes = payload.notes.map((note) =>
|
|
note.index === 0 ? { ...note, type: "NT_AUXV" } : note,
|
|
);
|
|
payload.threads = [];
|
|
}
|
|
if (scenario === "wrong-register-source")
|
|
payload.threads = payload.threads.map((thread) => ({
|
|
...thread,
|
|
registers: thread.registers.map((register) =>
|
|
register.name === "rdi"
|
|
? {
|
|
...register,
|
|
value: "0x0",
|
|
location: { offset: "0x104", bytes: "0x8" },
|
|
}
|
|
: register,
|
|
),
|
|
}));
|
|
if (scenario === "missing-thread") payload.threads = [];
|
|
if (scenario === "duplicate-thread")
|
|
payload.threads = [...payload.threads, ...payload.threads];
|
|
if (scenario === "missing-signal") payload.signals = [];
|
|
if (scenario === "duplicate-signal")
|
|
payload.signals = [...payload.signals, ...payload.signals];
|
|
if (scenario === "missing-register")
|
|
payload.threads = payload.threads.map((thread) => ({
|
|
...thread,
|
|
registers: thread.registers.slice(1),
|
|
}));
|
|
if (scenario === "wrong-register")
|
|
payload.threads = payload.threads.map((t) => ({
|
|
...t,
|
|
registers: t.registers.map((r) => ({ ...r, value: "0x1" })),
|
|
}));
|
|
if (scenario === "wrong-pid")
|
|
payload.threads = payload.threads.map((thread) => ({
|
|
...thread,
|
|
historical_pid: -2,
|
|
}));
|
|
if (scenario === "wrong-current-signal")
|
|
payload.threads = payload.threads.map((thread) => ({
|
|
...thread,
|
|
recorded_current_signal: -1,
|
|
}));
|
|
if (scenario.startsWith("wrong-signal"))
|
|
payload.signals = payload.signals.map((signal) => ({
|
|
...signal,
|
|
...(scenario === "wrong-signal-number"
|
|
? {
|
|
number: 10,
|
|
fault_address: null,
|
|
fault_address_meaning: "unknown" as const,
|
|
}
|
|
: scenario === "wrong-signal-code"
|
|
? { code: 2 }
|
|
: scenario === "wrong-signal-errno"
|
|
? { errno: 0 }
|
|
: { fault_address: "0x1" }),
|
|
}));
|
|
if (scenario === "wrong-raw-note")
|
|
payload.notes = payload.notes.map((n) => ({
|
|
...n,
|
|
descriptor_bytes_base64: Buffer.alloc(336, 1).toString("base64"),
|
|
}));
|
|
if (scenario === "malformed-range")
|
|
payload.notes = payload.notes.map((n) => ({
|
|
...n,
|
|
descriptor_location: { offset: "0x200", bytes: "0x150" },
|
|
}));
|
|
};
|