[GH-ISSUE #8116] [Bug] Legacy Mach-O rebase decoder does not wrap its cursor at 64 bits, losing slots after a backward ULEB step #4679

Open
opened 2026-10-31 00:52:22 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @sshworld on GitHub (Oct 30, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8116

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

The legacy LC_DYLD_INFO rebase decoder (readRebaseStream in src/native/AppleMachoFixups.ts) keeps its cursor as an unbounded bigint. dyld keeps the same cursor in a uint64_t, so a ULEB delta above 2^63 is a backward step that wraps.

REA does not wrap. The cursor becomes 2^64 + offset, the stream fails with Rebase expansion leaves its file-backed segment, and every slot after that point decodes as unknown with a rebase_stream_decode_failed issue. dyld rebases those slots.

The bind decoder in the same file already wraps at 64 bits ("ULEB deltas wrap at 64 bits to encode negative steps", added in 576b58cef from #3138). The rebase path still uses plain += in four places:

  • ADD_ADDR_ULEB
  • ADD_ADDR_IMM_SCALED
  • DO_REBASE_ADD_ADDR_ULEB
  • the post-run advance in addRun

Reproduction

Executed with the existing legacyRebaseInput harness in src/native/AppleMachoFixups.test.ts (one segment of 0x2000 bytes at BASE) on main 317bf3fe2:

11 20 80 02 51 30 f0 ff ff ff ff ff ff ff ff 01 51 00

These opcodes are:

  1. SET_TYPE(pointer)
  2. SET_SEGMENT_AND_OFFSET(0, 0x100)
  3. DO_REBASE_IMM_TIMES(1), after which the cursor is at 0x108
  4. ADD_ADDR_ULEB(0xFFFFFFFFFFFFFFF0), which is a step of -0x10
  5. DO_REBASE_IMM_TIMES(1)
  6. DONE

fixups.decode(BASE + 0xf8n, 0x300n) returns:

{ kind: 'unknown', raw: 768n, reason: "LC_DYLD_INFO pointer-fixup applicability was not completely decoded: rebase opcodes: Rebase expansion leaves its file-backed segment" }

Expected behavior

The result should match dyld: 0x108 + 0xFFFFFFFFFFFFFFF0 wraps to 0xF8, which lies inside the segment. Both 0x100 and 0xF8 should decode as { kind: "rebase" }, with no issues. A wrapped cursor that lands outside the segment should still be refused, as it is today.

Impact and scope

The effect is wrong pointer-fixup facts (unknown instead of rebase) for every slot after the backward step. Those facts feed Objective-C and Swift metadata decoding. The stream never crashes, and the failure is disclosed as an issue.

Likelihood: I did not find a mainstream linker that emits backward rebase deltas.

  • ld64's RebaseInfoAtom::encode (src/ld/LinkEdit.hpp) computes ADD_ADDR_ULEB(it->_address - address) as a uint64_t difference. However, it sorts by (type, address), and the rebase type follows the segment's protection, so the order is ascending within one segment in practice.
  • lld emits address order.

So the trigger is a stream that dyld accepts but that comes from hand-crafted or non-standard tooling. For a reverse-engineering tool, that is a parser/loader differential: REA reports less than the loader applies.

REA version

main 317bf3fe2 (6.8.0)

Runtime and platform

macOS 15 arm64, Node.js 24.22.1

Target and artifact context

A synthetic legacy rebase stream in the repository's test harness. No real-binary sample.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

See the Reproduction section. The bind-side equivalent is fixed and tested ("wraps a negative DO_BIND_ADD_ADDR_ULEB step and keeps decoding").

Typed outcome or recovery expected

Wrap the rebase cursor at 64 bits and keep addRun's existing bound on every emitted slot. A multi-slot run with a backward skip would revisit earlier slots; no linker emits it, so it can stay refused.

Analysis or suspected cause

bigint arithmetic never overflows, so the uint64_t wraparound that the format relies on has to be emulated explicitly, as the bind path already does. A PR with regression tests follows.

Additional context

NOT_ENOUGH_INFO: I have no real-world binary that contains a backward rebase delta.

Originally created by @sshworld on GitHub (Oct 30, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8116 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior The legacy `LC_DYLD_INFO` rebase decoder (`readRebaseStream` in `src/native/AppleMachoFixups.ts`) keeps its cursor as an unbounded `bigint`. dyld keeps the same cursor in a `uint64_t`, so a ULEB delta above 2^63 is a backward step that wraps. REA does not wrap. The cursor becomes `2^64 + offset`, the stream fails with `Rebase expansion leaves its file-backed segment`, and every slot after that point decodes as `unknown` with a `rebase_stream_decode_failed` issue. dyld rebases those slots. The bind decoder in the same file already wraps at 64 bits ("ULEB deltas wrap at 64 bits to encode negative steps", added in 576b58cef from #3138). The rebase path still uses plain `+=` in four places: - `ADD_ADDR_ULEB` - `ADD_ADDR_IMM_SCALED` - `DO_REBASE_ADD_ADDR_ULEB` - the post-run advance in `addRun` ### Reproduction Executed with the existing `legacyRebaseInput` harness in `src/native/AppleMachoFixups.test.ts` (one segment of 0x2000 bytes at `BASE`) on main `317bf3fe2`: ``` 11 20 80 02 51 30 f0 ff ff ff ff ff ff ff ff 01 51 00 ``` These opcodes are: 1. `SET_TYPE(pointer)` 2. `SET_SEGMENT_AND_OFFSET(0, 0x100)` 3. `DO_REBASE_IMM_TIMES(1)`, after which the cursor is at 0x108 4. `ADD_ADDR_ULEB(0xFFFFFFFFFFFFFFF0)`, which is a step of -0x10 5. `DO_REBASE_IMM_TIMES(1)` 6. `DONE` `fixups.decode(BASE + 0xf8n, 0x300n)` returns: ``` { kind: 'unknown', raw: 768n, reason: "LC_DYLD_INFO pointer-fixup applicability was not completely decoded: rebase opcodes: Rebase expansion leaves its file-backed segment" } ``` ### Expected behavior The result should match dyld: `0x108 + 0xFFFFFFFFFFFFFFF0` wraps to `0xF8`, which lies inside the segment. Both 0x100 and 0xF8 should decode as `{ kind: "rebase" }`, with no issues. A wrapped cursor that lands outside the segment should still be refused, as it is today. ### Impact and scope The effect is wrong pointer-fixup facts (`unknown` instead of `rebase`) for every slot after the backward step. Those facts feed Objective-C and Swift metadata decoding. The stream never crashes, and the failure is disclosed as an issue. Likelihood: I did not find a mainstream linker that emits backward rebase deltas. - ld64's `RebaseInfoAtom::encode` (`src/ld/LinkEdit.hpp`) computes `ADD_ADDR_ULEB(it->_address - address)` as a `uint64_t` difference. However, it sorts by (type, address), and the rebase type follows the segment's protection, so the order is ascending within one segment in practice. - lld emits address order. So the trigger is a stream that dyld accepts but that comes from hand-crafted or non-standard tooling. For a reverse-engineering tool, that is a parser/loader differential: REA reports less than the loader applies. ### REA version main 317bf3fe2 (6.8.0) ### Runtime and platform macOS 15 arm64, Node.js 24.22.1 ### Target and artifact context A synthetic legacy rebase stream in the repository's test harness. No real-binary sample. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence See the Reproduction section. The bind-side equivalent is fixed and tested ("wraps a negative DO_BIND_ADD_ADDR_ULEB step and keeps decoding"). ### Typed outcome or recovery expected Wrap the rebase cursor at 64 bits and keep `addRun`'s existing bound on every emitted slot. A multi-slot run with a backward skip would revisit earlier slots; no linker emits it, so it can stay refused. ### Analysis or suspected cause `bigint` arithmetic never overflows, so the `uint64_t` wraparound that the format relies on has to be emulated explicitly, as the bind path already does. A PR with regression tests follows. ### Additional context NOT_ENOUGH_INFO: I have no real-world binary that contains a backward rebase delta.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4679
No description provided.