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
+7
-1
@@ -38,6 +38,12 @@ were examined; an unavailable section name does not erase those note facts.
|
||||
For GNU notes, `gnu.build_id_hex`, `gnu.abi_tag`, or `gnu.gold_version` are
|
||||
filled when the descriptor matches that type. Other owners keep `gnu` null.
|
||||
|
||||
For Linux `CORE` notes, `core.prpsinfo`, `core.file`, `core.siginfo`, or
|
||||
`core.auxv` are decoded for process arguments and status (`NT_PRPSINFO`), mapped
|
||||
file memory regions and paths (`NT_FILE`), fault signal and code (`NT_SIGINFO`), and
|
||||
auxiliary vectors (`NT_AUXV`) across 32-bit and 64-bit images. Other owners or unparsed
|
||||
notes keep `core` null.
|
||||
|
||||
`PT_NOTE` and `SHT_NOTE` are examined independently. The same file range may
|
||||
appear twice when both tables declare it.
|
||||
|
||||
@@ -48,7 +54,7 @@ the image never carried notes in another container or overlay.
|
||||
|
||||
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
|
||||
32 note entries are examined by default across every table. Retained note
|
||||
names, section names, and GNU descriptor text share a scan budget based on
|
||||
names, section names, GNU descriptor text, and core dump string representations share a scan budget based on
|
||||
the source bytes plus any decoded section-name table, capped at
|
||||
`max_file_bytes`. The raw source and decoded table must also fit together
|
||||
within that ceiling. If admission fails, the error reports the required
|
||||
|
||||
@@ -111,7 +111,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
|
||||
...toolContractMetadata("inspect_elf_notes"),
|
||||
kind: "native-provider",
|
||||
description:
|
||||
"Inspect one explicit local ELF32/ELF64 image's PT_NOTE program headers and SHT_NOTE sections without executing it or opening a disassembler. Returns note-table file ranges, owners, types, descriptor SHA-256 and hex previews, plus GNU build-id, ABI-tag and gold-version fields when those descriptors match. A missing table is observed absence of those declarations, not proof the image never carried notes in another container. Reads a stable regular file; malformed ELF headers, truncated notes and exhausted budgets return no partial success. Portable on supported Node hosts; no debug package or catalog is required.",
|
||||
"Inspect one explicit local ELF32/ELF64 image's PT_NOTE program headers and SHT_NOTE sections without executing it or opening a disassembler. Returns note-table file ranges, owners, types, descriptor SHA-256 and hex previews, plus GNU build-id, ABI-tag and gold-version fields and Linux CORE process, mapping, signal, and auxv notes when those descriptors match. A missing table is observed absence of those declarations, not proof the image never carried notes in another container. Reads a stable regular file; malformed ELF headers, truncated notes and exhausted budgets return no partial success. Portable on supported Node hosts; no debug package or catalog is required.",
|
||||
inputSchema: inspectElfNotesInputSchema,
|
||||
outputSchema: evidenceResultOf(elfNotesSchema),
|
||||
examples: [
|
||||
|
||||
@@ -17,6 +17,10 @@ export const ELF_NOTE_TYPE_NAMES = [
|
||||
"gnu_build_id",
|
||||
"gnu_gold_version",
|
||||
"gnu_property",
|
||||
"core_prpsinfo",
|
||||
"core_file",
|
||||
"core_siginfo",
|
||||
"core_auxv",
|
||||
"unknown",
|
||||
] as const;
|
||||
export type ElfNoteTypeName = (typeof ELF_NOTE_TYPE_NAMES)[number];
|
||||
@@ -64,6 +68,22 @@ export const gnuNoteTypeName = (type: number): ElfNoteTypeName => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Map CORE n_type when the owner is CORE; other types stay unknown. */
|
||||
export const coreNoteTypeName = (type: number): ElfNoteTypeName => {
|
||||
switch (type) {
|
||||
case 3:
|
||||
return "core_prpsinfo";
|
||||
case 6:
|
||||
return "core_auxv";
|
||||
case 0x46494c45:
|
||||
return "core_file";
|
||||
case 0x53494749:
|
||||
return "core_siginfo";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
/** Complete bounded inspection of ELF PT_NOTE and SHT_NOTE tables. */
|
||||
export const inspectElfNotesInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
@@ -95,6 +115,51 @@ const gnuAbiTagSchema = z.strictObject({
|
||||
subminor: uint,
|
||||
});
|
||||
|
||||
const coreFileMappingSchema = z.strictObject({
|
||||
start_address: z.string().regex(/^0x[0-9a-f]+$/),
|
||||
end_address: z.string().regex(/^0x[0-9a-f]+$/),
|
||||
file_offset_pages: z.string().regex(/^0x[0-9a-f]+$/),
|
||||
path: z.string(),
|
||||
path_hex: z.string().regex(/^[0-9a-f]*$/),
|
||||
});
|
||||
|
||||
const coreFileSchema = z.strictObject({
|
||||
count: uint,
|
||||
page_size: uint,
|
||||
mappings: z.array(coreFileMappingSchema),
|
||||
});
|
||||
|
||||
const corePrpsinfoSchema = z.strictObject({
|
||||
state: uint,
|
||||
pid: z.number().int(),
|
||||
ppid: z.number().int(),
|
||||
pgrp: z.number().int(),
|
||||
sid: z.number().int(),
|
||||
uid: uint,
|
||||
gid: uint,
|
||||
filename: z.string(),
|
||||
psargs: z.string(),
|
||||
});
|
||||
|
||||
const coreSiginfoSchema = z.strictObject({
|
||||
signo: z.number().int(),
|
||||
code: z.number().int(),
|
||||
errno: z.number().int(),
|
||||
});
|
||||
|
||||
const coreAuxvEntrySchema = z.strictObject({
|
||||
type: uint,
|
||||
type_name: z.string().nullable(),
|
||||
value: z.string().regex(/^0x[0-9a-f]+$/),
|
||||
});
|
||||
|
||||
export const coreNoteSchema = z.strictObject({
|
||||
prpsinfo: corePrpsinfoSchema.nullable(),
|
||||
file: coreFileSchema.nullable(),
|
||||
siginfo: coreSiginfoSchema.nullable(),
|
||||
auxv: z.array(coreAuxvEntrySchema).nullable(),
|
||||
});
|
||||
|
||||
const noteEntrySchema = z.strictObject({
|
||||
index: uint,
|
||||
table_kind: z.enum(["program", "section"]),
|
||||
@@ -125,6 +190,7 @@ const noteEntrySchema = z.strictObject({
|
||||
gold_version: z.string().nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
core: coreNoteSchema.nullable(),
|
||||
});
|
||||
|
||||
/** Observed ELF note tables; absence is an empty list, not proof notes never existed. */
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
InspectElfNotesInput,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
import {
|
||||
coreNoteTypeName,
|
||||
elfObjectTypeName,
|
||||
gnuNoteTypeName,
|
||||
} from "../../domain/native/elfNotes.js";
|
||||
@@ -289,6 +290,322 @@ const decodeGnu = (
|
||||
};
|
||||
};
|
||||
|
||||
const AUXV_TYPE_NAMES: Readonly<Record<number, string>> = {
|
||||
0: "AT_NULL",
|
||||
1: "AT_IGNORE",
|
||||
2: "AT_EXECFD",
|
||||
3: "AT_PHDR",
|
||||
4: "AT_PHENT",
|
||||
5: "AT_PHNUM",
|
||||
6: "AT_PAGESZ",
|
||||
7: "AT_BASE",
|
||||
8: "AT_FLAGS",
|
||||
9: "AT_ENTRY",
|
||||
11: "AT_UID",
|
||||
12: "AT_EUID",
|
||||
13: "AT_GID",
|
||||
14: "AT_EGID",
|
||||
15: "AT_PLATFORM",
|
||||
16: "AT_HWCAP",
|
||||
17: "AT_CLKTCK",
|
||||
23: "AT_SECURE",
|
||||
25: "AT_RANDOM",
|
||||
26: "AT_HWCAP2",
|
||||
31: "AT_EXECFN",
|
||||
33: "AT_SYSINFO_EHDR",
|
||||
};
|
||||
|
||||
const hexWord = (val: bigint | number): string => `0x${val.toString(16)}`;
|
||||
|
||||
const decodeCorePrpsinfo = (
|
||||
descriptor: Buffer,
|
||||
little: boolean,
|
||||
wide: boolean,
|
||||
text: TextBudget,
|
||||
): ElfNotes["entries"][number]["core"] => {
|
||||
const minLen = wide ? 136 : 124;
|
||||
if (descriptor.length < minLen) return null;
|
||||
const u8 = (off: number) => descriptor.readUInt8(off);
|
||||
const u16 = (off: number) =>
|
||||
little ? descriptor.readUInt16LE(off) : descriptor.readUInt16BE(off);
|
||||
const u32 = (off: number) =>
|
||||
little ? descriptor.readUInt32LE(off) : descriptor.readUInt32BE(off);
|
||||
const i32 = (off: number) =>
|
||||
little ? descriptor.readInt32LE(off) : descriptor.readInt32BE(off);
|
||||
|
||||
const state = u8(0);
|
||||
const uid = wide ? u32(16) : u16(8);
|
||||
const gid = wide ? u32(20) : u16(10);
|
||||
const pid = wide ? i32(24) : i32(12);
|
||||
const ppid = wide ? i32(28) : i32(16);
|
||||
const pgrp = wide ? i32(32) : i32(20);
|
||||
const sid = wide ? i32(36) : i32(24);
|
||||
|
||||
const fnameOff = wide ? 40 : 28;
|
||||
const fnameBytes = descriptor.subarray(fnameOff, fnameOff + 16);
|
||||
const fnameEnd = fnameBytes.indexOf(0);
|
||||
const fname = (
|
||||
fnameEnd < 0 ? fnameBytes : fnameBytes.subarray(0, fnameEnd)
|
||||
).toString("latin1");
|
||||
chargeText(text, fname.length);
|
||||
|
||||
const psargsOff = wide ? 56 : 44;
|
||||
const psargsBytes = descriptor.subarray(psargsOff, psargsOff + 80);
|
||||
const psargsEnd = psargsBytes.indexOf(0);
|
||||
const psargs = (
|
||||
psargsEnd < 0 ? psargsBytes : psargsBytes.subarray(0, psargsEnd)
|
||||
).toString("latin1");
|
||||
chargeText(text, psargs.length);
|
||||
|
||||
return {
|
||||
prpsinfo: {
|
||||
state,
|
||||
pid,
|
||||
ppid,
|
||||
pgrp,
|
||||
sid,
|
||||
uid,
|
||||
gid,
|
||||
filename: fname,
|
||||
psargs,
|
||||
},
|
||||
file: null,
|
||||
siginfo: null,
|
||||
auxv: null,
|
||||
};
|
||||
};
|
||||
|
||||
const decodeCoreFile = (
|
||||
descriptor: Buffer,
|
||||
little: boolean,
|
||||
wide: boolean,
|
||||
text: TextBudget,
|
||||
): ElfNotes["entries"][number]["core"] => {
|
||||
const wordSize = wide ? 8 : 4;
|
||||
const headerSize = wordSize * 2;
|
||||
if (descriptor.length < headerSize) return null;
|
||||
|
||||
const readWord = (off: number): bigint => {
|
||||
if (wide) {
|
||||
return little
|
||||
? descriptor.readBigUInt64LE(off)
|
||||
: descriptor.readBigUInt64BE(off);
|
||||
}
|
||||
const val = little
|
||||
? descriptor.readUInt32LE(off)
|
||||
: descriptor.readUInt32BE(off);
|
||||
return BigInt(val);
|
||||
};
|
||||
|
||||
const countBig = readWord(0);
|
||||
const pageSizeBig = readWord(wordSize);
|
||||
if (countBig > 0xffffffffn || pageSizeBig > 0xffffffffn || pageSizeBig === 0n)
|
||||
return null;
|
||||
const count = Number(countBig);
|
||||
const pageSize = Number(pageSizeBig);
|
||||
|
||||
const entrySize = wordSize * 3;
|
||||
const neededBytes = BigInt(headerSize) + countBig * BigInt(entrySize);
|
||||
if (BigInt(descriptor.length) < neededBytes) return null;
|
||||
const tableBytes = Number(neededBytes);
|
||||
|
||||
const rowBytes = count * (wide ? 54 : 30);
|
||||
if (rowBytes > text.limit - text.used) {
|
||||
throw new ArtifactReaderFailure(
|
||||
"limit",
|
||||
`ELF note text exceeds the ${String(text.limit)}-byte budget.`,
|
||||
{ boundary: "memory" },
|
||||
);
|
||||
}
|
||||
|
||||
const scanKey = `file:${String(descriptor.byteOffset)}:${String(descriptor.length)}`;
|
||||
const cachedScan = text.scans.get(scanKey);
|
||||
if (cachedScan === -1) return null;
|
||||
if (cachedScan === undefined) {
|
||||
chargeText(text, descriptor.length - tableBytes, "scanned");
|
||||
}
|
||||
|
||||
let strPos = tableBytes;
|
||||
let totalPathBytes = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (strPos >= descriptor.length) {
|
||||
text.scans.set(scanKey, -1);
|
||||
return null;
|
||||
}
|
||||
const nullIdx = descriptor.indexOf(0, strPos);
|
||||
if (nullIdx < 0) {
|
||||
text.scans.set(scanKey, -1);
|
||||
return null;
|
||||
}
|
||||
totalPathBytes += nullIdx - strPos;
|
||||
strPos = nullIdx + 1;
|
||||
}
|
||||
text.scans.set(scanKey, count);
|
||||
|
||||
const pathBytes = totalPathBytes * 3;
|
||||
chargeText(text, rowBytes + pathBytes);
|
||||
|
||||
const mappings: {
|
||||
start_address: string;
|
||||
end_address: string;
|
||||
file_offset_pages: string;
|
||||
path: string;
|
||||
path_hex: string;
|
||||
}[] = new Array(count);
|
||||
|
||||
strPos = tableBytes;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const entryOff = headerSize + i * entrySize;
|
||||
const start = readWord(entryOff);
|
||||
const end = readWord(entryOff + wordSize);
|
||||
const offsetPages = readWord(entryOff + wordSize * 2);
|
||||
const nullIdx = descriptor.indexOf(0, strPos);
|
||||
const pathBuf = descriptor.subarray(strPos, nullIdx);
|
||||
strPos = nullIdx + 1;
|
||||
mappings[i] = {
|
||||
start_address: hexWord(start),
|
||||
end_address: hexWord(end),
|
||||
file_offset_pages: hexWord(offsetPages),
|
||||
path: pathBuf.toString("latin1"),
|
||||
path_hex: pathBuf.toString("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prpsinfo: null,
|
||||
file: {
|
||||
count,
|
||||
page_size: pageSize,
|
||||
mappings,
|
||||
},
|
||||
siginfo: null,
|
||||
auxv: null,
|
||||
};
|
||||
};
|
||||
|
||||
const decodeCoreSiginfo = (
|
||||
descriptor: Buffer,
|
||||
little: boolean,
|
||||
machine: number,
|
||||
): ElfNotes["entries"][number]["core"] => {
|
||||
if (descriptor.length < 12) return null;
|
||||
const i32 = (off: number) =>
|
||||
little ? descriptor.readInt32LE(off) : descriptor.readInt32BE(off);
|
||||
const isMips = machine === 8 || machine === 10;
|
||||
const signo = i32(0);
|
||||
const errno = isMips ? i32(8) : i32(4);
|
||||
const code = isMips ? i32(4) : i32(8);
|
||||
return {
|
||||
prpsinfo: null,
|
||||
file: null,
|
||||
siginfo: {
|
||||
signo,
|
||||
errno,
|
||||
code,
|
||||
},
|
||||
auxv: null,
|
||||
};
|
||||
};
|
||||
|
||||
const decodeCoreAuxv = (
|
||||
descriptor: Buffer,
|
||||
little: boolean,
|
||||
wide: boolean,
|
||||
text: TextBudget,
|
||||
): ElfNotes["entries"][number]["core"] => {
|
||||
const wordSize = wide ? 8 : 4;
|
||||
const entrySize = wordSize * 2;
|
||||
if (descriptor.length < entrySize || descriptor.length % entrySize !== 0)
|
||||
return null;
|
||||
const totalPairs = descriptor.length / entrySize;
|
||||
|
||||
const scanKey = `auxv:${String(descriptor.byteOffset)}:${String(descriptor.length)}`;
|
||||
let nullIndex = text.scans.get(scanKey);
|
||||
if (nullIndex === -1) return null;
|
||||
|
||||
const readWord = (off: number): bigint => {
|
||||
if (wide) {
|
||||
return little
|
||||
? descriptor.readBigUInt64LE(off)
|
||||
: descriptor.readBigUInt64BE(off);
|
||||
}
|
||||
return BigInt(
|
||||
little ? descriptor.readUInt32LE(off) : descriptor.readUInt32BE(off),
|
||||
);
|
||||
};
|
||||
|
||||
if (nullIndex === undefined) {
|
||||
chargeText(text, descriptor.length, "scanned");
|
||||
let found = false;
|
||||
for (let i = 0; i < totalPairs; i++) {
|
||||
const off = i * entrySize;
|
||||
const aTypeBig = readWord(off);
|
||||
if (aTypeBig > 0xffffffffn) {
|
||||
text.scans.set(scanKey, -1);
|
||||
return null;
|
||||
}
|
||||
if (aTypeBig === 0n) {
|
||||
found = true;
|
||||
nullIndex = i;
|
||||
for (let j = off + entrySize; j < descriptor.length; j++) {
|
||||
if (descriptor[j] !== 0) {
|
||||
text.scans.set(scanKey, -1);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found || nullIndex === undefined) {
|
||||
text.scans.set(scanKey, -1);
|
||||
return null;
|
||||
}
|
||||
text.scans.set(scanKey, nullIndex);
|
||||
}
|
||||
|
||||
const rowBytes = nullIndex * (wide ? 38 : 30);
|
||||
chargeText(text, rowBytes);
|
||||
|
||||
const auxv: { type: number; type_name: string | null; value: string }[] =
|
||||
new Array(nullIndex);
|
||||
for (let i = 0; i < nullIndex; i++) {
|
||||
const off = i * entrySize;
|
||||
const aType = Number(readWord(off));
|
||||
const aVal = readWord(off + wordSize);
|
||||
auxv[i] = {
|
||||
type: aType,
|
||||
type_name: AUXV_TYPE_NAMES[aType] ?? null,
|
||||
value: hexWord(aVal),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prpsinfo: null,
|
||||
file: null,
|
||||
siginfo: null,
|
||||
auxv,
|
||||
};
|
||||
};
|
||||
|
||||
const decodeCore = (
|
||||
owner: string,
|
||||
type: number,
|
||||
descriptor: Buffer,
|
||||
little: boolean,
|
||||
wide: boolean,
|
||||
machine: number,
|
||||
text: TextBudget,
|
||||
): ElfNotes["entries"][number]["core"] => {
|
||||
if (owner !== "CORE") return null;
|
||||
if (type === 3) return decodeCorePrpsinfo(descriptor, little, wide, text);
|
||||
if (type === 0x46494c45)
|
||||
return decodeCoreFile(descriptor, little, wide, text);
|
||||
if (type === 0x53494749)
|
||||
return decodeCoreSiginfo(descriptor, little, machine);
|
||||
if (type === 6) return decodeCoreAuxv(descriptor, little, wide, text);
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Decode PT_NOTE and SHT_NOTE tables from one in-memory ELF image. */
|
||||
export const parseElfNotes = (
|
||||
bytes: Buffer,
|
||||
@@ -439,7 +756,17 @@ export const parseElfNotes = (
|
||||
examined,
|
||||
text,
|
||||
))
|
||||
parsed.push(toEntry(note, "program", index, reader.little, text));
|
||||
parsed.push(
|
||||
toEntry(
|
||||
note,
|
||||
"program",
|
||||
index,
|
||||
reader.little,
|
||||
wide,
|
||||
header.machine,
|
||||
text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (shnum !== 0) {
|
||||
@@ -494,7 +821,17 @@ export const parseElfNotes = (
|
||||
examined,
|
||||
text,
|
||||
))
|
||||
parsed.push(toEntry(note, "section", index, reader.little, text));
|
||||
parsed.push(
|
||||
toEntry(
|
||||
note,
|
||||
"section",
|
||||
index,
|
||||
reader.little,
|
||||
wide,
|
||||
header.machine,
|
||||
text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
const absent = tables.length === 0;
|
||||
@@ -524,9 +861,12 @@ const toEntry = (
|
||||
tableKind: "program" | "section",
|
||||
tableIndex: number,
|
||||
little: boolean,
|
||||
wide: boolean,
|
||||
machine: number,
|
||||
text: TextBudget,
|
||||
): Omit<ElfNotes["entries"][number], "index"> => {
|
||||
const gnuOwner = note.owner === "GNU";
|
||||
const coreOwner = note.owner === "CORE";
|
||||
const range = `${String(note.descriptor_offset)}:${String(note.descsz)}`;
|
||||
let digest = text.hashes.get(range);
|
||||
if (digest === undefined) {
|
||||
@@ -541,7 +881,11 @@ const toEntry = (
|
||||
namesz: note.namesz,
|
||||
descsz: note.descsz,
|
||||
type: note.type,
|
||||
type_name: gnuOwner ? gnuNoteTypeName(note.type) : "unknown",
|
||||
type_name: gnuOwner
|
||||
? gnuNoteTypeName(note.type)
|
||||
: coreOwner
|
||||
? coreNoteTypeName(note.type)
|
||||
: "unknown",
|
||||
owner: note.owner,
|
||||
owner_bytes_hex: note.owner_bytes_hex,
|
||||
descriptor: {
|
||||
@@ -554,5 +898,14 @@ const toEntry = (
|
||||
: null,
|
||||
},
|
||||
gnu: decodeGnu(note.owner, note.type, note.descriptor, little, text),
|
||||
core: decodeCore(
|
||||
note.owner,
|
||||
note.type,
|
||||
note.descriptor,
|
||||
little,
|
||||
wide,
|
||||
machine,
|
||||
text,
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -11,7 +11,7 @@ const notePadding = (declared: number): number => (declared === 8 ? 8 : 4);
|
||||
const writeWord = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
value: number,
|
||||
value: bigint | number,
|
||||
bits: 32 | 64,
|
||||
little: boolean,
|
||||
): void => {
|
||||
@@ -20,8 +20,8 @@ const writeWord = (
|
||||
else bytes.writeBigUInt64BE(BigInt(value), offset);
|
||||
return;
|
||||
}
|
||||
if (little) bytes.writeUInt32LE(value, offset);
|
||||
else bytes.writeUInt32BE(value, offset);
|
||||
if (little) bytes.writeUInt32LE(Number(value), offset);
|
||||
else bytes.writeUInt32BE(Number(value), offset);
|
||||
};
|
||||
|
||||
const write16 = (
|
||||
@@ -40,8 +40,8 @@ const write32 = (
|
||||
value: number,
|
||||
little: boolean,
|
||||
): void => {
|
||||
if (little) bytes.writeUInt32LE(value, offset);
|
||||
else bytes.writeUInt32BE(value, offset);
|
||||
if (little) bytes.writeUInt32LE(Number(value), offset);
|
||||
else bytes.writeUInt32BE(Number(value), offset);
|
||||
};
|
||||
|
||||
/** Encode one ELF note, padding name/descriptor to the table alignment. */
|
||||
@@ -402,3 +402,154 @@ export const elfNoteTablePastEnd = (): Buffer => {
|
||||
writeWord(program, 48, 4, 64, true);
|
||||
return concat(header, program, note);
|
||||
};
|
||||
|
||||
export interface CorePrpsinfoOptions {
|
||||
readonly bits?: 32 | 64;
|
||||
readonly little?: boolean;
|
||||
readonly state?: number;
|
||||
readonly sname?: string;
|
||||
readonly zomb?: number;
|
||||
readonly nice?: number;
|
||||
readonly flag?: number;
|
||||
readonly uid?: number;
|
||||
readonly gid?: number;
|
||||
readonly pid?: number;
|
||||
readonly ppid?: number;
|
||||
readonly pgrp?: number;
|
||||
readonly sid?: number;
|
||||
readonly fname?: string;
|
||||
readonly psargs?: string;
|
||||
}
|
||||
|
||||
/** Linux CORE NT_PRPSINFO note (type 3). */
|
||||
export const corePrpsinfoNote = (options: CorePrpsinfoOptions = {}): Buffer => {
|
||||
const bits = options.bits ?? 64;
|
||||
const little = options.little ?? true;
|
||||
const desc = Buffer.alloc(bits === 64 ? 136 : 124);
|
||||
desc[0] = options.state ?? 0;
|
||||
desc[1] = (options.sname ?? "R").charCodeAt(0);
|
||||
desc[2] = options.zomb ?? 0;
|
||||
desc[3] = options.nice ?? 0;
|
||||
if (bits === 64) {
|
||||
if (little) desc.writeBigUInt64LE(BigInt(options.flag ?? 0), 8);
|
||||
else desc.writeBigUInt64BE(BigInt(options.flag ?? 0), 8);
|
||||
write32(desc, 16, options.uid ?? 1000, little);
|
||||
write32(desc, 20, options.gid ?? 1000, little);
|
||||
if (little) {
|
||||
desc.writeInt32LE(options.pid ?? 1234, 24);
|
||||
desc.writeInt32LE(options.ppid ?? 1, 28);
|
||||
desc.writeInt32LE(options.pgrp ?? 1234, 32);
|
||||
desc.writeInt32LE(options.sid ?? 1234, 36);
|
||||
} else {
|
||||
desc.writeInt32BE(options.pid ?? 1234, 24);
|
||||
desc.writeInt32BE(options.ppid ?? 1, 28);
|
||||
desc.writeInt32BE(options.pgrp ?? 1234, 32);
|
||||
desc.writeInt32BE(options.sid ?? 1234, 36);
|
||||
}
|
||||
const fname = Buffer.from(`${options.fname ?? "app"}\0`, "latin1");
|
||||
fname.copy(desc, 40, 0, Math.min(fname.length, 16));
|
||||
const psargs = Buffer.from(
|
||||
`${options.psargs ?? "./app --flag"}\0`,
|
||||
"latin1",
|
||||
);
|
||||
psargs.copy(desc, 56, 0, Math.min(psargs.length, 80));
|
||||
} else {
|
||||
write32(desc, 4, options.flag ?? 0, little);
|
||||
write16(desc, 8, options.uid ?? 1000, little);
|
||||
write16(desc, 10, options.gid ?? 1000, little);
|
||||
if (little) {
|
||||
desc.writeInt32LE(options.pid ?? 1234, 12);
|
||||
desc.writeInt32LE(options.ppid ?? 1, 16);
|
||||
desc.writeInt32LE(options.pgrp ?? 1234, 20);
|
||||
desc.writeInt32LE(options.sid ?? 1234, 24);
|
||||
} else {
|
||||
desc.writeInt32BE(options.pid ?? 1234, 12);
|
||||
desc.writeInt32BE(options.ppid ?? 1, 16);
|
||||
desc.writeInt32BE(options.pgrp ?? 1234, 20);
|
||||
desc.writeInt32BE(options.sid ?? 1234, 24);
|
||||
}
|
||||
const fname = Buffer.from(`${options.fname ?? "app"}\0`, "latin1");
|
||||
fname.copy(desc, 28, 0, Math.min(fname.length, 16));
|
||||
const psargs = Buffer.from(
|
||||
`${options.psargs ?? "./app --flag"}\0`,
|
||||
"latin1",
|
||||
);
|
||||
psargs.copy(desc, 44, 0, Math.min(psargs.length, 80));
|
||||
}
|
||||
return elfNote("CORE", 3, desc, little);
|
||||
};
|
||||
|
||||
export interface CoreFileMapping {
|
||||
readonly start: bigint | number;
|
||||
readonly end: bigint | number;
|
||||
readonly file_ofs: bigint | number;
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
/** Linux CORE NT_FILE note (type 0x46494c45). */
|
||||
export const coreFileNote = (
|
||||
mappings: readonly CoreFileMapping[],
|
||||
pageSize = 4096,
|
||||
bits: 32 | 64 = 64,
|
||||
little = true,
|
||||
): Buffer => {
|
||||
const wordBytes = bits === 64 ? 8 : 4;
|
||||
const headerBytes = 2 * wordBytes;
|
||||
const mapEntryBytes = 3 * wordBytes;
|
||||
const filenames = Buffer.concat(
|
||||
mappings.map((m) => Buffer.from(`${m.path}\0`, "latin1")),
|
||||
);
|
||||
const desc = Buffer.alloc(
|
||||
headerBytes + mappings.length * mapEntryBytes + filenames.length,
|
||||
);
|
||||
writeWord(desc, 0, mappings.length, bits, little);
|
||||
writeWord(desc, wordBytes, pageSize, bits, little);
|
||||
let offset = headerBytes;
|
||||
for (const m of mappings) {
|
||||
writeWord(desc, offset, m.start, bits, little);
|
||||
writeWord(desc, offset + wordBytes, m.end, bits, little);
|
||||
writeWord(desc, offset + 2 * wordBytes, m.file_ofs, bits, little);
|
||||
offset += mapEntryBytes;
|
||||
}
|
||||
filenames.copy(desc, offset);
|
||||
return elfNote("CORE", 0x46494c45, desc, little);
|
||||
};
|
||||
|
||||
/** Linux CORE NT_SIGINFO note (type 0x53494749). */
|
||||
export const coreSiginfoNote = (
|
||||
signo = 11,
|
||||
code = 1,
|
||||
errno = 0,
|
||||
little = true,
|
||||
): Buffer => {
|
||||
const desc = Buffer.alloc(128);
|
||||
if (little) {
|
||||
desc.writeInt32LE(signo, 0);
|
||||
desc.writeInt32LE(errno, 4);
|
||||
desc.writeInt32LE(code, 8);
|
||||
} else {
|
||||
desc.writeInt32BE(signo, 0);
|
||||
desc.writeInt32BE(errno, 4);
|
||||
desc.writeInt32BE(code, 8);
|
||||
}
|
||||
return elfNote("CORE", 0x53494749, desc, little);
|
||||
};
|
||||
|
||||
/** Linux CORE NT_AUXV note (type 6). */
|
||||
export const coreAuxvNote = (
|
||||
entries: readonly [key: number, value: bigint | number][],
|
||||
bits: 32 | 64 = 64,
|
||||
little = true,
|
||||
): Buffer => {
|
||||
const wordBytes = bits === 64 ? 8 : 4;
|
||||
const desc = Buffer.alloc((entries.length + 1) * 2 * wordBytes);
|
||||
let offset = 0;
|
||||
for (const [key, value] of entries) {
|
||||
writeWord(desc, offset, key, bits, little);
|
||||
writeWord(desc, offset + wordBytes, value, bits, little);
|
||||
offset += 2 * wordBytes;
|
||||
}
|
||||
writeWord(desc, offset, 0, bits, little);
|
||||
writeWord(desc, offset + wordBytes, 0, bits, little);
|
||||
return elfNote("CORE", 6, desc, little);
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
import {
|
||||
coreAuxvNote,
|
||||
coreFileNote,
|
||||
corePrpsinfoNote,
|
||||
coreSiginfoNote,
|
||||
elfNoteFixture,
|
||||
} from "../../src/native/elf/ElfNotes.fixture.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
const descriptorHash = (bytes: Buffer): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
const buildCoreNotes = (): Buffer =>
|
||||
Buffer.concat([
|
||||
corePrpsinfoNote({
|
||||
bits: 64,
|
||||
little: true,
|
||||
state: 0,
|
||||
sname: "R",
|
||||
pid: 31337,
|
||||
ppid: 1,
|
||||
pgrp: 31337,
|
||||
sid: 31337,
|
||||
uid: 1000,
|
||||
gid: 1000,
|
||||
fname: "target-service",
|
||||
psargs: "/opt/target-service --worker 1",
|
||||
}),
|
||||
coreFileNote(
|
||||
[
|
||||
{
|
||||
start: 0x555555554000n,
|
||||
end: 0x555555558000n,
|
||||
file_ofs: 0n,
|
||||
path: "/opt/target-service",
|
||||
},
|
||||
{
|
||||
start: 0x7ffff7dc0000n,
|
||||
end: 0x7ffff7de0000n,
|
||||
file_ofs: 0n,
|
||||
path: "/lib/x86_64-linux-gnu/libc.so.6",
|
||||
},
|
||||
],
|
||||
4096,
|
||||
64,
|
||||
true,
|
||||
),
|
||||
coreSiginfoNote(11, 1, 0, true),
|
||||
coreAuxvNote(
|
||||
[
|
||||
[6, 4096n],
|
||||
[3, 0x555555554040n],
|
||||
],
|
||||
64,
|
||||
true,
|
||||
),
|
||||
]);
|
||||
|
||||
const assertCoreEntries = (entries: unknown[]): void => {
|
||||
expect(entries).toHaveLength(4);
|
||||
expect(entries[0]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 3,
|
||||
type_name: "core_prpsinfo",
|
||||
gnu: null,
|
||||
core: {
|
||||
prpsinfo: {
|
||||
state: 0,
|
||||
pid: 31337,
|
||||
ppid: 1,
|
||||
pgrp: 31337,
|
||||
sid: 31337,
|
||||
uid: 1000,
|
||||
gid: 1000,
|
||||
filename: "target-service",
|
||||
psargs: "/opt/target-service --worker 1",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(entries[1]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 0x46494c45,
|
||||
type_name: "core_file",
|
||||
gnu: null,
|
||||
core: {
|
||||
file: {
|
||||
count: 2,
|
||||
page_size: 4096,
|
||||
mappings: [
|
||||
{
|
||||
start_address: "0x555555554000",
|
||||
end_address: "0x555555558000",
|
||||
file_offset_pages: "0x0",
|
||||
path: "/opt/target-service",
|
||||
path_hex: Buffer.from("/opt/target-service").toString("hex"),
|
||||
},
|
||||
{
|
||||
start_address: "0x7ffff7dc0000",
|
||||
end_address: "0x7ffff7de0000",
|
||||
file_offset_pages: "0x0",
|
||||
path: "/lib/x86_64-linux-gnu/libc.so.6",
|
||||
path_hex: Buffer.from("/lib/x86_64-linux-gnu/libc.so.6").toString(
|
||||
"hex",
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(entries[2]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 0x53494749,
|
||||
type_name: "core_siginfo",
|
||||
gnu: null,
|
||||
core: {
|
||||
siginfo: { signo: 11, errno: 0, code: 1 },
|
||||
},
|
||||
});
|
||||
expect(entries[3]).toMatchObject({
|
||||
owner: "CORE",
|
||||
type: 6,
|
||||
type_name: "core_auxv",
|
||||
gnu: null,
|
||||
core: {
|
||||
auxv: [
|
||||
{ type: 6, type_name: "AT_PAGESZ", value: "0x1000" },
|
||||
{ type: 3, type_name: "AT_PHDR", value: "0x555555554040" },
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"decodes Linux core dump notes through CLI and stdio MCP",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-elf-core-notes-");
|
||||
const path = join(root, "core.elf");
|
||||
const notes = buildCoreNotes();
|
||||
const bytes = elfNoteFixture({
|
||||
bits: 64,
|
||||
little: true,
|
||||
type: 4,
|
||||
ownership: "program",
|
||||
notes,
|
||||
});
|
||||
await writeFile(path, bytes);
|
||||
|
||||
const output = await cli.sharedCliRun!({
|
||||
arguments: ["inspect-elf-notes", path, "--json"],
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
|
||||
const call = await cli.sharedMcpCall!();
|
||||
const response = await call("inspect_elf_notes", { path });
|
||||
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
|
||||
|
||||
const schema = toolContract("inspect_elf_notes").outputSchema;
|
||||
for (const raw of [output.json, response.structuredContent]) {
|
||||
const evidence = schema.parse(raw);
|
||||
expect(evidence.subject).toMatchObject({
|
||||
local_path: path,
|
||||
digest: { sha256: descriptorHash(bytes) },
|
||||
});
|
||||
const report = evidence.normalized_result;
|
||||
expect(report).toMatchObject({
|
||||
format: "elf64",
|
||||
byte_order: "little",
|
||||
machine: 62,
|
||||
coverage: {
|
||||
status: "complete",
|
||||
examined_tables: 1,
|
||||
examined_entries: 4,
|
||||
},
|
||||
});
|
||||
assertCoreEntries(report.entries);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user