Skip to content

luajit: support statically linked Tarantool on amd64 and arm64 - #316

Draft
dalehamel wants to merge 8 commits into
parca-dev:mainfrom
Shopify:dale/luajit-tarantool-parca
Draft

dalehamel wants to merge 8 commits into
parca-dev:mainfrom
Shopify:dale/luajit-tarantool-parca

Conversation

@dalehamel

@dalehamel dalehamel commented Jul 15, 2026 •

Copy link
Copy Markdown

Summary

Ports Shopify's production Tarantool fixes for the LuaJIT unwinder onto Parca's merged extractor stack and includes the runtime corrections found while building deterministic coredump coverage in #317.

Final head: 39dd4c54 (rebased onto parca-dev/main@0cf69159).

This builds on the merged extractor series:

The first five commits are the curated Tarantool runtime port through Shopify's deployed revision 7916b625. The final three commits select the correct runtime-specific x64 C-frame layout, classify Tarantool's proven arm64 fiber trampoline as a synthetic stack root, and rebuild both eBPF blobs. The obsolete intermediate blob-only rebuild was dropped during the rebase; the final blobs were regenerated from the complete source against current main. #317 remains a separate one-commit fixture/documentation change.

This follows the request to feed downstream fixes back through the Parca fork for the next upstream batch of open-telemetry#1236.

Production evidence

The following completed production profile shows native Tarantool frames handing through lua_pcall into built-in Lua, application Lua, and LuaJIT FFI frames. The query identifier and application-specific Lua/schema labels are covered with solid opaque redactions; the image contains no node, cluster, or schedule identifier.

Redacted production Tarantool native and LuaJIT flamegraph

Why Tarantool needs this

Parca's LuaJIT work primarily targets a dynamically linked OpenResty libluajit-5.1.so. Tarantool differs in several relevant ways:

  • LuaJIT is statically linked into the tarantool executable.
  • VM symbols such as lj_vm_asm_begin and lj_vm_exit_handler are hidden in .symtab.
  • A large static binary defeats the existing first-large-unwind-gap VM heuristic.
  • Tarantool inserts global_State.mem_L between cur_L and jit_base.
  • Tarantool x64 stores its previous C-frame link at 48 bytes, while standard LuaJIT/OpenResty x64 uses 32 bytes.
  • Lua runs on fibers; the arm64 coro_startup assembly trampoline has no CFI and is a synthetic stack root.

Changes

Tarantool extraction and native handback

  • Detect the statically linked tarantool executable in the existing LuaJIT loader.
  • Scan static and dynamic symbols once and anchor the VM interval to lj_vm_asm_begin when available.
  • Extract global_State.jit_base independently from cur_L:
  • Keep cur_L+8 as the stripped OpenResty fallback, but fail Tarantool attachment if extraction is unavailable because that fallback is known to be wrong there.
  • Derive interpreter/JIT native-handback C-frame values from the VM interval's unwind data and handle both SP- and FP-based CFA rules.
  • Step over the interpreter gate when handing the bottom Lua frame back to the native unwinder.
  • Guard a transient NULL jit_base instead of subtracting from it.

Runtime-specific previous C-frame layout

LuaJITProcInfo now carries cframe_prev_offset, selected by the same exact executable-name contract used by the loader:

architecture/runtime previous C-frame offset
amd64 standard LuaJIT/OpenResty 32 bytes (4*8)
amd64 Tarantool 48 bytes (6*8)
arm64 standard LuaJIT/OpenResty/Tarantool 0

A zero field retains backward-compatible architecture-default behavior in eBPF, so old or zero-initialized map records remain valid. The Go/C ABI size is asserted as 0xe; generated Go types and both eBPF blobs are synchronized.

Exact arm64 synthetic-root classification

Only the proven Tarantool trampoline becomes UnwindInfoStop. Detection requires all of:

  1. AArch64 ELF input.
  2. A .symtab symbol named exactly coro_startup.
  3. The exact 12-byte mov x0,x20; blr x19; b trampoline signature.

The ELF string-table prefilter is capped at 16 MiB to match VisitSymbols; malformed/oversized tables, bad links, invalid addresses, overflow, and instruction mismatches fail closed. STOP and restore boundaries are marked UnwindHintKeep so generic delta compaction cannot erase the exact interval. Arbitrary missing-CFI ranges remain UnwindInfoInvalid.

Scope

Intentionally omitted:

Validation

Post-rebase validation was run in architecture-matched Linux environments:

  • Full non-integration go test ./... on linux/arm64, plus focused interpreter/luajit, support, and nativeunwind/elfunwindinfo coverage on linux/amd64 and linux/arm64.
  • Focused extractor, mapping, proto, C-frame-offset, root-signature, malformed-input, overflow, and exact-boundary regressions on both architectures.
  • TestOffsets and TestStructure against all 18 cached OpenResty binaries from the amd64 and arm64 build matrices.
  • Live native-amd64 OpenResty coverage for fib and comp on 1.19 and rolling Alpine images. The old 1.17 image exits with SIGILL under the x86 QEMU VM, but remains covered by the offline binary matrix.
  • A native-arm64 diagnostic comparison recovered equivalent Lua stacks on the Parca baseline and this branch, but all samples ended in abort markers in the nested-Docker harness; it is not counted as a passing live case. Arm64 acceptance comes from the exact coredump and binary matrices instead.
  • Exact Tarantool coredump goldens on both architectures via coredump: test Tarantool LuaJIT unwinding on amd64 and arm64 #317.
  • Broad coredump regression matrix after the generic native-root overlay, using Parca's configured GCS fallback for fork artifacts:
  • eBPF clang-format lint, generated-type consistency, gofmt, and git diff --check.
  • Forced clean dual-architecture eBPF rebuilds in independent clang-17 containers reproduced the committed blobs byte-for-byte:
    • amd64 SHA-256: e807bc2bd92380cb0983190d7e11f8088d5381364b04429c88c97ea018bee09d
    • arm64 SHA-256: 2f95d1576241a12682a5b9bbed6903002969f97fad1c175294c6400850ec507b

The branch was rebased onto parca-dev/main@0cf69159. Range-diff preserved every source commit; only the superseded intermediate blob rebuild was intentionally omitted. Both PRs remain ready for review.

@CLAassistant

CLAassistant commented Jul 15, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@dalehamel dalehamel closed this Jul 15, 2026
@dalehamel dalehamel reopened this Jul 15, 2026
@dalehamel
dalehamel marked this pull request as draft July 15, 2026 01:16
@dalehamel

Copy link
Copy Markdown
Author

not ready for review, just having a clanker try and port the code over - I haven't reviewed it myself yet.

@dalehamel
dalehamel changed the base branch from claude/luajit-arg-extractors to main July 15, 2026 02:16
@dalehamel
dalehamel force-pushed the dale/luajit-tarantool-parca branch from a57138b to 1cc9153 Compare July 15, 2026 02:16
@dalehamel
dalehamel marked this pull request as ready for review July 15, 2026 02:25
@dalehamel
dalehamel marked this pull request as draft July 15, 2026 02:26
@dalehamel
dalehamel marked this pull request as ready for review July 16, 2026 20:11
@gnurizen gnurizen mentioned this pull request Aug 5, 2026
@github-actions

Copy link
Copy Markdown

This PR was marked stale due to lack of activity. It will be closed in 14 days.

@github-actions github-actions Bot added the Stale label Aug 14, 2026
@dalehamel
dalehamel force-pushed the dale/luajit-tarantool-parca branch from b7d5699 to 39dd4c5 Compare August 14, 2026 13:49
@github-actions github-actions Bot removed the Stale label Aug 15, 2026
Tarantool embeds LuaJIT in its main executable rather than mapping a separate libluajit-5.1.so. Allow the tarantool binary through the loader's host-name gate so the existing symbol and offset extraction can probe it.

Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
Large statically linked binaries can contain an unrelated stack-delta gap that matches the interpreter heuristic before the real LuaJIT VM assembly. When the hidden lj_vm_asm_begin symbol is available, locate it through .symtab and select the large delta interval containing it. Preserve the existing heuristic as a fallback for stripped binaries.

Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
Tarantool inserts mem_L between global_State.cur_L and jit_base, so reading jit_base as the next field yields a lua_State pointer instead of the active JIT base. Extract jit_base from the trace-exit handler on amd64 and arm64, pass its offset to eBPF, and retain cur_L+8 as the OpenResty fallback.

Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
Tarantool's LuaJIT VM gate uses different C-frame layouts from OpenResty, and Lua runs on fiber stacks. Read the CFA offset and base register from the interpreter region's stack delta, derive the x86 JIT transition frame from it, and always step over the VM gate before returning to native unwinding. This preserves the native stack below lua_pcall and avoids ERROR_4012/4005 handback failures.

Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
Scan ELF symbols once, reject the OpenResty jit_base fallback for Tarantool, guard a transient NULL jit_base, centralize layout constants, and add direct amd64/arm64 extractor plus cframe boundary tests.

Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
Assisted-By: devx/c2bfb8bf-4688-4938-9621-a3654ee983f3
@dalehamel
dalehamel marked this pull request as draft August 28, 2026 19:15
@dalehamel
dalehamel force-pushed the dale/luajit-tarantool-parca branch from 39dd4c5 to 92085ce Compare August 28, 2026 19:15
@github-actions

Copy link
Copy Markdown

This PR was marked stale due to lack of activity. It will be closed in 14 days.

@github-actions github-actions Bot added the Stale label Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants