mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
The real-Hopper navigation probe derived its interior coordinate as anchor + 1, which assumes the fixture procedure's first instruction is longer than one byte. Host toolchains without endbr64-style prologue padding (Kali gcc 16, or any build with -fcf-protection=none) compile rea_entry to start with the one-byte push rbp, so anchor + 1 is itself an instruction boundary: goto_address correctly observes that boundary and the boundary-contract run rejects a correct bridge. Advance the probe to the first instruction followed by a coordinate gap larger than one byte and derive the interior-coordinate assertions from that instruction, so the probed coordinate is never an analyzed-object boundary on either prologue encoding.
260 lines
9.1 KiB
JavaScript
260 lines
9.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
|
|
/** Verify native cursor readback and reject unrepresentable text before mutations. */
|
|
export async function verifyHopperNavigationAndText(call, invalid, address) {
|
|
const cursor = await call("current_address");
|
|
const procedures = await call("list_procedures");
|
|
const other = procedures.find((item) => item.address !== address);
|
|
assert.ok(other, "fixture omitted a second procedure for batch validation");
|
|
const originalName = await call("address_name", { address });
|
|
const originalComment = await call("comment", { address });
|
|
const originalInline = await call("inline_comment", { address });
|
|
const originalBookmark = (await call("list_bookmarks")).find(
|
|
(item) => item.address === address,
|
|
);
|
|
try {
|
|
await verifyNavigation(call, invalid, address, {
|
|
originalName,
|
|
originalComment,
|
|
originalInline,
|
|
});
|
|
await verifyAnnotationText(call, invalid, { address, other, originalName });
|
|
return {
|
|
cursorReadback: true,
|
|
interiorObjectNavigation: true,
|
|
unmappedNavigationRejected: true,
|
|
nativeTextValidationBeforeMutation: true,
|
|
mappedAnnotationValidationBeforeMutation: true,
|
|
unselectedLabelOwnersPreserved: true,
|
|
explicitLabelSwap: true,
|
|
observedLocalMetadata: true,
|
|
};
|
|
} finally {
|
|
await call("goto_address", { address: cursor });
|
|
await call("set_comment", { address, comment: originalComment ?? "" });
|
|
await call("set_inline_comment", {
|
|
address,
|
|
comment: originalInline ?? "",
|
|
});
|
|
if (originalBookmark === undefined)
|
|
await call("unset_bookmark", { address });
|
|
else
|
|
await call("set_bookmark", {
|
|
address,
|
|
...(originalBookmark.name === null
|
|
? {}
|
|
: { name: originalBookmark.name }),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function verifyNavigation(
|
|
call,
|
|
invalid,
|
|
address,
|
|
{ originalName, originalComment, originalInline },
|
|
) {
|
|
const instructions = await call("read_function_instructions", {
|
|
procedure: address,
|
|
});
|
|
const info = await call("procedure_info", { procedure: address });
|
|
assert.equal(info.classification, null);
|
|
assert.equal(info.body.available, false);
|
|
assert.match(
|
|
info.body.reason,
|
|
/public Python API does not expose complete function body ranges/u,
|
|
);
|
|
assert.ok(info.locals.length > 0, "fixture omitted real stack locals");
|
|
for (const local of info.locals) {
|
|
assert.equal(typeof local.name, "string");
|
|
assert.match(local.stack_displacement, /^-?\d+$/u);
|
|
assert.ok(local.description.includes(local.name));
|
|
assert.ok(local.description.includes(local.stack_displacement));
|
|
assert.doesNotMatch(local.description, /object at 0x/u);
|
|
}
|
|
const dossier = await call("analyze_function", { procedure: address });
|
|
assert.deepEqual(dossier.procedure.locals, info.locals);
|
|
assert.equal(dossier.procedure.classification, null);
|
|
assert.equal(dossier.procedure.body.available, false);
|
|
assert.match(
|
|
dossier.procedure.body.reason,
|
|
/public Python API does not expose complete function body ranges/u,
|
|
);
|
|
assert.equal(dossier.native_api, null);
|
|
assert.equal(dossier.native_value_flow, null);
|
|
assert.ok(
|
|
Array.isArray(dossier.limitations) && dossier.limitations.length > 0,
|
|
"function dossier omitted Hopper's unsupported-facet limitations",
|
|
);
|
|
const coordinates = instructions.instructions.map(
|
|
(line) => /^0x[0-9a-f]+/u.exec(line)?.[0],
|
|
);
|
|
assert.equal(coordinates[0], address);
|
|
assert.ok(coordinates[1], "fixture omitted a second instruction");
|
|
// The host toolchain controls the prologue encoding: without endbr64-style
|
|
// padding the first instruction is one byte, so address + 1 is itself an
|
|
// instruction boundary. Probe from inside the first multi-byte instruction
|
|
// so the interior coordinate is never an analyzed-object boundary.
|
|
let anchor = 0;
|
|
while (anchor + 1 < coordinates.length) {
|
|
if (BigInt(coordinates[anchor + 1]) - BigInt(coordinates[anchor]) > 1n)
|
|
break;
|
|
anchor += 1;
|
|
}
|
|
assert.ok(
|
|
anchor + 1 < coordinates.length,
|
|
"fixture omitted a multi-byte instruction for interior navigation",
|
|
);
|
|
const containing = coordinates[anchor];
|
|
const interior = `0x${(BigInt(containing) + 1n).toString(16)}`;
|
|
assert.equal(await call("goto_address", { address: interior }), containing);
|
|
assert.equal(await call("current_address"), containing);
|
|
assert.equal(
|
|
await call("next_address", { address: interior }),
|
|
coordinates[anchor + 1],
|
|
);
|
|
assert.equal(await call("prev_address", { address: interior }), containing);
|
|
for (const unmapped of ["0x0", "0xfffffffffffffffe"]) {
|
|
for (const operation of ["goto_address", "next_address", "prev_address"])
|
|
await invalid(
|
|
operation,
|
|
{ address: unmapped },
|
|
/Address is outside every segment/u,
|
|
);
|
|
assert.equal(await call("current_address"), containing);
|
|
for (const operation of [
|
|
"set_address_name",
|
|
"set_bookmark",
|
|
"unset_bookmark",
|
|
"set_comment",
|
|
"set_inline_comment",
|
|
])
|
|
await invalid(
|
|
operation,
|
|
{
|
|
address: unmapped,
|
|
...(operation === "unset_bookmark"
|
|
? {}
|
|
: operation.endsWith("comment")
|
|
? { comment: "must_not_apply" }
|
|
: { name: "must_not_apply" }),
|
|
},
|
|
/Address is outside every segment/u,
|
|
);
|
|
await invalid(
|
|
"set_addresses_names",
|
|
{ names: { [address]: "must_not_apply", [unmapped]: "unmapped" } },
|
|
/Address is outside every segment/u,
|
|
);
|
|
assert.equal(await call("address_name", { address }), originalName);
|
|
assert.equal(await call("comment", { address }), originalComment);
|
|
assert.equal(await call("inline_comment", { address }), originalInline);
|
|
assert.ok(
|
|
!(await call("list_bookmarks")).some((item) => item.address === unmapped),
|
|
);
|
|
}
|
|
const segments = await call("list_segments");
|
|
const first = segments.reduce((left, right) =>
|
|
BigInt(left.start) < BigInt(right.start) ? left : right,
|
|
);
|
|
const last = segments.reduce((left, right) =>
|
|
BigInt(left.end) > BigInt(right.end) ? left : right,
|
|
);
|
|
await invalid(
|
|
"prev_address",
|
|
{ address: first.start },
|
|
/No previous address exists in mapped memory/u,
|
|
);
|
|
await invalid(
|
|
"next_address",
|
|
{ address: `0x${(BigInt(last.end) - 1n).toString(16)}` },
|
|
/No adjacent analyzed object exists in mapped memory/u,
|
|
);
|
|
}
|
|
|
|
async function verifyAnnotationText(
|
|
call,
|
|
invalid,
|
|
{ address, other, originalName },
|
|
) {
|
|
const comment = "REA representation boundary 测试";
|
|
await call("set_comment", { address, comment });
|
|
await call("set_inline_comment", { address, comment });
|
|
await call("set_bookmark", { address, name: comment });
|
|
for (const [text, reason] of [
|
|
["before\0after", /contains a NUL character/u],
|
|
["before\ud800after", /contains an unpaired Unicode surrogate/u],
|
|
]) {
|
|
for (const operation of ["procedure_info", "read_bytes"])
|
|
await invalid(
|
|
operation,
|
|
operation === "procedure_info"
|
|
? { procedure: text }
|
|
: { address: text, length: 1 },
|
|
reason,
|
|
);
|
|
for (const operation of ["set_address_name", "set_bookmark"])
|
|
await invalid(operation, { address, name: text }, reason);
|
|
for (const operation of ["set_comment", "set_inline_comment"])
|
|
await invalid(operation, { address, comment: text }, reason);
|
|
await invalid(
|
|
"set_addresses_names",
|
|
{ names: { [address]: "must_not_apply", [other.address]: text } },
|
|
reason,
|
|
);
|
|
assert.equal(await call("address_name", { address }), originalName);
|
|
assert.equal(
|
|
await call("address_name", { address: other.address }),
|
|
other.value,
|
|
);
|
|
assert.equal(await call("comment", { address }), comment);
|
|
assert.equal(await call("inline_comment", { address }), comment);
|
|
assert.equal(
|
|
(await call("list_bookmarks")).find((item) => item.address === address)
|
|
?.name,
|
|
comment,
|
|
);
|
|
}
|
|
await invalid(
|
|
"set_address_name",
|
|
{ address, name: other.value },
|
|
/already assigned to/u,
|
|
);
|
|
await invalid(
|
|
"set_addresses_names",
|
|
{ names: { [address]: other.value } },
|
|
/already assigned to/u,
|
|
);
|
|
await invalid(
|
|
"set_addresses_names",
|
|
{ names: { [address]: "duplicate", [other.address]: "duplicate" } },
|
|
/assigns the same name more than once/u,
|
|
);
|
|
await invalid(
|
|
"set_addresses_names",
|
|
{ names: { [address]: "first", [address.slice(2)]: "second" } },
|
|
/selects the same address more than once/u,
|
|
);
|
|
assert.equal(await call("address_name", { address }), originalName);
|
|
assert.equal(
|
|
await call("address_name", { address: other.address }),
|
|
other.value,
|
|
);
|
|
try {
|
|
const swapped = await call("set_addresses_names", {
|
|
names: { [address]: other.value, [other.address]: originalName },
|
|
});
|
|
assert.equal(swapped[address], true);
|
|
assert.equal(swapped[other.address], true);
|
|
assert.equal(await call("address_name", { address }), other.value);
|
|
assert.equal(
|
|
await call("address_name", { address: other.address }),
|
|
originalName,
|
|
);
|
|
} finally {
|
|
await call("set_addresses_names", {
|
|
names: { [address]: originalName, [other.address]: other.value },
|
|
});
|
|
}
|
|
}
|