mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(native): bind core segment names to their architecture
Validate recorded-core program-header projections against the selected pyelftools architecture table after binding e_machine to the snapshot. Preserve numeric unknowns and reject names borrowed from another machine or inconsistent header bytes. Add source-binding malformed controls and authored auxiliary-header carriers to the existing real CLI/MCP verification lane. Document pinned parser semantics separately from payload interpretation and real crash coverage. Fixes #6538
This commit is contained in:
@@ -25,6 +25,11 @@ note interpretation; an unspecified OSABI alone does not prove OS origin.
|
||||
are inline through CLI and MCP.
|
||||
- Original program headers and recorded segment file/memory ranges. Undumped
|
||||
memory and current executable/library identity remain unknown.
|
||||
Header type names follow the pinned parser's machine-specific projection:
|
||||
`PT_AARCH64_ARCHEXT` and `PT_AARCH64_UNWIND` denote `0x70000000` and
|
||||
`0x70000001` only for AArch64. The same codes on x86-64 remain numeric, as
|
||||
does AArch64's `0x70000002` in pyelftools 0.33. Accepting these exact header
|
||||
identities does not decode architecture compatibility, unwind or MTE payloads.
|
||||
- Every note's original identity, owner/descriptor bytes and source ranges,
|
||||
including unfamiliar owners and note kinds.
|
||||
- Every exact Linux CORE PRSTATUS note as a separate recorded thread, with
|
||||
@@ -105,8 +110,14 @@ GDB/Python/pwndbg/strace. Fixture generation explicitly launches its owned test
|
||||
program; subsequent inspection only reads the resulting core. Real acceptance
|
||||
checks recorded threads, high registers, missing/malformed notes, opaque owners,
|
||||
CLI/MCP parity, optional debugger context and historical PID collision with an
|
||||
owned live sentinel. An observed Node thread group must launch the exact ownership
|
||||
`ps` inventory before it can read that live process's `stat` or `status` for native
|
||||
owned live sentinel. Authored AArch64 auxiliary-header and numeric-unknown
|
||||
carriers also pass through the configured decoder and both public adapters;
|
||||
they are metadata fixtures, rather than captured AArch64 crashes. Their expected
|
||||
names follow [pyelftools 0.33's enums](https://github.com/eliben/pyelftools/blob/v0.33/elftools/elf/enums.py#L431-L435)
|
||||
and [machine-specific selection](https://github.com/eliben/pyelftools/blob/v0.33/elftools/elf/structs.py#L179-L184).
|
||||
The [AArch64 ELF ABI](https://github.com/ARM-software/abi-aa/blob/2025Q1/aaelf64/aaelf64.rst#program-header)
|
||||
defines their raw codes. An observed Node thread group must launch the exact
|
||||
ownership `ps` inventory before it can read that live process's `stat` or `status` for native
|
||||
identity/UID facts. Only traced `CLONE_THREAD` members share this allowance;
|
||||
exec-replaced threads, ordinary children, debugger and decoder processes cannot
|
||||
use it. The purpose of stat/status reads within the same Node thread group
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from elftools.elf.structs import ELFStructs
|
||||
from elftools.construct import Container
|
||||
from pwnlib.elf.datatypes import elf_prstatus_amd64
|
||||
|
||||
@@ -31,6 +32,26 @@ def note_header(content, note, **changes):
|
||||
content[note.n_offset:note.n_offset + len(encoded)] = encoded
|
||||
|
||||
|
||||
# Authored metadata carriers exercise actual parser projection through public
|
||||
# inspection, independently of the host that produced the captured amd64 core.
|
||||
for name, machine, kind in [
|
||||
("aarch64-archext", "EM_AARCH64", "PT_AARCH64_ARCHEXT"),
|
||||
("aarch64-unwind", "EM_AARCH64", "PT_AARCH64_UNWIND"),
|
||||
("aarch64-mte-opaque", "EM_AARCH64", 0x70000002),
|
||||
("amd64-processor-opaque", "EM_X86_64", 0x70000001),
|
||||
]:
|
||||
structs = ELFStructs(little_endian=True, elfclass=64)
|
||||
structs.create_basic_structs()
|
||||
structs.create_advanced_structs("ET_CORE", machine, image.header.e_ident.EI_OSABI)
|
||||
header_bytes, entry_bytes = structs.Elf_Ehdr.sizeof(), structs.Elf_Phdr.sizeof()
|
||||
header = Container(**dict(image.header, e_machine=machine, e_entry=0, e_ehsize=header_bytes,
|
||||
e_phoff=header_bytes, e_phentsize=entry_bytes, e_phnum=1,
|
||||
e_shoff=0, e_shentsize=64, e_shnum=0, e_shstrndx=0, e_flags=0))
|
||||
segment = Container(p_type=kind, p_flags=4, p_offset=header_bytes + entry_bytes,
|
||||
p_vaddr=0, p_paddr=0, p_filesz=4, p_memsz=4, p_align=4)
|
||||
save(name, structs.Elf_Ehdr.build(header) + structs.Elf_Phdr.build(segment) + bytes(4))
|
||||
|
||||
|
||||
save("truncated-header", original[:32])
|
||||
for name, osabi, version in [("linux-osabi", 3, 0), ("solaris-osabi", 6, 0), ("freebsd-osabi", 9, 0), ("unsupported-abi-version", 0, 1)]:
|
||||
content = bytearray(original)
|
||||
|
||||
@@ -189,6 +189,22 @@ try {
|
||||
assert.equal(noNotes.threads.length, 0);
|
||||
assert.equal(noNotes.note_interpretation_completeness, "unknown");
|
||||
cases++;
|
||||
for (const [name, architecture, type] of [
|
||||
["aarch64-archext", "aarch64-little-endian", "PT_AARCH64_ARCHEXT"],
|
||||
["aarch64-unwind", "aarch64-little-endian", "PT_AARCH64_UNWIND"],
|
||||
["aarch64-mte-opaque", "aarch64-little-endian", 0x70000002],
|
||||
["amd64-processor-opaque", "x86_64-little-endian", 0x70000001],
|
||||
]) {
|
||||
const auxiliary = await basic.inspect(mode, fixtures[name]);
|
||||
assert.equal(auxiliary.architecture, architecture);
|
||||
assert.equal(auxiliary.segments.length, 1);
|
||||
assert.equal(auxiliary.segments[0].type, type);
|
||||
assert.equal(auxiliary.notes.length, 0);
|
||||
assert.equal(auxiliary.threads.length, 0);
|
||||
assert.equal(auxiliary.note_interpretation_completeness, "unknown");
|
||||
assert.equal(auxiliary.live_process_identity, "unknown");
|
||||
cases++;
|
||||
}
|
||||
const negative = await basic.inspect(mode, fixtures["negative-pid"]);
|
||||
assert.ok(negative.threads.some((thread) => thread.historical_pid === -2));
|
||||
cases++;
|
||||
|
||||
@@ -2,10 +2,12 @@ import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import {
|
||||
RECORDED_LINUX_CORE_PROFILES,
|
||||
type RecordedCrash,
|
||||
type RecordedCrashArchitecture,
|
||||
} from "../../domain/native/recordedCrash.js";
|
||||
|
||||
// Exact amd64/core enum projections in the pinned pyelftools 0.33 profile.
|
||||
const segmentKinds = new Map([
|
||||
// Base plus machine-specific projections from pinned pyelftools 0.33. A name
|
||||
// establishes the original p_type only in the architecture that emits it.
|
||||
const baseSegmentKinds = new Map([
|
||||
[0, "PT_NULL"],
|
||||
[1, "PT_LOAD"],
|
||||
[2, "PT_DYNAMIC"],
|
||||
@@ -21,6 +23,16 @@ const segmentKinds = new Map([
|
||||
[0x6474e552, "PT_GNU_RELRO"],
|
||||
[0x6474e553, "PT_GNU_PROPERTY"],
|
||||
]);
|
||||
const segmentKinds: Readonly<
|
||||
Record<RecordedCrashArchitecture, ReadonlyMap<number, string>>
|
||||
> = {
|
||||
"x86_64-little-endian": baseSegmentKinds,
|
||||
"aarch64-little-endian": new Map([
|
||||
...baseSegmentKinds,
|
||||
[0x70000000, "PT_AARCH64_ARCHEXT"],
|
||||
[0x70000001, "PT_AARCH64_UNWIND"],
|
||||
]),
|
||||
};
|
||||
const noteKinds = new Map([
|
||||
[1, "NT_PRSTATUS"],
|
||||
[2, "NT_FPREGSET"],
|
||||
@@ -44,10 +56,11 @@ const matchesKind = (
|
||||
const segmentMatches = (
|
||||
segment: RecordedCrash["segments"][number],
|
||||
raw: Buffer,
|
||||
kinds: ReadonlyMap<number, string>,
|
||||
): boolean => {
|
||||
const type = raw.readUInt32LE(0);
|
||||
return (
|
||||
matchesKind(type, segment.type, segmentKinds) &&
|
||||
matchesKind(type, segment.type, kinds) &&
|
||||
BigInt(segment.flags) === BigInt(raw.readUInt32LE(4)) &&
|
||||
segment.file_backing ===
|
||||
(type === 0 || raw.readBigUInt64LE(32) === 0n ? "none" : "file") &&
|
||||
@@ -115,7 +128,8 @@ export function validateRecordedCrashStructure(
|
||||
)
|
||||
fail();
|
||||
const raw = bytesAt(offset, entryBytes);
|
||||
if (!segmentMatches(segment, raw)) fail();
|
||||
if (!segmentMatches(segment, raw, segmentKinds[report.architecture]))
|
||||
fail();
|
||||
}
|
||||
for (const note of report.notes) {
|
||||
const raw = bytesAt(BigInt(note.location.offset), 12);
|
||||
|
||||
@@ -10,8 +10,102 @@ import { validateRecordedCrashSources } from "../../../src/native/pwntools/Recor
|
||||
import {
|
||||
recordedCrashSchema,
|
||||
RECORDED_LINUX_CORE_PROFILES,
|
||||
type RecordedCrashArchitecture,
|
||||
} from "../../../src/domain/native/recordedCrash.js";
|
||||
|
||||
/** Header-only metadata carrier; no thread, unwind, or MTE payload is decoded. */
|
||||
const auxiliaryHeader = (
|
||||
architecture: RecordedCrashArchitecture,
|
||||
rawType: number,
|
||||
reportedType: string | number,
|
||||
) => {
|
||||
const report = recordedCrashFixture(undefined, architecture);
|
||||
report.segments = report.segments.map((segment) => ({
|
||||
...segment,
|
||||
type: reportedType,
|
||||
file_size: "0x4",
|
||||
memory_size: "0x4",
|
||||
}));
|
||||
report.notes = [];
|
||||
report.threads = [];
|
||||
const snapshot = recordedCrashFixtureBytes(report);
|
||||
snapshot.writeUInt32LE(rawType, 64);
|
||||
return { report: recordedCrashSchema.parse(report), snapshot };
|
||||
};
|
||||
|
||||
it.each([
|
||||
["aarch64-little-endian", 0x70000000, "PT_AARCH64_ARCHEXT"],
|
||||
["aarch64-little-endian", 0x70000001, "PT_AARCH64_UNWIND"],
|
||||
// The ABI names MTE, but pinned pyelftools still reports its numeric type.
|
||||
["aarch64-little-endian", 0x70000002, 0x70000002],
|
||||
["aarch64-little-endian", 0x70000003, 0x70000003],
|
||||
["x86_64-little-endian", 0x70000000, 0x70000000],
|
||||
["x86_64-little-endian", 0x70000001, 0x70000001],
|
||||
["aarch64-little-endian", 0x6474e551, "PT_GNU_STACK"],
|
||||
["x86_64-little-endian", 0x6474e551, "PT_GNU_STACK"],
|
||||
] as const)(
|
||||
"binds %s program type %s to its exact parser projection %s",
|
||||
(architecture, rawType, reportedType) => {
|
||||
const { report, snapshot } = auxiliaryHeader(
|
||||
architecture,
|
||||
rawType,
|
||||
reportedType,
|
||||
);
|
||||
const original = Buffer.from(snapshot);
|
||||
expect(() =>
|
||||
validateRecordedCrashStructure(
|
||||
report,
|
||||
snapshot,
|
||||
report.decoder_diagnostics,
|
||||
),
|
||||
).not.toThrow();
|
||||
expect(snapshot).toEqual(original);
|
||||
expect(report.note_interpretation_completeness).toBe("unknown");
|
||||
expect(report.live_process_identity).toBe("unknown");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["aarch64-little-endian", 0x70000000, "PT_AARCH64_UNWIND"],
|
||||
["aarch64-little-endian", 0x70000001, "PT_AARCH64_ARCHEXT"],
|
||||
["x86_64-little-endian", 0x70000000, "PT_AARCH64_ARCHEXT"],
|
||||
["x86_64-little-endian", 0x70000001, "PT_AARCH64_UNWIND"],
|
||||
["aarch64-little-endian", 0x70000001, 0x70000001],
|
||||
["aarch64-little-endian", 0x70000002, "PT_AARCH64_MEMTAG_MTE"],
|
||||
] as const)(
|
||||
"rejects %s program type %s reported as %s outside its pinned projection",
|
||||
(architecture, rawType, reportedType) => {
|
||||
const { report, snapshot } = auxiliaryHeader(
|
||||
architecture,
|
||||
rawType,
|
||||
reportedType,
|
||||
);
|
||||
expect(() =>
|
||||
validateRecordedCrashStructure(
|
||||
report,
|
||||
snapshot,
|
||||
report.decoder_diagnostics,
|
||||
),
|
||||
).toThrow(/Reported core structure differs/);
|
||||
},
|
||||
);
|
||||
|
||||
it("still binds the remaining AArch64 auxiliary header fields to their source", () => {
|
||||
const { report, snapshot } = auxiliaryHeader(
|
||||
"aarch64-little-endian",
|
||||
0x70000001,
|
||||
"PT_AARCH64_UNWIND",
|
||||
);
|
||||
snapshot.writeUInt32LE(4, 64 + 4);
|
||||
expect(() =>
|
||||
validateRecordedCrashStructure(
|
||||
report,
|
||||
snapshot,
|
||||
report.decoder_diagnostics,
|
||||
),
|
||||
).toThrow(/Reported core structure differs/);
|
||||
});
|
||||
|
||||
it.each(["x86_64-little-endian", "aarch64-little-endian"] as const)(
|
||||
"admits a %s core whose header carries that profile's e_machine",
|
||||
(architecture) => {
|
||||
|
||||
Reference in New Issue
Block a user