feat(recorded-crash): decode AArch64 core register notes (#2505)

Squash merge of https://github.com/morluto/rea/pull/2505.

Co-authored-by: NIU-123370 <191000457+NIU-123370@users.noreply.github.com>
This commit is contained in:
The one -Niu 2026-10-13 23:48:24 +08:00 committed by GitHub
parent 2b98fc5f51
commit 522b9927c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 532 additions and 90 deletions

View File

@ -11,6 +11,12 @@ import sys
PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
# Linux ET_CORE machine identifiers whose PRSTATUS layout is interpreted with
# unchanged pwntools 4.15.0 ctypes. Both share pr_cursig@12, pr_pid@32 and
# pr_reg@112; only the register window differs: amd64 is 27 x 8 bytes for a
# 336-byte descriptor, AArch64 is 34 x 8 bytes for a 392-byte descriptor.
CORE_MACHINES = (62, 183)
class CoreFailure(Exception):
def __init__(self, reason, message):
@ -51,12 +57,27 @@ def inspect_core(path, cache):
# Other declared OSABIs must never select Linux note ctypes.
if content[7] not in (0, 3) or content[8] != 0:
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires System V or GNU/Linux OSABI with ABI version zero; selected OSABI=" + str(content[7]) + ", ABI version=" + str(content[8]) + ".")
if int.from_bytes(content[16:18], "little") != 4 or int.from_bytes(content[18:20], "little") != 62:
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires x86-64 ET_CORE.")
machine = int.from_bytes(content[18:20], "little")
if int.from_bytes(content[16:18], "little") != 4 or machine not in CORE_MACHINES:
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires ET_CORE for x86-64 or AArch64; selected ET_TYPE=" + str(int.from_bytes(content[16:18], "little")) + ", e_machine=" + str(machine) + ".")
from elftools.common.exceptions import ELFError
from elftools.common.utils import struct_parse
from elftools.elf.elffile import ELFFile
from pwnlib.elf.datatypes import elf_prstatus_amd64, elf_siginfo_64, user_regs_struct_amd64
from pwnlib.elf.datatypes import (
elf_prstatus_aarch64,
elf_prstatus_amd64,
elf_siginfo_64,
user_regs_struct_aarch64,
user_regs_struct_amd64,
)
if machine == 62:
profile_label = "x86-64"
architecture = "x86_64-little-endian"
prstatus_type, registers_type = elf_prstatus_amd64, user_regs_struct_amd64
else:
profile_label = "AArch64"
architecture = "aarch64-little-endian"
prstatus_type, registers_type = elf_prstatus_aarch64, user_regs_struct_aarch64
try:
image = ELFFile(io.BytesIO(content))
header = image.header
@ -107,17 +128,17 @@ def inspect_core(path, cache):
if owner != b"CORE\0":
continue
if note.n_type == "NT_PRSTATUS":
if len(descriptor) < ctypes.sizeof(elf_prstatus_amd64):
raise CoreFailure("format", "Linux amd64 NT_PRSTATUS descriptor is truncated.")
status = elf_prstatus_amd64.from_buffer_copy(descriptor)
if len(descriptor) < ctypes.sizeof(prstatus_type):
raise CoreFailure("format", "Linux " + profile_label + " NT_PRSTATUS descriptor is truncated.")
status = prstatus_type.from_buffer_copy(descriptor)
registers = []
for name, kind in user_regs_struct_amd64._fields_:
relative = elf_prstatus_amd64.pr_reg.offset + getattr(user_regs_struct_amd64, name).offset
for name, kind in registers_type._fields_:
relative = prstatus_type.pr_reg.offset + getattr(registers_type, name).offset
registers.append({"name": name, "value": hex(getattr(status.pr_reg, name)), "location": location(descriptor_start + relative, ctypes.sizeof(kind), end)})
threads.append({"note_index": note_index, "historical_pid": ctypes.c_int32(status.pr_pid).value, "recorded_current_signal": status.pr_cursig, "registers": registers})
elif note.n_type == "NT_SIGINFO":
if len(descriptor) < ctypes.sizeof(elf_siginfo_64):
raise CoreFailure("format", "Linux amd64 NT_SIGINFO descriptor is truncated.")
raise CoreFailure("format", "Linux " + profile_label + " NT_SIGINFO descriptor is truncated.")
signal = elf_siginfo_64.from_buffer_copy(descriptor)
number = ctypes.c_int32(signal.si_signo).value
code = ctypes.c_int32(signal.si_code).value
@ -125,7 +146,7 @@ def inspect_core(path, cache):
signals.append({"note_index": note_index, "number": number, "code": code, "errno": ctypes.c_int32(signal.si_errno).value,
"fault_address": hex(signal.sigfault_addr) if known else None,
"fault_address_meaning": "recorded-sigsegv-address" if known else "unknown", "thread_association": "unknown"})
return {"format": "elf-core", "architecture": "x86_64-little-endian", "target_execution": "not-performed", "live_process_identity": "unknown",
return {"format": "elf-core", "architecture": architecture, "target_execution": "not-performed", "live_process_identity": "unknown",
"segments": segments, "notes": notes, "note_padding": padding, "threads": threads, "signals": signals,
"note_interpretation_completeness": "unknown",
"limitations": ["The initial Linux note profile accepts System V/unspecified or GNU/Linux OSABI with ABI version zero; an unspecified OSABI alone does not prove operating-system origin.",

View File

@ -1,7 +1,10 @@
# Recorded crash evidence
`inspect_recorded_crash` / `rea inspect-recorded-crash` inspect a caller-selected
Linux ELF64 x86-64 little-endian core independently of an active binary target.
Linux ELF64 little-endian core independently of an active binary target. Two
architecture profiles are interpreted, x86-64 and AArch64; each binds registers
to its own `NT_PRSTATUS` layout, and any other `e_machine` is refused as
`unsupported_target` before note interpretation.
```sh
export REA_PWNTOOLS_PYTHON=/absolute/existing/venv/bin/python
@ -10,8 +13,8 @@ rea inspect-recorded-crash ./crash.core --json
The selected Python needs unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2.
REA does not install these dependencies. ELFFile/its original ELF structures and
pwntools' original Linux amd64 ctypes provide parsing and ABI layout. The adapter
adds bounds, representation preservation and lifecycle ownership.
pwntools' original Linux amd64 and aarch64 ctypes provide parsing and ABI layout.
The adapter adds bounds, representation preservation and lifecycle ownership.
The initial Linux note profile accepts System V/unspecified and GNU/Linux OSABI
with ABI version zero. Other declared ABIs return `unsupported_target` before
note interpretation; an unspecified OSABI alone does not prove OS origin.
@ -27,7 +30,8 @@ note interpretation; an unspecified OSABI alone does not prove OS origin.
- Every exact Linux CORE PRSTATUS note as a separate recorded thread, with
historical PID, signal and lossless uint64 registers bound to source bytes.
Each supported note has exactly one interpretation; each thread contains the
complete 27-register ABI set, with names bound to their original ABI offsets.
complete ABI register set for its architecture (27 registers for amd64,
34 for AArch64), with names bound to their original ABI offsets.
Physical note ranges cannot overlap or leave segment gaps. ELF and note headers
are independently bound to the snapshot, including extended program counts.
- Signed SIGINFO number/code/errno. Initially only SIGSEGV codes 1/2 establish the

View File

@ -30,6 +30,29 @@ it("labels an envelope containing debugger-derived maps as derived", async () =>
expect(evidence.raw_result).toBeNull();
});
it.each([
["x86_64-little-endian", "x86_64"],
["aarch64-little-endian", "arm64"],
] as const)(
"names the CPU family carried by a %s core on created Evidence",
async (architecture, expected) => {
const fixture = recordedCrashFixture("/selected.core", architecture);
const service = new RecordedCrashService({
inspectAndClose(input, options) {
return this.inspect(input, options);
},
identity: RECORDED_CRASH_TEST_PROVIDER,
inspect: () => Promise.resolve(ok(fixture)),
});
const result = await service.inspect({
path: fixture.artifact.path,
include_debugger_context: false,
});
if (!result.ok) throw result.error;
expect(parseEvidence(result.value).subject?.architecture).toBe(expected);
},
);
it.each([
{ path: "relative.elf" },
{ path: "/selected.core", approval: true },

View File

@ -10,6 +10,7 @@ import {
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
inspectRecordedCrashInputSchema,
RECORDED_LINUX_CORE_PROFILES,
recordedCrashSchema,
} from "../../domain/native/recordedCrash.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
@ -101,7 +102,9 @@ export class RecordedCrashService {
{
path: value.artifact.path,
format: "elf",
architecture: "x86_64",
architecture:
RECORDED_LINUX_CORE_PROFILES[value.architecture]
.evidenceArchitecture,
sha256: value.artifact.sha256,
},
this.provider.identity,

View File

@ -67,7 +67,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_recorded_crash"),
kind: "native-provider",
description:
"Inspect a supplied Linux ELF64 amd64 little-endian core without launching its target or attaching to any live PID. Returns all recorded segments, raw note owners/descriptors and source ranges, every Linux CORE PRSTATUS thread/register and SIGINFO fields inline with SHA-256 Evidence. Historical PID is recorded metadata; current process/file identity, note coverage and signal-thread associations stay unknown. Optionally add derived core-only GDB/pwndbg mapping candidates through explicit BYO configuration; zero map flags mean unknown permissions. Uses the same stable snapshot for both stages, bounded complete output and owned cleanup. Initial Linux x64 profile: unchanged pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2; optional unchanged pwndbg 2026.09.15. No implicit executable/library pairing, GDB command language, host configuration changes or dependency installation.",
"Inspect a supplied Linux ELF64 amd64 or AArch64 little-endian core without launching its target or attaching to any live PID. Returns all recorded segments, raw note owners/descriptors and source ranges, every Linux CORE PRSTATUS thread/register and SIGINFO fields inline with SHA-256 Evidence. Historical PID is recorded metadata; current process/file identity, note coverage and signal-thread associations stay unknown. Optionally add derived core-only GDB/pwndbg mapping candidates through explicit BYO configuration; zero map flags mean unknown permissions. Uses the same stable snapshot for both stages, bounded complete output and owned cleanup. Linux x64 host profile: unchanged pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2; optional unchanged pwndbg 2026.09.15. No implicit executable/library pairing, GDB command language, host configuration changes or dependency installation.",
inputSchema: inspectRecordedCrashInputSchema,
outputSchema: evidenceResultOf(recordedCrashSchema),
examples: [

View File

@ -1,7 +1,8 @@
import { expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import {
inspectRecordedCrashInputSchema,
recordedCrashSchema,
type RecordedCrash,
} from "./recordedCrash.js";
import {
recordedCrashFixture,
@ -282,3 +283,95 @@ it("keeps absent map flags unknown and validates reported permission bits", () =
}).success,
).toBe(false);
});
describe("recorded AArch64 core profile", () => {
it("accepts the complete AArch64 register window in ABI order", () => {
const value = recordedCrashSchema.parse(
recordedCrashFixture("/selected/aarch64.core", "aarch64-little-endian"),
);
expect(value.architecture).toBe("aarch64-little-endian");
const registers = value.threads[0]?.registers ?? [];
expect(registers.map(({ name }) => name)).toEqual([
...Array.from({ length: 31 }, (_, index) => `x${String(index)}`),
"sp",
"pc",
"pstate",
]);
expect(registers[14]?.value).toBe("0x1122334455667788");
expect(registers.every(({ location }) => location.bytes === "0x8")).toBe(
true,
);
});
it("keeps the AArch64 note ranges inside their recorded segment", () => {
const value = recordedCrashSchema.parse(
recordedCrashSignalFixture(undefined, "aarch64-little-endian"),
);
const note = value.notes[0];
const segment = value.segments[0];
if (note === undefined || segment === undefined)
throw new Error("Expected recorded notes");
expect(
BigInt(note.location.offset) + BigInt(note.location.bytes),
).toBeLessThanOrEqual(BigInt(segment.offset) + BigInt(segment.file_size));
expect(value.signals[0]?.fault_address).toBe("0x1122334455667788");
});
it.each([
[
"an amd64 register name inside an AArch64 window",
(v: RecordedCrash) => ({
...v,
threads: v.threads.map((thread) => ({
...thread,
registers: thread.registers.map((register, index) =>
index === 0 ? { ...register, name: "rax" } : register,
),
})),
}),
],
[
"the amd64 register count",
(v: RecordedCrash) => ({
...v,
threads: v.threads.map((thread) => ({
...thread,
registers: thread.registers.slice(0, 27),
})),
}),
],
[
"a 4-byte AArch64 register source",
(v: RecordedCrash) => ({
...v,
threads: v.threads.map((thread) => ({
...thread,
registers: thread.registers.map((register, index) =>
index === 0
? {
...register,
location: { ...register.location, bytes: "0x4" },
}
: register,
),
})),
}),
],
] as const)("rejects %s", (_name, change) => {
const value = recordedCrashFixture(
"/selected/aarch64.core",
"aarch64-little-endian",
);
expect(recordedCrashSchema.safeParse(change(value)).success).toBe(false);
});
it("rejects an architecture outside the interpreted profiles", () => {
const value = recordedCrashFixture();
expect(
recordedCrashSchema.safeParse({
...value,
architecture: "arm-little-endian",
}).success,
).toBe(false);
});
});

View File

@ -1,5 +1,6 @@
import { z } from "zod";
import type { BinaryArchitecture } from "../binaryTargetTypes.js";
import { localPathStringSchema } from "../localPath.js";
import { CANONICAL_BASE64_PATTERN } from "../stringPatterns.js";
import { recordedNoteCoverageIssues } from "./recordedCrashNoteCoverage.js";
@ -18,7 +19,7 @@ const diagnostics = z.strictObject({
const LINUX_CORE_OWNER_BASE64 = "Q09SRQA=";
/** Complete register identities in the supported Linux amd64 PRSTATUS ABI order. */
export const RECORDED_LINUX_AMD64_REGISTERS: readonly string[] = [
const RECORDED_LINUX_AMD64_REGISTERS: readonly string[] = [
"r15",
"r14",
"r13",
@ -47,7 +48,95 @@ export const RECORDED_LINUX_AMD64_REGISTERS: readonly string[] = [
"fs",
"gs",
];
const supportedRegisters = new Set(RECORDED_LINUX_AMD64_REGISTERS);
const RECORDED_LINUX_AARCH64_REGISTERS: readonly string[] = [
"x0",
"x1",
"x2",
"x3",
"x4",
"x5",
"x6",
"x7",
"x8",
"x9",
"x10",
"x11",
"x12",
"x13",
"x14",
"x15",
"x16",
"x17",
"x18",
"x19",
"x20",
"x21",
"x22",
"x23",
"x24",
"x25",
"x26",
"x27",
"x28",
"x29",
"x30",
"sp",
"pc",
"pstate",
];
/** Recorded-core architectures whose Linux note ABI is interpreted. */
export const RECORDED_LINUX_CORE_ARCHITECTURES = [
"x86_64-little-endian",
"aarch64-little-endian",
] as const;
export type RecordedCrashArchitecture =
(typeof RECORDED_LINUX_CORE_ARCHITECTURES)[number];
/**
* ABI facts for one recorded-core architecture, measured from unchanged pwntools
* 4.15.0 `elf_prstatus_*` and `user_regs_struct_*` structures and mirrored by
* `bridge/pwntools/recorded_crash_impl.py`. Both profiles place `pr_cursig` at
* 12, `pr_pid` at 32 and the register window at 112; the ELF machine, the window
* contents and the Evidence CPU family differ per profile.
*/
export const RECORDED_LINUX_CORE_PROFILES: Record<
RecordedCrashArchitecture,
{
readonly machine: number;
readonly evidenceArchitecture: BinaryArchitecture;
readonly registers: readonly string[];
readonly registerBytes: number;
readonly registerOffset: number;
readonly prstatusBytes: number;
}
> = {
"x86_64-little-endian": {
machine: 62,
evidenceArchitecture: "x86_64",
registers: RECORDED_LINUX_AMD64_REGISTERS,
registerBytes: 8,
registerOffset: 112,
prstatusBytes: 336,
},
"aarch64-little-endian": {
machine: 183,
evidenceArchitecture: "arm64",
registers: RECORDED_LINUX_AARCH64_REGISTERS,
registerBytes: 8,
registerOffset: 112,
prstatusBytes: 392,
},
};
const recordedCoreRegisterSets: Record<
RecordedCrashArchitecture,
ReadonlySet<string>
> = {
"x86_64-little-endian": new Set(RECORDED_LINUX_AMD64_REGISTERS),
"aarch64-little-endian": new Set(RECORDED_LINUX_AARCH64_REGISTERS),
};
/** Select an explicit recording and optionally request debugger-derived mapping context. */
export const inspectRecordedCrashInputSchema = z.strictObject({
@ -108,7 +197,7 @@ const recordedCrashObjectSchema = z.strictObject({
bytes: index,
}),
format: z.literal("elf-core"),
architecture: z.literal("x86_64-little-endian"),
architecture: z.enum(RECORDED_LINUX_CORE_ARCHITECTURES),
target_execution: z.literal("not-performed"),
live_process_identity: z.literal("unknown"),
segments: z.array(
@ -284,6 +373,8 @@ export const recordedCrashSchema = recordedCrashObjectSchema.superRefine(
"Each supported raw note requires exactly one corresponding interpretation.",
);
}
const profile = RECORDED_LINUX_CORE_PROFILES[value.architecture];
const profileRegisters = recordedCoreRegisterSets[value.architecture];
for (const thread of value.threads) {
const note = value.notes[thread.note_index];
if (
@ -295,8 +386,8 @@ export const recordedCrashSchema = recordedCrashObjectSchema.superRefine(
continue;
}
if (
thread.registers.length !== RECORDED_LINUX_AMD64_REGISTERS.length ||
!thread.registers.every(({ name }) => supportedRegisters.has(name)) ||
thread.registers.length !== profile.registers.length ||
!thread.registers.every(({ name }) => profileRegisters.has(name)) ||
new Set(thread.registers.map(({ name }) => name)).size !==
thread.registers.length
)
@ -305,7 +396,7 @@ export const recordedCrashSchema = recordedCrashObjectSchema.superRefine(
);
for (const register of thread.registers)
if (
BigInt(register.location.bytes) !== 8n ||
BigInt(register.location.bytes) !== BigInt(profile.registerBytes) ||
!within(
register.location,
BigInt(note.descriptor_location.offset),

View File

@ -0,0 +1,135 @@
import { describe, expect, it } from "vitest";
import {
RECORDED_LINUX_CORE_ARCHITECTURES,
RECORDED_LINUX_CORE_PROFILES,
} from "../../domain/native/recordedCrash.js";
import {
recordedCrashFixture,
recordedCrashFixtureBytes,
} from "../../../tests/fixtures/binaryDiagnostics/recordedCrash.js";
import { validateRecordedCrashSources } from "./RecordedCrashSourceBindings.js";
const diagnostics = { stdout: "", stderr: "", truncated: false } as const;
const bind = (
architecture?: "x86_64-little-endian" | "aarch64-little-endian",
) => {
const report = recordedCrashFixture("/selected.core", architecture);
return { report, bytes: recordedCrashFixtureBytes(report) };
};
describe("recorded core source bindings", () => {
it.each(RECORDED_LINUX_CORE_ARCHITECTURES)(
"binds each %s register to its own source bytes",
(architecture) => {
const { report, bytes } = bind(architecture);
expect(() =>
validateRecordedCrashSources(report, bytes, diagnostics),
).not.toThrow();
},
);
it("measures the AArch64 register window one slot wide per name", () => {
const profile = RECORDED_LINUX_CORE_PROFILES["aarch64-little-endian"];
expect(profile.registers.length).toBe(34);
expect(profile.prstatusBytes).toBe(392);
expect(profile.machine).toBe(183);
// The fixtures and the bindings both read these numbers, so they are pinned
// here against the ABI record rather than only used consistently.
expect(profile.registerOffset).toBe(112);
expect(profile.registerBytes).toBe(8);
const amd64 = RECORDED_LINUX_CORE_PROFILES["x86_64-little-endian"];
expect(amd64.registers.length).toBe(27);
expect(amd64.prstatusBytes).toBe(336);
expect(amd64.machine).toBe(62);
expect(amd64.registerOffset).toBe(112);
expect(amd64.registerBytes).toBe(8);
});
it("stamps each crafted core with the machine its profile declares", () => {
for (const architecture of RECORDED_LINUX_CORE_ARCHITECTURES) {
const bytes = bind(architecture).bytes;
expect(bytes.readUInt16LE(18)).toBe(
RECORDED_LINUX_CORE_PROFILES[architecture].machine,
);
}
});
it("rejects an AArch64 register bound to another slot", () => {
const { report, bytes } = bind("aarch64-little-endian");
const shifted = {
...report,
threads: report.threads.map((thread) => ({
...thread,
registers: thread.registers.map((register, index) =>
index === 3
? {
...register,
location: {
...register.location,
offset: `0x${(BigInt(register.location.offset) + 8n).toString(
16,
)}`,
},
}
: register,
),
})),
};
expect(() =>
validateRecordedCrashSources(shifted, bytes, diagnostics),
).toThrow(/aarch64-little-endian register identity/);
});
it("rejects an AArch64 note shorter than the profile descriptor", () => {
const { report, bytes } = bind("aarch64-little-endian");
const shortened = {
...report,
notes: report.notes.map((note, index) =>
index === 0
? {
...note,
descriptor_location: {
...note.descriptor_location,
bytes: "0x150",
},
}
: note,
),
};
expect(() =>
validateRecordedCrashSources(shortened, bytes, diagnostics),
).toThrow(/aarch64-little-endian source bytes/);
});
it("rejects an amd64 register name reported for AArch64", () => {
const { report, bytes } = bind("aarch64-little-endian");
const renamed = {
...report,
threads: report.threads.map((thread) => ({
...thread,
registers: thread.registers.map((register, index) =>
index === 0 ? { ...register, name: "rax" } : register,
),
})),
};
expect(() =>
validateRecordedCrashSources(renamed, bytes, diagnostics),
).toThrow(/aarch64-little-endian register identity/);
});
it("rejects a reported PID that differs from the recorded source bytes", () => {
const { report, bytes } = bind("aarch64-little-endian");
const wrongPid = {
...report,
threads: report.threads.map((thread) => ({
...thread,
historical_pid: thread.historical_pid + 1,
})),
};
expect(() =>
validateRecordedCrashSources(wrongPid, bytes, diagnostics),
).toThrow(/aarch64-little-endian source bytes/);
});
});

View File

@ -1,23 +1,26 @@
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import {
RECORDED_LINUX_AMD64_REGISTERS,
RECORDED_LINUX_CORE_PROFILES,
type RecordedCrash,
} from "../../domain/native/recordedCrash.js";
const registerOffsets = new Map<string, number>(
RECORDED_LINUX_AMD64_REGISTERS.map((name, index) => [name, 112 + index * 8]),
);
/** Bind interpreted values and register identities to Linux amd64 source bytes. */
/** Bind interpreted values and register identities to recorded source bytes. */
export function validateRecordedCrashSources(
report: RecordedCrash,
snapshot: Buffer,
diagnostics: RecordedCrash["decoder_diagnostics"],
): void {
const profile = RECORDED_LINUX_CORE_PROFILES[report.architecture];
const registerOffsets = new Map<string, number>(
profile.registers.map((name, index) => [
name,
profile.registerOffset + index * profile.registerBytes,
]),
);
const fail = () => {
throw new AnalysisOutputError(
"inspect_recorded_crash",
"Reported PID or signal value differs from its recorded Linux amd64 source bytes.",
`Reported PID or signal value differs from its recorded ${report.architecture} source bytes.`,
{ capturedOutput: diagnostics },
);
};
@ -29,10 +32,10 @@ export function validateRecordedCrashSources(
if (length < minimumBytes) return fail();
return snapshot.subarray(start, start + length);
};
// Fixed offsets independently checked against unchanged pwntools 4.15.0
// elf_prstatus_amd64 / elf_siginfo_64; this is the sole supported note ABI.
// Offsets come from unchanged pwntools 4.15.0 elf_prstatus_amd64 /
// elf_prstatus_aarch64 and elf_siginfo_64 through the shared domain profile.
for (const thread of report.threads) {
const bytes = descriptor(thread.note_index, 336);
const bytes = descriptor(thread.note_index, profile.prstatusBytes);
if (
thread.historical_pid !== bytes.readInt32LE(32) ||
thread.recorded_current_signal !== bytes.readInt16LE(12)
@ -49,7 +52,7 @@ export function validateRecordedCrashSources(
)
throw new AnalysisOutputError(
"inspect_recorded_crash",
"Reported register source does not match its Linux amd64 register identity.",
`Reported register source does not match its ${report.architecture} register identity.`,
{ capturedOutput: diagnostics },
);
}

View File

@ -1,5 +1,8 @@
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import type { RecordedCrash } from "../../domain/native/recordedCrash.js";
import {
RECORDED_LINUX_CORE_PROFILES,
type RecordedCrash,
} from "../../domain/native/recordedCrash.js";
// Exact amd64/core enum projections in the pinned pyelftools 0.33 profile.
const segmentKinds = new Map([
@ -81,6 +84,8 @@ export function validateRecordedCrashStructure(
return snapshot.subarray(Number(offset), Number(offset) + bytes);
};
const header = bytesAt(0n, 64);
const expectedMachine =
RECORDED_LINUX_CORE_PROFILES[report.architecture].machine;
if (
!header
.subarray(0, 6)
@ -90,7 +95,7 @@ export function validateRecordedCrashStructure(
![0, 3].includes(header[7] ?? -1) ||
header[8] !== 0 ||
header.readUInt16LE(16) !== 4 ||
header.readUInt16LE(18) !== 62
header.readUInt16LE(18) !== expectedMachine
)
fail();
const table = header.readBigUInt64LE(32);

View File

@ -4,6 +4,40 @@ import {
recordedCrashFixtureBytes,
} from "../../fixtures/binaryDiagnostics/recordedCrash.js";
import { validateRecordedCrashStructure } from "../../../src/native/pwntools/RecordedCrashStructureBindings.js";
import { RECORDED_LINUX_CORE_PROFILES } from "../../../src/domain/native/recordedCrash.js";
it.each(["x86_64-little-endian", "aarch64-little-endian"] as const)(
"admits a %s core whose header carries that profile's e_machine",
(architecture) => {
const report = recordedCrashFixture(undefined, architecture);
expect(() =>
validateRecordedCrashStructure(
report,
recordedCrashFixtureBytes(report),
report.decoder_diagnostics,
),
).not.toThrow();
},
);
it.each([
["x86_64-little-endian", "aarch64-little-endian"],
["aarch64-little-endian", "x86_64-little-endian"],
] as const)(
"rejects a %s report whose header carries the other profile's e_machine",
(reported, embedded) => {
const report = recordedCrashFixture(undefined, reported);
const snapshot = recordedCrashFixtureBytes(report);
snapshot.writeUInt16LE(RECORDED_LINUX_CORE_PROFILES[embedded].machine, 18);
expect(() =>
validateRecordedCrashStructure(
report,
snapshot,
report.decoder_diagnostics,
),
).toThrow(/Reported core structure differs/);
},
);
it.each([
[6, 0],

View File

@ -1,5 +1,8 @@
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import type { RecordedCrash } from "../../../src/domain/native/recordedCrash.js";
import type {
RecordedCrash,
RecordedCrashArchitecture,
} from "../../../src/domain/native/recordedCrash.js";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished } from "vitest";
@ -48,6 +51,7 @@ it
.runIf(host)
.each([
"success",
"success-aarch64",
"wrong-register",
"wrong-register-source",
"missing-thread",
@ -77,9 +81,13 @@ it
] 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(
true,
scenario === "wrong-owner-display",
architecture,
);
if (scenario.startsWith("wrong-ei-version-"))
bytes[6] = Number(scenario.at(-1));
@ -123,8 +131,9 @@ it
path,
include_debugger_context: false,
});
if (scenario === "success") {
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
@ -340,13 +349,17 @@ const expectOptionalContextFailure = (
});
};
const fixture = async (includeSignal = false, opaqueOwner = false) => {
const fixture = async (
includeSignal = false,
opaqueOwner = false,
architecture: RecordedCrashArchitecture = "x86_64-little-endian",
) => {
const workspace = await createTestWorkspace("rea-recorded-core-seam-");
onTestFinished(() => removeTestWorkspace(workspace.root));
const path = join(workspace.root, "recorded.core");
const value = includeSignal
? recordedCrashSignalFixture(path)
: recordedCrashFixture(path);
? recordedCrashSignalFixture(path, architecture)
: recordedCrashFixture(path, architecture);
if (opaqueOwner) {
value.notes = value.notes.map((note) =>
note.index === 0

View File

@ -1,17 +1,41 @@
import type { RecordedCrash } from "../../../src/domain/native/recordedCrash.js";
import {
RECORDED_LINUX_CORE_PROFILES,
type RecordedCrash,
type RecordedCrashArchitecture,
} from "../../../src/domain/native/recordedCrash.js";
// One note is a 12-byte header, the "CORE\0" owner padded to 8 bytes, then the
// descriptor. The first note starts at the end of the owned header and phdr.
const NOTE_START = 0x80;
const OWNER_START = NOTE_START + 12;
const DESCRIPTOR_START = OWNER_START + 8;
const NOTE_HEADER_BYTES = 20;
const hex = (value: number): string => `0x${value.toString(16)}`;
/** Source-owned observation seam; these bytes do not simulate a verified ELF engine. */
export const recordedCrashFixture = (
path = "/artifacts/source-owned.core",
architecture: RecordedCrashArchitecture = "x86_64-little-endian",
): RecordedCrash => {
const descriptor = Buffer.alloc(336);
const profile = RECORDED_LINUX_CORE_PROFILES[architecture];
const noteBytes = NOTE_HEADER_BYTES + profile.prstatusBytes;
const descriptor = Buffer.alloc(profile.prstatusBytes);
descriptor.writeInt32LE(123, 32);
descriptor.writeInt16LE(11, 12);
descriptor.writeBigUInt64LE(0x1122334455667788n, 224);
// One distinguishable register carries a full uint64 so retention is asserted.
descriptor.writeBigUInt64LE(
0x1122334455667788n,
profile.registerOffset + 14 * profile.registerBytes,
);
return {
artifact: { path, sha256: "a".repeat(64), bytes: 512 },
artifact: {
path,
sha256: "a".repeat(64),
bytes: Math.max(512, DESCRIPTOR_START + profile.prstatusBytes),
},
format: "elf-core",
architecture: "x86_64-little-endian",
architecture,
target_execution: "not-performed",
live_process_identity: "unknown",
segments: [
@ -20,7 +44,7 @@ export const recordedCrashFixture = (
type: "PT_NOTE",
header_location: { offset: "0x40", bytes: "0x38" },
offset: "0x80",
file_size: "0x164",
file_size: hex(noteBytes),
memory_size: "0x0",
virtual_address: "0x0",
physical_address: "0x0",
@ -34,11 +58,14 @@ export const recordedCrashFixture = (
index: 0,
segment_index: 0,
type: "NT_PRSTATUS",
location: { offset: "0x80", bytes: "0x164" },
owner_location: { offset: "0x8c", bytes: "0x5" },
location: { offset: hex(NOTE_START), bytes: hex(noteBytes) },
owner_location: { offset: hex(OWNER_START), bytes: "0x5" },
owner_bytes_base64: Buffer.from("CORE\0").toString("base64"),
owner_display: "CORE",
descriptor_location: { offset: "0x94", bytes: "0x150" },
descriptor_location: {
offset: hex(DESCRIPTOR_START),
bytes: hex(profile.prstatusBytes),
},
descriptor_bytes_base64: descriptor.toString("base64"),
},
],
@ -48,40 +75,16 @@ export const recordedCrashFixture = (
note_index: 0,
historical_pid: 123,
recorded_current_signal: 11,
registers: [
"r15",
"r14",
"r13",
"r12",
"rbp",
"rbx",
"r11",
"r10",
"r9",
"r8",
"rax",
"rcx",
"rdx",
"rsi",
"rdi",
"orig_rax",
"rip",
"cs",
"eflags",
"rsp",
"ss",
"fs_base",
"gs_base",
"ds",
"es",
"fs",
"gs",
].map((name, index) => ({
registers: profile.registers.map((name, index) => ({
name,
value: name === "rdi" ? "0x1122334455667788" : "0x0",
value: index === 14 ? "0x1122334455667788" : "0x0",
location: {
offset: `0x${(148 + 112 + index * 8).toString(16)}`,
bytes: "0x8",
offset: hex(
DESCRIPTOR_START +
profile.registerOffset +
index * profile.registerBytes,
),
bytes: hex(profile.registerBytes),
},
})),
},
@ -100,27 +103,38 @@ export const recordedCrashFixture = (
};
/** Source-owned SIGSEGV descriptor for scalar/source binding regressions. */
export const recordedCrashSignalFixture = (path?: string): RecordedCrash => {
const value = recordedCrashFixture(path);
export const recordedCrashSignalFixture = (
path?: string,
architecture: RecordedCrashArchitecture = "x86_64-little-endian",
): RecordedCrash => {
const value = recordedCrashFixture(path, architecture);
const profile = RECORDED_LINUX_CORE_PROFILES[architecture];
const descriptor = Buffer.alloc(32);
descriptor.writeInt32LE(11, 0);
descriptor.writeInt32LE(-2, 4);
descriptor.writeInt32LE(1, 8);
descriptor.writeBigUInt64LE(0x1122334455667788n, 16);
value.artifact.bytes = 640;
const prstatusNoteBytes = NOTE_HEADER_BYTES + profile.prstatusBytes;
const signalNoteStart = NOTE_START + prstatusNoteBytes;
const signalDescriptorStart = signalNoteStart + NOTE_HEADER_BYTES;
const signalNoteBytes = NOTE_HEADER_BYTES + 32;
value.artifact.bytes = Math.max(640, signalDescriptorStart + 32);
value.segments = value.segments.map((segment) => ({
...segment,
file_size: "0x198",
file_size: hex(prstatusNoteBytes + signalNoteBytes),
}));
value.notes.push({
index: 1,
segment_index: 0,
type: "NT_SIGINFO",
location: { offset: "0x1e4", bytes: "0x34" },
owner_location: { offset: "0x1f0", bytes: "0x5" },
location: { offset: hex(signalNoteStart), bytes: hex(signalNoteBytes) },
owner_location: { offset: hex(signalNoteStart + 12), bytes: "0x5" },
owner_bytes_base64: Buffer.from("CORE\0").toString("base64"),
owner_display: "CORE",
descriptor_location: { offset: "0x1f8", bytes: "0x20" },
descriptor_location: {
offset: hex(signalDescriptorStart),
bytes: "0x20",
},
descriptor_bytes_base64: descriptor.toString("base64"),
});
value.signals.push({
@ -141,7 +155,10 @@ export const recordedCrashFixtureBytes = (value: RecordedCrash): Buffer => {
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]);
bytes.writeUInt32LE(1, 20);
bytes.writeUInt16LE(4, 16);
bytes.writeUInt16LE(62, 18);
bytes.writeUInt16LE(
RECORDED_LINUX_CORE_PROFILES[value.architecture].machine,
18,
);
bytes.writeBigUInt64LE(64n, 32);
bytes.writeUInt16LE(56, 54);
bytes.writeUInt16LE(value.segments.length, 56);