[GH-ISSUE #435] [Unraid] Gamepad Input from Applications Leaking into WolfUI #275

Closed
opened 2026-10-15 12:59:11 +00:00 by bsakaguchi · 35 comments
Owner

Originally created by @AbsoZed on GitHub (Jun 16, 2026).
Original GitHub issue: https://github.com/games-on-whales/wolf/issues/435

Hello,

I'm not sure if I am the only one who has noticed/had this issue, but I am using the Unraid Plugin and an Xbox One controller, and have noticed (seemingly randomly) that controller input will additionally control the Wolf UI under whatever application is currently running (generally Steam, for me).

This leads to random applications opening in response to input and closing the current application being used, which is less than ideal. I think I might have identified the issue, and am currently working on a PR for it. Wanted to raise the issue to tie the PR and confirm others are also seeing this issue?

Originally created by @AbsoZed on GitHub (Jun 16, 2026). Original GitHub issue: https://github.com/games-on-whales/wolf/issues/435 Hello, I'm not sure if I am the only one who has noticed/had this issue, but I am using the Unraid Plugin and an Xbox One controller, and have noticed (seemingly randomly) that controller input will additionally control the Wolf UI under whatever application is currently running (generally Steam, for me). This leads to random applications opening in response to input and closing the current application being used, which is less than ideal. I think I might have identified the issue, and am currently working on a PR for it. Wanted to raise the issue to tie the PR and confirm others are also seeing this issue?
bsakaguchi 2026-10-15 12:59:11 +00:00
Author
Owner

@thewave1408 commented on GitHub (Jun 16, 2026):

I also have this issue using the Unraid plugin.

<!-- gh-comment-id:4714073345 --> @thewave1408 commented on GitHub (Jun 16, 2026): I also have this issue using the Unraid plugin.
Author
Owner

@AbsoZed commented on GitHub (Jun 16, 2026):

Cause appears to be that joypads are inputtino uinput devices created on the host.

On JoinLobbyEvent we fire PlugDeviceEvent to the lobby and UnplugDeviceEvent to Wolf-UI's container, but the Wolf-UI consumer (Godot) has already opened /dev/input/eventN directly. fake-udev injects a REMOVE on the container's netlink and the node is rm'd, but Godot's open fd stays valid and keeps delivering events; so input lands in both containers simultaneously.

Fix that appears to be working for me on a new build is on lobby transition, destroy and recreate the inputtino joypad. UI_DEV_DESTROY kills the host eventN, Godot's read EIOs and the device is dropped; a fresh uinput is created and plugged into the lobby in the same call so Moonlight input continues seamlessly.

Unfortunately, this means that if the session is resumed from a pause, the input works nowhere, requiring a restart of Wolf-UI as well as the target application. Working to resolve this issue before commit as well, but it's still miles more usable this way than with the leaky input.

<!-- gh-comment-id:4718423356 --> @AbsoZed commented on GitHub (Jun 16, 2026): Cause appears to be that joypads are `inputtino` uinput devices created on the host. On `JoinLobbyEvent` we fire `PlugDeviceEvent` to the lobby and `UnplugDeviceEvent` to Wolf-UI's container, but the Wolf-UI consumer (Godot) has already opened `/dev/input/eventN` directly. `fake-udev` injects a REMOVE on the container's netlink and the node is `rm`'d, but Godot's open fd stays valid and keeps delivering events; so input lands in both containers simultaneously. Fix that appears to be working for me on a new build is on lobby transition, destroy and recreate the inputtino joypad. `UI_DEV_DESTROY` kills the host eventN, Godot's read EIOs and the device is dropped; a fresh uinput is created and plugged into the lobby in the same call so Moonlight input continues seamlessly. Unfortunately, this means that if the session is resumed from a pause, the input works nowhere, requiring a restart of Wolf-UI as well as the target application. Working to resolve this issue before commit as well, but it's still miles more usable this way than with the leaky input.
Author
Owner

@ABeltramo commented on GitHub (Jun 16, 2026):

Most likely something wrong in the LXC2Docker layer, input isolation between apps currently works when running on Docker.

<!-- gh-comment-id:4718507509 --> @ABeltramo commented on GitHub (Jun 16, 2026): Most likely something wrong in the [LXC2Docker](https://github.com/games-on-whales/LXC2Docker) layer, input isolation between apps currently works when running on Docker.
Author
Owner

@JBailes commented on GitHub (Jun 16, 2026):

Unraid doesn't use LXC2Docker, Abe.

<!-- gh-comment-id:4718577043 --> @JBailes commented on GitHub (Jun 16, 2026): Unraid doesn't use LXC2Docker, Abe.
Author
Owner

@ABeltramo commented on GitHub (Jun 16, 2026):

And that's why I shouldn't reply when on my phone 😅

<!-- gh-comment-id:4718685730 --> @ABeltramo commented on GitHub (Jun 16, 2026): And that's why I shouldn't reply when on my phone 😅
Author
Owner

@AbsoZed commented on GitHub (Jun 16, 2026):

@ABeltramo, Unraid uses Docker to execute the containers. It's not clear to me (if indeed it's true) why it only appears in the Unraid plugin. The bugs I've identified as causing it appear entirely within Wolf, and should theoretically exist on any implementation. Testing with my half-assed patch apparently shows my theory to be somewhat substantiated on a manual build on my Unraid.

The following files in the Wolf container were affected by the fix:

  • control.cpp
  • input_handler.cpp
  • input_handler.hpp
  • events.hpp
  • lobbies.cpp

I still need to fix the aforementioned issue, but testing is made hard by other, unrelated issues w/ network latency and the Steam container itself, though I'll get it figured out sometime today I think.

<!-- gh-comment-id:4718710718 --> @AbsoZed commented on GitHub (Jun 16, 2026): @ABeltramo, Unraid uses Docker to execute the containers. It's not clear to me (if indeed it's true) why it only appears in the Unraid plugin. The bugs I've identified as causing it appear entirely within Wolf, and should theoretically exist on any implementation. Testing with my half-assed patch apparently shows my theory to be somewhat substantiated on a manual build on my Unraid. The following files in the Wolf container were affected by the fix: - control.cpp - input_handler.cpp - input_handler.hpp - events.hpp - lobbies.cpp I still need to fix the aforementioned issue, but testing is made hard by other, unrelated issues w/ network latency and the Steam container itself, though I'll get it figured out sometime today I think.
Author
Owner

@JBailes commented on GitHub (Jun 16, 2026):

Dug into this. @AbsoZed had it right: the joypad is one host uinput device shared into both containers via mknod + fake-udev. On a lobby switch we rm the node and fire a REMOVE, but Godot in Wolf-UI already has the fd open, and rm'ing a node doesn't close someone else's fd, so it keeps reading. Nothing Docker/Unraid specific about it, same on stock Docker.

The only way to evict a reader from outside is to destroy the device. Fix is in two parts:

  • inputtino games-on-whales/inputtino#39: a recreate_device() that tears the device down (UI_DEV_DESTROY / UHID_DESTROY, which kills the stale fd) and rebuilds it in place, keeping the object and its callbacks so rumble and friends survive. Covers Xbox, Switch and DualSense.
  • wolf #436: route the join/leave joypad hand-offs through it, so the old container's fd dies before the fresh device gets plugged into the new one.

It recreates in place rather than swapping the joypad object out, so it should dodge the resume-from-pause regression you ran into, @AbsoZed. Both untested on hardware so far, would appreciate eyes on the pause/resume path.

<!-- gh-comment-id:4719062310 --> @JBailes commented on GitHub (Jun 16, 2026): Dug into this. @AbsoZed had it right: the joypad is one host uinput device shared into both containers via mknod + fake-udev. On a lobby switch we rm the node and fire a REMOVE, but Godot in Wolf-UI already has the fd open, and rm'ing a node doesn't close someone else's fd, so it keeps reading. Nothing Docker/Unraid specific about it, same on stock Docker. The only way to evict a reader from outside is to destroy the device. Fix is in two parts: - inputtino games-on-whales/inputtino#39: a `recreate_device()` that tears the device down (UI_DEV_DESTROY / UHID_DESTROY, which kills the stale fd) and rebuilds it in place, keeping the object and its callbacks so rumble and friends survive. Covers Xbox, Switch and DualSense. - wolf #436: route the join/leave joypad hand-offs through it, so the old container's fd dies before the fresh device gets plugged into the new one. It recreates in place rather than swapping the joypad object out, so it should dodge the resume-from-pause regression you ran into, @AbsoZed. Both untested on hardware so far, would appreciate eyes on the pause/resume path.
Author
Owner

@AbsoZed commented on GitHub (Jun 16, 2026):

Yeah - looking at what you propose here in these two PRs, @JBailes, it's cleaner than my approach, which was to destroy and recreate the inputtino joypad on every container transition, so the host eventN actually dies (Godot's read EIOs and it drops the device) and a brand-new eventM is created and plugged into the destination container.

The two named PRs will need to be merged to fix the issue properly in lieu of what I have at the moment. I have not independently tested them together to ensure the behavior is resolved as expected, but reviewing them I expect that it would once merged.

<!-- gh-comment-id:4719179302 --> @AbsoZed commented on GitHub (Jun 16, 2026): Yeah - looking at what you propose here in these two PRs, @JBailes, it's cleaner than my approach, which was to destroy and recreate the inputtino joypad on every container transition, so the host eventN actually dies (Godot's read EIOs and it drops the device) and a brand-new eventM is created and plugged into the destination container. The two named PRs will need to be merged to fix the issue properly in lieu of what I have at the moment. I have not independently tested them together to ensure the behavior is resolved as expected, but reviewing them I expect that it would once merged.
Author
Owner

@ABeltramo commented on GitHub (Jun 16, 2026):

Sounds a tad too complex, and it doesn't explain why this isn't deterministic but happening seemingly randomly (as reported above). I'd like to understand better what's the root cause here before introducing all these changes.

Can you guys reproduce this issue reliably? I can't reproduce this on my machine.

<!-- gh-comment-id:4719182462 --> @ABeltramo commented on GitHub (Jun 16, 2026): Sounds a tad too complex, and it doesn't explain why this isn't deterministic but happening seemingly randomly (as reported above). I'd like to understand better what's the root cause here before introducing all these changes. Can you guys reproduce this issue reliably? I can't reproduce this on my machine.
Author
Owner

@AbsoZed commented on GitHub (Jun 16, 2026):

@ABeltramo I can reproduce this issue basically constantly. I wouldn't say it isn't deterministic, but rather that logging from the container perspective is not verbose enough to identify the issue during execution flow.

I'm honestly not sure how, based on code, you aren't able to repro this issue. I feel like this is obvious, so forgive me as I'm sure you already understand this, but you DO have to be using a gamepad and not M/KB for the issue to surface.

<!-- gh-comment-id:4719207701 --> @AbsoZed commented on GitHub (Jun 16, 2026): @ABeltramo I can reproduce this issue basically constantly. I wouldn't say it isn't deterministic, but rather that logging from the container perspective is not verbose enough to identify the issue during execution flow. I'm honestly not sure how, based on code, you _aren't_ able to repro this issue. I feel like this is obvious, so forgive me as I'm sure you already understand this, but you DO have to be using a gamepad and not M/KB for the issue to surface.
Author
Owner

@ABeltramo commented on GitHub (Jun 16, 2026):

Which one is true:

and have noticed (seemingly randomly) that controller input will additionally control the Wolf UI

or

I can reproduce this issue basically constantly

?

As far as I remember this is the first time that this is reported, here and on Discord and I can't reproduce it on my dev machine (yes I'm using a joypad 😅). I'm trying to understand what's the root cause before jumping to writing code.

<!-- gh-comment-id:4719247191 --> @ABeltramo commented on GitHub (Jun 16, 2026): Which one is true: > and have noticed (seemingly randomly) that controller input will additionally control the Wolf UI or > I can reproduce this issue basically constantly ? As far as I remember this is the first time that this is reported, here and on Discord and I can't reproduce it on my dev machine (yes I'm using a joypad 😅). I'm trying to understand what's the root cause before jumping to writing code.
Author
Owner

@AbsoZed commented on GitHub (Jun 16, 2026):

Those two statements are not really mutually exclusive, but I think you should be able to see what we're talking about following these steps:

  1. Wolf running from the published ghcr.io/games-on-whales/wolf:stable image
  2. Pair a controller prior to connecting in Moonlight. Xbox or DualSense are both affected.
  3. Connect Moonlight, Wolf-UI launches. Wait until you see in the Wolf-UI container logs: WolfUI.ControllerMap detected
  4. That line is the signal that Godot has open()'d /dev/input/eventN.
  5. From Wolf-UI, launch a lobby app (Steam Big Picture) Wait until Steam is fully up and focused.
  6. Press DPAD-UP or A on the controller. Steam will respond AND Wolf-UI behind it will also respond.

Should be able to validate it's doing so by following:

  • docker exec -it <wolf-ui-container> bash

While you press buttons in Steam:

  • cat /proc/$(pgrep wolf-ui)/fdinfo/* 2>/dev/null | grep -B1 input
  • ls -l /proc/$(pgrep wolf-ui)/fd | grep input

You should see Godot still has /dev/input/eventN open while you're inside Steam, causing the leak.

<!-- gh-comment-id:4719366540 --> @AbsoZed commented on GitHub (Jun 16, 2026): Those two statements are not really mutually exclusive, but I think you should be able to see what we're talking about following these steps: 1. Wolf running from the published ghcr.io/games-on-whales/wolf:stable image 2. Pair a controller prior to connecting in Moonlight. Xbox or DualSense are both affected. 3. Connect Moonlight, Wolf-UI launches. Wait until you see in the Wolf-UI container logs: WolfUI.ControllerMap <controller name> detected 3. That line is the signal that Godot has open()'d /dev/input/eventN. 4. From Wolf-UI, launch a lobby app (Steam Big Picture) Wait until Steam is fully up and focused. 5. Press DPAD-UP or A on the controller. Steam will respond AND Wolf-UI behind it will also respond. Should be able to validate it's doing so by following: - `docker exec -it <wolf-ui-container> bash` While you press buttons in Steam: - `cat /proc/$(pgrep wolf-ui)/fdinfo/* 2>/dev/null | grep -B1 input` - `ls -l /proc/$(pgrep wolf-ui)/fd | grep input` You should see Godot still has /dev/input/eventN open while you're inside Steam, causing the leak.
Author
Owner

@AbsoZed commented on GitHub (Jun 16, 2026):

In short, based on what I can tell, the root cause is as follows:

The joypad is a single host uinput device shared into both containers by fake-udev mknod. On lobby switch we rm the in-container node and inject a udev REMOVE, but neither closes Godot's already-open file descriptor on the host eventN.

rm only unlinks the directory entry, and Godot's evdev consumer doesn't act on the injected REMOVE. So Godot keeps reading from the same fd while Steam also reads from its own mknod'd entry pointing at the same kernel device. The only signal that reliably evicts a third-party reader is destroying the kernel device itself (UI_DEV_DESTROY / UHID_DESTROY), which is what inputtino's recreate_device() provides.

Issue is not Docker, Unraid, or distro specific that I can tell; same on stock Docker on any host.

<!-- gh-comment-id:4719601433 --> @AbsoZed commented on GitHub (Jun 16, 2026): In short, based on what I can tell, the root cause is as follows: The joypad is a single host uinput device shared into both containers by fake-udev mknod. On lobby switch we rm the in-container node and inject a udev REMOVE, but neither closes Godot's already-open file descriptor on the host eventN. rm only unlinks the directory entry, and Godot's evdev consumer doesn't act on the injected REMOVE. So Godot keeps reading from the same fd while Steam also reads from its own mknod'd entry pointing at the same kernel device. The only signal that reliably evicts a third-party reader is destroying the kernel device itself (UI_DEV_DESTROY / UHID_DESTROY), which is what inputtino's recreate_device() provides. Issue is not Docker, Unraid, or distro specific that I can tell; same on stock Docker on any host.
Author
Owner

@ABeltramo commented on GitHub (Jun 16, 2026):

Just tested out on my machine:

docker exec -it Wolf-UI_5733117292241342039 bash
root@9123a39571c0:~# cat /proc/$(pgrep wolf-ui)/fdinfo/* | grep -B1 input
root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input
lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted)

I've played a short gaming session and Wolf UI doesn't pick any of those events up.
After closing Steam and switching back to Wolf UI I get the following:

root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input
lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted)
lrwx------ 1 root root 64 Jun 16 15:00 11 -> /dev/input/event27
lrwx------ 1 root root 64 Jun 16 15:04 150 -> /dev/input/event25

So the fd is leaking (I guess) but Wolf-UI isn't reading events from it. My guess is that Godot is responding to the fake-udev REMOVE event and stops polling the fd.

I feel that this is either an issue with the setup in Unraid or specifically on your machine, most likely there's something wrong with fake-udev; for example, does Wolf have access to the host /run/udev? I'm going to change the title until someone can confirm that this issue is present on other platforms as well.

<!-- gh-comment-id:4719691103 --> @ABeltramo commented on GitHub (Jun 16, 2026): Just tested out on my machine: ``` docker exec -it Wolf-UI_5733117292241342039 bash root@9123a39571c0:~# cat /proc/$(pgrep wolf-ui)/fdinfo/* | grep -B1 input root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted) ``` I've played a short gaming session and Wolf UI doesn't pick any of those events up. After closing Steam and switching back to Wolf UI I get the following: ``` root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted) lrwx------ 1 root root 64 Jun 16 15:00 11 -> /dev/input/event27 lrwx------ 1 root root 64 Jun 16 15:04 150 -> /dev/input/event25 ``` So the `fd` is leaking (I guess) but Wolf-UI isn't reading events from it. My guess is that Godot is responding to the `fake-udev` REMOVE event and stops polling the `fd`. I feel that this is either an issue with the setup in Unraid or specifically on your machine, most likely there's something wrong with `fake-udev`; for example, does Wolf have access to the host `/run/udev`? I'm going to change the title until someone can confirm that this issue is present on other platforms as well.
Author
Owner

@Skerga commented on GitHub (Jun 16, 2026):

If you want then i can write a small patch that logs the whole controller detection / attaching and detaching to the debug in wolf-ui if that helps finding the issue.

<!-- gh-comment-id:4719767767 --> @Skerga commented on GitHub (Jun 16, 2026): If you want then i can write a small patch that logs the whole controller detection / attaching and detaching to the debug in wolf-ui if that helps finding the issue.
Author
Owner

@JBailes commented on GitHub (Jun 16, 2026):

Just tested out on my machine:

docker exec -it Wolf-UI_5733117292241342039 bash
root@9123a39571c0:~# cat /proc/$(pgrep wolf-ui)/fdinfo/* | grep -B1 input
root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input
lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted)

I've played a short gaming session and Wolf UI doesn't pick any of those events up. After closing Steam and switching back to Wolf UI I get the following:

root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input
lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted)
lrwx------ 1 root root 64 Jun 16 15:00 11 -> /dev/input/event27
lrwx------ 1 root root 64 Jun 16 15:04 150 -> /dev/input/event25

So the fd is leaking (I guess) but Wolf-UI isn't reading events from it. My guess is that Godot is responding to the fake-udev REMOVE event and stops polling the fd.

I feel that this is either an issue with the setup in Unraid or specifically on your machine, most likely there's something wrong with fake-udev; for example, does Wolf have access to the host /run/udev? I'm going to change the title until someone can confirm that this issue is present on other platforms as well.

We've gotten other reports of this from outside of Unraid, Abe, and I've had it before myself.

I don't agree with this summation. I've put some more thought into this, and my 2c are if fd is leaking, what's the harm in fixing it?

<!-- gh-comment-id:4720254601 --> @JBailes commented on GitHub (Jun 16, 2026): > Just tested out on my machine: > > ``` > docker exec -it Wolf-UI_5733117292241342039 bash > root@9123a39571c0:~# cat /proc/$(pgrep wolf-ui)/fdinfo/* | grep -B1 input > root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input > lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted) > ``` > > I've played a short gaming session and Wolf UI doesn't pick any of those events up. After closing Steam and switching back to Wolf UI I get the following: > > ``` > root@9123a39571c0:~# ls -l /proc/$(pgrep wolf-ui)/fd | grep input > lrwx------ 1 root root 64 Jun 16 15:00 10 -> /dev/input/event27 (deleted) > lrwx------ 1 root root 64 Jun 16 15:00 11 -> /dev/input/event27 > lrwx------ 1 root root 64 Jun 16 15:04 150 -> /dev/input/event25 > ``` > > So the `fd` is leaking (I guess) but Wolf-UI isn't reading events from it. My guess is that Godot is responding to the `fake-udev` REMOVE event and stops polling the `fd`. > > I feel that this is either an issue with the setup in Unraid or specifically on your machine, most likely there's something wrong with `fake-udev`; for example, does Wolf have access to the host `/run/udev`? I'm going to change the title until someone can confirm that this issue is present on other platforms as well. We've gotten other reports of this from outside of Unraid, Abe, and I've had it before myself. I don't agree with this summation. I've put some more thought into this, and my 2c are if fd is leaking, what's the harm in fixing it?
Author
Owner

@ABeltramo commented on GitHub (Jun 16, 2026):

Can you reproduce the issue @JBailes ? You had it before but you don't have it now? I'd like to understand the root cause before jumping to writing code. What if this is something that should be fixed in Wolf UI/Godot instead?

Claude was very confident that this is a problem until I showed that the erratic behaviour isn't present on my machine. Let's take a moment to fully understand what's the issue here instead of jumping to conclusions.

<!-- gh-comment-id:4720308435 --> @ABeltramo commented on GitHub (Jun 16, 2026): Can you reproduce the issue @JBailes ? You had it before but you don't have it now? I'd like to understand the root cause before jumping to writing code. What if this is something that should be fixed in Wolf UI/Godot instead? Claude was very confident that this is a problem until I showed that the erratic behaviour isn't present on my machine. Let's take a moment to fully understand what's the issue here instead of jumping to conclusions.
Author
Owner

@JBailes commented on GitHub (Jun 16, 2026):

@ABeltramo took this to a real box on stock Docker and tested both mechanisms directly.

  1. Unplug today is rm node + REMOVE uevent. Made a uinput device, opened eventN from a second fd, rm'd the node: reader keeps getting every event. UI_DEV_DESTROY on the same device: poll() returns POLLHUP, read() returns ENODEV, input stops dead. rm can't evict a reader, destroy can. That's the whole basis for #436.
  2. On your delivery theory, I ran the fake-udev REMOVE against Godot's exact path (udev_monitor_new_from_netlink(udev,"udev") + subsystem=input filter + udev_monitor_receive_device), with Wolf-UI's own libudev 1.7.10:

RECEIVED action=remove devnode=/dev/input/event99 sysname=event99

Arrives fine. The REMOVE isn't getting lost.

Gotcha if you retest: udevadm monitor --udev shows nothing for fake-udev on recent distros (new udevadm uses sd-device, not the libudev API Godot calls). Raw netlink and libudev both see it. Easy to misread as "REMOVE never arrives".

So agreed, it's not a missing event. Your fd was (deleted) but unpolled, so Godot did drop it on your box. Smells like a race in Godot's monitor thread, stops polling on one machine, keeps polling on another, which fits the random reports and why you can't repro. I won't pin the Godot side down fast.

destroy/recreate kills the leak regardless of the race, and it's a clean ENODEV any reader handles. I'd take it now and chase Godot separately rather than leave input bleeding into the UI.

<!-- gh-comment-id:4720501941 --> @JBailes commented on GitHub (Jun 16, 2026): @ABeltramo took this to a real box on stock Docker and tested both mechanisms directly. 1. Unplug today is rm node + REMOVE uevent. Made a uinput device, opened eventN from a second fd, rm'd the node: reader keeps getting every event. UI_DEV_DESTROY on the same device: poll() returns POLLHUP, read() returns ENODEV, input stops dead. rm can't evict a reader, destroy can. That's the whole basis for #436. 2. On your delivery theory, I ran the fake-udev REMOVE against Godot's exact path (udev_monitor_new_from_netlink(udev,"udev") + subsystem=input filter + udev_monitor_receive_device), with Wolf-UI's own libudev 1.7.10: RECEIVED action=remove devnode=/dev/input/event99 sysname=event99 Arrives fine. The REMOVE isn't getting lost. Gotcha if you retest: udevadm monitor --udev shows nothing for fake-udev on recent distros (new udevadm uses sd-device, not the libudev API Godot calls). Raw netlink and libudev both see it. Easy to misread as "REMOVE never arrives". So agreed, it's not a missing event. Your fd was (deleted) but unpolled, so Godot did drop it on your box. Smells like a race in Godot's monitor thread, stops polling on one machine, keeps polling on another, which fits the random reports and why you can't repro. I won't pin the Godot side down fast. destroy/recreate kills the leak regardless of the race, and it's a clean ENODEV any reader handles. I'd take it now and chase Godot separately rather than leave input bleeding into the UI.
Author
Owner

@ABeltramo commented on GitHub (Jun 16, 2026):

I'm not convinced, I've never experienced this and we don't get reports about this either; it should be fairly a fairly common issue since everyone is using WolfUI. I'm still not sure if you can actually replicate the original issue when playing during a Moonlight streaming session. And if you can how reliably you can replicate it.

If rm + REMOVE is not enough I'd like to explore alternatives that we can run in the container when we call fake-udev instead of exposing this behaviour in both inputtino and Wolf.

<!-- gh-comment-id:4721134950 --> @ABeltramo commented on GitHub (Jun 16, 2026): I'm not convinced, I've never experienced this and we don't get reports about this either; it should be fairly a fairly common issue since everyone is using WolfUI. I'm still not sure if you can actually replicate the original issue when playing during a Moonlight streaming session. And if you can how reliably you can replicate it. If `rm` + `REMOVE` is not enough I'd like to explore alternatives that we can run in the container when we call fake-udev instead of exposing this behaviour in both inputtino and Wolf.
Author
Owner

@AbsoZed commented on GitHub (Jun 16, 2026):

It seems a little disingenuous to say "we don't get reports" when there are two distinct users reporting it in this thread.

<!-- gh-comment-id:4721248184 --> @AbsoZed commented on GitHub (Jun 16, 2026): It seems a little disingenuous to say "we don't get reports" when there are two distinct users reporting it in this thread.
Author
Owner

@CocoShun commented on GitHub (Jun 18, 2026):

Had some issues that might be related by this, I'm using the unraid plugin too. I had encounter few times during steam gameplay that some other applications from wolfUI got unitentionally opened too. Never thought is being an input leak problem before though.

<!-- gh-comment-id:4737975886 --> @CocoShun commented on GitHub (Jun 18, 2026): Had some issues that might be related by this, I'm using the unraid plugin too. I had encounter few times during steam gameplay that some other applications from wolfUI got unitentionally opened too. Never thought is being an input leak problem before though.
Author
Owner

@thewave1408 commented on GitHub (Jun 18, 2026):

Had some issues that might be related by this, I'm using the unraid plugin too. I had encounter few times during steam gameplay that some other applications from wolfUI got unitentionally opened too. Never thought is being an input leak problem before though.

This is most likely the same issue

<!-- gh-comment-id:4740691770 --> @thewave1408 commented on GitHub (Jun 18, 2026): > Had some issues that might be related by this, I'm using the unraid plugin too. I had encounter few times during steam gameplay that some other applications from wolfUI got unitentionally opened too. Never thought is being an input leak problem before though. This is most likely the same issue
Author
Owner

@AbsoZed commented on GitHub (Jun 18, 2026):

Agree @CocoShun, your issue is the same. That is how this problem manifests for me as well. Navigating the UI of one app causes that of another application to open, because the input leaks to Wolf-UI.

For search purposes, I'll add "Apps opening unexpectedly on Unraid Plugin in Games on Whales" to this comment since that appears to be the primary symptom.

<!-- gh-comment-id:4742020014 --> @AbsoZed commented on GitHub (Jun 18, 2026): Agree @CocoShun, your issue is the same. That is how this problem manifests for me as well. Navigating the UI of one app causes that of another application to open, because the input leaks to Wolf-UI. For search purposes, I'll add "Apps opening unexpectedly on Unraid Plugin in Games on Whales" to this comment since that appears to be the primary symptom.
Author
Owner

@shoesio commented on GitHub (Jun 18, 2026):

I have a very similar issue under SteamOS on my deck - after forcing a pathway through so the controller works, it then controls the main ui on my deck as well as the container's contents via moonlight.

Wolf X-Box One virtual pad appears as event11/js1. udev assigns ID_SEAT=seat9 and uaccess is removed, but host Steam/Desktop still receives input.

<!-- gh-comment-id:4747051893 --> @shoesio commented on GitHub (Jun 18, 2026): I have a very similar issue under SteamOS on my deck - after forcing a pathway through so the controller works, it then controls the main ui on my deck as well as the container's contents via moonlight. Wolf X-Box One virtual pad appears as event11/js1. udev assigns ID_SEAT=seat9 and uaccess is removed, but host Steam/Desktop still receives input.
Author
Owner

@ABeltramo commented on GitHub (Jun 19, 2026):

@shoesio your issue sounds unrelated to this one, you've installed Wolf on SteamOS and the Moonlight joypad is taking over the host, right?

If that's the case, please open a new issue with the full details of the installation especially about "forcing a pathway through so the controller works". Sounds like you've messed up with the udev rules..

<!-- gh-comment-id:4749602204 --> @ABeltramo commented on GitHub (Jun 19, 2026): @shoesio your issue sounds unrelated to this one, you've installed Wolf on SteamOS and the Moonlight joypad is taking over the host, right? If that's the case, please open a new issue with the full details of the installation especially about "forcing a pathway through so the controller works". Sounds like you've messed up with the udev rules..
Author
Owner

@AbsoZed commented on GitHub (Jun 19, 2026):

@ABeltramo @JBailes I believe I have found the source of the issue after additional testing, and I think it's far simpler than I anticipated. While the issue with gamepad input "leaking" does exist, the current architecture does ensure that it's non impactful.

However, on the Unraid plugin, based on my testing with a fresh installation, fake-udev is not getting mounted into the WolfUI container, meaning that the command docker exec wolf-ui /bin/bash -c "fake-udev -m <msg> && rm /dev/input/eventN" that's run on every UnplugDeviceEvent never executes the rm because the return code for fake-udev execution is non-zero.

I need to dig in to why the bind mount isn't working, but don't have time just at the moment. I don't expect this is specific to my configuration, but rather, something with Unraid itself; making this a far simpler problem than initially suspected. The bind mount is THERE in the form of /usr/bin/fake-udev <-> /mnt/user/appdata/gow/fake-udev but ls /usr/bin/fake-udev and which fake-udev show that it's not in the container at all.

Of note, I also tried this on an ArchLinux box and it worked precisely as expected, fake-udev existed in the container, and all expected events were received for detach via monitoring with udevadm monitor, so whatever is going on is specific to Unraid as far as I can tell.

@thewave1408 @CocoShun - can you spin up a Wolf-UI and try to which fake-udev in the container to confirm this please?

<!-- gh-comment-id:4751808838 --> @AbsoZed commented on GitHub (Jun 19, 2026): @ABeltramo @JBailes I believe I have found the source of the issue after additional testing, and I think it's far simpler than I anticipated. While the issue with gamepad input "leaking" does exist, the current architecture does ensure that it's non impactful. However, on the Unraid plugin, based on my testing with a fresh installation, `fake-udev` is not getting mounted into the WolfUI container, meaning that the command `docker exec wolf-ui /bin/bash -c "fake-udev -m <msg> && rm /dev/input/eventN"` that's run on every `UnplugDeviceEvent` never executes the `rm` because the return code for `fake-udev` execution is non-zero. I need to dig in to why the bind mount isn't working, but don't have time just at the moment. I don't expect this is specific to my configuration, but rather, something with Unraid itself; making this a far simpler problem than initially suspected. The bind mount is THERE in the form of `/usr/bin/fake-udev <-> /mnt/user/appdata/gow/fake-udev` but `ls /usr/bin/fake-udev` and `which fake-udev` show that it's not in the container at all. Of note, I also tried this on an ArchLinux box and it worked precisely as expected, `fake-udev` existed in the container, and all expected events were received for detach via monitoring with `udevadm monitor`, so whatever is going on is specific to Unraid as far as I can tell. @thewave1408 @CocoShun - can you spin up a Wolf-UI and try to `which fake-udev` in the container to confirm this please?
Author
Owner

@ABeltramo commented on GitHub (Jun 19, 2026):

@AbsoZed thanks for confirming my initial suspicion, it should be a simple fix then. 😉

Have you checked the logs? There should be a warning when docker exec fails
github.com/games-on-whales/wolf@2f790f6651/src/core/src/platforms/all/docker/docker/docker.cpp (L499-L508)

which should at least confirm this issue.

Actually, if you could post the full Wolf output it should give some clues as to why fake-udev isn't being mounted. Possibly with the env variable WOLF_LOG_LEVEL=DEBUG (assuming you can tweak that in Unraid)

<!-- gh-comment-id:4751945100 --> @ABeltramo commented on GitHub (Jun 19, 2026): @AbsoZed thanks for confirming my initial suspicion, it should be a simple fix then. 😉 Have you checked the logs? There should be a warning when `docker exec` fails https://github.com/games-on-whales/wolf/blob/2f790f665134a47a9a49a7a5fa3eb111c802a5ed/src/core/src/platforms/all/docker/docker/docker.cpp#L499-L508 which should at least confirm this issue. Actually, if you could post the full Wolf output it should give some clues as to why `fake-udev` isn't being mounted. Possibly with the env variable `WOLF_LOG_LEVEL=DEBUG` (assuming you can tweak that in Unraid)
Author
Owner

@AbsoZed commented on GitHub (Jun 19, 2026):

I'm still digging right now, I'll update in a bit with something more substantial. The mount IS making it in, as it turns out - but what's happening is that the directory itself is being mounted, not the binary. So,

/mnt/user/gow/fake-udev/ (dir) <-> /usr/bin/fake-udev/ (dir) is being mounted; resulting in fake-udev not being in $PATH,

At least from what I'm seeing now. I'll post again in a little bit with some substantial detail and logging (and hopefully, 100% confirmed root cause), but this feels imminently and easily solvable if we're on the right track here.

Edit: You're right about warnings.

12:57:27.503189227 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

12:57:27.540985456 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

12:57:27.579834194 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

12:57:27.617086787 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.016060005 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.044854180 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.078943297 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.114632228 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.150060754 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.186728309 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.221560164 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.248007208 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.280854965 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.312048340 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.344715134 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.374992671 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.411020741 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.441276904 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.477795880 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.511178722 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.545900210 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.577572687 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.603172445 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.635952116 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.665991601 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.697733568 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.718098063 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.746778639 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.779008406 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.807968028 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.836204137 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.865099546 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.892392601 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found

13:08:11.925875584 WARN  | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found
<!-- gh-comment-id:4751983492 --> @AbsoZed commented on GitHub (Jun 19, 2026): I'm still digging right now, I'll update in a bit with something more substantial. The mount IS making it in, as it turns out - but what's happening is that the directory itself is being mounted, not the binary. So, `/mnt/user/gow/fake-udev/` (dir) <-> `/usr/bin/fake-udev/` (dir) is being mounted; resulting in `fake-udev` not being in `$PATH`, At least from what I'm seeing now. I'll post again in a little bit with some substantial detail and logging (and hopefully, 100% confirmed root cause), but this feels imminently and easily solvable if we're on the right track here. Edit: You're right about warnings. ``` 12:57:27.503189227 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 12:57:27.540985456 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 12:57:27.579834194 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 12:57:27.617086787 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.016060005 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.044854180 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.078943297 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.114632228 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.150060754 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.186728309 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.221560164 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.248007208 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.280854965 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.312048340 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.344715134 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.374992671 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.411020741 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.441276904 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.477795880 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.511178722 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.545900210 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.577572687 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.603172445 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.635952116 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.665991601 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.697733568 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.718098063 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.746778639 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.779008406 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.807968028 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.836204137 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.865099546 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.892392601 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found 13:08:11.925875584 WARN | Docker exec failed (127), 0/bin/bash: line 1: fake-udev: command not found ```
Author
Owner

@ABeltramo commented on GitHub (Jun 19, 2026):

The way we mount fake-udev is a bit tricky, but the idea is that on container startup we copy it from the binary bundled in Wolf to $HOST_APPS_STATE_FOLDER

github.com/games-on-whales/wolf@2f790f6651/docker/startup.sh (L21-L22)

And then we mount that path here

github.com/games-on-whales/wolf@2f790f6651/src/moonlight-server/sessions/common.cpp (L69-L73)

Obviously this assumes that HOST_APPS_STATE_FOLDER (defaults to /etc/wolf) is available and mounted in Wolf. I guess the Unraid deployment changed that in a way that it's breaking this assumption..

<!-- gh-comment-id:4752036616 --> @ABeltramo commented on GitHub (Jun 19, 2026): The way we mount `fake-udev` is a bit tricky, but the idea is that on container startup we copy it from the binary bundled in Wolf to `$HOST_APPS_STATE_FOLDER` https://github.com/games-on-whales/wolf/blob/2f790f665134a47a9a49a7a5fa3eb111c802a5ed/docker/startup.sh#L21-L22 And then we mount that path here https://github.com/games-on-whales/wolf/blob/2f790f665134a47a9a49a7a5fa3eb111c802a5ed/src/moonlight-server/sessions/common.cpp#L69-L73 Obviously this assumes that `HOST_APPS_STATE_FOLDER` (defaults to `/etc/wolf`) is available and mounted in Wolf. I guess the Unraid deployment changed that in a way that it's breaking this assumption..
Author
Owner

@AbsoZed commented on GitHub (Jun 19, 2026):

So... I think this is a really esoteric bug of incredible coincidence. I first noticed the issue on May 30 or May 31 as I recall.

A commit on May 30 from the Unraid-Plugin Repository:

2026.05.30

  • Set HOST_APPS_STATE_FOLDER to the selected appdata path so Wolf bind-mounts spawned app containers from the chosen appdata directory instead of host /etc/wolf

That is commit c861cd1 (PR #46) in that repository. Before it, HOST_APPS_STATE_FOLDER was either unset or pointed at /etc/wolf, so Wolf (the process) told Docker to bind-mount source /etc/wolf/fake-udev into spawned containers, and /etc/wolf actually existed inside the Wolf container thanks to the May 25 change (${APPDATA}:/etc/wolf:rw).

So startup.sh's cp reached the right host file, the bind mount sources resolved to real files, and everything worked.

On May 30, the plugin changed HOST_APPS_STATE_FOLDER to ${APPDATA} (e.g. /mnt/user/appdata/gow); but did not also change the in-container mount point. Inside the Wolf container, the appdata was still mounted at /etc/wolf, not at /mnt/user/appdata/gow.

So, as a result:

  1. startup.sh ran mkdir -p $HOST_APPS_STATE_FOLDER/cfg = mkdir -p /mnt/user/appdata/gow/cfg inside the container. Since /mnt/user/appdata/gow wasn't a mount, this just created the path in the container's overlay layer.

  2. cp /wolf/fake-udev /mnt/user/appdata/gow/fake-udev, same overlay.

  3. Wolf-the-process started up. app_state->host->host_base_state_folder = /mnt/user/appdata/gow. When it spawned Wolf-UI, it called Docker's create API with bind-mount source /mnt/user/appdata/gow/fake-udev.

  4. Host's /mnt/user/appdata/gow/fake-udev did not exist. Docker daemon's bind-mount-source-doesn't-exist behavior: auto-create as a directory.

  5. Every subsequent Wolf restart re-wrote the binary into the ephemeral overlay, never touching the host directory. Every app container Wolf spawned got /usr/bin/fake-udev as an empty directory.

So the trigger was the May 30 change, and the bug remained latent until I hit the leak symptom (which depends on Godot opening the controller fd before you transition into a lobby).

PR #56 on June 14 fixed the identity mount for unrelated reasons (config persistence), and as a side effect startup.sh's cp started reaching the host. But by then the host had a directory there for two weeks, so cp file dir/ → dir/file. Now you have a directory containing the binary. Bind mount still mounts a directory at /usr/bin/fake-udev.

The two-week window between May 30 and June 14, and the latent persistence of the auto-created directory afterwards appears to me to be the entire causative chain.

There is a precise identifiable population who would be affected by this: anyone who deployed via the GoW Unraid plugin between 2026-05-30 (PR #46) and 2026-06-14 (PR #56), and hasn't manually deleted ${APPDATA}/fake-udev since. The cleanup is a relatively trivial PR to the Unraid-Plugin repository:

  # In scripts/deploy.sh, in setup_appdata_dirs() or a new function called before it:
  if [ -d "${APPDATA}/fake-udev" ]; then
      info "Removing legacy fake-udev directory left over from pre-PR-#56 install (#46/#56 migration cleanup)"
      rm -rf "${APPDATA}/fake-udev"
  fi

I do still believe it to be worthwhile to address the fd remaining open in Wolf-UI, though perhaps with a little less urgency. It's not the root cause, and functionally speaking, it does continue to work as far as I can tell regardless of the "leak".

Someone please double-check me here, but I think this is what has happened, and it aligns well with my timeline. I've uninstalled/reinstalled so many times I can't stat my directories to confirm the dates at this point 😅

<!-- gh-comment-id:4752556168 --> @AbsoZed commented on GitHub (Jun 19, 2026): So... I think this is a really esoteric bug of incredible coincidence. I first noticed the issue on May 30 or May 31 as I recall. A commit on May 30 from the Unraid-Plugin Repository: > 2026.05.30 > - Set HOST_APPS_STATE_FOLDER to the selected appdata path so Wolf bind-mounts spawned app containers from the chosen appdata directory instead of host /etc/wolf That is commit c861cd1 (PR #46) in that repository. Before it, `HOST_APPS_STATE_FOLDER` was either unset or pointed at `/etc/wolf`, so Wolf (the process) told Docker to bind-mount source `/etc/wolf/fake-udev` into spawned containers, and `/etc/wolf` actually existed inside the Wolf container thanks to the May 25 change (`${APPDATA}:/etc/wolf:rw`). So startup.sh's cp reached the right host file, the bind mount sources resolved to real files, and everything worked. On May 30, the plugin changed `HOST_APPS_STATE_FOLDER` to `${APPDATA}` (e.g. /mnt/user/appdata/gow); but did not also change the in-container mount point. Inside the Wolf container, the appdata was still mounted at `/etc/wolf`, not at `/mnt/user/appdata/gow`. So, as a result: 1. startup.sh ran `mkdir -p $HOST_APPS_STATE_FOLDER/cfg = mkdir -p /mnt/user/appdata/gow/cfg` inside the container. Since `/mnt/user/appdata/gow` wasn't a mount, this just created the path in the container's overlay layer. 2. `cp /wolf/fake-udev /mnt/user/appdata/gow/fake-udev`, same overlay. 3. Wolf-the-process started up. app_state->host->host_base_state_folder = `/mnt/user/appdata/gow`. When it spawned Wolf-UI, it called Docker's create API with bind-mount source `/mnt/user/appdata/gow/fake-udev`. 4. Host's /mnt/user/appdata/gow/fake-udev did not exist. Docker daemon's bind-mount-source-doesn't-exist behavior: auto-create as a directory. 5. Every subsequent Wolf restart re-wrote the binary into the ephemeral overlay, never touching the host directory. Every app container Wolf spawned got /usr/bin/fake-udev as an empty directory. So the trigger was the May 30 change, and the bug remained latent until I hit the leak symptom (which depends on Godot opening the controller fd before you transition into a lobby). PR #56 on June 14 fixed the identity mount for unrelated reasons (config persistence), and as a side effect startup.sh's cp started reaching the host. But by then the host had a directory there for two weeks, so cp file dir/ → dir/file. Now you have a directory containing the binary. Bind mount still mounts a directory at `/usr/bin/fake-udev`. The two-week window between May 30 and June 14, and the latent persistence of the auto-created directory afterwards appears to me to be the entire causative chain. There is a precise identifiable population who would be affected by this: anyone who deployed via the GoW Unraid plugin between 2026-05-30 (PR #46) and 2026-06-14 (PR #56), and hasn't manually deleted ${APPDATA}/fake-udev since. The cleanup is a relatively trivial PR to the Unraid-Plugin repository: ``` # In scripts/deploy.sh, in setup_appdata_dirs() or a new function called before it: if [ -d "${APPDATA}/fake-udev" ]; then info "Removing legacy fake-udev directory left over from pre-PR-#56 install (#46/#56 migration cleanup)" rm -rf "${APPDATA}/fake-udev" fi ``` I do still believe it to be worthwhile to address the `fd` remaining open in Wolf-UI, though perhaps with a little less urgency. It's not the root cause, and functionally speaking, it does continue to work as far as I can tell regardless of the "leak". Someone please double-check me here, but I think this is what has happened, and it aligns well with my timeline. I've uninstalled/reinstalled so many times I can't `stat` my directories to confirm the dates at this point 😅
Author
Owner

@CocoShun commented on GitHub (Jun 19, 2026):

@CocoShun - can you spin up a Wolf-UI and try to which fake-udev in the container to confirm this please?

@AbsoZed I've tested "which fake-udev" inside the wolfUI container, it gives no output.

<!-- gh-comment-id:4752560441 --> @CocoShun commented on GitHub (Jun 19, 2026): >[@CocoShun](https://github.com/CocoShun) - can you spin up a Wolf-UI and try to `which fake-udev` in the container to confirm this please? @AbsoZed I've tested "which fake-udev" inside the wolfUI container, it gives no output.
Author
Owner

@AbsoZed commented on GitHub (Jun 19, 2026):

Thank you @CocoShun.

For anyone affected by this, you should be able to fix it quickly without waiting on a code fix by doing the following, tested and working on my Unraid server:

  1. rm -rf /mnt/user/appdata/gow/fake-udev
  2. Navigate to Plugins > Games on Whales
  3. Stop and Start Games on Whales using the plugin controls.

This will remove the directory, create a new fake-udev binary instead of directory. Subsequent mounts into containers will work perfectly, and you can confirm by docker exec <wolf-ui ID> /bin/bash and then which fake-udev, which will now return properly.

I believe this issue can be closed as resolved, with a subsequent issue and PR in the Unraid-Plugin repository. I'll await confirmation from @CocoShun or @thewave1408 that this solves their issue as well before closing.

<!-- gh-comment-id:4752624474 --> @AbsoZed commented on GitHub (Jun 19, 2026): Thank you @CocoShun. For anyone affected by this, you should be able to fix it quickly without waiting on a code fix by doing the following, tested and working on my Unraid server: 1. `rm -rf /mnt/user/appdata/gow/fake-udev` 2. Navigate to Plugins > Games on Whales 3. Stop and Start Games on Whales using the plugin controls. This will remove the directory, create a new `fake-udev` binary instead of directory. Subsequent mounts into containers will work perfectly, and you can confirm by `docker exec <wolf-ui ID> /bin/bash` and then `which fake-udev`, which will now return properly. I believe this issue can be closed as resolved, with a subsequent issue and PR in the Unraid-Plugin repository. I'll await confirmation from @CocoShun or @thewave1408 that this solves their issue as well before closing.
Author
Owner

@CocoShun commented on GitHub (Jun 19, 2026):

I believe this issue can be closed as resolved, with a subsequent issue and PR in the Unraid-Plugin repository. I'll await confirmation from @CocoShun or @thewave1408 that this solves their issue as well before closing.

Thank you for the quick fix @AbsoZed! I followed your guide and now it returns "/usr/bin/fake-udev" inside the wolfUI container, I guess that means problem solved?

<!-- gh-comment-id:4753715857 --> @CocoShun commented on GitHub (Jun 19, 2026): > I believe this issue can be closed as resolved, with a subsequent issue and PR in the Unraid-Plugin repository. I'll await confirmation from [@CocoShun](https://github.com/CocoShun) or [@thewave1408](https://github.com/thewave1408) that this solves their issue as well before closing. Thank you for the quick fix @AbsoZed! I followed your guide and now it returns "/usr/bin/fake-udev" inside the wolfUI container, I guess that means problem solved?
Author
Owner

@AbsoZed commented on GitHub (Jun 19, 2026):

It should! Give it a shot, play a game and see if it works for you as well. Does for me.

I'm going to go ahead and mark this issue resolved. Thanks for the contributions, all.

<!-- gh-comment-id:4755473990 --> @AbsoZed commented on GitHub (Jun 19, 2026): It should! Give it a shot, play a game and see if it works for you as well. Does for me. I'm going to go ahead and mark this issue resolved. Thanks for the contributions, all.
Author
Owner

@kmreisi commented on GitHub (Jun 26, 2026):

Hi all,

A late data point from my local playground, in case it's useful down the line — I ended up at the same "no device-destroy needed" conclusion. (I leaned on Claude to dig into this further, so treat the mechanism claims as worth a sanity-check.)

I ran into a close relative of this bug — a controller not being picked up again after a Moonlight reconnect, rather than the leak — and spent a while on the recreate_device() question. What it came down to for me is a single point: whether the udev REMOVE actually reaches the reader inside the container. Every case in this thread seems to fit that:

  • Unraid reports: it didn't arrive — fake-udev was the broken-directory mount, so fake-udev -m … && rm eventN exited 127 and the rm/REMOVE never ran. The reader keeps its fd, so input leaks.
  • @ABeltramo's machine: it did arrive — you saw Godot's fd go (deleted) but unpolled, which is the reader dropping the device on the REMOVE by itself. No leak, and nothing to reproduce.

When the REMOVE is delivered, udev-aware readers release the device on their own: gilrs does epoll.delete(fd) + disconnect() on HotplugEvent::Removed, and SDL/Godot watch udev the same way. So recreate_device() mainly compensates for a REMOVE that never showed up. I removed it and now just do unplug(source) → plug(target) — the REMOVE evicts the old reader, the ADD attaches the new one, and the device plus its rumble callbacks survive. That matches @ABeltramo's sense that destroy/recreate was heavier than the problem.

There's one more way the REMOVE can quietly go missing, separate from the Unraid cause (stock apps already have the cap, so it's not what bit those reporters): fake-udev needs CAP_NET_ADMIN to broadcast, and without it the send fails with EPERM even as root. A custom app, or a base_create_json that drops NET_ADMIN, would break hotplug with no obvious signal.

If it's useful, I'm happy to open a small PR that adds the cap automatically whenever wolf runs fake-udev (so it can't be lost by accident), and/or makes the exec-failure log say why it failed — that one line would have pointed straight at both this and the Unraid mount.

One repro note: on recent distros udevadm monitor --udev shows nothing for fake-udev, because newer udevadm uses sd-device rather than the libudev API the apps call. Raw netlink and libudev both still see the event, so it's easy to misread as "REMOVE never arrives."

Thanks
Karim

<!-- gh-comment-id:4808832432 --> @kmreisi commented on GitHub (Jun 26, 2026): Hi all, A late data point from my local playground, in case it's useful down the line — I ended up at the same "no device-destroy needed" conclusion. (I leaned on Claude to dig into this further, so treat the mechanism claims as worth a sanity-check.) I ran into a close relative of this bug — a controller not being picked up again after a Moonlight *reconnect*, rather than the leak — and spent a while on the `recreate_device()` question. What it came down to for me is a single point: **whether the udev `REMOVE` actually reaches the reader inside the container.** Every case in this thread seems to fit that: - **Unraid reports:** it didn't arrive — `fake-udev` was the broken-directory mount, so `fake-udev -m … && rm eventN` exited 127 and the `rm`/REMOVE never ran. The reader keeps its fd, so input leaks. - **@ABeltramo's machine:** it did arrive — you saw Godot's fd go `(deleted)` but unpolled, which is the reader dropping the device on the REMOVE by itself. No leak, and nothing to reproduce. When the REMOVE is delivered, udev-aware readers release the device on their own: gilrs does `epoll.delete(fd)` + `disconnect()` on `HotplugEvent::Removed`, and SDL/Godot watch udev the same way. So `recreate_device()` mainly compensates for a REMOVE that never showed up. I removed it and now just do `unplug(source) → plug(target)` — the REMOVE evicts the old reader, the ADD attaches the new one, and the device plus its rumble callbacks survive. That matches @ABeltramo's sense that destroy/recreate was heavier than the problem. There's one more way the REMOVE can quietly go missing, separate from the Unraid cause (stock apps already have the cap, so it's not what bit those reporters): `fake-udev` needs **`CAP_NET_ADMIN`** to broadcast, and without it the send fails with `EPERM` even as root. A custom app, or a `base_create_json` that drops `NET_ADMIN`, would break hotplug with no obvious signal. If it's useful, I'm happy to open a small PR that adds the cap automatically whenever wolf runs `fake-udev` (so it can't be lost by accident), and/or makes the exec-failure log say *why* it failed — that one line would have pointed straight at both this and the Unraid mount. One repro note: on recent distros `udevadm monitor --udev` shows nothing for fake-udev, because newer udevadm uses sd-device rather than the libudev API the apps call. Raw netlink and libudev both still see the event, so it's easy to misread as "REMOVE never arrives." Thanks Karim
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/wolf#275
No description provided.