aarch64: SIGSEGV in libunwind _ULaarch64_step during tcmalloc::GetStackTrace (2.17.2 + libunwind 1.8.3)
Environment
- gperftools: 2.17.2 (Gentoo dev-util/google-perftools-2.17.2, built with libunwind support)
- libunwind: 1.8.3 (Gentoo sys-libs/libunwind package)
- OS: Gentoo Linux, Linux aarch64
- Page size: 16 KiB (getconf PAGE_SIZE = 16384)
- Compiler: gcc 16.1.1
- Consumer: Ceph 20.2.4 (ceph-osd), linked against libtcmalloc.so.4 and libunwind.so.8
$ ldd /usr/bin/ceph-osd | grep -E 'tcmalloc|unwind'
libtcmalloc.so.4 => /usr/lib64/libtcmalloc.so.4
libunwind.so.8 => /usr/lib64/libunwind.so.8Summary
On aarch64, tcmalloc's libunwind-based stack-trace capture hard-crashes with SIGSEGV inside _ULaarch64_step during an ordinary page-heap allocation. Ceph OSDs crash at startup as soon as BlueFS appends to its WAL and tcmalloc records heap-growth stack traces.
Backtrace
From a coredump (gdb -batch -ex bt /usr/bin/ceph-osd core):
#0 0x0000ffff29617d0c in tcmalloc::CentralFreeList::ReleaseToSpans(void*) () from /usr/lib64/libtcmalloc.so.4
#1 0x0000ffff29617d50 in tcmalloc::CentralFreeList::ReleaseListToSpans(void*) () from /usr/lib64/libtcmalloc.so.4
#2 0x0000ffff29618028 in tcmalloc::CentralFreeList::InsertRange(void*, void*, int) () from /usr/lib64/libtcmalloc.so.4
...
#13 0x0000aaaab230eb80 in handle_oneshot_fatal_signal (signum=11) ... <-- ceph's SIGSEGV handler
#14 <signal handler called>
#15 0x0000ffff275160d0 in ?? () from /usr/lib64/libunwind.so.8
#16 0x0000ffff27518008 in ?? () from /usr/lib64/libunwind.so.8
#17 0x0000ffff27519b74 in ?? () from /usr/lib64/libunwind.so.8
#18 0x0000ffff2751b048 in ?? () from /usr/lib64/libunwind.so.8
#19 0x0000ffff275167d8 in _ULaarch64_step () from /usr/lib64/libunwind.so.8
#20 0x0000ffff29621de4 in ?? () from /usr/lib64/libtcmalloc.so.4
#21 0x0000ffff2962296c in GetStackTrace(void**, int, int) () from /usr/lib64/libtcmalloc.so.4
#22 0x0000ffff29621048 in ?? () from /usr/lib64/libtcmalloc.so.4
#23 0x0000ffff2961cbe0 in tcmalloc::ThreadCachePtr::WithStacktraceScope(void (*)(bool, void*), void*) () from /usr/lib64/libtcmalloc.so.4
#24 0x0000ffff2962109c in ?? () from /usr/lib64/libtcmalloc.so.4
#25 0x0000ffff29618a88 in tcmalloc::PageHeap::HandleUnlock(tcmalloc::PageHeap::LockingContext*) () from /usr/lib64/libtcmalloc.so.4
#26 0x0000ffff2961a2e0 in tcmalloc::PageHeap::NewWithSizeClass(unsigned long, unsigned int) () from /usr/lib64/libtcmalloc.so.4
#27 0x0000ffff296181fc in tcmalloc::CentralFreeList::Populate() () from /usr/lib64/libtcmalloc.so.4
#28 0x0000ffff296183c0 in tcmalloc::CentralFreeList::FetchFromOneSpansSafe(int, void**, void**) () from /usr/lib64/libtcmalloc.so.4
#29 0x0000ffff29618468 in tcmalloc::CentralFreeList::RemoveRange(void**, void**, int) () from /usr/lib64/libtcmalloc.so.4
#30 0x0000ffff2961b7bc in tcmalloc::ThreadCache::FetchFromCentralCache(unsigned int, int, void* (*)(unsigned long)) () from /usr/lib64/libtcmalloc.so.4
#31 0x0000aaaab22aadcc in std::__new_allocator<BlueFS::File::envelope_t>::allocate ...
...
#37 BlueFS::_envmode_index_file ...The interrupted context is the important part: frames #15–#37 show the original fault — a normal allocation in Ceph's BlueFS reaching tcmalloc::GetStackTrace, which called libunwind's aarch64 unwinder and segfaulted in _ULaarch64_step. The #0–#13 frames are the secondary crash inside Ceph's signal handler while it printed the backtrace.
Workaround that confirms the cause
Setting the runtime stack-trace method to libgcc makes the crash disappear completely:
set the environent TCMALLOC_STACKTRACE_METHOD=libgcc
With that, the same workload (Ceph OSD startup + WAL writes) runs stably.
Analysis / suspicion
- The SIGSEGV occurs in libunwind's
_ULaarch64_step, but it is reached through gperftools' stacktrace_libunwind-inl.h (unw_getcontext + unw_init_local + unw_step). - gperftools already has maybe_convert_libunwind_to_generic_fp() for aarch64, which tests libunwind and falls back to frame pointers when it returns ≤2 frames. That fallback cannot help here because the failure is a hard SIGSEGV during unwinding, not a graceful unw_step() failure.
- Related issue while researching this: https://github.com/libunwind/libunwind/issues/1056
So the bug could be:
- a libunwind aarch64 unwinder bug, and/or
- gperftools misusing libunwind (e.g., wrong context/cursor initialization for this platform).
Requested fix
At minimum, make gperftools resilient on aarch64:
- Prefer the libgcc (_Unwind_Backtrace) implementation over libunwind when available on aarch64, or
- Extend the existing maybe_convert_libunwind_to_generic_fp() fallback so a broken libunwind can never take the process down (for example, also consider libgcc), or
- Fix the libunwind invocation in stacktrace_libunwind-inl.h if the misuse is on gperftools' side.
What I ended up doing for now...
--- a/src/stacktrace.cc
+++ b/src/stacktrace.cc
@@ -194,6 +194,14 @@
# endif
#endif
+#if defined(__aarch64__) && defined(HAVE_GST_libgcc)
+// libunwind's aarch64 unwinder has caused hard crashes (SIGSEGV in
+// _ULaarch64_step). The libgcc (_Unwind_Backtrace) unwinder is
+// available and stable on this architecture, so prefer it.
+#define PREFER_LIBGCC_UNWINDER 1
+#endif
+
#if (__x86_64__ || __i386__) && FORCED_FRAME_POINTERSAdditional notes
- Reproducer: run ceph-osd on an aarch64 host with 16 KiB pages, gperftools 2.17.2 linked against libunwind 1.8.3; the crash occurs during startup WAL writes. A smaller standalone reproducer is not yet available.
- The same gperftools build works with
TCMALLOC_STACKTRACE_METHOD=libgcc. - Can regenerate failing build / core / logs if needed. Was using a pretty routine sys-cluster/ceph ebuild
Source: gperftools/gperftools