Steps to Reproduce
Crash Report Summary
Platform Information
Platform: Android (arm64), Vulkan ES3.1 mobile feature level
Frequency: 264 events / 133 unique users over 90 days, ongoing since Aug 2025
Severity: Crash (unhandled SIGSEGV), player-facing, reproduces across device vendors
Summary
FTaskBase::Close() and FTaskBase::TryUnlock() are mutually recursive with no depth bound. Closing the head of a linear task chain of length N consumes N stack frames. On Android, where the default pthread stack is 1 MB, sufficiently long chains overflow the stack and terminate the process with SIGSEGV (SEGV_ACCERR — consistent with hitting the PROT_NONE guard page).
TaskPrivate.h (5.5.4):
void Close()
{
...
for (FTaskBase* Subsequent : Subsequents.Close())
{
Subsequent->TryUnlock(bWakeUpWorker); // (A)
}
...
}
bool TryUnlock(bool& bWakeUpWorker)
{
...
// execution already started, this is nested tasks unlocking their parent
...
Close(); // (B)
Release();
...
}
(A) → (B) → (A) → … recurses once per chain link. Neither function has a depth guard, and the recursion in (A) is not in tail position.
Note that FSubsequents is declared as:
FSubsequents> Subsequents;The inline allocator sized for exactly one element indicates the expected common case is one task → one subsequent. In that shape, a chain of N tasks recurses N frames deep with no branching to bound it.
Stack Trace (Symbolicated, Tombstone-Derived)
Fatal signal SIGSEGV (11), SEGV_ACCERR (2), pid = 24739
Crashing thread: “ForegroundWorker #1” (LowLevelTasks scheduler worker)
libUnreal UE::Tasks::Private::FTaskBase::FSubsequents::Close (TaskPrivate.h:778)
libUnreal UE::Tasks::Private::FTaskBase::Close (TaskPrivate.h:539)
libUnreal UE::Tasks::Private::FTaskBase::TryUnlock (TaskPrivate.h:654)
libUnreal UE::Tasks::Private::FTaskBase::Close (TaskPrivate.h:543)
libUnreal UE::Tasks::Private::FTaskBase::TryUnlock (TaskPrivate.h:654)
... [Close/TryUnlock pair repeats until the captured frame limit] ...
The trace is truncated by the tombstone frame cap before reaching the entry point, so the originating subsystem is not visible in any of the ~260 captured events. This is itself a diagnostic problem: the failure mode erases the evidence needed to attribute it.
Reproduction Pattern
Consistent across ~12 months of internal reports (CL 10476 → CL 44399):
- Application sits with the renderer running but no scene-destruction or shutdown event occurring — main menu, landing page, error dialog, CDN download screen, or a low-activity client in a networked match.
- Idle duration ranges from ~2.5 min to ~28 min across confirmed captures.
- A state transition occurs (screen change, scene teardown, app resume).
- SIGSEGV within milliseconds of that transition.
Confirmed device log (Samsung Z Fold 6 / SM-F956B):
07:08:18 Landing page shown
07:08:18 → 07:15:26 idle, no logged activity (~7 min)
07:15:26 SIGSEGV
A separate confirmed capture showed ~28 min idle (including an app background/foreground cycle, which did not reset the accumulation) followed by crash within ~73 ms of the screen transition.
During investigation we independently identified CL 43539816, which adds the missing `FAsyncDeleter::LastTask = {}` reset in FRDGBuilder::WaitForAsyncDeleteTask(). We have not yet integrated it, but we believe it is a genuine and necessary fix: without it, the RDG async-delete chain is never severed even at its drain points (FScene destruction, rendering-thread stop, engine shutdown) and grows for the process lifetime. However, we do not believe it resolves this crash on mobile, because IsParallelExecuteEnabled() (RenderGraphPrivate.cpp) contains: && !IsMobilePlatform(GMaxRHIShaderPlatform) so on Android, ParallelExecute.TaskMode never leaves Inline, FRDGBuilder::~FRDGBuilder() never assigns AsyncDeleter.Function, and the entire FAsyncDeleter chain is inert. We verified this empirically: a depth-instrumented build logs continuous chain growth in the Editor (~180 links/sec) and zero occurrences across multi-minute Android sessions.
[Attachment Removed]