Skip to content

Audio: guest audio thread mixes from freed memory while the process exits #5864

Closed

Description

@eliyarson

What happens. When the process exits, the guest's FMOD mixer thread keeps calling sceAudioOut2ContextPush while exit() is already destroying static objects. The push then mixes from freed memory. In one run it crashed with SIGSEGV at exit, and the faulting pointer had been read from freed heap memory.

Measured. Hades II (hub scene of #5485), Linux, RTX 3080 Ti, NVIDIA 610.57.04, commit 1e9e3ca68 (local branch on main from 2026-10-13; libSceAudioOut and the exit path in libc/src/Process.cpp differ from current main only in the pad haptics and nested-exit changes). Game in the hub for about 3 minutes, then SIGTERM, which goes through LibcRequestExit_nid_postfix → LibcExit_nid_no_patch → std::exit.

With MALLOC_PERTURB_=165 (freed memory filled with 0xa5), the core shows:

Thread 1 (LWP 2640381), SIGSEGV
#0  libc.so.6 memcpy        vmovdqu (%rsi),%ymm0   rsi=0xa5a5a5a5a5a5a5a5 rdx=0x20
#1  AudioOut2MixPorts(...)                (libSceAudioOut.prx +0x307ce)
#2  Render(AudioOut2Context&, ...)        (libSceAudioOut.prx +0x2aff9)
#3  sceAudioOut2ContextPush               (libSceAudioOut.prx +0x2d027)
#4-6 libfmod.prx.guest.prx
#7  StartNativeThread                     (libkernel.prx)

Thread 19 (LWP 2652484), at the same moment
#0-2 std::map<unsigned long, PhysicalPage>::~map   (libkernel static destructor)
#3-4 exit()
#5  LibcExit_nid_no_patch                 (libc.prx)
#6  LibcRequestExit_nid_postfix lambda    (libc.prx)

The source pointer is the 0xa5 fill pattern, so it was read from freed memory while thread 19 was running the exit handlers. Two other runs that exited the same way, with libc_malloc_debug.so and glibc.malloc.check=3, did not crash.

Why (reading main bb7e47a88; the second point is my inference, not confirmed in the core):

  • LibcExit_nid_no_patch runs the registered shutdown callbacks, then CxaFinalize and std::exit. Only the AGC driver and the VideoOut driver register a callback (LibcRegisterShutdown_nid_postfix); libSceAudioOut does not, so nothing stops the guest's audio thread before static destructors run.
  • AudioOut2MixPorts and AudioOut2CaptureGrain read the static g_ports vector and g_portsLock (AudioOut2Port.cpp:36-38). Once those are destroyed, a push that is still running reads freed memory.

We have also seen double free or corruption (out) at exit after SIGTERM in this title (#5485), with and without APS5_MT_RECORD. Those aborts left no core with a stack, so I can't say they come from this, but an exit that frees memory still used by running threads would explain them.

Proposal. Register a libSceAudioOut shutdown callback that closes the SDL devices and makes sceAudioOut2ContextPush (and the AudioOut1 output path, if it has the same shape) return without touching the port tables once shutdown has started, so no audio thread is inside the mixer when static destructors run. A test in the style of core/libs/tests/GuestSystemExit.cpp could keep a thread pushing while exit runs.

I can send a PR for this unless someone already has the audio shutdown in progress.

Activity

  1. isnisk commented on Oct 14, 2026

    @isnisk
    Contributor

    I'd like to work on this unless you've already started. My plan follows your proposal:

    • libSceAudioOut registers a shutdown callback with LibcRegisterShutdown_nid_postfix, like the VideoOut and AGC drivers do. The callback sets a shutdown flag and closes the SDL devices.
    • Once the flag is set, sceAudioOut2ContextPush and the AudioOut1 output path return without touching g_ports / g_portsLock. The callback waits for any push already inside the mixer to leave, so no audio thread is mixing when the static destructors run.
    • A test in the style of core/libs/tests/GuestSystemExit.cpp keeps one thread pushing while another thread calls exit.

    If you already have a branch for this, say so and I'll leave it to you.

    AI-assisted: yes (Claude Code).

  2. eliyarson commented on Oct 14, 2026

    @eliyarson
    ContributorAuthor

    @isnisk Go ahead, I have not started. One pointer: the AGC entry points park guest threads at shutdown with LibcAwaitExit_nid_postfix instead of returning an error the game might retry in a loop (5550ca63d); the audio calls could do the same. I can test your branch on Hades II, where it crashed.

  3. isnisk commented on Oct 14, 2026

    @isnisk
    Contributor

    @eliyarson the fix is in #6000 (branch isnisk:fix/audio-out-shutdown, one commit on main 73efd9407):

    git fetch https://github.com/isnisk/AnyPS5.git fix/audio-out-shutdown
    git checkout FETCH_HEAD
    cmake --build build --target libs

    It follows your pointer: the AudioOut2 port, push and advance calls and AudioOut1 output park with LibcAwaitExit_nid_postfix once exit is requested, and a new shutdown callback waits until no audio call is still inside before the static destructors run. A push that waits for an advance also wakes at exit, so it can't hold the exit up.

    A run of the Hades II hub, then SIGTERM, with MALLOC_PERTURB_=165 as in your report would show whether the crash at exit is gone. It would also be useful to know whether the double free or corruption (out) from #5485 still appears. #5971 (JJenkx) covers the same crash from the other side, by keeping the port table alive; the two don't depend on each other.

    AI-assisted: yes (Claude Code).

  4. hugodearaujo commented on Oct 14, 2026

    @hugodearaujo
    Contributor

    Another title with this: ASTRO BOT (PPSA21567, my own dump) aborts at exit on Linux in the same place. After SIGTERM at the title screen, one run printed malloc(): unsorted double linked list corrupted and aborted. Under gdb, the aborting thread was the guest's audio thread inside the push:

    #0-6  libc (abort from malloc's consistency check)
    #7    AudioOut2CaptureGrain(AudioOut2Context const&)   libSceAudioOut.prx
    #8    sceAudioOut2ContextPush                          libSceAudioOut.prx
    #9    (guest)
    

    Of 13 exits after SIGTERM on main f0854887d with #5905 (RX 9070 XT, Linux), 3 aborted (the stack above is from the one under gdb), 2 ended in SIGSEGV (no stack captured), 1 hung until it was killed and 7 exited cleanly. @isnisk, I can run your branch on ASTRO BOT when it is up.

    AI-assisted: written with Claude Code; I reviewed the stack and the runs.

  5. eliyarson commented on Oct 14, 2026

    @eliyarson
    ContributorAuthor

    Ran #6000 (e7219e4) against its base (73efd94) on Hades II: hub, 60 s hold, SIGTERM, MALLOC_PERTURB_=165, interleaved. Linux (CachyOS), Ryzen 9 5950X, RTX 3080 Ti; only the .prx files differ between the two game dirs.

    Build Exits Clean Crash at exit Hangs
    base 73efd94 12 11 1 0
    #6000 10 10 0 0

    SIGTERM to process exit took 0.5 to 1.3 s in every clean run on both builds, so the shutdown wait adds no visible delay. Four more runs per build with glibc's heap checks (libc_malloc_debug.so, glibc.malloc.check=3) all exited cleanly. The double free or corruption (out) from #5485 didn't appear in any run on either build.

    The base crash was a SIGSEGV 29.8 s after SIGTERM, on a guest FMOD thread in sceAudioOut2PortSetAttributes, one of the exports #6000 now guards:

    #0 sceAudioOut2PortSetAttributes (libSceAudioOut.prx + 0x3f5df)
    #1-#3 libfmod.prx.guest.prx
    #4 StartNativeThread (libkernel.prx)
    

    The crash is rare in this scene (1 in 12 here), so 10 clean runs on #6000 don't prove much on their own: with no fix at all, 10 clean runs in a row would happen about 4 times in 10. What the runs do show is that #6000 doesn't hang or slow down exit, and that the crash I hit on base is in the path it covers. If you have a way to make it deterministic (SIGTERM while FMOD is changing port attributes, say), I'm happy to run that.

    LLM assistance used.

  6. hugodearaujo commented on Oct 15, 2026

    @hugodearaujo
    Contributor

    Correction to my count above: of the 13 exits, 3 ended in SIGSEGV, not 2, and none hung (3 aborted, 3 SIGSEGV, 7 clean). The run I counted as hung was killed by timeout (exit 137) while its SIGSEGV core dump was still being written; systemd-coredump recorded the SIGSEGV. That run and one other also had MALLOC_PERTURB_=165, which I left out. The abort stack is unchanged.

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