mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
fix(native): wrap the legacy rebase cursor at 64 bits like dyld (#8111)
dyld keeps the LC_DYLD_INFO rebase cursor in a uint64_t, so an
ADD_ADDR_ULEB, DO_REBASE_ADD_ADDR_ULEB or single-slot skip operand above
2^63 steps backward. REA tracked the cursor as an unbounded bigint, so the
same stream overflowed past 2^64, failed as "Rebase expansion leaves its
file-backed segment" and left every later slot unknown. The bind stream
already wraps the same way since 576b58cef.
Every advance now wraps at 64 bits. addRun still bounds each emitted slot
to its segment, so a wrapped cursor outside the segment and multi-slot
runs with a backward skip remain refused.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
2 files changed
+90
-4
No files matched your search
@@ -580,6 +580,87 @@ it("rejects count expansion outside the selected file-backed segment without exp
|
||||
});
|
||||
});
|
||||
|
||||
// ULEB encodings of 64-bit two's-complement backward steps.
|
||||
const MINUS_0X10 = [0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01];
|
||||
const MINUS_0X18 = [0xe8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01];
|
||||
const MINUS_0X200 = [
|
||||
0x80, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
|
||||
];
|
||||
|
||||
it("wraps a backward ADD_ADDR_ULEB rebase step at 64 bits like dyld", () => {
|
||||
// SET_TYPE(pointer), SET_SEGMENT(offset 0x100), DO once,
|
||||
// ADD_ADDR(-0x10), DO once, DONE. dyld's uint64 cursor lands on 0xf8.
|
||||
const { fixups } = legacyRebaseInput(
|
||||
Buffer.from([0x11, 0x20, 0x80, 0x02, 0x51, 0x30, ...MINUS_0X10, 0x51, 0]),
|
||||
);
|
||||
|
||||
expect(fixups.decode(BASE + 0x100n, 0x200n)).toEqual({
|
||||
kind: "rebase",
|
||||
target: 0x200n,
|
||||
});
|
||||
expect(fixups.decode(BASE + 0xf8n, 0x300n)).toEqual({
|
||||
kind: "rebase",
|
||||
target: 0x300n,
|
||||
});
|
||||
expect(fixups.issues).toEqual([]);
|
||||
expect(fixups.unreadRebaseBytes).toBe(0);
|
||||
});
|
||||
|
||||
it("wraps backward rebase-and-advance and single-slot skip steps", () => {
|
||||
// SET_TYPE(pointer), SET_SEGMENT(offset 0x100),
|
||||
// DO_REBASE_ADD_ADDR(-0x18) → 0xf0, DO 1 skipping -0x18 → 0xe0, DO once.
|
||||
const { fixups } = legacyRebaseInput(
|
||||
Buffer.from([
|
||||
0x11,
|
||||
0x20,
|
||||
0x80,
|
||||
0x02,
|
||||
0x70,
|
||||
...MINUS_0X18,
|
||||
0x80,
|
||||
0x01,
|
||||
...MINUS_0X18,
|
||||
0x51,
|
||||
0,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const offset of [0x100n, 0xf0n, 0xe0n])
|
||||
expect(fixups.decode(BASE + offset, 0x200n)).toEqual({
|
||||
kind: "rebase",
|
||||
target: 0x200n,
|
||||
});
|
||||
expect(fixups.decode(BASE + 0xe8n, 0n)).toEqual({ kind: "raw", target: 0n });
|
||||
expect(fixups.issues).toEqual([]);
|
||||
});
|
||||
|
||||
it("still refuses a wrapped rebase cursor that leaves the segment", () => {
|
||||
// SET_SEGMENT(offset 0x100), ADD_ADDR(-0x200) wraps below the segment start.
|
||||
const { fixups } = legacyRebaseInput(
|
||||
Buffer.from([0x11, 0x20, 0x80, 0x02, 0x30, ...MINUS_0X200, 0x51, 0]),
|
||||
);
|
||||
|
||||
expect(fixups.decode(BASE + 0x100n, 0n)).toMatchObject({
|
||||
kind: "unknown",
|
||||
reason: expect.stringContaining("leaves its file-backed segment"),
|
||||
});
|
||||
expect(fixups.issues).toMatchObject([
|
||||
{ code: "rebase_stream_decode_failed" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("still refuses a multi-slot run with a backward skip", () => {
|
||||
// DO 2 skipping -0x18 would revisit earlier slots; this bounded decoder refuses that run.
|
||||
const { fixups } = legacyRebaseInput(
|
||||
Buffer.from([0x11, 0x20, 0x80, 0x02, 0x80, 0x02, ...MINUS_0X18, 0]),
|
||||
);
|
||||
|
||||
expect(fixups.decode(BASE + 0x100n, 0n)).toMatchObject({
|
||||
kind: "unknown",
|
||||
reason: expect.stringContaining("leaves its file-backed segment"),
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds cumulative work across repeated full-range runs and retains the prefix", () => {
|
||||
const data = Buffer.alloc(0x10000);
|
||||
const repeatedRun = Buffer.from([
|
||||
|
||||
@@ -778,6 +778,11 @@ const readRebaseStream = (
|
||||
let segmentIndex = -1;
|
||||
let segmentOffset = 0n;
|
||||
let type = 0;
|
||||
// dyld keeps the cursor in a uint64_t, so ULEB deltas wrap to encode
|
||||
// backward steps; addRun still bounds every emitted slot to the segment.
|
||||
const advance = (delta: bigint): void => {
|
||||
segmentOffset = (segmentOffset + delta) & 0xffffffffffffffffn;
|
||||
};
|
||||
const addRun = (count: bigint, skip: bigint): void => {
|
||||
if (count === 0n) return;
|
||||
if (type === 0 || type > 3)
|
||||
@@ -812,7 +817,7 @@ const readRebaseStream = (
|
||||
);
|
||||
// dyld advances after the final record too; that cursor may legally be
|
||||
// one past the segment because it is only bounds-checked on the next emit.
|
||||
segmentOffset += count * stride;
|
||||
advance(count * stride);
|
||||
};
|
||||
while (cursor.at < end) {
|
||||
if (index.workRemaining === 0)
|
||||
@@ -842,10 +847,10 @@ const readRebaseStream = (
|
||||
break;
|
||||
}
|
||||
case 0x30: // REBASE_OPCODE_ADD_ADDR_ULEB
|
||||
segmentOffset += uleb(bytes, cursor, end, "rebase", index);
|
||||
advance(uleb(bytes, cursor, end, "rebase", index));
|
||||
break;
|
||||
case 0x40: // REBASE_OPCODE_ADD_ADDR_IMM_SCALED
|
||||
segmentOffset += BigInt(immediate) * REBASE_POINTER_SIZE;
|
||||
advance(BigInt(immediate) * REBASE_POINTER_SIZE);
|
||||
break;
|
||||
case 0x50: // REBASE_OPCODE_DO_REBASE_IMM_TIMES
|
||||
addRun(BigInt(immediate), 0n);
|
||||
@@ -859,7 +864,7 @@ const readRebaseStream = (
|
||||
case 0x70: {
|
||||
// REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB
|
||||
addRun(1n, 0n);
|
||||
segmentOffset += uleb(bytes, cursor, end, "rebase", index);
|
||||
advance(uleb(bytes, cursor, end, "rebase", index));
|
||||
break;
|
||||
}
|
||||
case 0x80: {
|
||||
|
||||
Reference in new issue
Block a user