Skip to content

SONIC SUPERSTARS (PPSA06888): guest heap metadata overwritten with texel data after respawns #5934

Open

Description

@billythemenace2

SONIC SUPERSTARS (PPSA06888) crashes in its own memory allocator after a few deaths and respawns in a stage. The game's heap metadata is overwritten with what looks like texel data. AnyPS5 logs no error before it. One earlier run froze at the same point instead: the game's render thread stopped calling sce::Agc::suspendPoint for 52 s, with no AnyPS5 error and no WAIT_REG_MEM timeout. That may be the same corruption hitting the allocator another way.

The crash (Windows minidump of the relinked app.exe):

EXCEPTION_ACCESS_VIOLATION (non-canonical address) at app.exe+0x1697ee9, in the guest eboot code
  141697edb  mov 0x10(%r8),%rsi
  141697edf  mov 0x18(%r8),%rdx
  141697ee9  mov %rdx,0x18(%rsi)      <- faults
  141697eed  mov %rsi,0x10(%rdx)
r8 = 0x28e18fff0   rsi = rdx = 0x00ffffff00ffffff   rdi = 0x200040890

The surrounding code (lzcnt, size classes, cmp %r8,0x88(%rdx,%r10,8)) is a TLSF-style allocator unlinking a free block. The block header is at 0x28e18fff0, so its free-list pointers at +0x10 and +0x18 are the first 16 bytes at 0x28e190000, and they hold 0x00ffffff dwords.

The last AnyPS5 log lines before the crash create a storage image at exactly that address:

StorageTexture address=0x28e190000 32x32 mips=1 mip=0 layers=0 base=0 dim=1 tile=7 format=56 vk=37
Texture address=0x28e190000 32x32 mips=1 viewed from storage image (vk=37)

So the game freed that block and the first texels of a 32x32 RGBA8 storage image (0x00ffffff: white, alpha 0) were stored over its free-list pointers. Either the game's GPU work on the console wrote them before the free, while on AnyPS5 the result reaches guest memory after it, or something wrote them over memory that was already free.

What I checked: a log in StorageTexture::writeBackLayers over a later run (306 write-backs). Every write-back in this title is untracked (tracked=0), and every stored layer was compared with its saved original first (compared equals stored, unsavedOutsideWatch=0). So the deferred write-back skips 64 KiB blocks the CPU changed before the comparison, and I could not confirm it as the writer. That run hit the multisampled-memory stop (#5933) before the crash, so it did not include the crash itself. Not checked: writes the CPU makes after the comparison and before the store, shader stores through BDA, and DMA destinations.

Write watching: GuestMemory::Watched was false for every multisampled target checked in these runs (see #5933), so CPU stores into that memory are not tracked. Whether the storage image's memory at 0x28e190000 is watched was not checked.

Setup: Windows 11, MinGW-w64 GCC 15, Release, AMD Radeon RX 5700 XT. main 5a6ad0ed3 plus #5865, #5375, #5456, #5882, #4820 and #5445, and a local footprint for the Z64KBX layout. The dump and logs are available on request.

AI-assisted (Claude Code).

Activity

  1. romainveurrier commented on Oct 14, 2026

    @romainveurrier

    Hi!

    I've reproduced a compare-to-publication ordering window in a controlled synthetic test on NVIDIA (RTX 2050, driver 615.71.09) under Fedora 44.

    CPU changes made after the storage-image keep-range selection can be overwritten by subsequent publication. However, the experiment deliberately forces that ordering, and I haven't established that such CPU modifications are valid under the guest synchronization contract.

    Would you be able to share the exact commit SHAs for your reproduction patch stack, your local Z64KBX layout changes, the AMD driver version, and the reproducible death/respawn sequence?
    Sanitized logs showing the events around the corrupted address would also be very useful. If sharing minidump-derived information is possible, please let me know what you can safely provide.

  2. billythemenace2 commented on Oct 14, 2026

    @billythemenace2
    Author

    @romainveurrier Thanks for looking into this. Here is what I can give.

    Exact build: the crash came from this tree, now pushed as billythemenace2/AnyPS5@fb4b8d650 (branch test/sonic-superstars-5934). It starts from main 5a6ad0ed3, as the issue says, but merging #4820 also brought in main up to 066b58fa1. On top of that it has:

    70c29a7e3 and its revert fb4b8d650 cancel out. No diagnostic logging was in this build.

    Driver: AMD Radeon RX 5700 XT, AMD driver 32.0.21045.5002 (17 August 2026), Windows 11 26200. The app selects the 5700 XT over the Ryzen iGPU.

    Sequence: dying and respawning three times in the last act of the first zone, not in a bonus stage. The crash came on the third respawn. An earlier run on a nearly identical tree froze at a respawn instead. The run that hit the reuse stop (#5933) stopped before any respawn crash.

    Logs: the stdout and stderr of the crash run were overwritten by later runs, so only the minidump remains. Its last AnyPS5 lines before the crash, quoted in the issue, came from the live log of that run. I won't attach the minidump itself, since it contains the game's memory. I can extract more from it: other threads' stacks, registers, or which memory ranges the dump holds around 0x28e190000.

    AI-assisted (Claude Code).

  3. romainveurrier commented on Oct 16, 2026

    @romainveurrier

    Thanks for the build details. Synthetic tests on your branch reproduce two ways the deferred storage-image write-back stores the old texels over links the title wrote after the dispatch's label. Both leave 0x00ffffff00ffffff at +0x0 and +0x8, as in your dump:

    1. A Windows host import is compared, not write-watched. If the allocator frees the block with the same links it held when the image was uploaded, the compare sees no change and the results are stored over them. This fits a crash on a repeated respawn.
    2. A CPU store landing between the write-back's block decision and its store.

    Both reproduce on NVIDIA and Intel and are fixed in #6960 . I haven't confirmed in-game that one of them is your crash. Could you try the PR branch on your 5700 XT?

    • The same sequence: three deaths in the first zone's last act.
    • If it no longer crashes, the same run with APS5_DEFER_COMPARED_RESULTS=1, which restores the old deferral. If the crash returns, that points to case 1.
    • Whether the frame rate changes in that stage. If it does, the summary lines and traces from the Performance report form would help.
  4. billythemenace2 commented on Oct 17, 2026

    @billythemenace2
    Author

    @romainveurrier thank you, both cases fit what I saw, and the repeated-respawn timing especially fits case 1.

    I can't run the respawn sequence on #6960 yet. On current main plus the PRs the title needs (#6297, #6300 and #6808), SONIC SUPERSTARS now stops a few seconds in, at sample-preserving guest publication of native multisample depth is not implemented (details on #6808). That's before the first zone. The 14 October tree that reached the stages predates too much: #6960's Texture.cpp changes don't apply to it without a manual port, and a port would no longer be testing your code as written.

    Once the depth publication work (Amoenus's stack ending in #7002) gets the title back into the stages on main, I'll run your three steps: the three deaths in the last act, then APS5_DEFER_COMPARED_RESULTS=1 if it no longer crashes, and the frame-rate comparison. I'll post the results here.

    AI-assisted (Claude Code).

  5. billythemenace2 commented on Oct 17, 2026

    @billythemenace2
    Author

    @romainveurrier I ran #6960 on the 5700 XT. The result is partial: neither run reached three deaths, but the frame-rate difference is clear.

    Build. Current main still can't reach the stages. With #6808, Amoenus's stack ending in #7002, #6727 and #6960 merged, the title stops a few seconds in at sample-preserving guest publication of native multisample depth is not implemented. #6727 doesn't cover it because the game draws into that surface. So I ported #6960 onto the 14 October tree (fb4b8d650, the one in my earlier comment). The non-test files applied as written except two Texture.cpp hunks: that tree has one HostImportFor import instead of importSegments(). So writeBackLayers takes const HostImport* import under your recordedWriteBack || pendingSinceCompletion == guestCompletions condition, reads original when it's null, and stores through the import when it isn't. StoreBeforeGuestCompletion, both PacketMemory.cpp and VulkanDevice.cpp call sites, and WriteChangedOverOriginal(Committed) are unchanged. This tests a port, not your branch as written.

    Run 1, #6960 default. It crashed in the first zone before any death, at the multisampled reuse stop from #5933: a texture read of 0x29ddc1000+0x20000 overlaps multisampled color target 0x29dde0000+0x10000 (4 samples). No the CPU stored into ... during its write-back line was logged.

    Run 2, APS5_DEFER_COMPARED_RESULTS=1. One death and respawn without a crash, then the act finished. It froze on the level-complete screen with no AnyPS5 error and no GPU timeout, and the frame counter stopped. A gdb dump shows every thread waiting, none inside a driver lock. The 14 October build froze the same way at a respawn. One respawn proves nothing about case 1 or case 2.

    Frame rate. The title's FPS counter, sampled every 15 s in the same stretch of the first zone:

    in-stage FPS
    #6960 default 11 to 14 (one 35 sample)
    APS5_DEFER_COMPARED_RESULTS=1 20 to 36

    So on this title the store at each label/interrupt roughly halves the frame rate. I didn't collect a Performance report form trace. I can if you tell me which switches you want.

    AI-assisted (Claude Code).

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