You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
video_out_flip_lifetime intermittently aborts with 0xc0000374 (heap corruption) on Windows instead of skipping #2540
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.
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.
Same on the controls mode. mainc72d5ce1, 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:
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.
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 mainafb8cd2, 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:
video_out_flip_lifetime: 0 of 150 crashes, against about 7 of 160 before.
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:
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.
@Yuva1024 thanks for the measurement. I surveyed the rest of the prx code for the same pattern (mainbc40a6b):
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.
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.
Tested @AlxcBTW's fix, AlxcBTW/AnyPS5#6 (20e6d0bc), merged onto mainb7bb2684; 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.
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 mainc72d5ce 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.
Activity
frankea commentedon Oct 10, 2026
Same on the
controlsmode.mainc72d5ce1, 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:0xc0000374video_out_flip_tests.exe controlsdirectly, 40 runsIt also surfaced once as the only failure of a full
ctest -j4on 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).
popcoder565 commentedon Oct 10, 2026
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
mainafb8cd2, and in 4 of 40 in another build on top ofmain. Here it shows up as an access violation inside the hostexception_ptrrelease:What I think happens
When the window can't be created,
presentLooppasses the exception to the constructor throughstd::promise<void>::set_exception(std::current_exception()). The constructor rethrows it fromattached.get().libSceVideoOut.prximports__cxa_*,current_exception,rethrow_exceptionand theexception_ptrmembers fromlibc.prx. The promise's shared result is a different case.std::__future_base::_Result_base, which holds the storedexception_ptr, has its destructor inlibstdc++-6.dll. So thatexception_ptris released by the host_M_release, on an exception object thatlibc.prx's runtime created and reference-counts. That fits both symptoms: the access violation here and the heap corruption on your machines.Moving
startedout of thetryblock, so it outlives the join in thecatch, 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_ptrmember. Then everyexception_ptroperation is inline in VideoOut and goes throughlibc.prx:video_out_flip_lifetime: 0 of 150 crashes, against about 7 of 160 before.lifetime,decodeandcontrolspass 3 of 3.reopenfails the same way with and without it (VideoOut: port is closed, the case fix(videoout): cancel flips on a port the title closed instead of failing the driver #1648 fixes).I haven't checked the
controlsmode @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:🤖 Investigated with Claude Code
Yuva1024 commentedon Oct 10, 2026
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.
_Result_base::~_Result_basetail-jumps to its ownexception_ptr::_M_release(not through an import, solibc.prx's override is never used), which doeslock subl $0x1,-0xa0(obj)and, on zero,call *-0x88(obj)atlibstdc++-6.dll+0xd9259, the address in the trace above.libc.prxputssizeof(LibcException::Allocation)= 128 bytes before the object (refcount atobj-0x80), so the decrement lands 32 bytes before the heap block.implib/liblibc.dll.awith the libraries' unwind setup (-fno-asynchronous-unwind-tables,.eh_framerenamed to.ehfram). After destroying astd::future<void>that holds a caughtstd::runtime_error:obj-0xA0goes 141496 → 141495 (worker thread) and 0 → 4294967295 (same thread);libc.prxrefcount atobj-0x80stays 2 → 2, so the exception also leaks;obj-0xA0goes 2 → 1.std::promise<void>aborts in 9/300 runs without SDL. WithSDL_InitSubSystem/SDL_QuitSubSystemaround it, it aborts in 28/300 runs, against 0/200 for the same program built on libstdc++'s runtime.std::rethrow_exceptionwithout astd::futurestays at 0/300.libkernel/Pthread/src/Thread.cppuses the same pattern (initialized.set_exception(std::current_exception())at line 359,initialized.get()at line 404 onbc40a6b5), andlibkernel.prximports_ZNSt13__future_base12_Result_baseD2Evfromlibstdc++-6.dlltoo. It is an error path; I have not seen it abort. The diff above changes VideoOut only.AI-assisted: yes (Claude Code).
popcoder565 commentedon Oct 10, 2026
@Yuva1024 thanks for the measurement. I surveyed the rest of the prx code for the same pattern (
mainbc40a6b):std::promise/std::futureappear only in the two sites you named:libSceVideoOut/src/VideoOutDriver.cppandlibkernel/Pthread/src/Thread.cpp. No prx code usesstd::async,std::packaged_task,std::make_exception_ptrorstd::nested_exception.libSceVideoOut.prxandlibkernel.prxare the only prx files that import_ZNSt13__future_base12_Result_baseD2Evfromlibstdc++-6.dll.__cxa_init_primary_exceptionfromlibstdc++-6.dll. With no directmake_exception_ptrin 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 thatlibc.prx's__cxa_allocate_exceptionreturned. 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: inThread.cpp's failure paths, the futures go first orstartgets a value.So moving both sites off
std::promise/std::futurewould close all three paths.🤖 Addressed by Claude Code
popcoder565 commentedon Oct 10, 2026
This now reaches
main's CI too. The Windows job of run 38025301590 on bc40a6b failed only onvideo_out_flip_lifetime(Exit code 0xc0000374). The rerun of the same commit passed.🤖 Addressed by Claude Code
Yuva1024 commentedon Oct 10, 2026
Tested @AlxcBTW's fix, AlxcBTW/AnyPS5#6 (
20e6d0bc), merged ontomainb7bb2684; it still merges cleanly onto00112489, and the commits since don't touchlibc, VideoOut orlibkernel/Pthread. Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, RTX 4060.libSceVideoOut.prxandlibkernel.prximport_ZNSt13__future_base12_Result_baseD2Evfromlibc.prxinstead oflibstdc++-6.dll.ctest: 607/607, including the newwindows_exceptionscase; 3 expected skips.libc: the word atobj-0xA0is unchanged, and thelibc.prxrefcount 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_ALIASis empty, solibc.prxexports only__cxa_init_primary_exception_nid_postfix, and modules take__cxa_init_primary_exceptionfromlibstdc++-6.dllwhile__cxa_allocate_exceptioncomes fromlibc.prx. A standalone program linked like the libraries that doesstd::make_exception_ptr(std::runtime_error(...))and rethrows it faults on the first iteration inlibc.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).
reyani-dev commentedon Oct 10, 2026
Two more VideoOut tests hit the same
0xc0000374on the Windows CI job, on branches that don't touch libc, VideoOut or libkernel (fork runs onmainc72d5ce 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_statussegfaults on unmodifiedmainas well. So the fix should probably be checked against the whole VideoOut set, not onlyvideo_out_flip_lifetime.