https://github.com/Algunenano created https://github.com/llvm/llvm-project/pull/226964
With `_LIBUNWIND_REMEMBER_STACK_ALLOC` (the default on Linux, Apple, Android, MinGW and bare metal), `DW_CFA_remember_state` allocates a `PrologInfoStackEntry` with `alloca` and `DW_CFA_restore_state` cannot free it. So interpreting an FDE takes stack proportional to the total number of remember/restore pairs before the target PC, rather than to their nesting depth. Compilers emit a pair around each epilogue in the middle of a function, and each entry holds a whole `PrologInfo`: about 570 bytes on x86-64 and about 1.6 KB on AArch64. We hit this in ClickHouse on AArch64. Its query profiler samples threads with a signal handler that captures a stack trace with libunwind, on whatever stack the thread is running. ClickHouse establishes connections to remote servers in Boost fibers, whose stacks are 320 KiB. Unwinding through a function with 191 remember/restore pairs, a single `parseFDEInstructions` call used about 300 KB and ran the fiber's stack into its guard page. This patch keeps the entries popped by `DW_CFA_restore_state` on a free list and reuses them for the next `DW_CFA_remember_state`, so the stack used is bounded by the nesting depth. With the heap allocator, the destructor frees both lists. Testing: the new `libunwind/test/remember_state_stack.pass.cpp` unwinds through a frame with 5000 pairs after lowering the soft `RLIMIT_STACK` to 256 KiB. On x86-64 Linux it crashes with `SIGSEGV` without the change and passes with it. `check-unwind` (shared and static configs) and `check-cxxabi` pass. This PR was prepared with the help of Claude Code; I reviewed the change and the test. From cfb4f99495606a612f3ace86d2c60df41fe0ce07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Mar=C3=ADn?= <[email protected]> Date: Mon, 28 Sep 2026 12:37:00 +0200 Subject: [PATCH] [libunwind] Reuse remember-state entries instead of allocating one per pair With _LIBUNWIND_REMEMBER_STACK_ALLOC, which is the default on Linux, Apple, Android, MinGW and bare metal, every DW_CFA_remember_state allocates a PrologInfoStackEntry with alloca, and DW_CFA_restore_state cannot free it. Interpreting an FDE therefore needs stack in proportion to the total number of remember/restore pairs before the target PC, not to their nesting depth. Compilers emit one such pair around each epilogue in the middle of a function, and an entry holds a whole PrologInfo (about 1.6 KB on AArch64), so unwinding through a large function with many epilogues can overflow a small stack, such as that of a Boost fiber or of a thread with a reduced stack size. Keep the entries popped by DW_CFA_restore_state on a free list and reuse them for the next DW_CFA_remember_state, which bounds the stack by the nesting depth. The test unwinds through a frame with 5000 pairs on a 256 KiB stack. Assisted-by: Claude Code --- libunwind/src/DwarfParser.hpp | 34 +++++++++---- libunwind/test/remember_state_stack.pass.cpp | 52 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 libunwind/test/remember_state_stack.pass.cpp diff --git a/libunwind/src/DwarfParser.hpp b/libunwind/src/DwarfParser.hpp index d60e3e11325d3..36d645ed22f32 100644 --- a/libunwind/src/DwarfParser.hpp +++ b/libunwind/src/DwarfParser.hpp @@ -145,20 +145,30 @@ class CFI_Parser { struct RememberStack { PrologInfoStackEntry *entry; - RememberStack() : entry(nullptr) {} + // Entries popped by DW_CFA_restore_state, reused by the next + // DW_CFA_remember_state. With the stack allocator the free is a no-op, so + // without reuse every pair in an FDE would take a new entry of stack. + PrologInfoStackEntry *freeEntries; + RememberStack() : entry(nullptr), freeEntries(nullptr) {} ~RememberStack() { #if defined(_LIBUNWIND_REMEMBER_CLEANUP_NEEDED) // Clean up rememberStack. Even in the case where every // DW_CFA_remember_state is paired with a DW_CFA_restore_state, // parseInstructions can skip restore opcodes if it reaches the target PC // and stops interpreting, so we have to make sure we don't leak memory. - while (entry) { - PrologInfoStackEntry *next = entry->next; - _LIBUNWIND_REMEMBER_FREE(entry); - entry = next; - } + freeList(entry); + freeList(freeEntries); #endif } + + private: + static void freeList(PrologInfoStackEntry *list) { + while (list) { + PrologInfoStackEntry *next = list->next; + _LIBUNWIND_REMEMBER_FREE(list); + list = next; + } + } }; template <typename R> @@ -601,9 +611,12 @@ bool CFI_Parser<A>::parseFDEInstructions( case DW_CFA_remember_state: { // Avoid operator new because that would be an upward dependency. // Avoid malloc because it needs heap allocation. - PrologInfoStackEntry *entry = - (PrologInfoStackEntry *)_LIBUNWIND_REMEMBER_ALLOC( - sizeof(PrologInfoStackEntry)); + PrologInfoStackEntry *entry = rememberStack.freeEntries; + if (entry != NULL) + rememberStack.freeEntries = entry->next; + else + entry = (PrologInfoStackEntry *)_LIBUNWIND_REMEMBER_ALLOC( + sizeof(PrologInfoStackEntry)); if (entry != NULL) { entry->next = rememberStack.entry; entry->info = *results; @@ -619,7 +632,8 @@ bool CFI_Parser<A>::parseFDEInstructions( PrologInfoStackEntry *top = rememberStack.entry; *results = top->info; rememberStack.entry = top->next; - _LIBUNWIND_REMEMBER_FREE(top); + top->next = rememberStack.freeEntries; + rememberStack.freeEntries = top; } else { return false; } diff --git a/libunwind/test/remember_state_stack.pass.cpp b/libunwind/test/remember_state_stack.pass.cpp new file mode 100644 index 0000000000000..9ed916dad4b8f --- /dev/null +++ b/libunwind/test/remember_state_stack.pass.cpp @@ -0,0 +1,52 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// REQUIRES: linux +// UNSUPPORTED: libunwind-arm-ehabi + +// Inline assembly isn't supported by Memory Sanitizer +// UNSUPPORTED: msan + +// Unwinding through a frame interprets its CFI up to the PC. Every +// DW_CFA_remember_state that is later matched by a DW_CFA_restore_state must +// not keep its saved state alive: a function with many epilogues in the middle +// of its code has one such pair per epilogue, and the unwinder must not need +// stack in proportion to their number. Here the pairs would take several MiB, +// far more than the stack is allowed to grow to. + +#undef NDEBUG +#include <assert.h> +#include <sys/resource.h> +#include <unwind.h> + +static _Unwind_Reason_Code count_frames(struct _Unwind_Context *, void *arg) { + ++*static_cast<int *>(arg); + return _URC_NO_REASON; +} + +__attribute__((noinline)) static int unwind_through_remember_states() { + // Emits no instructions, only 5000 remember/restore pairs in this function's + // FDE. They precede the call, so unwinding from it interprets all of them. + asm volatile(".rept 5000\n.cfi_remember_state\n.cfi_restore_state\n.endr"); + int frames = 0; + _Unwind_Backtrace(count_frames, &frames); + return frames; +} + +int main(int, char **) { + // The main thread's stack only grows up to the soft limit. + struct rlimit limit; + assert(getrlimit(RLIMIT_STACK, &limit) == 0); + limit.rlim_cur = 256 * 1024; + assert(setrlimit(RLIMIT_STACK, &limit) == 0); + + // At least `unwind_through_remember_states` and `main`. + assert(unwind_through_remember_states() >= 2); + return 0; +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
