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:
parent
2b98fc5f51
commit
522b9927c8
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue