From 462b73ac9b1caefcc4dcc0e6cbdc476a66e818a2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:54:47 +0800 Subject: [PATCH] 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 --- docs/recorded-crashes.md | 15 ++- scripts/lib/recorded-core-fixtures.py | 21 +++++ scripts/verify-recorded-crash.mjs | 16 ++++ .../RecordedCrashStructureBindings.ts | 22 ++++- .../RecordedCrashStructureBindings.test.ts | 94 +++++++++++++++++++ 5 files changed, 162 insertions(+), 6 deletions(-) diff --git a/docs/recorded-crashes.md b/docs/recorded-crashes.md index 7af17e7bbe..c16e12a2af 100644 --- a/docs/recorded-crashes.md +++ b/docs/recorded-crashes.md @@ -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 diff --git a/scripts/lib/recorded-core-fixtures.py b/scripts/lib/recorded-core-fixtures.py index ba65bb3cab..b897c4dda2 100644 --- a/scripts/lib/recorded-core-fixtures.py +++ b/scripts/lib/recorded-core-fixtures.py @@ -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) diff --git a/scripts/verify-recorded-crash.mjs b/scripts/verify-recorded-crash.mjs index c8eb29f1df..7aba409053 100644 --- a/scripts/verify-recorded-crash.mjs +++ b/scripts/verify-recorded-crash.mjs @@ -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++; diff --git a/src/native/pwntools/RecordedCrashStructureBindings.ts b/src/native/pwntools/RecordedCrashStructureBindings.ts index c1e2790b20..127c19788f 100644 --- a/src/native/pwntools/RecordedCrashStructureBindings.ts +++ b/src/native/pwntools/RecordedCrashStructureBindings.ts @@ -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> +> = { + "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, ): 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); diff --git a/tests/boundary/binaryDiagnostics/RecordedCrashStructureBindings.test.ts b/tests/boundary/binaryDiagnostics/RecordedCrashStructureBindings.test.ts index 9edfa061ce..c0295c7df9 100644 --- a/tests/boundary/binaryDiagnostics/RecordedCrashStructureBindings.test.ts +++ b/tests/boundary/binaryDiagnostics/RecordedCrashStructureBindings.test.ts @@ -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) => {