fix(hopper): probe interior navigation from multi-byte instructions (#1285)

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.
This commit is contained in:
BaaRai 2026-10-10 00:07:01 +02:00 committed by GitHub
parent 392f1f3107
commit bf934d5592
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 21 additions and 6 deletions

View File

@ -91,14 +91,29 @@ async function verifyNavigation(
);
assert.equal(coordinates[0], address);
assert.ok(coordinates[1], "fixture omitted a second instruction");
const interior = `0x${(BigInt(address) + 1n).toString(16)}`;
assert.equal(await call("goto_address", { address: interior }), address);
assert.equal(await call("current_address"), address);
// 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[1],
coordinates[anchor + 1],
);
assert.equal(await call("prev_address", { address: interior }), address);
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(
@ -106,7 +121,7 @@ async function verifyNavigation(
{ address: unmapped },
/Address is outside every segment/u,
);
assert.equal(await call("current_address"), address);
assert.equal(await call("current_address"), containing);
for (const operation of [
"set_address_name",
"set_bookmark",