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.