mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
feat(elf): decode Linux core dump process and memory mapping notes (#8830)
* feat(elf): decode Linux core dump process and memory mapping notes Inspect Linux core dump note tables and decode structured metadata without requiring external tools or debuggers: - NT_PRPSINFO (type 3): decode process ID, parent ID, process group, session, UID, GID, execution state, filename, and command line arguments bounded by the text budget. - NT_FILE (type 0x46494c45): decode mapped memory ranges (start_address, end_address, file_offset_pages) and mapped file paths with page size. - NT_SIGINFO (type 0x53494749): decode faulting signal number, errno, and code. - NT_AUXV (type 6): decode auxiliary vector pairs with known AT_* tag names. - Support both 32-bit and 64-bit, little-endian and big-endian targets. - Retain unknown core note types gracefully without partial success errors. - Add comprehensive unit tests, fixtures, acceptance tests, and documentation. * fix(elf): preserve raw path bytes and enforce auxv termination * fix(elf): account for path hex text budget and enforce auxv trailing padding * fix(elf): account for row and scan limits, ABI signal order, and lossless source IDs * fix(elf): cache auxv padding scans, pre-charge entries, and isolate page-size test
This commit is contained in:
1 parent
c3647ade1e
commit
e75d1c741c
7 files changed
+1149
-10
No files matched your search
@@ -8,6 +8,10 @@ import { readElfDirectoryFromBuffer } from "../metadata/NativeSectionDirectory.j
|
||||
import {
|
||||
compressedElfNoteFixture,
|
||||
compressedElfNoteSectionFixture,
|
||||
coreAuxvNote,
|
||||
coreFileNote,
|
||||
corePrpsinfoNote,
|
||||
coreSiginfoNote,
|
||||
elfNote,
|
||||
elfNoteFixture,
|
||||
elfNoteTablePastEnd,
|
||||
@@ -702,3 +706,377 @@ it.each([
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("decodes Linux core dump notes (NT_PRPSINFO, NT_FILE, NT_SIGINFO, NT_AUXV) for 64-bit little-endian", () => {
|
||||
const notes = Buffer.concat([
|
||||
corePrpsinfoNote({
|
||||
bits: 64,
|
||||
little: true,
|
||||
state: 0,
|
||||
sname: "R",
|
||||
pid: 4242,
|
||||
ppid: 1,
|
||||
pgrp: 4242,
|
||||
sid: 4242,
|
||||
uid: 1001,
|
||||
gid: 1002,
|
||||
fname: "test-proc",
|
||||
psargs: "/usr/bin/test-proc --daemon -v",
|
||||
}),
|
||||
coreFileNote(
|
||||
[
|
||||
{
|
||||
start: 0x400000n,
|
||||
end: 0x405000n,
|
||||
file_ofs: 0n,
|
||||
path: "/usr/bin/test-proc",
|
||||
},
|
||||
{
|
||||
start: 0x7fff0000n,
|
||||
end: 0x7fff2000n,
|
||||
file_ofs: 10n,
|
||||
path: "/lib/x86_64-linux-gnu/libc.so.6",
|
||||
},
|
||||
],
|
||||
4096,
|
||||
64,
|
||||
true,
|
||||
),
|
||||
coreSiginfoNote(11, 1, 0, true), // SIGSEGV, SEGV_MAPERR
|
||||
coreAuxvNote(
|
||||
[
|
||||
[6, 4096n], // AT_PAGESZ
|
||||
[3, 0x400040n], // AT_PHDR
|
||||
[9, 0x401000n], // AT_ENTRY
|
||||
[999, 0x1234n], // Unknown auxv tag
|
||||
],
|
||||
64,
|
||||
true,
|
||||
),
|
||||
]);
|
||||
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 64,
|
||||
little: true,
|
||||
type: 4, // ET_CORE
|
||||
ownership: "program",
|
||||
notes,
|
||||
});
|
||||
|
||||
const report = parse(bytes);
|
||||
expect(report.entries).toHaveLength(4);
|
||||
|
||||
// 1. NT_PRPSINFO
|
||||
expect(report.entries[0]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 3,
|
||||
type_name: "core_prpsinfo",
|
||||
gnu: null,
|
||||
core: {
|
||||
prpsinfo: {
|
||||
state: 0,
|
||||
pid: 4242,
|
||||
ppid: 1,
|
||||
pgrp: 4242,
|
||||
sid: 4242,
|
||||
uid: 1001,
|
||||
gid: 1002,
|
||||
filename: "test-proc",
|
||||
psargs: "/usr/bin/test-proc --daemon -v",
|
||||
},
|
||||
file: null,
|
||||
siginfo: null,
|
||||
auxv: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 2. NT_FILE
|
||||
expect(report.entries[1]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 0x46494c45,
|
||||
type_name: "core_file",
|
||||
gnu: null,
|
||||
core: {
|
||||
prpsinfo: null,
|
||||
file: {
|
||||
count: 2,
|
||||
page_size: 4096,
|
||||
mappings: [
|
||||
{
|
||||
start_address: "0x400000",
|
||||
end_address: "0x405000",
|
||||
file_offset_pages: "0x0",
|
||||
path: "/usr/bin/test-proc",
|
||||
path_hex: Buffer.from("/usr/bin/test-proc").toString("hex"),
|
||||
},
|
||||
{
|
||||
start_address: "0x7fff0000",
|
||||
end_address: "0x7fff2000",
|
||||
file_offset_pages: "0xa",
|
||||
path: "/lib/x86_64-linux-gnu/libc.so.6",
|
||||
path_hex: Buffer.from("/lib/x86_64-linux-gnu/libc.so.6").toString(
|
||||
"hex",
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
siginfo: null,
|
||||
auxv: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 3. NT_SIGINFO
|
||||
expect(report.entries[2]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 0x53494749,
|
||||
type_name: "core_siginfo",
|
||||
gnu: null,
|
||||
core: {
|
||||
prpsinfo: null,
|
||||
file: null,
|
||||
siginfo: {
|
||||
signo: 11,
|
||||
errno: 0,
|
||||
code: 1,
|
||||
},
|
||||
auxv: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 4. NT_AUXV
|
||||
expect(report.entries[3]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 6,
|
||||
type_name: "core_auxv",
|
||||
gnu: null,
|
||||
core: {
|
||||
prpsinfo: null,
|
||||
file: null,
|
||||
siginfo: null,
|
||||
auxv: [
|
||||
{ type: 6, type_name: "AT_PAGESZ", value: "0x1000" },
|
||||
{ type: 3, type_name: "AT_PHDR", value: "0x400040" },
|
||||
{ type: 9, type_name: "AT_ENTRY", value: "0x401000" },
|
||||
{ type: 999, type_name: null, value: "0x1234" },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("decodes Linux core dump notes for 32-bit big-endian", () => {
|
||||
const notes = Buffer.concat([
|
||||
corePrpsinfoNote({
|
||||
bits: 32,
|
||||
little: false,
|
||||
state: 1,
|
||||
pid: 123,
|
||||
ppid: 1,
|
||||
pgrp: 123,
|
||||
sid: 123,
|
||||
uid: 500,
|
||||
gid: 500,
|
||||
fname: "proc32",
|
||||
psargs: "proc32 arg",
|
||||
}),
|
||||
coreFileNote(
|
||||
[
|
||||
{
|
||||
start: 0x10000,
|
||||
end: 0x12000,
|
||||
file_ofs: 0,
|
||||
path: "/bin/proc32",
|
||||
},
|
||||
],
|
||||
4096,
|
||||
32,
|
||||
false,
|
||||
),
|
||||
coreAuxvNote(
|
||||
[
|
||||
[6, 4096],
|
||||
[11, 500],
|
||||
],
|
||||
32,
|
||||
false,
|
||||
),
|
||||
]);
|
||||
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 32,
|
||||
little: false,
|
||||
type: 4,
|
||||
ownership: "program",
|
||||
notes,
|
||||
});
|
||||
|
||||
const report = parse(bytes);
|
||||
expect(report.entries).toHaveLength(3);
|
||||
expect(report.entries[0]?.core?.prpsinfo).toMatchObject({
|
||||
state: 1,
|
||||
pid: 123,
|
||||
uid: 500,
|
||||
gid: 500,
|
||||
filename: "proc32",
|
||||
psargs: "proc32 arg",
|
||||
});
|
||||
expect(report.entries[1]?.core?.file).toMatchObject({
|
||||
count: 1,
|
||||
page_size: 4096,
|
||||
mappings: [
|
||||
{
|
||||
start_address: "0x10000",
|
||||
end_address: "0x12000",
|
||||
file_offset_pages: "0x0",
|
||||
path: "/bin/proc32",
|
||||
path_hex: Buffer.from("/bin/proc32").toString("hex"),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(report.entries[2]?.core?.auxv).toEqual([
|
||||
{ type: 6, type_name: "AT_PAGESZ", value: "0x1000" },
|
||||
{ type: 11, type_name: "AT_UID", value: "0x1f4" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("tolerates malformed core notes by returning null without throwing", () => {
|
||||
const truncatedPrpsinfo = elfNote("CORE", 3, Buffer.alloc(20)); // requires 136 for 64-bit
|
||||
const truncatedFile = elfNote(
|
||||
"CORE",
|
||||
0x46494c45,
|
||||
Buffer.from("00000000000000010000000000001000", "hex"), // count 1, page_size 4096, but no map entries or filenames
|
||||
);
|
||||
const truncatedSiginfo = elfNote("CORE", 0x53494749, Buffer.alloc(4)); // requires 12 bytes
|
||||
const unterminatedAuxv = elfNote(
|
||||
"CORE",
|
||||
6,
|
||||
Buffer.from("00000000000000060000000000001000", "hex"), // AT_PAGESZ=4096 but lacks AT_NULL terminator
|
||||
);
|
||||
const truncatedAuxv = elfNote("CORE", 6, Buffer.alloc(7)); // not multiple of entry size
|
||||
const notes = Buffer.concat([
|
||||
truncatedPrpsinfo,
|
||||
truncatedFile,
|
||||
truncatedSiginfo,
|
||||
unterminatedAuxv,
|
||||
truncatedAuxv,
|
||||
]);
|
||||
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 64,
|
||||
ownership: "program",
|
||||
notes,
|
||||
});
|
||||
|
||||
const report = parse(bytes);
|
||||
expect(report.entries).toHaveLength(5);
|
||||
expect(report.entries[0]?.core).toBeNull();
|
||||
expect(report.entries[0]?.type_name).toBe("core_prpsinfo");
|
||||
expect(report.entries[1]?.core).toBeNull();
|
||||
expect(report.entries[1]?.type_name).toBe("core_file");
|
||||
expect(report.entries[2]?.core).toBeNull();
|
||||
expect(report.entries[2]?.type_name).toBe("core_siginfo");
|
||||
expect(report.entries[3]?.core).toBeNull();
|
||||
expect(report.entries[3]?.type_name).toBe("core_auxv");
|
||||
expect(report.entries[4]?.core).toBeNull();
|
||||
expect(report.entries[4]?.type_name).toBe("core_auxv");
|
||||
});
|
||||
|
||||
it("decodes siginfo using target ABI for MIPS machines", () => {
|
||||
const desc = Buffer.alloc(128);
|
||||
desc.writeInt32LE(11, 0); // signo = SIGSEGV
|
||||
desc.writeInt32LE(1, 4); // code = SEGV_MAPERR (offset 4 on MIPS)
|
||||
desc.writeInt32LE(0, 8); // errno = 0 (offset 8 on MIPS)
|
||||
const mipsSiginfo = elfNote("CORE", 0x53494749, desc, true);
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 32,
|
||||
machine: 8, // EM_MIPS
|
||||
ownership: "program",
|
||||
notes: mipsSiginfo,
|
||||
});
|
||||
const report = parse(bytes);
|
||||
expect(report.entries[0]?.core?.siginfo).toEqual({
|
||||
signo: 11,
|
||||
code: 1,
|
||||
errno: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves negative process IDs and signal numbers without rewriting to zero", () => {
|
||||
const prps = corePrpsinfoNote({
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
pgrp: -1,
|
||||
sid: -1,
|
||||
});
|
||||
const sig = coreSiginfoNote(-1, -2, -3);
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 64,
|
||||
ownership: "program",
|
||||
notes: Buffer.concat([prps, sig]),
|
||||
});
|
||||
const report = parse(bytes);
|
||||
expect(report.entries[0]?.core?.prpsinfo).toMatchObject({
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
pgrp: -1,
|
||||
sid: -1,
|
||||
});
|
||||
expect(report.entries[1]?.core?.siginfo).toMatchObject({
|
||||
signo: -1,
|
||||
code: -2,
|
||||
errno: -3,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects 64-bit auxv tags and out-of-range page sizes with core: null without schema error", () => {
|
||||
const validAuxv = coreAuxvNote([[6, 4096n]], 64, true);
|
||||
const wideTagAuxv = Buffer.from(validAuxv);
|
||||
wideTagAuxv.writeBigUInt64LE(0x100000000n, 20);
|
||||
|
||||
const validFile = coreFileNote(
|
||||
[
|
||||
{
|
||||
start: 0x400000n,
|
||||
end: 0x401000n,
|
||||
file_ofs: 0n,
|
||||
path: "/bin/app",
|
||||
},
|
||||
],
|
||||
4096,
|
||||
64,
|
||||
true,
|
||||
);
|
||||
const widePageSizeFile = Buffer.from(validFile);
|
||||
widePageSizeFile.writeBigUInt64LE(0x100000000n, 28);
|
||||
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 64,
|
||||
ownership: "program",
|
||||
notes: Buffer.concat([wideTagAuxv, widePageSizeFile]),
|
||||
});
|
||||
const report = parse(bytes);
|
||||
expect(report.entries[0]?.core).toBeNull();
|
||||
expect(report.entries[1]?.core).toBeNull();
|
||||
});
|
||||
|
||||
it("does not spend text budget on malformed or discarded NT_FILE paths", () => {
|
||||
const header = Buffer.alloc(16);
|
||||
header.writeBigUInt64LE(2n, 0); // count = 2
|
||||
header.writeBigUInt64LE(4096n, 8); // page_size = 4096
|
||||
const mappings = Buffer.alloc(48); // 2 mappings * 24 bytes
|
||||
const paths = Buffer.from(
|
||||
"/bin/app\0unterminated_path_without_null",
|
||||
"latin1",
|
||||
);
|
||||
const badFileNote = elfNote(
|
||||
"CORE",
|
||||
0x46494c45,
|
||||
Buffer.concat([header, mappings, paths]),
|
||||
);
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 64,
|
||||
ownership: "program",
|
||||
notes: badFileNote,
|
||||
});
|
||||
const report = parse(bytes);
|
||||
expect(report.entries[0]?.core).toBeNull();
|
||||
});
|
||||
Reference in new issue
Block a user