Skip to content

Zero the frame-pointer terminator in aarch64 make_fcontext#339

Open
Algunenano wants to merge 1 commit into
boostorg:developfrom
Algunenano:aarch64-fiber-fp-terminator
Open

Zero the frame-pointer terminator in aarch64 make_fcontext#339
Algunenano wants to merge 1 commit into
boostorg:developfrom
Algunenano:aarch64-fiber-fp-terminator

Conversation

@Algunenano

Copy link
Copy Markdown

On arm64, make_fcontext initializes the entry PC and the finish return address in the context block, but leaves the saved frame-pointer slot (x29, offset 0x90) uninitialized. When a fiber runs, its bottom frame inherits whatever was in that memory (fiber stacks are generally not zeroed).

CFI-based unwinders (libunwind / _Unwind_Backtrace) are unaffected: they follow the unwind tables and stop at the fiber entry (cf. #321, which added .eh_frame info so libunwind terminates there). A frame-pointer walker, however (Apple's backtrace()/__thread_stack_pcs, sample, lldb, or OS crash reporters), chases the x29 chain, reads the uninitialized terminator, and follows it off the fiber stack. When that junk is an aligned, in-range, unmapped pointer, the walk dereferences it and the process crashes with SIGSEGV.

This surfaced in ClickHouse CI on macOS/arm64 after the sampling profilers were enabled there: the OS-level stack capture (backtrace()) faulted while the profiler sampled an allocation running on a Boost.Context fiber (ClickHouse/ClickHouse#111579).

The fix zeroes the frame-pointer slot (str xzr, [x0, #0x90]) in both arm64 backends (macho and elf), so a fiber presents a null frame-pointer terminator and a walker stops cleanly at the fiber entry, like a real thread's bottom frame (thread_start leaves fp = 0). This complements #321: that made CFI unwinders terminate at make_fcontext; this does the same for frame-pointer walkers.

x86_64 is not affected and is left unchanged: it already stores &finish in the rbp slot (the trampoline uses it as the return address), so it has a defined terminator.

Test: test/test_fp_terminator.cpp runs a fiber on a poisoned stack backed by a guard page and walks the frame-pointer chain from inside the fiber. Without the fix the walk follows the uninitialized terminator into the guard and faults; with it, the walk stops at the null terminator. The test is guarded to arm64, so it also exercises Linux arm64 CI.

A fiber's saved frame-pointer (x29) slot is left uninitialized by make_fcontext, so a
frame-pointer stack walker (e.g. macOS backtrace()/__thread_stack_pcs, sample, lldb, crash
reporters) follows that junk off the fiber stack and can crash. Zero it so the walk stops
cleanly at the fiber entry, like a real thread's bottom frame. Adds a regression test.
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