abseil / abseil/abseil-cpp

absl::GetStackTrace SIGSEGVs walking a frame-pointer chain into the thread stack guard page

Đang mở
#2,143 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
C++
Star
18.1k
Fork
3.2k
Merge trung bình
20 giờ 36 phút
Pull request đã merge (30 ngày)
1

Mô tả

### Describe the issue

We've run into this issue in Envoy, and analyzing (with the help of LLMs) the core dump points to a possible issue in absl.

From what I can see, it looks like the tcmalloc calls `absl::GetStackTrace` as part of it's heap profiling code, which throws an exception, which then causes the signal handle to throw another exception in `GetStackTraceWithContext` too.

Stack Trace:
```
* thread #1, name = 'envoy', stop reason = SIGSEGV: invalid permissions for mapped object (fault address=0x7f535000ff08)
* frame #0: 0x0000560eb3ecc54d envoy-bin`___lldb_unnamed_symbol463622 + 93
frame #1: 0x0000560eb3ecc248 envoy-bin`absl::lts_20260107::GetStackTraceWithContext(void**, int, int, void const*, int*) + 216
frame #2: 0x0000560eb3734a68 envoy-bin`Envoy::SignalAction::sigHandler(int, siginfo_t*, void*) + 88
frame #3: 0x00007f5367a66520 libc.so.6`new_exp [inlined] __gettext_free_exp(exp=) at plural.c:1558:7
frame #4: 0x00007f5367a66500 libc.so.6`new_exp [inlined] __gettext_free_exp(exp=0x00007f53500056a0) at plural.c:1561:7
frame #5: 0x00007f5367a66500 libc.so.6`new_exp [inlined] __gettext_free_exp(exp=0x0000000000000040) at plural.c:1561:7
frame #6: 0x00007f5367a664c0 libc.so.6`new_exp [inlined] __gettext_free_exp(exp=0x0000000000000000) at plural.c:1561:7
frame #7: 0x00007f5367a663e8 libc.so.6`new_exp(nargs=, op=, args=0x00007f535000ff00) at plural.c:265:5
frame #8: 0x0000560eb3ecc10f envoy-bin`absl::lts_20260107::GetStackTrace(void**, int, int) + 207
frame #9: 0x0000560eb3fff397 envoy-bin`__sized_ptr_t tcmalloc::tcmalloc_internal::SampleifyAllocation>(tcmalloc:: +++tcmalloc_internal::Static&, tcmalloc::tcmalloc_internal::TCMallocPolicy, unsigned long, unsigned long, unsigned long, void*, tcmalloc::tcmalloc_internal::Span*) + 151 frame #10: 0x0000560eb3fed311 envoy-bin`___lldb_unnamed_symbol464898 + 385
frame #11: 0x0000560eb3fe9b38 envoy-bin`TCMallocInternalMalloc + 184
frame #12: 0x0000560eb3950be1 envoy-bin`___lldb_unnamed_symbol452507 + 49
frame #13: 0x0000560eb3955e84 envoy-bin`___lldb_unnamed_symbol452746 + 212
frame #14: 0x0000560eb3955986 envoy-bin`___lldb_unnamed_symbol452743 + 422
frame #15: 0x0000560eb3957272 envoy-bin`___lldb_unnamed_symbol452788 + 50
frame #16: 0x0000560eb327c992 envoy-bin`___lldb_unnamed_symbol442237 + 66
frame #17: 0x0000560eb327c87e envoy-bin`___lldb_unnamed_symbol442234 + 46
frame #18: 0x0000560eb3279853 envoy-bin`std::__1::basic_string, std::__1::allocator> const& Envoy::Extensions::TransportSockets::Tls::ConnectionInfoImplBase::getCachedValueOrCreate, std::__1::allocator>>(Envoy::Extensions::TransportSockets::Tls::ConnectionInfoImplBase::CachedValueTag, std::__1::function, std:: +++__1::allocator> (ssl_st*)>) const + 339
frame #19: 0x0000560eb3279c68 envoy-bin`Envoy::Extensions::TransportSockets::Tls::ConnectionInfoImplBase::urlEncodedPemEncodedPeerCertificate() const + 56
frame #20: 0x0000560eb1935d9f envoy-bin`___lldb_unnamed_symbol412703 + 5039
frame #21: 0x0000560eb193468b envoy-bin`Envoy::Http::ConnectionManagerUtility::mutateXfccRequestHeader(Envoy::Http::RequestHeaderMap&, Envoy::StreamInfo::StreamInfo const&, Envoy::Network::Connection&, Envoy::Http:: +++ConnectionManagerConfig&) + 459
frame #22: 0x0000560eb1933b7b envoy-bin`Envoy::Http::ConnectionManagerUtility::mutateRequestHeaders(Envoy::Http::RequestHeaderMap&, Envoy::Network::Connection&, Envoy::Http::ConnectionManagerConfig&, Envoy::Router::Config const&, Envoy:: +++LocalInfo::LocalInfo const&, Envoy::StreamInfo::StreamInfo const&) + 3371
frame #23: 0x0000560eb19200a1 envoy-bin`Envoy::Http::ConnectionManagerImpl::ActiveStream::decodeHeaders(std::__1::shared_ptr&&, bool) + 5169
```

### Steps to reproduce the problem

Still trying to figure something out. I'm not sure it's possible to reproduce reliably.

### What version of Abseil are you using?

20260107.1

### What operating system and version are you using?

ubuntu 22/amd64

### What compiler and version are you using?

Clang 18.1.8

### What build system are you using?

Bazel 7.7.1

### Additional context

I can't really share the core, but I can provide what Claude is telling me from using lldb:

**Summary**

On x86-64, absl::GetStackTrace / GetStackTraceWithContext can dereference a frame pointer that points past the top of the current thread's stack, into the PROT_NONE guard page, causing a SIGSEGV (SEGV_ACCERR). It happens when the frame-pointer chain does not terminate at a clean NULL (e.g. the thread-entry frame's saved rbp slot holds a stale near-top-of-stack value) and the walk takes one step beyond the outermost frame.

**Impact**

- Any caller of GetStackTrace in strict mode can crash the process. In our case tcmalloc's allocation sampler (SampleifyAllocation, which calls GetStackTrace(stack, /*max_depth=*/64, 0)) crashes on a normal, sampled allocation.
- It also breaks crash handlers that symbolize via GetStackTraceWithContext: our SIGSEGV handler re-entered the same unwinder and faulted a second time on the same guard-page address, turning a loggable crash into an opaque guard-page SIGSEGV.

**Observed (from the core)**

- Faulting instruction: mov rsi, [r13+8] (the *(fp+1) return-address read at the top of UnwindImpl's loop).
- r13 = 0x7f535000ff00 (the current fp); fault address 0x7f535000ff08.
- r15 = 0x40 = 64 = the caller's max_depth.
- Memory map:
- stack: [0x7f534f80f000, 0x7f535000f000) rw- (only ~43 KB used; not a stack overflow)
- fault page: [0x7f535000f000, 0x7f5350010000) --- (guard page, immediately above the stack top)
- Disassembly of the compiled unwinder: 171 instructions, the only external call is getpagesize. No AddressIsReadable call and no syscall in the function.

0x7f535000ff00 is 16-byte aligned, greater than the previous frame pointer, and only a few KB above it — so it passes every arithmetic check and is treated as a valid frame.

**Root cause**

In absl/debugging/internal/stacktrace_x86-inl.inc:

1. UnwindImpl initializes bounds as
size_t stack_low = static_cast(getpagesize());
size_t stack_high = kUnknownStackEnd; // never updated
and never queries the thread's real stack extent (no pthread_getattr_np / pthread_attr_getstack / __libc_stack_end). So the new_fp <= stack_high guard in NextStackFrame is effectively new_fp <= ~2^64 — always true.
2. The non-faulting AddressIsReadable(new_fp) probe is gated behind else if (!STRICT_UNWINDING). GetStackTrace / GetStackTraceWithContext instantiate the unwinder with IS_STACK_FRAMES = false, i.e. STRICT_UNWINDING = true, so that branch is dead-code-eliminated (consistent with the disassembly having no AddressIsReadable call).

Net: in strict mode the only surviving defenses are alignment, monotonicity (new_fp > old_fp), and new_fp - old_fp <= kMaxFrameBytes. A frame-pointer value pointing just past the stack top into the guard page satisfies all three, so it is dereferenced and faults. Strict mode trusts frame-pointer arithmetic and skips the readability probe, but the one bound that could catch an out-of-range pointer (stack_high) is never populated.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.