mirror of
https://github.com/games-on-whales/wolf.git
synced 2026-10-15 13:02:03 +00:00
[GH-ISSUE #81] Fix Steam input in unprivileged containers #60
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#60
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 @ABeltramo on GitHub (Jun 27, 2024).
Original GitHub issue: https://github.com/games-on-whales/wolf/issues/81
Steam input works by creating virtual devices using
uinput(similar to how we do it in inputtino). So the first step is obviously to allow the Steam container to access it:When you start a game with steam input enabled the following
ueventswill be generated:As you can see it's missing the corresponding
udevevents (network isolation for the win); we'll have to pick those kernel events up,mknodthe devices and relay the correspondentudevevent using fake-udev.The only bit that I'm not sure is how to avoid mounting other containers devices. Unfortunately kernel events aren't isolated, for example I can see the event for when a device is plugged in the host.. How can we
mknodonly the devices that are actually generated by this steam instance? Intercept/dev/uinputioctl calls?Quick fix
This will break isolation, but for a single user this should work:
And set the env variable
WOLF_DOCKER_FAKE_UDEV_PATH=""to disable ourfake_udevimplementation.@Drakulix commented on GitHub (Jun 27, 2024):
Yes. A valve engineer confirm to me, that LD_PRELOAD-ing steam to do this should give us all the infos we need.
Basically we should intercept UI_DEV_CREATE and then internally call UI_GET_SYSNAME, matching the name to our container and returning from the intercepted ioctl.
Everything that is not
UI_DEV_CREATEwe can just passthrough and ignore.@ABeltramo commented on GitHub (Jun 27, 2024):
Thank you so much for confirming it! It should be fairly straightforward then, just need to plug together
fake-udevandmknodin a simple library that I'll preload.Damn I wish I could bother a Valve engineer too 😅
@pahaze commented on GitHub (Dec 16, 2024):
Hi! Just wondering if there's any update on this. I tried the quick fix, as I just want to connect to my TV and phone (separate times, home / work respectively), but I get the following in my logs:
[DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"}I set the runner to what you put in the quick fix in
config.toml, and I also set the environment variable to not use fake-udev. I'm using Fedora 41, fully updated, and I'm using the Docker compose file. Here's my config:And here's my full log output if it's needed:
@ABeltramo commented on GitHub (Dec 16, 2024):
@pahaze is there a specific reason why you are trying to use this?
Joypads should be already fully working, you just have to disable Steam input from the settings.
@pahaze commented on GitHub (Dec 16, 2024):
There is, sadly. I use a DualSense/DualShock 4 to play my games, and a lot of the games I own only work with Xbox controllers or need to map the controller to keyboard keys using Steam Input
@ABeltramo commented on GitHub (Dec 16, 2024):
Right, I see what you mean.
You can override the joypad mapping by editing the
config.toml, that should allow you to get around this.@pahaze commented on GitHub (Dec 16, 2024):
That should do the job! Minus the keyboard/mouse games, but I can get a wireless combo later on for those. Thanks!
@ABeltramo commented on GitHub (Dec 16, 2024):
Nice, unfortunately that's just a workaround.. Hopefully I'll get around to properly support Steam input in Wolf!
@pahaze commented on GitHub (Dec 16, 2024):
If possible, I might try to look into it some myself too. I'm not too experienced with Docker, or Linux to that level, and I'd like to start learning! I've not been doing much programming as is, too :). But, just an idea, and please tell me if this isn't possible or I'm wrong, but is it possible to set up seats between the Wayland sessions to know which Steam Input device is being used for each Wolf session, rather than having to rely on kernel events and potentially accessing other containers?
@ABeltramo commented on GitHub (Dec 16, 2024):
Unfortunately no, historically compositors just ignored joypads and apps had to direcly access them so everything relies on accessing stuff directly under
/dev/input.If you want to take a look into this I suggest reading how we achieved hotplug in Docker and the rationale behind this issue and the status of the corresponding stale PR.
Feel free to jump in our Discord server if you want to ask anything in more detail!
@pahaze commented on GitHub (Dec 16, 2024):
I see.. I'll definitely try to look into it some if I can, at the very least to try to get a better understanding. I did change my config, and I got this at first:
Which, scared me, until I went past the screen and saw it was actually working as an Xbox controller (hello from work!) 😆
Thanks again for the help and all info!
@joleuger commented on GitHub (Nov 29, 2025):
Hi all,
I just wanted to quickly share the alternative solution I’ve been working on, since it solves the same Steam Input problem but from a different angle. Maybe some parts of it are useful for Wolf as well. The solution is called vuinputd and is currently a working prototype implemented in rust.
Overview of my approach
Instead of intercepting only
UI_DEV_CREATEviaLD_PRELOAD, I move the whole uinput handling out of the container and let a host-side daemon take care of everything that normally requires privileges.Inside the container,
/dev/uinputis replaced with a virtual character device implemented via CUSE (part of the FUSE project, fully supported in the mainline linux kernel). This CUSE device forwards allioctl()andwrite()operations to a small root-running host daemon (vuinputd).The host daemon then implements the full uinput ABI in userspace:
/dev/input/eventXnodes by forwarding the requests to the real uinputmknod, permission fixes, etc.From the container’s perspective, Steam Input just talks to “/dev/uinput” normally.
Benefits compared to the LD_PRELOAD approach
/dev/uinputdirectly.Drawbacks
Why I’m sharing this
I think some parts of this design — especially the idea of using a CUSE-backed virtual uinput device + a host helper for privileged operations — might fit nicely into your architecture and remove the need for Steam-specific LD_PRELOAD tricks.
If you’re interested, I’m happy to provide more details or diagrams.
Cheers!
@ABeltramo commented on GitHub (Nov 30, 2025):
@joleuger thanks for sharing that, sounds great!
Ideally we wouldn't need a host helper: Wolf already has access to the host
/dev/uinputand/dev/inputso we can either run your tool directly in our container or, even better, embed your solution into Wolf.From a high level point of view we already have most of what you had to re-create like the
udevevents andmknod+ device lifecycle the only bit that I'm missing is the most critical one: a fakeuinputdevice and usingCUSEmight be the right path. I'm a bit swamped at the moment with a few other bits in Wolf, but I'll definitely look back at this as soon as I have a bit of free time!@DatCaptainHorse commented on GitHub (Nov 30, 2025):
Going to butt my horse head in here as well with this: https://github.com/DatCaptainHorse/vimputti/
It may not be as clean and production ready as what joleuger has, but it's alternative solution.. the LD_PRELOAD trick is pretty messy though, I haven't heard of
CUSEbefore, that might be what I need to fully emulate linux input system(s) under containers though 🤔I need to work more on vimputti regardless, but if there's interest to utilize it, it'd atleast put pressure on me to make the code not so ugly 😛
@M-Reimer commented on GitHub (Jan 1, 2026):
By searching on Google for this exact issue and possible solutions, I came to this Issue.
Keep in mind that passing over uinput calls to the "host side" basically allows to escape your sandbox completely:
https://github.com/ValveSoftware/steam-devices/issues/71
@joleuger commented on GitHub (Jan 1, 2026):
Thanks for the report. You are absolutely right—unrestricted /dev/uinput access is a known privilege escalation vector. If a sandboxed application can inject input events into the user's active session (seat0), it can easily break out of the sandbox (e.g., via GNOME's Alt+F2 or terminal shortcuts).
To mitigate this, my plan is to implement a two-layer defense strategy in vuinputd:
Goal: Block lower-level attacks like VT switching (Ctrl+Alt+Fn) and Magic SysRq commands.
Effect: This prevents a malicious actor from tampering with the kernel state or switching to a text console to hijack getty.
Mechanism: Using udev rules, virtual keyboards created by vuinputd are assigned to a dedicated seat (e.g., seat_vuinput) rather than the default seat0.
Effect: The user's desktop environment (GNOME/KDE running on seat0) effectively "ignores" these devices. Even if a malicious app injects "Alt+F2", the event is routed to seat_vuinput and never reaches the shell running on the user's screen.
To complete this isolation, I am developing fallbackdm https://github.com/joleuger/vuinputd/issues/3 . This is a lightweight, headless display manager that owns the VT when no graphical session is active. It ensures that even if the system is sitting at a login prompt or text console, the VTs remain locked in KD_GRAPHICS mode, preventing any leaked input events from reaching a shell.
@M-Reimer commented on GitHub (Jan 2, 2026):
Isn't that pretty complex just to be able to use the host uinput?
Is the LD_PRELOAD solution lacking something?
@joleuger commented on GitHub (Jan 2, 2026):
Hi @M-Reimer,
To provide some context on the design choices here: the goal isn't just to make Steam Input work, but to find the most "future-proof" way to handle virtual devices in a containerized environment like Wolf.
While an
LD_PRELOADapproach (as mentioned by Drakulix via the Valve engineer) is definitely a valid path for interceptinguinputcalls, there are a few "puzzle pieces" that make a CUSE-based approach likevuinputdparticularly interesting for the long term:1. Reliability across the Stack
LD_PRELOADworks great for dynamic libraries, but it can be bypassable. If a binary is statically linked or uses direct syscalls to talk to the kernel, the preload is skipped. With Steam's complex environment—Proton, different runtimes, and nested namespaces—ensuring a preload hook stays active and compatible across every single game launcher and sub-process can become a significant maintenance task.2. The VFS Boundary (The "Clean" Intercept)
By using CUSE, we move the interception to the Kernel/VFS boundary.
It doesn't matter what libraries the app uses or how it's compiled; if it opens
/dev/uinput, it hits the CUSE node. It’s an "invisible" emulation that the application can't distinguish from real hardware.3. Elegant Isolation via PID-Routing
One of the coolest parts of this approach is how it handles multi-tenancy. The daemon can use the PID of the process calling
open()to identify exactly which container or session the request is coming from.4. Supporting Unprivileged/Rootless Setups
Because the actual
mknodand privileged uinput operations happen inside the daemon (which can run with the necessary caps on the host), the game containers themselves can remain entirely unprivileged.There are definitely multiple ways to tackle the problem —projects like @DatCaptainHorse's vimputti are also exploring how to bridge these gaps. I just believe that moving the logic to the device-emulation level (CUSE) provides the most stable foundation for a project that needs to support thousands of different games without constant per-game tweaking.
@M-Reimer commented on GitHub (Jan 3, 2026):
Good points. Probably CUSE could also be the simpler option for my usecase. What I want to accomplish is (without the need of an actual sandbox) that Steam is not able to create anything but game controllers. I don't mind if Steam creates additional game controllers as doing "bad things" with a joystick is more or less impossible. I'll keep that in mind. Thanks.
@joleuger commented on GitHub (Jan 3, 2026):
Glad we're on the same page! That specific use case ("Steam should only be allowed to create game controllers") is actually the perfect candidate for a CUSE-based policy.
I've opened an issue on the vuinputd repo to track exactly this feature: https://github.com/joleuger/vuinputd/issues/4
The idea is to add a flag like --capability-policy gamepad-only which would strictly reject any device creation request that includes standard keyboard keys (QWERTY), while still allowing standard Xbox/PlayStation button ranges. Trackpad features should also get filtered out, beause they are mouselike.
If you're interested, feel free to drop any specific requirements (e.g., specific keys you want blocked) in that issue. It would be great to have your security perspective there to ensure the "gamepad-only / Strict Mode" is actually strict enough for your needs.
Some additional thoughts: I guess the implementation of wolf could just use this gamepad-only functionality, because mouse and keyboard are created by wolf outside of the container. But one downside should also be considered: Steam-input modes that emulate mouses or keyboards will also not work. Also, I haven't tested out how steam has implemented remote play for mouse and keyboard. If it is directly in gamescope than there should be no problem.
@Markus328 commented on GitHub (Feb 23, 2026):
The GoW docs said it's possible to run Wolf rootlessly, but I even can't figure how wolf could deal with virtual device creation. I think something like @joleuger vuinputd should be more modular. I've already created a fully rootless nspawn container with strong device isolation using that tool. Even though I can't do much as Wolf does in creating a whole stack of containers and etc etc. I thinkg offloading the device creation and managment to an external specific tool would allow running Wolf 100% rootless and functional. Or even add it as an option, I wonder just disabling the internal module and moving the device creation logic to the actual app container (and allowing the external root tool to identify the container, and inject the udev into the container) would do the trick
@Nurgus commented on GitHub (Oct 6, 2026):
I'm trying running like this. While it initially works, my controller stops responding in Steam when Moonlight disconnects and reconnects. I then have to restart the wolf docker container to get controller functionality in Steam again. Restarting the Steam container via the Wolf UI doesn't fix it.
I'm assuming this env variable goes in the Wolf container's main compose.yml? I don't notice a difference with or without it set.
I had to remove the mount "/run/udev:/run/udev:ro" as it's apparently already mounted and prevents the container from starting. So this is the app profile I have in use:
What have I done wrong?