Skip to content

Support CFA based on general purpose registers - #131

Open
SSE4 wants to merge 3 commits into
yandex:mainfrom
SSE4:unwinder-gpr-cfa
Open

SSE4 wants to merge 3 commits into
yandex:mainfrom
SSE4:unwinder-gpr-cfa

Conversation

@SSE4

@SSE4 SSE4 commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #130.

Problem

Hand-written assembly keeps its CFA in registers other than rsp and rbp. The unwind table carries these
rules (UNWIND_RULE_REGISTER_OFFSET with any register), but the eBPF unwinder only resolved rsp and rbp,
so the stack stopped at such frames. In OpenSSL's libcrypto:

Function CFA Calls while the CFA is in the register
aesni_gcm_encrypt, aesni_gcm_decrypt rax + 8 _aesni_ctr32_ghash_6x, _aesni_ctr32_6x
aesni_cbc_encrypt, aesni_ctr32_encrypt_blocks, aesni_xts_* r11 + 8 _aesni_encrypt*, _aesni_decrypt*
other perlasm rsi, r9, rcx, rdi, r15 none (leaf)

GCC stack realignment also uses r10 in its prologue and epilogue (see #130).

In practice this cuts TLS-heavy profiles: samples in AES-GCM, AES-CBC or AES-CTR end at the perlasm
frame, detached from the code that called OpenSSL.

Fix

The eBPF unwinder keeps the general purpose registers of the sample next to the unwind context, indexed
by DWARF register number, with a mask of known ones:

  • At the leaf every register is exact. For a sample taken in the kernel, rax, rcx and r11 are
    dropped if orig_ax is not negative: syscall entry overwrites them (rax holds the syscall number,
    the syscall instruction stores rip and rflags in rcx and r11). Exceptions with an error code also
    pass that check; for them the three registers are lost, never wrong.
  • After a DWARF step only caller-saved registers are kept. DWARF treats a register without a rule as
    having the same value in the caller, and the OpenSSL helpers above rely on it. Callee-saved registers
    would need DW_CFA_offset rules, which the unwind table does not carry.
  • After a frame pointer or JVM step nothing is known.
  • A CFA computed from such a register must be above the current stack pointer, otherwise the register
    no longer holds the CFA base.

arm64 keeps the previous behavior.

Testing

Repro

repro-c.c + repro-c-asm.S, gcc -O2 -g -fno-omit-frame-pointer:

repro-c.c

repro-c-asm.S.txt

  • main -> rax_caller -> rax_frame -> spin_leaf: CFA = rax + 8 in rax_frame, and spin_leaf
    preserves rax, like aesni_gcm_encrypt -> _aesni_ctr32_ghash_6x. Checks rax kept across a DWARF step.
  • main -> r11_caller -> r11_frame: CFA = r11 + 8, samples land in r11_frame itself, like
    aesni_cbc_encrypt. Checks the leaf registers.
  • main -> syscall_caller -> rbx_syscall_frame -> getrandom(2): CFA = rbx + 16 in a frame sampled
    while in the kernel. Checks the registers saved on syscall entry.
Before After
before after
Samples reaching main Before After
rax_frame 0% 100%
r11_frame 0% 100%
rbx_syscall_frame 0% 100%

The kernel leaf init_module@[nft_reject_inet] in both graphs is fixed separately by #128.

before after

SSE4 added 3 commits November 13, 2026 13:19
TDwarfExpressionPattern::Matches() kept comparing pattern operations
after the expression ran out. A DWARFExpression end iterator still holds
the last decoded operation, so a pattern that repeats the last operation
matched a shorter expression: [breg7 8, deref, deref] matched
[breg7 8, deref].

None of the current CFA patterns repeat their last operation, so this
does not change the unwind tables built today, but it makes adding
patterns that share a prefix safe. Add unit tests for the matcher.
Two kinds of frames keep the caller's stack pointer in memory and could
not be unwound: the stack stopped at them.

- GCC dynamic stack realignment (DRAP), used for functions with
  over-aligned locals that pass arguments on the stack or use VLAs:
    CFA = *(rbp - 8)
    rbp = *(rbp)
  Common in GCC-built code: 259 functions in Debian's libhs, 345 in
  libx265.
- Hand-written OpenSSL perlasm (sha*_block_data_order_avx2 and others):
    CFA = *(rsp + offset) + 8

The unwind table builder already recognized the OpenSSL form, but the
rule never reached the eBPF unwinder. Now:

- ehframe: match *(reg + offset) without a bias as well, and
  DW_OP_bregN offset as a register location, which describes rbp saved
  at rbp;
- unwindtable: emit UNWIND_RULE_REGISTER_DEREF_OFFSET (bias 0) and
  UNWIND_RULE_REGISTER_DEREF_OFFSET_PLUS_8. The bias is encoded in the
  kind because struct cfa_unwind_rule has no room for it; other biases
  stay unsupported. Emit DWARF_UNWIND_RBP_RULE_AT_RBP for rbp saved at
  rbp;
- eBPF: evaluate both deref kinds, rejecting a CFA that is not above the
  current stack pointer since it comes from memory, and restore rbp from
  *(rbp) for DWARF_UNWIND_RBP_RULE_AT_RBP.

UNWIND_RULE_REGISTER_DERREF_OFFSET was unused and is renamed to fix the
typo.
Hand-written assembly keeps its CFA in registers other than rsp and rbp,
so the stack stopped at these frames:

- openssl aesni_gcm_encrypt/decrypt: CFA = rax + 8 while calling
  _aesni_ctr32_ghash_6x;
- openssl aesni_cbc_encrypt, aesni_ctr32_encrypt_blocks, aesni_xts_*:
  CFA = r11 + 8 while calling _aesni_encrypt*/_aesni_decrypt*;
- other perlasm uses rsi, r9, rcx, rdi, r15; gcc stack realignment uses
  r10 in its prologue and epilogue.

The unwind table already carries such rules; the eBPF unwinder rejected
any register but rsp and rbp. Now it keeps the general purpose registers
of the sample next to the unwind context:

- at the leaf every register is exact. For a sample taken in the
  kernel, rax, rcx and r11 are dropped when orig_ax shows a syscall:
  syscall entry overwrites them;
- after a DWARF step only caller-saved registers are kept. DWARF treats
  registers without a rule as having the same value in the caller, and
  the openssl helpers above rely on it. Callee-saved registers would
  need DW_CFA_offset rules the table does not carry;
- after a frame pointer or JVM step nothing is known.

A CFA computed from such a register must be above the current stack
pointer, otherwise the register no longer holds the CFA base.

arm64 keeps the previous behavior.
@robot-magpie

robot-magpie Bot commented Nov 13, 2026

Copy link
Copy Markdown

✅ All contributors are covered under a CLA with Yandex

See CONTRIBUTING.md for more info about Yandex Contributor License Agreement.


The following contributors were found:
✅ e2ef2bb Author: @SSE4 (to*****de@gmail.com)

(Only the first commit for a unique contributor is listed)

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