[Bug 60215] New: esync: eventfd() exhaustion (EMFILE) on sync object creation not handled gracefully, propagates as guest null-pointer crash
http://bugs.winehq.org/show_bug.cgi?id=60215 Bug ID: 60215 Summary: esync: eventfd() exhaustion (EMFILE) on sync object creation not handled gracefully, propagates as guest null-pointer crash Product: Wine Version: unspecified Hardware: arm OS: Android Status: UNCONFIRMED Severity: normal Priority: P2 Component: ntdll Assignee: wine-bugs@list.winehq.org Reporter: aydar.kamaltdinov@gmail.com Target Milestone: --- Note: this report was drafted/formatted with AI assistance (for structuring the technical writeup), but all testing, reproduction, log capture, and root-causing (attaching to the process's stderr, extracting the fault address/backtrace, confirming the WINEESYNC=0 workaround across multiple real runs) was done by me personally on physical hardware. Happy to answer follow-up questions or provide additional logs/raw captures directly. Possibly the same root cause as bug 58059 (eventfd leak in wineserver on thread create/destroy with WINEESYNC=1, UNCONFIRMED since 2025-04-04) — filing separately since I have a different real-world reproduction with more diagnostic detail (exact crash address + confirmed workaround), in case it helps unstick triage either way. Feel free to merge/dup if appropriate. ## Summary When WINEESYNC=1 is active and a running application leaks Windows synchronization objects (Events/Semaphores/etc.) faster than it releases them, esync's eventfd()-per-object model eventually exhausts the process's RLIMIT_NOFILE. At that point eventfd() creation fails with EMFILE, but the failure does not appear to be surfaced/handled in a way that prevents the guest application from later dereferencing an invalid/null handle — resulting in a hard crash (EXCEPTION_ACCESS_VIOLATION) instead of a recoverable API-level failure. This is architecture-independent in principle, but is far easier to trigger on platforms with a low default RLIMIT_NOFILE (e.g. Android, ~32768 in my case) than on typical desktop Linux, where the same underlying handle leak in the application would likely go unnoticed for a very long time (or indefinitely, given Windows' much larger practical handle-table ceiling). ## Observed on - A wine-staging-derived ARM64EC/WOW64 fork (used by several Android "run Windows games" projects — built from a "wine-proton-ec" tree, Proton 10.0 base), running on ARM64 Android via FEX-Emu as the WOW64 CPU backend. - I was not able to test on a vanilla x86-64 desktop Wine+wine-staging build to confirm the exact same code path applies verbatim, but the esync eventfd-creation code itself (dlls/ntdll/esync.c) appears architecture-independent, so I believe this is worth tracking upstream regardless of the specific fork. ## Reproduction case Game: Thief (2014), Steam AppID 239160, `Shipping-ThiefGame.exe`. Crashes deterministically (100% reproducible, identical fault address and full backtrace across repeated runs) during active gameplay with `WINEESYNC=1`. Captured via `WINEDEBUG=+seh,+exception`: code=c0000005 (EXCEPTION_ACCESS_VIOLATION) addr=000000014011F9D9 rip=000000014011f9d9 rcx=0000000000000000 ... Shipping-ThiefGame.exe+0x11F9D6: movq (%rbx), %rcx ; load object pointer Shipping-ThiefGame.exe+0x11F9D9: movq (%rcx), %rax <-- FAULT: rcx == 0 Shipping-ThiefGame.exe+0x11F9DC: callq *0x18(%rax) ; virtual call through vtable One capture shows this immediately preceding the crash: eventfd: Too many open files Setting `WINEESYNC=0` for this title eliminates the crash entirely (23+ minutes of gameplay with no crash, vs. a deterministic crash within minutes with esync on). ## Ask Is there existing handling in esync's object-creation path (CreateEvent/CreateSemaphore/etc. server calls) for eventfd() returning EMFILE? If not, would a graceful fallback (e.g. falling back to the non-esync/server-based wait object implementation for that single object, or returning a proper NT status the app is expected to check) be feasible, rather than leaving a handle in a state that leads to guest-side null dereferences? Happy to provide the full captured trace/disassembly if useful. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60215 Stian Low <wineryyyyy@gmail.com> changed: What |Removed |Added ---------------------------------------------------------------------------- CC| |wineryyyyy@gmail.com --- Comment #1 from Stian Low <wineryyyyy@gmail.com> --- Thanks for reporting and indicating AI assisted which helps devs address bugs more contextually. I'll take a closer look at esync because I'm less familiar but recently interested in diving deeper. In the meantime attaching backtrace and logs may be helpful for comparison with other reports that may be related. If default logs seem less useful then `WINEDEBUG=warn+all,err+all` may include extra hints. Thief 3 bug recently came on my radar which seems related to new WoW64 builds: - https://bugs.winehq.org/show_bug.cgi?id=59897 It may be worth testing old shared wow64 build if you're able. Maybe Thief 1 & 3 share enough code mechs to reproduce bugs across one another. Maybe its related to some other odd subtle bugs between old shared vs new wow64 that I've been collecting. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60215 --- Comment #2 from Aydar Kamaltdinov <aydar.kamaltdinov@gmail.com> --- Created attachment 81918 --> http://bugs.winehq.org/attachment.cgi?id=81918 Thief crash log -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60215 --- Comment #3 from Aydar Kamaltdinov <aydar.kamaltdinov@gmail.com> --- Reproduced again with `WINEDEBUG=warn+all,err+all` and `WINEESYNC=1` as requested — full log attached (thief_wine_debug_full_crash_2026-08-22.log). Notable: this crash is at a *different* fault address than the one in the original report — `Shipping-ThiefGame.exe+0x11F2A9` this time vs `+0x11F9D9` previously — but the backtrace has the same overall shape (same offset ranges throughout: 0x6F0xxx, 0x109xxxx, 0x10Dxxxx, 0x4A1xxx/0x4A8xxx/0x4AAxxx, 0x11Cxxx, 0x3A9xxx, 0x352xxx, 0x871xxx/0x875xxx). Both are 100% reproducible at their own respective address across repeated runs, but differ from each other run-to-run in what looks like a small, non-deterministic way. This suggests it's not one single isolated null-check bug, but a whole class of similarly-shaped call sites that all assume a sync-object handle is valid. Also much clearer evidence of resource exhaustion this time — right after the crash, cascading `STATUS_INSUFFICIENT_RESOURCES` (c000011f) failures on completely unrelated operations: loading `steam.dll`, writing the engine's own crash dump (`Thief64_4158_2026.08.22-15.24.33.dmp`), and saving `ThiefEngine.ini`. All consistent with the process being genuinely out of file descriptors at that point, not just the one eventfd allocation that happened to matter. Confirmed reproduction on a second, different physical device (Snapdragon 8 Elite Gen 5, less extensively tested than the original Snapdragon 8 Elite device, but same crash behavior observed) — not specific to one SoC/device. Re: testing an old shared-wow64 build vs the new one (per bug 59897) — haven't tried this yet, happy to if you think it'd help narrow things down; let me know which build/version would be most useful to test. Side note, possibly useful, possibly a separate issue: with WINEESYNC=0 (or WINEFSYNC=1 as an alternative), the crash disappears but framerate drops from a stable 60fps to a hard 30fps — GPU-driver/DXVK-version independent, tried several combinations. Haven't dug into why yet; mentioning in case it's a known tradeoff or points at something else in the sync fallback path. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60215 --- Comment #4 from Aydar Kamaltdinov <aydar.kamaltdinov@gmail.com> --- ## Root cause found and verified: `esync_apc_fd` leaked on every thread exit ### Methodology Since Bugzilla's `FileDescriptorsLeakDemo.exe` attachment wasn't reachable from my environment, I independently reproduced and root-caused this from scratch: 1. Built **Wine 10.4 + Wine-staging 10.4** (`eventfd_synchronization` patchset — note this patchset has since been dropped from current wine-staging master, presumably in favor of ntsync, but it's still what ships in Proton-derived/ARM64EC builds like the one I originally hit this bug on) natively for **aarch64 Linux**, using `llvm-mingw`'s `aarch64-w64-mingw32-clang` for the PE side — specifically to avoid cross-arch emulation artifacts. An earlier attempt under an x86_64 container via Rosetta, and separately under real QEMU-user, both produced a permanently spinning `wineserver` (100% CPU, hung indefinitely, ptrace not even available under qemu-user) — thismulation artifact unrelated to esync; building natively foraarch64 resolved it completely. 2. Wrote two minimal native test programs (`CreateThread` → `Wa one with and one without `CloseHandle`) and measured`wineserver`'s open eventfd count directly (`/proc/<wineserver-pid>/fd`) before/after N iterations. **Versions tested:** - Wine commit `0927c5c3da7cda8cf476416260286bd299ad6319` ("Rele - Wine-staging tag `v10.4` (commit `c110178b0d74cd0483d91aa2c19a8a02a4f2e10c`) - Specific patch introducing the affected code: `patches/eventfver-Create-eventfd-file-descriptors-for-thread-ob.patch` ### Finding `server/thread.c`, `create_thread()` allocates **two** eventfds ```c thread->esync_fd = esync_create_fd( 0, 0 ); thread->esync_apc_fd = esync_create_fd( 0, 0 ); but destroy_thread() (line 638 in the tested tree) only ever cl 636 if (thread->token) release_object( thread->token ); 637 638 if (do_esync()) 639 close( thread->esync_fd ); // esync_apc_fd 640 } esync_apc_fd is referenced elsewhere only via esync_wake_fd() /ing) — it is never passed to close() anywhere in the tree. Consequence: every CreateThread() leaks exactly one real eventfserver, deterministically, on every single thread — includingwith textbook-correct application code (CreateThread + WaitForSingleObject + CloseHandle). This is not a race or an edge case; it fires 100% of the time, on every thread, unconditionally. Empirical verification (200-iteration loop, measuring wineserve count) ┌───────────────────────────────────────────────┬─────────────────────────────┬────────────────────────────────────────────┐ │ Scenario │ Before fix │ After fix │ ├───────────────────────────────────────────────┼─────────────────────────────┼────────────────────────────────────────────┤ │ 200× CreateThread without CloseHandle │ 852 (baseline≈452 + 200×2) │ 853 (unchanged — object never destroyed, │ │ │ │ expected) │ ├───────────────────────────────────────────────┼───────────────────────────────────────────────────────┼────────────────────────────────────────────┤ │ 200× CreateThread with CloseHandle (correct │ 652 (baselinepite fully │ 453 (= baseline, zero leak) │ │ app code) │ correct code) │ │ └───────────────────────────────────────────────┴─────────────────────────────┴────────────────────────────────────────────┘ The fix eliminates the leak completely in the "correct code" cathe predicted baseline to within measurement noise. Fix server/thread.c, destroy_thread(), replacing line 638-639: if (do_esync()) - close( thread->esync_fd ); + { + close( thread->esync_fd ); + close( thread->esync_apc_fd ); + } Why this matches the original report A game/engine that spawns one short-lived worker thread per simulation tick (a very common pattern — job systems, audio callbacks, etc.) leaks exactly one real fd per tick regardless of application code correctness. Thc rate — matching the originally reported ~14400-tick-exactcrash threshold precisely, and explaining why it's invisible on real Windows (a handle-table leak, essentially unlimited there) but fatal under esync (a real, much lower RLIMIT_NOFILE). ### Note on where this actually needs fixing Current wine-staging `master` no longer carries the `eventfd_synchronization` patchset at all (last touched Oct 2025, fully absent since) — so a fix landing there specifically is moot. But this is **not** a "fixed in a newer version" situation: the affected code (patch `0018`, originally from 2018-06-08) hasn't changed in the years since it was written, and the bug has simply never been fixed in it — it's equally present in every fork/vintage that still carries this patchset, which in practice is most of what's actually deployed: - **Proton** (`ValveSoftware/wine`) — still ships esync as a fallback for systems without ntsync/fsync support. - The various **Android "run Windows games" ARM64EC forks** (e.g. `wine-proton-ec`, `fathonix/wine-proton-arm64ec`, `GameNative/proton-wine`) that GameHub/GameNative/BannerHub/Winlator-family apps bundle — these are Proton-derived and pin their own snapshots, all containing the same unfixed code. Since the underlying bug is a single unconditional missing `close()` call that's been identical since 2018, this almost certainly needs to be reported/patched separately in each of these downstream projects rather than relying on wine-staging upstream (which has moved on). Happy to open corresponding reports/PRs against Proton and the specific Android forks if useful — let me know if that'd help or if you'd rather handle triage from here. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60215 --- Comment #5 from Aydar Kamaltdinov <aydar.kamaltdinov@gmail.com> --- Empirical verification (200-iteration loop, measuring wineserver's /proc/<pid>/fd eventfd count) ------- Scenario: 200x CreateThread WITHOUT CloseHandle Before fix: 852 (baseline ~452 + 200x2) After fix: 853 (unchanged -- object never destroyed, expected) Scenario: 200x CreateThread WITH CloseHandle (correct app code) Before fix: 652 (baseline ~452 + 200x1 -- leaking despite fully correct code) After fix: 453 (= baseline, ZERO leak) -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60215 Zeb Figura <z.figura12@gmail.com> changed: What |Removed |Added ---------------------------------------------------------------------------- CC| |z.figura12@gmail.com Resolution|--- |INVALID Status|UNCONFIRMED |RESOLVED --- Comment #6 from Zeb Figura <z.figura12@gmail.com> ---
Is there existing handling in esync's object-creation path (CreateEvent/CreateSemaphore/etc. server calls) for eventfd() returning EMFILE?
Yes, failure from eventfd() is propagated and we do return STATUS_TOO_MANY_OPEN_FILES. It is not particularly likely that applications are going to handle this gracefully, because it never happens on Windows. In any case, esync is no longer distributed in any project supported in this bug tracker, and I am no longer doing any fixes for it. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
participants (1)
-
WineHQ Bugzilla