Skip to content

video_out_flip_lifetime intermittently aborts with 0xc0000374 (heap corruption) on Windows instead of skipping #2540

Closed

Description

@Yuva1024

On Windows, video_out_flip_lifetime usually skips with exit 77 (SDL_VIDEODRIVER=offscreen has no Vulkan surface), but sometimes the process ends with STATUS_HEAP_CORRUPTION, which ctest reports as a failure.

main c963432d, Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, Ryzen 5 5600G, RTX 4060 + Radeon iGPU:

Run 0xc0000374 exit 77
video_out_flip_tests.exe directly, ctest's environment 6 54
ctest -R ^video_out_flip_lifetime$ 3 47
same with the other VideoOut tests, --parallel 4 5 45
under gdb 0 80

One of three full ctest --parallel 4 runs failed on it.

Failing runs write only the [time] scheduler tick ... line to stderr. Skipping runs also print DisplayWindow: Vulkan support is either not configured in SDL ... from main's catch (Flip.cpp), so the corruption happens in testLifetime or while its exception unwinds, before main's handler runs. I have not located it.

AI-assisted: yes (Claude Code).

Activity

  1. frankea commented on Oct 10, 2026

    @frankea
    Contributor

    Same on the controls mode. main c72d5ce1, Windows 11 10.0.26200, WinLibs GCC 15.2.0 r7, Release, Ryzen 7 5800X, RTX 4090 (NVIDIA 616.92), SDL_VIDEODRIVER=offscreen, ctest's working directory and PATH:

    Run 0xc0000374 exit 77
    video_out_flip_tests.exe controls directly, 40 runs 2 38
    same binary built with #862 merged, 40 runs 2 38
    under gdb, 80 runs 0 80

    It also surfaced once as the only failure of a full ctest -j4 on a PR build (#862), so it reaches the summary on this machine about one run in twenty. The identical rate with and without #862 says it is not that PR.

    AI-assisted: yes (Claude Code).

  2. popcoder565 commented on Oct 10, 2026

    @popcoder565
    Contributor

    I see the same failure: Windows 10 (19045), RTX 3060 Laptop (NVIDIA 616.92), WinLibs GCC 15.2.0 r7, Release. It crashed in 1 of 40 runs on main afb8cd2, and in 4 of 40 in another build on top of main. Here it shows up as an access violation inside the host exception_ptr release:

    rip libstdc++-6.dll+0xd9259   (std::__exception_ptr::exception_ptr::_M_release)
    read of 0xffffffffffffffff
      #0 libSceVideoOut.prx  VideoOutDriver::VideoOutDriver() [clone .cold]
      #1 libSceVideoOut.prx  VideoOutDriver::Get()
      #2 video_out_flip_tests.exe  testLifetime
      stack also holds ~promise<void>, ~_State_baseV2 and ~_Result<void>
    

    What I think happens

    When the window can't be created, presentLoop passes the exception to the constructor through std::promise<void>::set_exception(std::current_exception()). The constructor rethrows it from attached.get().

    libSceVideoOut.prx imports __cxa_*, current_exception, rethrow_exception and the exception_ptr members from libc.prx. The promise's shared result is a different case. std::__future_base::_Result_base, which holds the stored exception_ptr, has its destructor in libstdc++-6.dll. So that exception_ptr is released by the host _M_release, on an exception object that libc.prx's runtime created and reference-counts. That fits both symptoms: the access violation here and the heap corruption on your machines.

    Moving started out of the try block, so it outlives the join in the catch, did not help (2 of 80 still crashed).

    A change that removes it here

    Report the start through a mutex, a condition variable and an exception_ptr member. Then every exception_ptr operation is inline in VideoOut and goes through libc.prx:

    I haven't checked the controls mode @frankea reports separately, but its port-opening path goes through the same constructor. I can't open a PR while intake is paused, so here is the diff:

    diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp
    index cc7bcfd47..ac6da1fbf 100644
    --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp
    +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp
    @@ -209,7 +209,13 @@ public:
     
     private:
         bool close(int handle);
    -    void presentLoop(std::stop_token token, std::promise<void>& started);
    +    struct StartSignal {
    +        std::mutex mutex;
    +        std::condition_variable ready;
    +        bool done = false;
    +        std::exception_ptr error;
    +    };
    +    void presentLoop(std::stop_token token, StartSignal& started);
         void vblankLoop(std::stop_token token);
         void vblankEnd();
         void processFlip(FlipRequest& req);
    diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp
    index 4d590926f..2f3997153 100644
    --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp
    +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp
    @@ -235,12 +235,15 @@ VideoOutDriver::VideoOutDriver() {
         if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
             throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError());
         }
    +    StartSignal started;
         try {
             AgcDriverWaitIdle_nid_postfix();
    -        std::promise<void> started;
    -        auto attached = started.get_future();
             presentThread = std::jthread([this, &started](std::stop_token token) { presentLoop(token, started); });
    -        attached.get();
    +        {
    +            std::unique_lock lock(started.mutex);
    +            started.ready.wait(lock, [&] { return started.done; });
    +        }
    +        if (started.error) std::rethrow_exception(started.error);
             vblankThread = std::jthread([this](std::stop_token token) { vblankLoop(token); });
             LibcRegisterShutdown_nid_postfix([] { VideoOutDriver::Get().Shutdown(); });
         } catch (...) {
    @@ -491,17 +494,26 @@ void VideoOutDriver::processFlip(FlipRequest& req) {
         timing.Mark("notify_game");
     }
     
    -void VideoOutDriver::presentLoop(std::stop_token token, std::promise<void>& started) {
    +void VideoOutDriver::presentLoop(std::stop_token token, StartSignal& started) {
         try {
             window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT);
             const auto extensions = windowExtensions(window.Handle());
             AgcDriverAttachWindow_nid_postfix({window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT, nullptr});
         } catch (...) {
             window.Destroy();
    -        started.set_exception(std::current_exception());
    +        {
    +            std::lock_guard lock(started.mutex);
    +            started.error = std::current_exception();
    +            started.done = true;
    +        }
    +        started.ready.notify_all();
             return;
         }
    -    started.set_value();
    +    {
    +        std::lock_guard lock(started.mutex);
    +        started.done = true;
    +    }
    +    started.ready.notify_all();
         std::shared_ptr<FlipRequest> current;
     #if APS5_ENABLE_TIMING_LOG
         std::unique_ptr<FrameTimingLog> timingLog;

    🤖 Investigated with Claude Code

  3. Yuva1024 commented on Oct 10, 2026

    @Yuva1024
    ContributorAuthor

    Confirming @popcoder565's analysis with a direct measurement, and one more site with the same pattern. Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release.

    • libstdc++'s _Result_base::~_Result_base tail-jumps to its own exception_ptr::_M_release (not through an import, so libc.prx's override is never used), which does lock subl $0x1,-0xa0(obj) and, on zero, call *-0x88(obj) at libstdc++-6.dll+0xd9259, the address in the trace above. libc.prx puts sizeof(LibcException::Allocation) = 128 bytes before the object (refcount at obj-0x80), so the decrement lands 32 bytes before the heap block.
    • Measured in a standalone program linked against implib/liblibc.dll.a with the libraries' unwind setup (-fno-asynchronous-unwind-tables, .eh_frame renamed to .ehfram). After destroying a std::future<void> that holds a caught std::runtime_error:
      • the word at obj-0xA0 goes 141496 → 141495 (worker thread) and 0 → 4294967295 (same thread);
      • the libc.prx refcount at obj-0x80 stays 2 → 2, so the exception also leaks;
      • the same program on libstdc++'s own runtime: obj-0xA0 goes 2 → 1.
    • It does not need a second thread, SDL or VideoOut: a worker throwing through std::promise<void> aborts in 9/300 runs without SDL. With SDL_InitSubSystem/SDL_QuitSubSystem around it, it aborts in 28/300 runs, against 0/200 for the same program built on libstdc++'s runtime. std::rethrow_exception without a std::future stays at 0/300.
    • libkernel/Pthread/src/Thread.cpp uses the same pattern (initialized.set_exception(std::current_exception()) at line 359, initialized.get() at line 404 on bc40a6b5), and libkernel.prx imports _ZNSt13__future_base12_Result_baseD2Ev from libstdc++-6.dll too. It is an error path; I have not seen it abort. The diff above changes VideoOut only.

    AI-assisted: yes (Claude Code).

  4. popcoder565 commented on Oct 10, 2026

    @popcoder565
    Contributor

    @Yuva1024 thanks for the measurement. I surveyed the rest of the prx code for the same pattern (main bc40a6b):

    • std::promise/std::future appear only in the two sites you named: libSceVideoOut/src/VideoOutDriver.cpp and libkernel/Pthread/src/Thread.cpp. No prx code uses std::async, std::packaged_task, std::make_exception_ptr or std::nested_exception.
    • libSceVideoOut.prx and libkernel.prx are the only prx files that import _ZNSt13__future_base12_Result_baseD2Ev from libstdc++-6.dll.
    • Both also import __cxa_init_primary_exception from libstdc++-6.dll. With no direct make_exception_ptr in prx code, that most likely comes from <future>'s broken-promise path (make_exception_ptr(future_error(broken_promise)) in ~promise). That path would write libstdc++'s exception header in front of a block that libc.prx's __cxa_allocate_exception returned. I haven't triggered it. It's only reached when a promise is destroyed unset while a future still waits on it. In the current code I found no such path: in Thread.cpp's failure paths, the futures go first or start gets a value.

    So moving both sites off std::promise/std::future would close all three paths.

    🤖 Addressed by Claude Code

  5. popcoder565 commented on Oct 10, 2026

    @popcoder565
    Contributor

    This now reaches main's CI too. The Windows job of run 38025301590 on bc40a6b failed only on video_out_flip_lifetime (Exit code 0xc0000374). The rerun of the same commit passed.

    🤖 Addressed by Claude Code

  6. Yuva1024 commented on Oct 10, 2026

    @Yuva1024
    ContributorAuthor

    Tested @AlxcBTW's fix, AlxcBTW/AnyPS5#6 (20e6d0bc), merged onto main b7bb2684; it still merges cleanly onto 00112489, and the commits since don't touch libc, VideoOut or libkernel/Pthread. Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, RTX 4060.

    • libSceVideoOut.prx and libkernel.prx import _ZNSt13__future_base12_Result_baseD2Ev from libc.prx instead of libstdc++-6.dll.
    • Full ctest: 607/607, including the new windows_exceptions case; 3 expected skips.
    • The probe from my earlier comment, linked against the patched libc: the word at obj-0xA0 is unchanged, and the libc.prx refcount goes 2 → 1, so the exception is no longer leaked (worker thread and same thread).
    • video_out_flip_tests.exe (lifetime), 300 runs each: 0 aborts with the fix, 33 without.

    Not covered by it, and not reached by current code: on Windows GCC_GLOBAL_ALIAS is empty, so libc.prx exports only __cxa_init_primary_exception_nid_postfix, and modules take __cxa_init_primary_exception from libstdc++-6.dll while __cxa_allocate_exception comes from libc.prx. A standalone program linked like the libraries that does std::make_exception_ptr(std::runtime_error(...)) and rethrows it faults on the first iteration in libc.prx (read of 0xffffffffffffffff), with the fix applied; on libstdc++'s own runtime it runs 1000 iterations cleanly. This is the broken-promise path @popcoder565 described.

    Not tested on a title.

    AI-assisted: yes (Claude Code).

  7. reyani-dev commented on Oct 10, 2026

    @reyani-dev
    Contributor

    Two more VideoOut tests hit the same 0xc0000374 on the Windows CI job, on branches that don't touch libc, VideoOut or libkernel (fork runs on main c72d5ce and bc40a6b):

    • guest_video_out_resolution_status: run 38022700264, first attempt (the rerun passed 573/573);
    • guest_video_out_wqhd_detection: run 38027642514, first attempt.

    Locally (Windows 11, WinLibs GCC 15.2.0 r7, Release), guest_video_out_resolution_status segfaults on unmodified main as well. So the fix should probably be checked against the whole VideoOut set, not only video_out_flip_lifetime.

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