Interpret source-bound ELF64 NT_FILE descriptors once before debugger launch. Bound counts and pathname coverage by the descriptor, retain exact path bytes, and refuse relative names or dot segments rather than letting GDB resolve them against the host. Pin GDB's pathname semantics to Unix. Preserve the decoded partial observation on refusal, basic-only recovery, and pre-admission cancellation. Share an exact mapping fixture and replace the old fixture boolean modes with explicit report producers. Validation: final series has 33 passing Vitest cases plus five Python producer controls; 38 Linux x64 provider cases are explicitly skipped on macOS. Typecheck/lint, docs checks and test discovery pass. Real GDB16.3 and strace reproduce the relative-path escape using only an owned fixture; compiled admission admits a real absolute-path core and refuses that relative-path core. Full native pwndbg / Linux x64 remain unverified here. Task token, CPU and peak-family-memory costs are unmeasured; no speed claim. Fixes #4942
605 lines
20 KiB
TypeScript
605 lines
20 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",
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
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" },
|
|
}));
|
|
};
|