mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 13:56:06 +00:00
* feat(process): add owned debugger shutdown lifecycle * feat(reverse): add local inspection commands * feat(gdb): add persistent debugger integration * feat(cutter): add Cutter bridge integration * test(reverse): enforce complete integration coverage * fix(process): preserve debugger shutdown ownership * fix(reverse): retain evidence across interrupted commands * fix(gdb): honor captured provider environment * test(process): cover debugger shutdown retries * fix(reverse): retain evidence for interrupted providers * test(process): cover debugger shutdown retries * fix(reverse): retain evidence for interrupted providers * fix(server): snapshot provider environment at creation * fix(reverse): close review gaps with complete coverage * fix(reverse): address inspection review findings * fix(gdb): bound sessions and retain cleanup ownership * fix(ci): require reverse-tools quality lane * fix(process): unref inherited debugger output pipes * fix(reverse): address remaining PR 1960 findings * fix(gdb): address remaining PR 1961 findings * fix(reverse): account for unrestricted provider execution * fix(process): retain debugger ownership until safe shutdown * fix(process): preserve replacement debugger sessions * fix(reverse): preserve admission and output provenance * fix(process): retain exited debugger until output settles * fix(mcp): bound provider admission and debugger cleanup * fix(reverse): preserve failure provenance and classify E2BIG * fix(gdb): preserve startup cleanup and detach safety * fix(process): retain debugger ownership after detach failure * fix(reverse): preserve mutation and failure provenance * fix(gdb): address debugger review findings * fix(reverse): rebind sessions after external mutation * fix(process): cap debugger output after exit * fix(gdb): protect one-shot detach verification * feat(debugger): add reverse tool coverage gate * test(debugger): cover integrated failure paths * fix(gdb): bound and preserve session observations * fix(gdb): address review regressions * style: format merged integration files * fix(reverse): preserve session and output evidence * refactor(gdb): share repeated test and partial result setup * fix(gdb): invalidate TTY proof for Python aliases * fix(reverse): retain failure output byte counts * test(process): synchronize late debugger output * fix(gdb): invalidate TTY proof for wrapped commands * fix(gdb): recognize wit command abbreviation * test(reverse): wait for cancelled descendant readiness * test(reverse): bound descendant cancellation cleanup * style(reverse): format integration tests and adapters * style: format reverse engineering integration files * test(reverse): remove duplicate environment harness * test(gdb): share session startup assertions * fix(gdb): invalidate output proof for command abbreviations * test(reverse): share composition edge case scenarios * fix(ci): recognize verification and mutation tool entries * test(rizin): cover successful fake inferior detach * test(cli): align full output failure envelope * test(cli): isolate reverse command lifecycle service * test(process): share debugger output fixture setup * fix(binary): preserve high-bit Windows signature bytes * style(binary): format Windows signature parser * test(mcp): preserve call coverage in unchanged comparison * fix(ci): include reverse tools Vitest configs in lint cache * fix(mcp): preserve target content on delivery errors * ci: align duplication ceiling with expanded suite * test(reverse): allow signal admission mutants * fix(reverse): bound retained cleanup failures to process owners * fix(test): replace drifting mutation ranges with owned selectors * fix(reverse): admit persistent commands and retain cleanup ownership * fix(cutter): recover timed-out commands from exact receipts * fix(cutter): reject unsupported bridge request kinds * fix(ci): repair client format and shared verification fixtures * fix(native): preserve debugger cleanup ownership and bridge authentication * test(native): retain diagnostics without perfect coverage requirements * chore(licenses): refresh locked dependency inventory and notices * fix(test): exclude transient command locks from mutation sandboxes * fix(gdb): admit only platforms with safe detach verification * fix(cutter): retain bounded completion receipts until acknowledgement * fix(cutter): acknowledge commands rejected before UI dispatch * test(reverse): fail cancellation checks before fixture teardown * test(gdb): clean up the retained broken-stdin fixture owner * docs(test): clarify focused provider verification scope * fix(rizin): retain close cleanup failures * fix(gdb): wait for inferior stop and bound MI buffering * fix(cutter): retain failed receipt acknowledgements * fix(rizin): return retained session identity on rebind failure * fix(gdb): preserve startup cancellation and lossy status * fix(rizin): retain launch provenance and assemble frames once * fix(cutter): recover receipts across callers and bound discovery * docs: describe debugger evidence and bridge recovery * fix(process): keep cleanup incomplete while writers remain Stopping a debugger no longer reports success when inherited output streams stay open, so BinarySession cannot reopen a binary a child is still changing. Concurrent Cutter cleanup retries share one receipt acknowledgement instead of treating a consumed receipt as failure. * fix(debuggers): recover cleanup with captured process identities * fix(analysis): retire sessions after unrestricted commands * fix(cli): preserve completed evidence after cleanup recovery * fix(cutter): bound frame collection and coalesce cleanup * fix(windows): resolve selected provider paths absolutely * fix(gdb): report decoding loss in failed startup evidence * fix(rizin): scope decoding loss to collected command frames * test(mcp): cover snapshot retirement in reverse tools CI --------- Co-authored-by: Daniel Gonçalves Araujo <47118053+dandgabr@users.noreply.github.com> Co-authored-by: REA Maintainers <noreply@github.com> Co-authored-by: OpenAI Codex <codex@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
265 lines
9.5 KiB
TypeScript
265 lines
9.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
assertMipsRelocations,
|
|
parseMipsRelocatableReadelf,
|
|
} from "./mips-relocatable-checks.mjs";
|
|
|
|
// GNU readelf excerpt for a tiny ET_REL object; tests the reader boundary.
|
|
const readelf = `Section Headers:
|
|
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
|
[ 0] NULL 00000000 000000 000000 00 0 0 0
|
|
[ 1] .text PROGBITS 00000000 000040 000010 00 AX 0 0 16
|
|
[ 2] .rel.text REL 00000000 000100 000018 08 I 5 1 4
|
|
[ 3] .data PROGBITS 00000000 000050 008010 00 WA 0 0 16
|
|
[ 4] .rel.data REL 00000000 000118 000010 08 I 5 3 4
|
|
[ 5] .symtab SYMTAB 00000000 000128 000040 10 1 1 4
|
|
|
|
Relocation section '.rel.text' at offset 0x100 contains 3 entries:
|
|
Offset Info Type Sym. Value Symbol's Name
|
|
00000000 00000205 R_MIPS_HI16 00008000 far_data
|
|
00000004 00000206 R_MIPS_LO16 00008000 far_data
|
|
00000008 00000104 R_MIPS_26 00000000 func
|
|
|
|
Relocation section '.rel.data' at offset 0x118 contains 2 entries:
|
|
Offset Info Type Sym. Value Symbol's Name
|
|
00000000 00000102 R_MIPS_32 00000000 func
|
|
00000004 00000302 R_MIPS_32 00000000 ext_data
|
|
|
|
Symbol table '.symtab' contains 4 entries:
|
|
Num: Value Size Type Bind Vis Ndx Name
|
|
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
|
|
1: 00000000 16 FUNC GLOBAL DEFAULT 1 func
|
|
2: 00008000 4 OBJECT GLOBAL DEFAULT 3 far_data
|
|
3: 00000000 0 NOTYPE GLOBAL DEFAULT UND ext_data
|
|
`;
|
|
|
|
// Ghidra placement: .text 0x10000, .data 0x10010, EXTERNAL 0x19000. far_data
|
|
// is at 0x18010, so its LO16 is 0x8010 and the HI16 must carry to 2.
|
|
const fileBytes = Buffer.alloc(0x50 + 0x8010);
|
|
fileBytes.writeUInt32LE(0x3c010000, 0x40); // lui $1, 0
|
|
fileBytes.writeUInt32LE(0x8c220000, 0x44); // lw $2, 0($1)
|
|
fileBytes.writeUInt32LE(0x0c000000, 0x48); // jal 0
|
|
const segments = [
|
|
{ name: ".text", start: "0x10000", end: "0x10010" },
|
|
{ name: ".data", start: "0x10010", end: "0x18020" },
|
|
{ name: "EXTERNAL", start: "0x19000", end: "0x19004" },
|
|
];
|
|
const mappings = [
|
|
{ block: ".text", start: "0x10000", length: 0x10, file_offset: 0x40 },
|
|
{ block: ".data", start: "0x10010", length: 0x8010, file_offset: 0x50 },
|
|
];
|
|
const names = [
|
|
{
|
|
address: "0x19000",
|
|
value: "ext_data",
|
|
symbol: { external: false, type: "label" },
|
|
},
|
|
];
|
|
const relocations = () => [
|
|
{ address: "0x10000", type: 5, status: "PARTIAL" },
|
|
{ address: "0x10004", type: 6, status: "APPLIED" },
|
|
{ address: "0x10008", type: 4, status: "APPLIED" },
|
|
{ address: "0x10010", type: 2, status: "APPLIED" },
|
|
{ address: "0x10014", type: 2, status: "APPLIED" },
|
|
{ address: ".pdr:0x0", type: 2, status: "APPLIED" },
|
|
];
|
|
const words = () =>
|
|
new Map([
|
|
[0x10000n, 0x3c010002],
|
|
[0x10004n, 0x8c228010],
|
|
[0x10008n, 0x0c004000],
|
|
[0x10010n, 0x10000],
|
|
[0x10014n, 0x19000],
|
|
]);
|
|
const check = (
|
|
overrides: Partial<Parameters<typeof assertMipsRelocations>[0]> = {},
|
|
) =>
|
|
assertMipsRelocations({
|
|
facts: parseMipsRelocatableReadelf(readelf),
|
|
fileBytes,
|
|
segments,
|
|
mappings,
|
|
ghidraRelocations: relocations(),
|
|
words: words(),
|
|
names,
|
|
byteOrder: "little",
|
|
...overrides,
|
|
});
|
|
|
|
describe("MIPS relocatable real-verifier expectations", () => {
|
|
it("reads sections, symbols and REL entries from r_info", () => {
|
|
const facts = parseMipsRelocatableReadelf(readelf);
|
|
expect(facts.sections.get(3)).toMatchObject({
|
|
name: ".data",
|
|
offset: 0x50,
|
|
allocated: true,
|
|
});
|
|
expect(facts.symbols.get(3)).toMatchObject({
|
|
ndx: "UND",
|
|
name: "ext_data",
|
|
});
|
|
expect(facts.relocations).toEqual([
|
|
expect.objectContaining({ section: ".text", type: 5, symbolIndex: 2 }),
|
|
expect.objectContaining({ section: ".text", type: 6, symbolIndex: 2 }),
|
|
expect.objectContaining({ section: ".text", type: 4, symbolIndex: 1 }),
|
|
expect.objectContaining({ section: ".data", type: 2, symbolIndex: 1 }),
|
|
expect.objectContaining({ section: ".data", type: 2, symbolIndex: 3 }),
|
|
]);
|
|
});
|
|
|
|
it("rejects a REL table whose entry count disagrees with its header", () => {
|
|
expect(() =>
|
|
parseMipsRelocatableReadelf(
|
|
readelf.replace("contains 2 entries", "contains 3 entries"),
|
|
),
|
|
).toThrow(/entry count/u);
|
|
});
|
|
|
|
it("accepts relocations that encode S + A, including a HI16 carry", () => {
|
|
expect(check()).toMatchObject({
|
|
lo16_carry: true,
|
|
external_data: true,
|
|
types: { R_MIPS_HI16: 1, R_MIPS_LO16: 1, R_MIPS_26: 1, R_MIPS_32: 2 },
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["a HI16 without the signed-low carry", 0x10000n, 0x3c010001],
|
|
["a jump to the wrong target", 0x10008n, 0x0c004001],
|
|
["a rewritten opcode", 0x10004n, 0x8c238010],
|
|
["an unrelocated external word", 0x10014n, 0],
|
|
])("rejects %s", (_label, address, value) => {
|
|
const changed = words();
|
|
changed.set(address, value);
|
|
expect(() => check({ words: changed })).toThrow();
|
|
});
|
|
|
|
it("rejects a failed or missing Ghidra relocation", () => {
|
|
const failed = relocations().map((item, index) =>
|
|
index === 2 ? { ...item, status: "FAILURE" } : item,
|
|
);
|
|
expect(() => check({ ghidraRelocations: failed })).toThrow(/FAILURE/u);
|
|
expect(() => check({ ghidraRelocations: relocations().slice(1) })).toThrow(
|
|
/relocation count/u,
|
|
);
|
|
});
|
|
|
|
it("rejects a section Ghidra placed from different file bytes", () => {
|
|
expect(() =>
|
|
check({
|
|
mappings: mappings.map((item) =>
|
|
item.block === ".data" ? { ...item, file_offset: 0x60 } : item,
|
|
),
|
|
}),
|
|
).toThrow(/\.data offset/u);
|
|
});
|
|
|
|
it("counts a relocation against an empty section as unplaced, not verified", () => {
|
|
const withEmpty = readelf
|
|
.replace(
|
|
" [ 5] .symtab",
|
|
" [ 6] .sbss NOBITS 00000000 008060 000000 00 WAp 0 0 4\n [ 5] .symtab",
|
|
)
|
|
.replace("contains 2 entries", "contains 3 entries")
|
|
.replace(
|
|
"00000004 00000302 R_MIPS_32 00000000 ext_data\n",
|
|
"00000004 00000302 R_MIPS_32 00000000 ext_data\n00000008 00000402 R_MIPS_32 00000000 .sbss\n",
|
|
)
|
|
.replace(
|
|
" 3: 00000000 0 NOTYPE GLOBAL DEFAULT UND ext_data\n",
|
|
" 3: 00000000 0 NOTYPE GLOBAL DEFAULT UND ext_data\n 4: 00000000 0 SECTION LOCAL DEFAULT 6 .sbss\n",
|
|
);
|
|
const loaded = words();
|
|
loaded.set(0x10018n, 0x12345678);
|
|
expect(
|
|
check({
|
|
facts: parseMipsRelocatableReadelf(withEmpty),
|
|
ghidraRelocations: [
|
|
...relocations(),
|
|
{ address: "0x10018", type: 2, status: "APPLIED" },
|
|
],
|
|
words: loaded,
|
|
}).unplaced,
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("MIPS relocatable HI16 entries resolved by one LO16", () => {
|
|
// lui $1; lui $3; jal (unrelated R_MIPS_26); lw: both HI16s pair with the LO16.
|
|
const multi = readelf.replace(
|
|
/Relocation section '\.rel\.text'[^]*?\n\n/u,
|
|
[
|
|
"Relocation section '.rel.text' at offset 0x100 contains 4 entries:",
|
|
" Offset Info Type Sym. Value Symbol's Name",
|
|
"00000000 00000205 R_MIPS_HI16 00008000 far_data",
|
|
"00000004 00000205 R_MIPS_HI16 00008000 far_data",
|
|
"00000008 00000104 R_MIPS_26 00000000 func",
|
|
"0000000c 00000206 R_MIPS_LO16 00008000 far_data",
|
|
"",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
const bytes = Buffer.from(fileBytes);
|
|
bytes.writeUInt32LE(0x3c010000, 0x40); // lui $1, 0
|
|
bytes.writeUInt32LE(0x3c030000, 0x44); // lui $3, 0
|
|
bytes.writeUInt32LE(0x0c000000, 0x48); // jal 0
|
|
bytes.writeUInt32LE(0x8c220000, 0x4c); // lw $2, 0($1)
|
|
const ghidra = () => [
|
|
{ address: "0x10000", type: 5, status: "PARTIAL" },
|
|
{ address: "0x10004", type: 5, status: "PARTIAL" },
|
|
{ address: "0x10008", type: 4, status: "APPLIED" },
|
|
{ address: "0x1000c", type: 6, status: "APPLIED" },
|
|
{ address: "0x10010", type: 2, status: "APPLIED" },
|
|
{ address: "0x10014", type: 2, status: "APPLIED" },
|
|
];
|
|
const loaded = () =>
|
|
new Map([
|
|
[0x10000n, 0x3c010002],
|
|
[0x10004n, 0x3c030002],
|
|
[0x10008n, 0x0c004000],
|
|
[0x1000cn, 0x8c228010],
|
|
[0x10010n, 0x10000],
|
|
[0x10014n, 0x19000],
|
|
]);
|
|
const checkMulti = (
|
|
overrides: Partial<Parameters<typeof assertMipsRelocations>[0]> = {},
|
|
) =>
|
|
check({
|
|
facts: parseMipsRelocatableReadelf(multi),
|
|
fileBytes: bytes,
|
|
ghidraRelocations: ghidra(),
|
|
words: loaded(),
|
|
...overrides,
|
|
});
|
|
|
|
it("checks every HI16 against the LO16, across unrelated relocations", () => {
|
|
expect(checkMulti()).toMatchObject({
|
|
multiple_hi16: true,
|
|
lo16_carry: true,
|
|
});
|
|
});
|
|
|
|
it("rejects a bad earlier HI16 that a later HI16 would have hidden", () => {
|
|
const words = loaded();
|
|
words.set(0x10000n, 0x3c010001);
|
|
expect(() => checkMulti({ words })).toThrow(/HI16 at 0x10000 /u);
|
|
});
|
|
|
|
it("rejects a HI16 that no LO16 resolves", () => {
|
|
const unmatched = multi
|
|
.replace("contains 4 entries", "contains 3 entries")
|
|
.replace(
|
|
"0000000c 00000206 R_MIPS_LO16 00008000 far_data\n",
|
|
"",
|
|
);
|
|
expect(() =>
|
|
checkMulti({
|
|
facts: parseMipsRelocatableReadelf(unmatched),
|
|
ghidraRelocations: ghidra().filter(
|
|
(item) => item.address !== "0x1000c",
|
|
),
|
|
}),
|
|
).toThrow(/without a following LO16/u);
|
|
});
|
|
});
|