Platform
iOS
Environment
Production
Installed
Swift Package Manager
Other Error Monitoring Solution
Yes
Other Error Monitoring Solution Name
Native app store crash reports
Version
9.15.0
Xcode Version
26.2
Did it work on previous versions?
No response
Steps to Reproduce
Hard to force deterministically (it requires a fatal signal to land while the same thread is inside malloc/realloc and has not yet instantiated tl_ignoreSignum), but it occurs in the field on iOS 26. A targeted repro:
- On a fresh thread that has never entered
handleSignal, hold the allocator lock (be inside a large realloc).
- Deliver a fatal signal to that thread at that instant.
- Observe the abort at
_os_unfair_lock_recursive_abort instead of a normal crash report.
Expected Result
The handler does not allocate; the original signal is captured/reported faithfully.
Actual Result
The handler allocates via lazy _Thread_local instantiation, re-enters the held allocator lock, and aborts — masking the original crash.
Exception Type: EXC_BREAKPOINT (SIGKILL)
Exception Codes: 0x0000000000000001, 0x00000002c0f1f154 ; PC == _os_unfair_lock_recursive_abort + 36
Termination Reason: FOUNDATION 1
Triggered by Thread: 0
Thread 0 Crashed:
0 libsystem_platform.dylib _os_unfair_lock_recursive_abort + 36 (lock.c:515)
1 libsystem_platform.dylib _os_unfair_lock_lock_slow + 296 (lock.c:597)
2 libsystem_malloc.dylib xzm_segment_group_alloc_chunk + 476
3 libsystem_malloc.dylib _xzm_malloc_large_huge + 456
4 libdyld.dylib dyld::ThreadLocalVariables::instantiateVariable(...) + 96 (ThreadLocalVariables.cpp:351)
5 libdyld.dylib _tlv_get_addr + 104
6 <App> handleSignal + 72 ; <-- SentryCrash signal handler
7 libsystem_platform.dylib _sigtramp + 56 (sigtramp.c:116) ; <-- signal delivered here
8 libsystem_malloc.dylib _xzm_reclaim_mark_used_locked + 60 ; <-- interrupted mid-alloc, lock HELD
9 libsystem_malloc.dylib _xzm_segment_group_span_mark_smaller + 240
10 libsystem_malloc.dylib _xzm_segment_group_find_and_allocate_chunk + 528
11 libsystem_malloc.dylib xzm_segment_group_alloc_chunk + 504
12 libsystem_malloc.dylib _xzm_malloc_large_huge + 456
13 libsystem_malloc.dylib xzm_realloc + 748
14 libsystem_malloc.dylib malloc_type_realloc + 180
15 AttributeGraph AG::details::realloc_vector<...>(...) + 104
17 AttributeGraph AG::Graph::propagate_dirty(AG::AttributeID) + 900
18 SwiftUICore ObservationGraphMutation.apply() + 928
... (normal SwiftUI render/observation work below)
41 <App> main + 180
42 dyld start + 6928
Summary
SentryCrashMonitor_Signal.c:handleSignal performs a heap allocation as its first action, which is not async-signal-safe. Its first statement reads the lazily-instantiated thread-local tl_ignoreSignum. On a thread that has never touched that TLV, dyld instantiates it on first access via _tlv_get_addr, which calls malloc.
If the trapped signal interrupted that thread while it was already inside malloc/realloc holding the allocator lock, the handler re-enters malloc on the same thread and trips the non-recursive lock check, terminating with _os_unfair_lock_recursive_abort (EXC_BREAKPOINT).
The net effect: the original crash signal is masked and replaced by this misleading abort, in both Sentry's report and Apple's crash report (because the POSIX handler runs in-process before re-raising). This is a regression — it makes a class of crashes uninvestigable.
The offending code
Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c
static _Thread_local int tl_ignoreSignum = 0; // line ~52
...
static void
handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext)
{
int ignoreSignum = tl_ignoreSignum; // line ~103 — FIRST action: lazy TLV read
tl_ignoreSignum = 0; // -> _tlv_get_addr -> malloc on first touch
...
}
tl_ignoreSignum (and this first-action read) was introduced in #7340 "Write reports on concurrent crashes", shipped in 9.5.1, as the re-entrancy guard for the handler's terminal raise(sigNum). It is still present on main / 9.18.0; no release fixes it.
Why this is a real bug
Sentry's own native signal-handler design docs state that in a signal handler "not even allocations are possible" and "malloc can (and will!) hold a lock when entering into a signal handler which can cause us to either deadlock or crash," prescribing a bump allocator + an in-handler spinlock as the workaround. https://develop.sentry.dev/sdk/platform-specifics/native-sdks/signal-handlers/
handleSignal violates this invariant before any of that protection runs, because the very first line can allocate via lazy TLV instantiation.
Are you willing to submit a PR?
No response
Platform
iOS
Environment
Production
Installed
Swift Package Manager
Other Error Monitoring Solution
Yes
Other Error Monitoring Solution Name
Native app store crash reports
Version
9.15.0
Xcode Version
26.2
Did it work on previous versions?
No response
Steps to Reproduce
Hard to force deterministically (it requires a fatal signal to land while the same thread is inside
malloc/reallocand has not yet instantiatedtl_ignoreSignum), but it occurs in the field on iOS 26. A targeted repro:handleSignal, hold the allocator lock (be inside a largerealloc)._os_unfair_lock_recursive_abortinstead of a normal crash report.Expected Result
The handler does not allocate; the original signal is captured/reported faithfully.
Actual Result
The handler allocates via lazy
_Thread_localinstantiation, re-enters the held allocator lock, and aborts — masking the original crash.Summary
SentryCrashMonitor_Signal.c:handleSignalperforms a heap allocation as its first action, which is not async-signal-safe. Its first statement reads the lazily-instantiated thread-localtl_ignoreSignum. On a thread that has never touched that TLV, dyld instantiates it on first access via_tlv_get_addr, which callsmalloc.If the trapped signal interrupted that thread while it was already inside
malloc/reallocholding the allocator lock, the handler re-entersmallocon the same thread and trips the non-recursive lock check, terminating with_os_unfair_lock_recursive_abort(EXC_BREAKPOINT).The net effect: the original crash signal is masked and replaced by this misleading abort, in both Sentry's report and Apple's crash report (because the POSIX handler runs in-process before re-raising). This is a regression — it makes a class of crashes uninvestigable.
The offending code
Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.ctl_ignoreSignum(and this first-action read) was introduced in #7340 "Write reports on concurrent crashes", shipped in 9.5.1, as the re-entrancy guard for the handler's terminalraise(sigNum). It is still present onmain/ 9.18.0; no release fixes it.Why this is a real bug
Sentry's own native signal-handler design docs state that in a signal handler "not even allocations are possible" and "malloc can (and will!) hold a lock when entering into a signal handler which can cause us to either deadlock or crash," prescribing a bump allocator + an in-handler spinlock as the workaround. https://develop.sentry.dev/sdk/platform-specifics/native-sdks/signal-handlers/
handleSignalviolates this invariant before any of that protection runs, because the very first line can allocate via lazy TLV instantiation.Are you willing to submit a PR?
No response