diff --git a/src/native/AppleMachoFixups.test.ts b/src/native/AppleMachoFixups.test.ts index d44825707f..5eb1d03078 100644 --- a/src/native/AppleMachoFixups.test.ts +++ b/src/native/AppleMachoFixups.test.ts @@ -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([ diff --git a/src/native/AppleMachoFixups.ts b/src/native/AppleMachoFixups.ts index d0b09f1070..b7608bd416 100644 --- a/src/native/AppleMachoFixups.ts +++ b/src/native/AppleMachoFixups.ts @@ -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: {