mirror of
https://github.com/games-on-whales/wolf.git
synced 2026-10-15 13:02:03 +00:00
[GH-ISSUE #435] [Unraid] Gamepad Input from Applications Leaking into WolfUI #275
Labels
No labels
🧠 1 - 🤖 999
AMD
bug
bug
cannot reproduce
documentation
duplicate
easy
enhancement
enhancement
enhancement
fixed?
fixed?
good first issue
hard
help wanted
Intel
invalid
LXC
medium
moonlight-protocol
Multi GPU
nixos
not a bug
nvidia
pull-request
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
games-on-whales/wolf#275
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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?
@thewave1408 commented on GitHub (Jun 16, 2026):
I also have this issue using the Unraid plugin.
@AbsoZed commented on GitHub (Jun 16, 2026):
Cause appears to be that joypads are
inputtinouinput devices created on the host.On
JoinLobbyEventwe firePlugDeviceEventto the lobby andUnplugDeviceEventto Wolf-UI's container, but the Wolf-UI consumer (Godot) has already opened/dev/input/eventNdirectly.fake-udevinjects a REMOVE on the container's netlink and the node isrm'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_DESTROYkills 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.
@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.
@JBailes commented on GitHub (Jun 16, 2026):
Unraid doesn't use LXC2Docker, Abe.
@ABeltramo commented on GitHub (Jun 16, 2026):
And that's why I shouldn't reply when on my phone 😅
@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:
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.
@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:
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.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.
@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.
@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.
@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.
@ABeltramo commented on GitHub (Jun 16, 2026):
Which one is true:
or
?
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.
@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:
Should be able to validate it's doing so by following:
docker exec -it <wolf-ui-container> bashWhile you press buttons in Steam:
cat /proc/$(pgrep wolf-ui)/fdinfo/* 2>/dev/null | grep -B1 inputls -l /proc/$(pgrep wolf-ui)/fd | grep inputYou should see Godot still has /dev/input/eventN open while you're inside Steam, causing the leak.
@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.
@ABeltramo commented on GitHub (Jun 16, 2026):
Just tested out on my machine:
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:
So the
fdis leaking (I guess) but Wolf-UI isn't reading events from it. My guess is that Godot is responding to thefake-udevREMOVE event and stops polling thefd.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.@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.
@JBailes commented on GitHub (Jun 16, 2026):
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?
@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.
@JBailes commented on GitHub (Jun 16, 2026):
@ABeltramo took this to a real box on stock Docker and tested both mechanisms directly.
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.
@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+REMOVEis 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.@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.
@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.
@thewave1408 commented on GitHub (Jun 18, 2026):
This is most likely the same issue
@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.
@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.
@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..
@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-udevis not getting mounted into the WolfUI container, meaning that the commanddocker exec wolf-ui /bin/bash -c "fake-udev -m <msg> && rm /dev/input/eventN"that's run on everyUnplugDeviceEventnever executes thermbecause the return code forfake-udevexecution 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-udevbutls /usr/bin/fake-udevandwhich fake-udevshow 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-udevexisted in the container, and all expected events were received for detach via monitoring withudevadm 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-udevin the container to confirm this please?@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 execfailsgithub.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-udevisn't being mounted. Possibly with the env variableWOLF_LOG_LEVEL=DEBUG(assuming you can tweak that in Unraid)@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 infake-udevnot 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.
@ABeltramo commented on GitHub (Jun 19, 2026):
The way we mount
fake-udevis a bit tricky, but the idea is that on container startup we copy it from the binary bundled in Wolf to$HOST_APPS_STATE_FOLDERgithub.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..@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:
That is commit c861cd1 (PR #46) in that repository. Before it,
HOST_APPS_STATE_FOLDERwas either unset or pointed at/etc/wolf, so Wolf (the process) told Docker to bind-mount source/etc/wolf/fake-udevinto spawned containers, and/etc/wolfactually 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_FOLDERto${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:
startup.sh ran
mkdir -p $HOST_APPS_STATE_FOLDER/cfg = mkdir -p /mnt/user/appdata/gow/cfginside the container. Since/mnt/user/appdata/gowwasn't a mount, this just created the path in the container's overlay layer.cp /wolf/fake-udev /mnt/user/appdata/gow/fake-udev, same overlay.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.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.
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:
I do still believe it to be worthwhile to address the
fdremaining 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
statmy directories to confirm the dates at this point 😅@CocoShun commented on GitHub (Jun 19, 2026):
@AbsoZed I've tested "which fake-udev" inside the wolfUI container, it gives no output.
@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:
rm -rf /mnt/user/appdata/gow/fake-udevThis will remove the directory, create a new
fake-udevbinary instead of directory. Subsequent mounts into containers will work perfectly, and you can confirm bydocker exec <wolf-ui ID> /bin/bashand thenwhich 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.
@CocoShun commented on GitHub (Jun 19, 2026):
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?
@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.
@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 udevREMOVEactually reaches the reader inside the container. Every case in this thread seems to fit that:fake-udevwas the broken-directory mount, sofake-udev -m … && rm eventNexited 127 and therm/REMOVE never ran. The reader keeps its fd, so input leaks.(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()onHotplugEvent::Removed, and SDL/Godot watch udev the same way. Sorecreate_device()mainly compensates for a REMOVE that never showed up. I removed it and now just dounplug(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-udevneedsCAP_NET_ADMINto broadcast, and without it the send fails withEPERMeven as root. A custom app, or abase_create_jsonthat dropsNET_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 --udevshows 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