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

Reply via email to