abseil / abseil/abseil-cpp

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

Aberta
#2,143 0 comentários 0 reações 0 responsáveis Ver no GitHub
Linguagem predominante
C++
Estrelas
18.1k
Forks
3.2k
Merge médio
20h 36min
PRs com merge (30d)
1

Descrição

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

Guia de contribuição

Abrir o guia de contribuição

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.