Skip to content

cranelift-jit: handle out-of-range x86_64 calls and symbol addresses#13975

Open
glebpom wants to merge 2 commits into
bytecodealliance:mainfrom
glebpom:jit-x64-far-call-veneers
Open

cranelift-jit: handle out-of-range x86_64 calls and symbol addresses#13975
glebpom wants to merge 2 commits into
bytecodealliance:mainfrom
glebpom:jit-x64-far-call-veneers

Conversation

@glebpom

@glebpom glebpom commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Handle JIT functions and data allocated outside x86_64's signed 32-bit PC-relative range.

This change:

  • Routes out-of-range direct calls and tail jumps through veneers.
  • Materializes symbol addresses through GOT-relative loads that the JIT resolves itself, relaxing them to direct leas when the symbol turns out to be in range.

Fixes #4000.

Motivation

A JITMemoryProvider may allocate generated code, data objects, and functions independently. It does not guarantee that these allocations will be within ±2 GiB of one another.

Both relevant x86_64 relocation forms use signed 32-bit PC-relative displacements:

  • X86CallPCRel4 performs a direct call or jump.
  • X86PCRel4 produces the address of a symbol.

If the target is outside that range, finalize_definitions previously panicked when converting the displacement to i32.

The problem was observed in Nervix while compiling Roto UDFs. The CI run intermittently failed with both TryFromIntError(NegOverflow) and TryFromIntError(PosOverflow):

https://github.com/nervix-io/nervix/actions/runs/30129378927/job/89600292950?pr=57#step:17:56674

After adding call veneers, a follow-up run showed that Roto could still fail while materializing addresses of separately allocated data:

https://github.com/nervix-io/nervix/actions/runs/30147477548/job/89651977365?pr=57

Implementation

The work is organized as two commits.

1. Calls and tail jumps: veneers

  • Reserve a worst-case 16-byte veneer slot for each X86CallPCRel4 relocation.
  • Keep in-range calls and jumps direct.
  • Redirect out-of-range control transfers through a jmp qword ptr [rip] veneer containing the absolute target address, the same way Arm64Call is already handled.
  • Create a separate, densely packed veneer for each out-of-range transfer.
  • Fix an off-by-one in the AArch64 veneer bounds assertion.

Redirecting through a veneer is only sound for control transfers, so X86CallPCRel4 is now reserved for call/jmp displacements and this invariant is documented on the relocation. The RIP-relative lea emitted by LoadExtName for near symbols now uses X86PCRel4 instead; LabelUse::from_reloc and the text-section builder learned to resolve it, and object files now get a PC-relative rather than a branch relocation for the lea, matching its semantics.

2. Symbol addresses: a per-blob GOT

A branch veneer cannot fix an address-producing instruction: it would produce the address of the veneer rather than the requested symbol. Instead, as suggested by @bjorn3 in review, the JIT now uses GOT-based address materialization:

  • JITBuilder defaults to is_pic=true on x86_64 (and JITModule rejects PIC on other architectures), so LoadExtName emits movq sym@GOTPCREL(%rip) loads with X86GOTPCRel4 relocations.
  • The JIT reserves an 8-byte GOT entry per X86GOTPCRel4 relocation at the end of the blob's allocation ([code][veneers][GOT entries]), stores the absolute symbol address there at finalize time, and points the load's displacement at the entry, which is always in range.
  • When the symbol is within displacement range, the load is relaxed to a direct lea (a one-byte movlea opcode rewrite), mirroring the R_X86_64_REX_GOTPCRELX relaxation performed by linkers, so near symbol accesses don't pay for the indirection.
  • X86CallPLTRel4, emitted for the ELF TLS model's __tls_get_addr call, resolves like X86CallPCRel4: direct when in range, through a veneer otherwise.

Direct calls are unaffected: they keep their colocated references and use veneers when needed. GOT entries hold the symbol's real address, preserving pointer identity with the finalized definitions.

Testing

The JIT integration tests use a custom memory provider that deterministically places allocations on opposite sides of a virtual address range larger than 2 GiB.

Call and veneer coverage:

  • A compiler-generated call with a negative out-of-range displacement.
  • A tail jump with a positive out-of-range displacement.
  • A mixture of one near and two far calls with distinct, densely packed veneers.
  • Veneer instructions, absolute targets, and allocation sizing.
  • Execution of the resulting JIT code.

Address-materialization coverage:

  • A read-only data object and a function allocated more than 2 GiB from the code, verifying the X86GOTPCRel4 relocation, the GOT entry contents, pointer identity, and execution.
  • A shared function reference used by both call and func_addr, verifying the call remains X86CallPCRel4 while the address goes through the GOT.
  • The movlea relaxation for a near symbol, verifying the rewritten instruction points directly at the data.
  • LoadExtName near-symbol relocation kind, MachTextSectionBuilder resolution of X86PCRel4, and non-branch address relocations for ELF, Mach-O, and COFF objects.

The test ISA helper now enables is_pic on x86_64, resolving an old FIXME from when the JIT last supported PIC; libcall_function therefore exercises GOT resolution against real host symbols.

Commands run:

  • cargo test -p cranelift-codegen --lib
  • cargo test -p cranelift-jit
  • cargo test -p cranelift-object
  • cargo test -p cranelift-tools --test filetests -- --nocapture
  • cargo clippy -p cranelift-codegen -p cranelift-jit -p cranelift-object --tests -- -D warnings
  • cargo fmt --all -- --check

Notes

AArch64 direct calls already use veneers, and its non-colocated materialization uses an inline literal, so it works at any distance. Its colocated adrp+add sequence retains a pre-existing ±2 GiB assumption in the Aarch64AdrPrelPgHi21 handler; supporting is_pic with the AArch64 GOT relocations would be the analogous fix and is left as a follow-up.

AI-assisted development

Claude Code assisted with the call-veneer implementation and with the GOT-based address materialization following the review suggestion. OpenAI Codex assisted with auditing the fix, recovering and evaluating stashed work, strengthening the tests, and porting the changes to the current main branch.

All AI-assisted code and text were reviewed by the human contributor, who understands the changes and accepts full responsibility for the contribution.
`

@glebpom
glebpom marked this pull request as ready for review July 25, 2026 05:08
@glebpom
glebpom requested a review from a team as a code owner July 25, 2026 05:08
@glebpom
glebpom requested review from fitzgen and removed request for a team July 25, 2026 05:08
@github-actions github-actions Bot added cranelift Issues related to the Cranelift code generator cranelift:area:x64 Issues related to x64 codegen cranelift:module labels Jul 25, 2026
@glebpom
glebpom marked this pull request as draft July 25, 2026 18:03
@glebpom glebpom changed the title cranelift-jit: route out-of-range x86_64 calls through veneers cranelift-jit: handle far x86_64 calls and address materialization Jul 25, 2026
@glebpom
glebpom marked this pull request as ready for review July 25, 2026 19:08
Comment thread cranelift/jit/src/backend.rs Outdated
/// Function references can be shared by `call` and `func_addr`
/// instructions. Keep the original reference for direct calls, which can
/// use veneers when necessary, and give `func_addr` its own far reference.
fn use_far_address_relocations(func: &mut ir::Function) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a hack to me. cranelift-jit shouldn't modify the function. A better solution is probably to just add support for PLT relocations and then tell Cranelift to emit PLT relocations.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjorn3 thanks, please check the updated version

glebpom added 2 commits July 25, 2026 17:11
`CompiledBlob::perform_relocations` computed the 32-bit displacement of
`X86CallPCRel4` relocations with `i32::try_from(..).unwrap()`, so a
`call` whose target ended up further than ±2 GiB away from the call site
panicked:

    panicked at cranelift/jit/src/compiled_blob.rs:142:80:
    called `Result::unwrap()` on an `Err` value: TryFromIntError(NegOverflow)

Nothing guarantees that two allocations of a `JITMemoryProvider` are
within ±2 GiB of each other: mmap is free to place separate code
allocations arbitrarily far apart in a 64-bit address space, so
module-local calls between separately defined functions can exceed the
`rel32` range. AArch64 already handles this by routing out-of-range
`Arm64Call` relocations through a veneer at the end of the function; do
the same for x86_64 by redirecting out-of-range `X86CallPCRel4`
relocations to a `jmp qword ptr [rip]` + absolute-address veneer,
reserving the exact worst-case veneer size per branch relocation.

Redirecting a control transfer through a veneer is only sound if the
relocation is applied to the displacement of a `call`/`jmp` instruction.
The x64 backend used `X86CallPCRel4` also for the RIP-relative `lea`
emitted by `Inst::LoadExtName` with `RelocDistance::Near`, which
materializes a symbol's address and must not be redirected. Switch that
to `X86PCRel4` (keeping the existing behavior of failing loudly when the
symbol is out of range), document the invariant on
`Reloc::X86CallPCRel4`, and teach x64's `LabelUse::from_reloc` about
`X86PCRel4` so that `TextSectionBuilder::resolve_reloc` can still
resolve such relocations against in-module functions. For object files
this also means the `lea` now gets a PC-relative relocation rather than
a branch relocation, matching its semantics; verify the relocations
emitted for address materialization across ELF, Mach-O, and COFF.

Also fix an off-by-one in the AArch64 veneer bounds assert:
`veneer_idx <= veneer_count` tolerated writing one veneer past the
reserved space. This is unreachable today because one veneer slot is
reserved per call relocation, but as a sanity check it was wrong.

The new JIT tests use a custom memory provider that deterministically
places allocations on opposite sides of a virtual reservation larger
than 2 GiB. They exercise a compiler-emitted call, a tail jump, both
displacement directions, and mixed near/far targets with densely packed
veneers; verify veneer shape, absolute targets, and allocation sizing;
and execute the JIT-ed code to check that the calls actually reach
their callees.
Routing out-of-range calls through veneers is not enough: JIT code can
also materialize the address of a function or data object with
`func_addr` or `symbol_value`. For colocated symbols the x64 backend
emits a RIP-relative `lea` whose `X86PCRel4` relocation likewise
assumes the symbol is within ±2 GiB of the code, and a branch veneer
cannot fix an address-producing instruction: it would produce the
address of the veneer rather than the requested symbol. When a
`JITMemoryProvider` placed the symbol out of range,
`finalize_definitions` panicked.

As suggested by bjorn3, enable is_pic on x86_64 so that address
materialization emits GOT-relative loads, and resolve them in the JIT
relocation pass:

- Reserve an 8-byte GOT entry for each `X86GOTPCRel4` relocation after
  the veneers at the end of the blob's allocation, store the absolute
  symbol address there, and point the load's displacement at the
  entry, which is always in range.
- Relax the load to a direct `lea` when the symbol does turn out to be
  within displacement range, mirroring the `R_X86_64_REX_GOTPCRELX`
  relaxation performed by linkers, so near symbol accesses don't pay
  for the indirection.
- Resolve `X86CallPLTRel4`, emitted for the ELF TLS model's
  `__tls_get_addr` call, like `X86CallPCRel4`: direct when in range
  and through a veneer otherwise.

`JITBuilder` now defaults to is_pic=true on x86_64 hosts, and
`JITModule` rejects is_pic=true elsewhere. Direct calls are
unaffected: they keep using `X86CallPCRel4` with veneers for
out-of-range targets, and the GOT entry holds the symbol's real
address, preserving pointer identity with the finalized definitions.

The new tests cover a read-only data object and a function allocated
more than ±2 GiB from the code, verifying the emitted relocations, the
GOT entry contents, pointer identity, and execution of the generated
code, as well as the mov-to-lea relaxation for near symbols. Enable
is_pic in the test ISA helper on x86_64, resolving an old FIXME from
the era when the JIT last supported PIC.
@glebpom
glebpom force-pushed the jit-x64-far-call-veneers branch from 2334e13 to 6d9998a Compare July 25, 2026 22:14
@glebpom glebpom changed the title cranelift-jit: handle far x86_64 calls and address materialization cranelift-jit: handle out-of-range x86_64 calls and symbol addresses Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cranelift:area:x64 Issues related to x64 codegen cranelift:module cranelift Issues related to the Cranelift code generator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cranelift: JIT relocations depend on system allocator behaviour

2 participants