Skip to content

Bind every unwinder entry point to the executable's libunwind - #7

Open
buyemune-sys wants to merge 2 commits into
pg83:mainfrom
buyemune-sys:fix/one-unwinder
Open

buyemune-sys wants to merge 2 commits into
pg83:mainfrom
buyemune-sys:fix/one-unwinder

Conversation

@buyemune-sys

@buyemune-sys buyemune-sys commented Oct 17, 2026 •

Copy link
Copy Markdown

The problem

The bridge overrides _Unwind_RaiseException and the accessors C++ personalities use (GetLanguageSpecificData, GetRegionStart, GetIPInfo, SetGR, SetIP, …). It does not override _Unwind_GetCFA, _Unwind_GetIP, _Unwind_GetGR, _Unwind_Backtrace, _Unwind_ForcedUnwind, _Unwind_FindEnclosingFunction, _Unwind_Find_FDE, nor __register_frame / __deregister_frame. In a guest linked against libgcc_s, those come from the host's libgcc while the exception itself is unwound by the vendored libunwind. Two things break:

  1. A personality reads a foreign context. libgcc's _Unwind_GetCFA(context) reads libunwind's _Unwind_Context with libgcc's layout and returns garbage. LuaJIT's personality does exactly this (lj_err_unwind_dwarf → _Unwind_GetCFA). So any glibc binary embedding LuaJIT (Tarantool, in our case) segfaults in err_unwind under solo run on its first Lua error, even one caught by pcall.
  2. Unwind info for JIT code lands in the wrong unwinder. __register_frame, which a JIT uses to register the .eh_frame of generated code, goes to libgcc. The exception, however, looks FDEs up in libunwind, so the search phase stops at the first generated frame (_URC_END_OF_STACK before the personality is ever reached).

The table already says "_Unwind_Context is private to the unwinder that created it" (lib/glibc_shim.cpp:4975); the list just wasn't complete.

Reproduction

LD_DEBUG=bindings solo run tarantool -e 'pcall(error, "x")' on main:

solo: bind .../tarantool: _Unwind_RaiseException -> glibc bridge (override)
solo: bind .../tarantool: _Unwind_GetCFA -> /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
Segmentation fault ... #1 crash_signal_cb  (rip in err_unwind)

A minimal C program does the same: its own personality via .cfi_personality, _Unwind_RaiseException, and _Unwind_GetCFA inside the personality. Natively on glibc the CFA points into the stack next to the frame's locals; on main under solo it is cfa=7de00faf32fd with anchor=7fff85d3df9f.

The fix

  • Every entry point listed above is now in the override table and points at the executable's libunwind, so the process has a single unwinder.
  • __register_frame needs an adapter. libgcc's takes a whole .eh_frame section (CIEs and FDEs up to a zero terminator), while LLVM libunwind 15's takes one FDE (ext/llvm/libunwind/src/UnwindLevel1-gcc-ext.c:235). The bridge walks the section and registers each FDE, skipping CIEs. That is the semantics LuaJIT and other JITs expect.

Test

tst/glibc_guest_test.c gains:

  • a guest-owned personality that reads _Unwind_GetCFA and checks that the CFA sits in the stack just below the frame's locals (inside a personality, both libgcc and libunwind report the examined frame's SP at its call site);
  • an exception raised through a "JIT" trampoline in RWX memory, whose .eh_frame is built at run time and registered through __register_frame with libgcc semantics. There is code for both x86_64 and aarch64.

Guests now link the sysroot's libgcc_s.so.1, and the bundle carries it too, since the bundle runs without LD_LIBRARY_PATH. New needle: guest unwind personality=called cfa=ok.

  • on main (test only, no fix): solo run misses: guest unwind personality=called cfa=ok;
  • with the fix: ./build test passes (x86_64, Ubuntu, GCC). The aarch64 code is written but was not run locally.

Context

We found this while running the static build of Tarantool (a glibc binary embedding LuaJIT) as a solo bundle. With this fix and the other PRs in the series, Tarantool passes its smoke test on Alpine, CentOS 6 (glibc 2.12) and busybox:musl: memtx and vinyl, net.box, JIT, popen, http, C modules.

buyemune-sys and others added 2 commits October 20, 2026 15:42
Security updates replaced libapparmor1 4.0.1really4.0.1-0ubuntu0.24.04.7
with .9 and libexpat1 2.6.1-2ubuntu0.4 with ubuntu0.6, and the pool kept
only the new files, so the cold-cache Fetch x86_64 job answers 404 and
every x86-64 job behind it is skipped. Both pins move to the versions
noble-updates ships now; their SHA256s match the archive's Packages index.
The other twelve pins are still served.
The bridge overrode _Unwind_RaiseException and the accessors C++
personalities use, but not _Unwind_GetCFA, _Unwind_GetIP, _Unwind_GetGR,
_Unwind_Backtrace, _Unwind_ForcedUnwind, _Unwind_FindEnclosingFunction,
_Unwind_Find_FDE, nor __register_frame and __deregister_frame. A guest
that links libgcc_s got those from the host's libgcc while the
exception itself ran in the vendored libunwind:

- a personality calling _Unwind_GetCFA read libunwind's context with
  libgcc's layout. LuaJIT does exactly that, so the first Lua error in
  a glibc-linked LuaJIT (Tarantool, for one) crashed in err_unwind;
- unwind info a JIT registers for its generated code went into libgcc,
  which never walks the exception, so the search phase stopped at the
  first generated frame.

All of them are now overrides. __register_frame also needs an adapter:
libgcc's takes a whole .eh_frame section up to its zero terminator,
libunwind's takes one FDE, so the bridge walks the section and registers
each FDE.

The guest test gains a personality of its own reading _Unwind_GetCFA,
and raises through a generated frame registered the libgcc way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant