#1644·gperftools

aarch64: SIGSEGV in libunwind _ULaarch64_step during tcmalloc::GetStackTrace (2.17.2 + libunwind 1.8.3)

Author: ben-efrosCreated Sep 13, 2026Updated Sep 15, 2026

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.8

Summary

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

  1. 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).
  2. 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.
  3. 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_POINTERS

Additional 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