Skip to content

High on Life (PPSA15388): a few minutes into the first level, Unreal's FMemStack frees a chunk whose Next link is -1 (SIGSEGV in FreeChunks) #4185

Closed

Description

@Tamalero

High on Life (PPSA15388 v01.120.000, Unreal Engine 4) reaches gameplay in its first level and then crashes on a guest thread at the same instruction: in 2 of 4 gameplay runs, 5 and 13 minutes in, the second right after a checkpoint reload. A third run stopped earlier on a different fatal (below), and a fourth was closed after 10 minutes without a crash.

app.elf[…]: segfault at ffffffffffffffff ip …cfd sp 0000000203e64840 error 5 in app.elf[1e52cfd, …]

The function (app.elf vaddr 0x1e4eca0) is Unreal's FMemStackBase::FreeChunks, read from the code. It pops TopChunk ([this+0x10]) and frees chunks whose DataSize is 0x3e0 or 0xfff0 to the page allocator, whose lock-free free list is next to it. In both cores, TopChunk is 0xffffffffffffffff when the loop dereferences it, so a chunk still owned by a thread's mem stack had its Next link set to -1. The chunk was most likely freed or reused while still in use.

An earlier run on the same build stopped with a different symptom that fits the same kind of corruption: LowLevelFatalError … Internal Error: RemoveFromHash NumRemoved = 0 for AkMediaAssetData None.None:AkMediaAssetData_3, then a GPF in the title's libc.

What I checked and what I don't know:

  • Not AnyPS5's GPU write-back. With every storage-image write-back range logged (283 distinct ranges, all between 0x2001f20000 and 0x8fc3fe0000), none comes near the corrupted objects at 0x1060e403d0 and 0x1060ef6390. One run with the idle store disabled survived 10 minutes, but a run with it enabled survived 13, so that was chance.
  • scePthreadGetspecific/Setspecific (libkernel/Pthread/src/Tsd.cpp): per-host-thread values with key sequence numbers. I see nothing wrong there, but I haven't proven that the title's TLS (pthread keys or ELF TLS) gives each guest thread its own FMemStack.
  • What writes the -1: I don't know yet. Catching it needs a hardware watchpoint on the chunk, which I'm setting up next.
Details

Setup: CachyOS (Linux 7.2), RTX 4090 (NVIDIA 615.78), GCC, Release. main f5c0fe00 plus local integration patches and ports of #423/#1077, #425, #454, #2904, #3202, #3546, #3279 and AlxcBTW#39, #1732/#1825/#2261 and #3738. #4139's shapes are bound as null images (local experiment).

Crash site (file offset 0x1e52cfd, vaddr 0x1e4ecfd):

mov    0x10(%rdi),%rbx      ; TopChunk
cmp    %rsi,%rbx            ; == the chunk to stop at?
je     …
mov    (%rbx),%rax          ; rbx = 0xffffffffffffffff -> SIGSEGV
mov    %rax,0x10(%rdi)      ; TopChunk = TopChunk->Next
movslq 0x8(%rbx),%rax       ; DataSize
cmp    $0x3e0,%rax / cmp $0xfff0,%rax

Registers in core 2: rdi = 0x1060ef6390, rbx = -1, rsp = 0x203e64840 (a guest stack; the same rsp in core 1). Object words: 00000017217dd818 00000017217e0000 ffffffffffffffff 00000010414c4da0 …. In core 1 they are 000000172131d368 0000001721320000 ffffffffffffffff 00000010412512f0 …: the same layout (Top, End, TopChunk, …).

Tested and written by Claude (AI assistant) on a contributor's machine.

Activity

  1. Tamalero commented on Oct 12, 2026

    @Tamalero
    ContributorAuthor

    Two more things ruled out, from a third core with the same crash (3 minutes into the level; the FMemStack at 0x1060ef5f90 has TopChunk = -1, while Top/End = 0x1721b4d368/0x1721b50000 are still valid):

    • GPU buffer writes: with every distinct guest range written by a draw's or dispatch's storage buffers logged (1528 ranges with the storage-image write-backs, including many legitimate GPU writes into low system memory such as 56 and 74 KB blocks at 0x16e1…, 0x1781… and 0x1924…), none comes within 512 MB of the FMemStack object or its chunk page.
    • TLS key exhaustion: read from the core, libkernel's key table has 38 keys created and 34 in use, far below its 512 limit.

    All three FreeChunks crashes happen on a guest thread with the same stack pointer (0x203e64840), so it is the same thread every time. Next I'll put a hardware watchpoint on that thread's TopChunk to catch the write.

    Tested and written by Claude (AI assistant) on a contributor's machine.

  2. Tamalero commented on Oct 12, 2026

    @Tamalero
    ContributorAuthor

    Found the cause, and it is already fixed on main: this is the stale DCC key store that #3458 (@alfalfa-star) fixed for RIDE 4. My build predated it. High on Life hits the same Unreal allocator pattern.

    The 0xFF that turns TopChunk/Next into -1 is AnyPS5 marking a dead render target's DCC keys uncompressed. The game frees a 1920x1080 RGBA8 DCC target and Unreal's allocator reuses the 64 KiB block that held its keys. Later a dispatch touches the old surface's memory, the driver writes back the stale storage image, and MarkDccUncompressed fills the old key range with 0xFF, on top of the allocator's data. Depending on what lands there, the game crashes in FMemStackBase::FreeChunks (this issue) or, earlier, popping an allocator free list.

    With a guard that skips the key store when the CPU rewrote the key range since the image was last in sync (equivalent here to #3458's gate, see below), the title ran 17 minutes with 4 checkpoint reloads and quit cleanly. Without it, it crashed within 3 to 10 minutes. I think this issue can be closed as fixed by #3458.

    Details and logs

    Setup: CachyOS (Linux 7.2), RTX 4090 (NVIDIA 615.78), GCC, Release. High on Life (PPSA15388 v01.120.000) on main f5c0fe00 plus the ports listed in #4139. #3458 is not in that base.

    Evidence. The corrupted blocks are whole runs of 0xFF: 48 KiB at a 64 KiB boundary (the allocator's per-thread cache, reached through pthread_getspecific), or a whole 64 KiB FMemStack chunk page. APS5_TRACE_MEMORY=1 shows each page mapped once and never remapped, with no physical page mapped at two addresses. So it is not a mapping bug. With APS5_TRACE_DCC_KEYS=1, the crashing run's corrupted block matched a key range exactly:

    [memory] remap fixed <A>+0x10000 prot=0xf2 phys=…
    [dcc-keys] uncompressed store by first write (?) for surface <S>+0x870000: keys <A>+0xc000 (packet 0x2d queue 0x0)
    [dcc-keys] uncompressed keys stored on the gpu: <A>+0xc000 (packet 0x2d queue 0x0)
    … (the title frees <S>; its allocator takes the block at <A>) …
    [dcc-keys] uncompressed store by block write-back (imported buffer region) for surface <S>+0x870000: keys <A>+0xc000 (packet 0x15 queue 0x0)
    [dcc-keys] uncompressed keys stored on the gpu: <A>+0xc000 (packet 0x15 queue 0x0)
    

    At the crash, r14 (the per-thread cache) = <A>, the free-list head = -1, and [<A>, <A>+0xc000) is all 0xFF.

    Local guard, on the old base only: in the three write-back paths (block, layer, CPU), skip MarkDccUncompressed when GuestMemory::UnchangedSinceCollected(dccAddress, keyCount, generation) is false, that is when the CPU rewrote the keys since the image's generation. It logged every skip together with the image's uploaded keys. In 6 further launches the guard skipped 2 stores, and both times the uploaded keys were already Uncompressed, so #3458's uploadedKeys != Uncompressed gate would have skipped the same stores. I have no case where the two differ, so I am not proposing a change.

    Results with the guard: startup allocator crash 0 of 18 launches (about 7 of 12 before). In-game: 12 minutes and 17 minutes with 3 and 4 checkpoint reloads, at least 64 stale key stores skipped, no FreeChunks crash, and a clean quit.

    Tested and written by Claude (AI assistant) on a contributor's machine.

  3. oneandonlydean commented on Oct 13, 2026

    @oneandonlydean
    Collaborator

    Closing: fixed by #3458, as found above. Reopen if it crashes in FMemStackBase::FreeChunks on current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions