llvm / llvm/llvm-project

[ORC][MachO] Compact-unwind dso_base underflow when an object is emitted below the JITDylib header

Open
#208,930 1 comment 0 reactions 0 assignees View on GitHub
orcjit
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

### Summary

Under `MachOPlatform`, JIT-linking an object whose functions/LSDAs/personality GOT slot land at an address **below** the JITDylib header aborts linking with:

```
In __TEXT,__unwind_info, delta to end of functions 0x... exceeds 32 bits
```

(and similarly `... is out of 32-bit delta range of compact-unwind base at ...` for personalities).

### Root cause

`MachOPlatform` pins the `__jitlink$libunwind_dso_base` absolute symbol to the JITDylib header so that every object in the JITDylib shares a single compact-unwind base (to avoid synthesizing a header per object), in `MachOPlatform.cpp` (`modifyPassConfig`).

The `__unwind_info` writer encodes function/function-end/LSDA/personality offsets as **unsigned** 32-bit deltas from that base (`llvm/lib/ExecutionEngine/JITLink/CompactUnwindSupport.h`, `writePersonalities`/`writeIndexes`/`writeLSDAs`/`writeSecondLevelPages`, each guarded by `isUInt<32>`). `ExecutorAddrDiff` is a `uint64_t` and `operator-` wraps on underflow, so any covered address below the shared base becomes ~2^64 and fails the `isUInt<32>` check.

Nothing guarantees later graphs are laid out above the JITDylib header — the personality is first rewritten to a GOT entry in the *current* user graph, so the compared address belongs to this graph's allocation. A memory manager whose allocations can fall below the header (or simply an object linked before the header, or with a custom slab layout) triggers the abort. An upward-growing slab starting at the header only masks the problem by accident.

### Why widening to a signed delta is not the fix

The `__unwind_info` format stores unsigned image-base-relative u32 offsets that libunwind adds back to the registered `dso_base` at runtime (`libunwind/src/UnwindCursor.hpp`, `getInfoFromCompactEncodingSection`: `pc - sects.dso_base`, and `... + sects.dso_base`). Widening the JITLink check to `isInt<32>` would emit offsets the runtime cannot interpret. The base itself must be chosen so all deltas are non-negative.

### Reproduction (deterministic, link-time, host-agnostic)

`llvm-mc`/`llvm-jitlink` under `-noexec` reproduce the faulty math on any host (the compact-unwind writer runs at graph-build time). Define the base symbol above the code and place the slab below it:

```asm
.section __TEXT,__text,regular,pure_instructions
.globl _main
.p2align 2
_main:
.cfi_startproc
ret
.cfi_endproc
.globl __jitlink$libunwind_dso_base
.set __jitlink$libunwind_dso_base, 0xffff000000000000
.subsections_via_symbols
```

```
llvm-mc -triple=arm64-apple-darwin -filetype=obj -o neg.o neg.s
llvm-jitlink -noexec -num-threads=0 -entry=_main \
-slab-allocate 1Mb -slab-address 0x1000 -slab-page-size 4096 neg.o
# => ... __TEXT,__unwind_info, delta to end of functions 0x2004 exceeds 32 bits
```

Confirmed on real Apple Silicon with a released LLVM 22 toolchain, and cross-linking Mach-O on Linux.

### Proposed fix

Stop pinning the shared header as the compact-unwind base and let the writer synthesize a **per-graph** local Mach-O header (`getOrCreateCompactUnwindBase` -> `getOrCreateLocalMachOHeader`), which is forced to lay out first and so is always `<=` every covered address. Since libunwind decodes `offset + dso_base`, the per-graph base must also be reported to the runtime as `dso_base` (the runtime currently hard-codes the JITDylib header), which requires threading the base through the `register/deregister_object_platform_sections` wire.

PR forthcoming.

Contributor guide

Open the contributing guide

Research direction

Start with the deterministic llvm-mc/llvm-jitlink -noexec reproduction, then read MachOPlatform.cpp around modifyPassConfig and CompactUnwindSupport.h at getOrCreateCompactUnwindBase and the listed writers. Trace libunwind/src/UnwindCursor.hpp and the register/deregister_object_platform_sections wire to understand how dso_base is consumed. Done means below-base allocations no longer abort and the reported runtime base matches the base used for compact-unwind offsets.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.