[GH-ISSUE #81] Fix Steam input in unprivileged containers #60

Open
opened 2026-10-15 12:55:35 +00:00 by bsakaguchi · 22 comments
Owner

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:

devices = ["/dev/uinput:/dev/uinput"]
env = [
  "PROTON_LOG=1",
  "RUN_GAMESCOPE=true",
  "GOW_REQUIRED_DEVICES=/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia*"
]

When you start a game with steam input enabled the following uevents will be generated:

➜ docker exec -it -u retro WolfSteam_4135727842959053255 udevadm monitor -p
monitor will print the received events for:
UDEV - the event which udev sends out after rule processing
KERNEL - the kernel uevent

KERNEL[13943.544155] add      /devices/virtual/input/input165 (input)
ACTION=add
DEVPATH=/devices/virtual/input/input165
SUBSYSTEM=input
PRODUCT=3/28de/11ff/1
NAME="Microsoft X-Box 360 pad 0"
PROP=0
EV=20000b
KEY=7cdb0000 0 0 0 0 0 0 0 0 0
ABS=3003f
FF=10000 0 0
MODALIAS=input:b0003v28DEp11FFe0001-e0,1,3,15,k130,131,133,134,136,137,13A,13B,13C,13D,13E,ra0,1,2,3,4,5,10,11,mlsf50,w
SEQNUM=23561

KERNEL[13943.544214] add      /devices/virtual/input/input165/event26 (input)
ACTION=add
DEVPATH=/devices/virtual/input/input165/event26
SUBSYSTEM=input
DEVNAME=/dev/input/event26
SEQNUM=23562
MAJOR=13
MINOR=90

KERNEL[13943.544244] add      /devices/virtual/input/input165/js3 (input)
ACTION=add
DEVPATH=/devices/virtual/input/input165/js3
SUBSYSTEM=input
DEVNAME=/dev/input/js3
SEQNUM=23563
MAJOR=13
MINOR=3

As you can see it's missing the corresponding udev events (network isolation for the win); we'll have to pick those kernel events up, mknod the devices and relay the correspondent udev event 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 mknod only the devices that are actually generated by this steam instance? Intercept /dev/uinput ioctl calls?

Quick fix

This will break isolation, but for a single user this should work:

[[apps]]
title = "Steam"
start_virtual_compositor = true

[apps.runner]
type = "docker"
name = "WolfSteam"
image = "ghcr.io/games-on-whales/steam:edge"
mounts = ["/dev/input:/dev/input:rw", "/run/udev:/run/udev:ro"]
env = [
  "PROTON_LOG=1",
  "RUN_GAMESCOPE=true",
  "GOW_REQUIRED_DEVICES=/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia*",
]
devices = ["/dev/uinput:/dev/uinput"]
ports = []
base_create_json = """
{
  "HostConfig": {
    "NetworkMode": "host",
    "IpcMode": "host",
    "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"],
    "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"],
    "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}],
    "Privileged": false,
    "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]
  }
}
\
"""

And set the env variable WOLF_DOCKER_FAKE_UDEV_PATH="" to disable our fake_udev implementation.

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](https://github.com/games-on-whales/inputtino)). So the first step is obviously to allow the Steam container to access it: ```toml devices = ["/dev/uinput:/dev/uinput"] env = [ "PROTON_LOG=1", "RUN_GAMESCOPE=true", "GOW_REQUIRED_DEVICES=/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia*" ] ``` When you start a game with steam input enabled the following `uevents` will be generated: ```bash ➜ docker exec -it -u retro WolfSteam_4135727842959053255 udevadm monitor -p monitor will print the received events for: UDEV - the event which udev sends out after rule processing KERNEL - the kernel uevent KERNEL[13943.544155] add /devices/virtual/input/input165 (input) ACTION=add DEVPATH=/devices/virtual/input/input165 SUBSYSTEM=input PRODUCT=3/28de/11ff/1 NAME="Microsoft X-Box 360 pad 0" PROP=0 EV=20000b KEY=7cdb0000 0 0 0 0 0 0 0 0 0 ABS=3003f FF=10000 0 0 MODALIAS=input:b0003v28DEp11FFe0001-e0,1,3,15,k130,131,133,134,136,137,13A,13B,13C,13D,13E,ra0,1,2,3,4,5,10,11,mlsf50,w SEQNUM=23561 KERNEL[13943.544214] add /devices/virtual/input/input165/event26 (input) ACTION=add DEVPATH=/devices/virtual/input/input165/event26 SUBSYSTEM=input DEVNAME=/dev/input/event26 SEQNUM=23562 MAJOR=13 MINOR=90 KERNEL[13943.544244] add /devices/virtual/input/input165/js3 (input) ACTION=add DEVPATH=/devices/virtual/input/input165/js3 SUBSYSTEM=input DEVNAME=/dev/input/js3 SEQNUM=23563 MAJOR=13 MINOR=3 ``` As you can see it's missing the corresponding `udev` events (network isolation for the win); we'll have to pick those kernel events up, `mknod` the devices and relay the correspondent `udev` event using [fake-udev](https://games-on-whales.github.io/wolf/stable/dev/fake-udev.html). 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 `mknod` only the devices that are actually generated by this steam instance?** Intercept `/dev/uinput` ioctl calls? ## Quick fix This **will break isolation**, but for a single user this should work: ```toml [[apps]] title = "Steam" start_virtual_compositor = true [apps.runner] type = "docker" name = "WolfSteam" image = "ghcr.io/games-on-whales/steam:edge" mounts = ["/dev/input:/dev/input:rw", "/run/udev:/run/udev:ro"] env = [ "PROTON_LOG=1", "RUN_GAMESCOPE=true", "GOW_REQUIRED_DEVICES=/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia*", ] devices = ["/dev/uinput:/dev/uinput"] ports = [] base_create_json = """ { "HostConfig": { "NetworkMode": "host", "IpcMode": "host", "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"], "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}], "Privileged": false, "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ """ ``` And set the env variable `WOLF_DOCKER_FAKE_UDEV_PATH=""` to disable our `fake_udev` implementation.
Author
Owner

@Drakulix commented on GitHub (Jun 27, 2024):

Intercept /dev/uinput ioctl calls?

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_CREATE we can just passthrough and ignore.

<!-- gh-comment-id:2195527036 --> @Drakulix commented on GitHub (Jun 27, 2024): > Intercept /dev/uinput ioctl calls? 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](https://github.com/whot/libevdev/blob/master/libevdev/libevdev-uinput.c#L398C17-L398C30) and then internally call [UI_GET_SYSNAME](https://github.com/whot/libevdev/blob/master/libevdev/libevdev-uinput.c#L225), matching the name to our container and returning from the intercepted ioctl. Everything that is not `UI_DEV_CREATE` we can just passthrough and ignore.
Author
Owner

@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-udev and mknod in a simple library that I'll preload.
Damn I wish I could bother a Valve engineer too 😅

<!-- gh-comment-id:2195532803 --> @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-udev` and `mknod` in a simple library that I'll preload. Damn I wish I could bother a Valve engineer too 😅
Author
Owner

@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:

version: "3"
services:
  wolf:
    image: ghcr.io/games-on-whales/wolf:stable
    environment:
      - XDG_RUNTIME_DIR=/tmp/sockets
      - HOST_APPS_STATE_FOLDER=/etc/wolf
      - WOLF_DOCKER_FAKE_UDEV_PATH=""
    volumes:
      - /etc/wolf/:/etc/wolf
      - /tmp/sockets:/tmp/sockets:rw
      - /var/run/docker.sock:/var/run/docker.sock:rw
      - /dev/:/dev/:rw
      - /run/udev:/run/udev:rw
    device_cgroup_rules:
      - 'c 13:* rmw'
    devices:
      - /dev/dri
      - /dev/uinput
      - /dev/uhid
    network_mode: host
    restart: unless-stopped

And here's my full log output if it's needed:

wolf-1  | [2024-12-16 16:09:06] 
wolf-1  | [2024-12-16 16:09:06] [ /etc/cont-init.d/10-setup_user.sh: executing... ]
wolf-1  | [2024-12-16 16:09:06] **** Configure default user ****
wolf-1  | [2024-12-16 16:09:06] Container running as root. Nothing to do.
wolf-1  | [2024-12-16 16:09:06] DONE
wolf-1  | [2024-12-16 16:09:06] 
wolf-1  | [2024-12-16 16:09:06] [ /etc/cont-init.d/15-setup_devices.sh: executing... ]
wolf-1  | [2024-12-16 16:09:06] **** Configure devices ****
wolf-1  | [2024-12-16 16:09:06] Exec device groups
wolf-1  | [2024-12-16 16:09:06] Adding user 'root' to groups: gow-gid-104
wolf-1  | [2024-12-16 16:09:06] DONE
wolf-1  | [2024-12-16 16:09:06] 
wolf-1  | [2024-12-16 16:09:06] [ /etc/cont-init.d/30-nvidia.sh: executing... ]
wolf-1  | [2024-12-16 16:09:06] 
wolf-1  | [2024-12-16 16:09:06] [ /etc/cont-init.d/init-gamescope.sh: executing... ]
wolf-1  | [2024-12-16 16:09:06] Launching the container's startup script as user 'root'
wolf-1  | 0:00:00.048611408   193 0x55ca3806e460 WARN          adaptivedemux2 gstadaptivedemuxelement.c:41:adaptivedemux2_base_element_init: Failed to load libsoup library
wolf-1  | 0:00:00.048715749   193 0x55ca3806e460 WARN          adaptivedemux2 gstadaptivedemuxelement.c:41:adaptivedemux2_base_element_init: Failed to load libsoup library
wolf-1  | 0:00:00.048803249   193 0x55ca3806e460 WARN          adaptivedemux2 gstadaptivedemuxelement.c:41:adaptivedemux2_base_element_init: Failed to load libsoup library
wolf-1  | 0:00:00.219949922   193 0x55ca3806e460 WARN                 default ges-meta-container.c:236:_set_value:<GESAsset@0x55ca38474f80> Could not set value on item: format-version
wolf-1  | 0:00:00.219970062   193 0x55ca3806e460 WARN                 default ges-meta-container.c:236:_set_value:<GESAsset@0x55ca384756f0> Could not set value on item: format-version
wolf-1  | 0:00:00.219985432   193 0x55ca3806e460 WARN                 default ges-meta-container.c:236:_set_value:<GESAsset@0x55ca38475e40> Could not set value on item: format-version
wolf-1  | 0:00:00.220246443   193 0x55ca3806e460 WARN               structure gststructure.c:2375:priv_gst_structure_parse_fields: Failed to find delimiter, r=mimetype
wolf-1  | 0:00:00.266264172   193 0x55ca3806e460 WARN              cudaloader gstcudaloader.cpp:260:gst_cuda_load_library_once_func: Could not open library libcuda.so.1, libcuda.so.1: cannot open shared object file: No such file or directory
wolf-1  | amdgpu: os_same_file_description couldn't determine if two DRM fds reference the same file description.
wolf-1  | If they do, bad things may happen!
wolf-1  | 0:00:00.414302906   193 0x55ca3806e460 WARN                vafilter gstvafilter.c:1743:gst_va_filter_has_compose:<vafilter0> VPP does not support alpha blending
wolf-1  | 0:00:00.443903880   193 0x55ca3806e460 WARN                 default gstvaapi.c:229:plugin_init: Cannot create a VA display
wolf-1  | 16:09:07.005177572 INFO  | Gstreamer version: 1.25.0-1
wolf-1  | 16:09:07.005829074 INFO  | Reading config file from: /etc/wolf/cfg/config.toml
wolf-1  | 0:00:00.535793416     1 0x5592820ea520 WARN     GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vah264lpenc"!
wolf-1  | amdgpu: os_same_file_description couldn't determine if two DRM fds reference the same file description.
wolf-1  | If they do, bad things may happen!
wolf-1  | 0:00:00.567071538     1 0x5592820ea520 WARN                vafilter gstvafilter.c:1743:gst_va_filter_has_compose:<vafilter0> VPP does not support alpha blending
wolf-1  | 16:09:07.057857466 INFO  | Using H264 encoder: vaapi
wolf-1  | 0:00:00.580524700     1 0x5592820ea520 WARN     GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vah265lpenc"!
wolf-1  | 16:09:07.066066547 INFO  | Using HEVC encoder: vaapi
wolf-1  | 0:00:00.588726952     1 0x5592820ea520 WARN     GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vaav1enc"!
wolf-1  | 0:00:00.588739162     1 0x5592820ea520 WARN     GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vaav1lpenc"!
wolf-1  | 16:09:07.066501009 INFO  | Using AV1 encoder: aom
wolf-1  | 16:09:07.066510259 WARN  | Software AV1 encoder detected
wolf-1  | 16:09:07.067504743 INFO  | Starting API server on /tmp/wolf.sock
wolf-1  | 16:09:07.067520973 INFO  | Starting mDNS service
wolf-1  | 16:09:07.067540773 INFO  | RTSP server started on port: 48010
wolf-1  | 16:09:07.067568653 INFO  | HTTP server listening on port: 47989 
wolf-1  | 16:09:07.067575263 INFO  | Control server started on port: 47999
wolf-1  | XDG_RUNTIME_DIR (/tmp/sockets) is not owned by us (uid 0), but by uid 1000! (This could e.g. happen if you try to connect to a non-root PulseAudio as a root user, over the native protocol. Don't do that.)
wolf-1  | 16:09:07.067732404 WARN  | [PULSE] Unable to connect, Access denied
wolf-1  | 16:09:07.067776524 INFO  | Starting PulseAudio docker container
wolf-1  | 16:09:07.068444977 WARN  | [DOCKER] Container WolfPulseAudio already present, removing first
wolf-1  | 16:09:07.068904469 INFO  | HTTPS server listening on port: 47984 
wolf-1  | 16:09:10.839454254 WARN  | HTTPS error during request at  error code: 2 - End of file
wolf-1  | 16:09:11.103662469 WARN  | [HTTP] Can't find icon_png_path for app with id: 1
wolf-1  | 16:09:11.177125904 WARN  | [HTTP] Can't find icon_png_path for app with id: 1
wolf-1  | 16:09:11.212713281 WARN  | [HTTP] Can't find icon_png_path for app with id: 4
wolf-1  | 16:09:11.213254394 WARN  | [HTTP] Can't find icon_png_path for app with id: 2
wolf-1  | 16:09:12.289008784 WARN  | [HTTP] Can't find icon_png_path for app with id: 1
wolf-1  | 16:09:12.329672681 WARN  | [HTTP] Can't find icon_png_path for app with id: 1
wolf-1  | 16:09:12.697998049 WARN  | [HTTP] Can't find icon_png_path for app with id: 4
wolf-1  | 16:09:12.728541217 WARN  | [HTTP] Can't find icon_png_path for app with id: 2
wolf-1  | 16:09:13.004705398 WARN  | Moonlight requested an impossible number of channels: 2
wolf-1  | 16:09:13.005892782 INFO  | RTP server started on port: 48100
wolf-1  | 16:09:13.005917523 INFO  | RTP server started on port: 48200
wolf-1  | 16:09:13.068433505 WARN  | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"}
wolf-1  | 
wolf-1  | 0:00:06.591309000     1 0x7f527c000e60 WARN                audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc0> error reading data -1 (reason: Success), skipping segment
wolf-1  | 16:09:13.076542667 WARN  | [RTSP] received packet from unrecognised client: 192.168.42.58
wolf-1  | 16:09:13.658596813 WARN  | [HTTP] Can't find icon_png_path for app with id: 1
wolf-1  | 16:09:13.744568276 WARN  | [HTTP] Can't find icon_png_path for app with id: 1
wolf-1  | 16:09:13.842027864 WARN  | [HTTP] Can't find icon_png_path for app with id: 4
wolf-1  | 16:09:14.037075340 WARN  | [HTTP] Can't find icon_png_path for app with id: 2
wolf-1  | 16:09:16.239103116 WARN  | Moonlight requested an impossible number of channels: 2
wolf-1  | 16:09:16.239630198 WARN  | [RTP] Unable to start RTP server on 48200: bind: Address already in use [system:98 at /usr/include/boost/asio/detail/reactive_socket_service.hpp:161:33 in function 'boost::system::error_code boost::asio::detail::reactive_socket_service<boost::asio::ip::udp>::bind(implementation_type &, const endpoint_type &, boost::system::error_code &) [Protocol = boost::asio::ip::udp]']
wolf-1  | 16:09:16.239654218 WARN  | [RTP] Unable to start RTP server on 48100: bind: Address already in use [system:98 at /usr/include/boost/asio/detail/reactive_socket_service.hpp:161:33 in function 'boost::system::error_code boost::asio::detail::reactive_socket_service<boost::asio::ip::udp>::bind(implementation_type &, const endpoint_type &, boost::system::error_code &) [Protocol = boost::asio::ip::udp]']
wolf-1  | 16:09:16.296417958 WARN  | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"}
wolf-1  | 
wolf-1  | 0:00:09.819306723     1 0x7f5318067650 WARN                audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc1> error reading data -1 (reason: Success), skipping segment
wolf-1  | 16:09:16.298561876 WARN  | [RTSP] received packet from unrecognised client: 192.168.42.58
wolf-1  | 16:09:19.369840202 WARN  | Moonlight requested an impossible number of channels: 2
wolf-1  | 16:09:19.370088533 INFO  | RTP server started on port: 48100
wolf-1  | 16:09:19.370114353 INFO  | RTP server started on port: 48200
wolf-1  | 16:09:19.430283216 WARN  | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"}
wolf-1  | 
wolf-1  | 0:00:12.953280521     1 0x7f52d8001380 WARN                audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc2> error reading data -1 (reason: Success), skipping segment
wolf-1  | 16:09:19.436972322 WARN  | [RTSP] received packet from unrecognised client: 192.168.42.58
wolf-1  | 16:09:21.931747033 WARN  | HTTPS error during request at  error code: 2 - End of file
wolf-1  | 16:09:26.593724064 WARN  | Moonlight requested an impossible number of channels: 2
wolf-1  | 16:09:26.594006025 INFO  | RTP server started on port: 48100
wolf-1  | 16:09:26.594018655 INFO  | RTP server started on port: 48200
wolf-1  | 16:09:26.648927938 WARN  | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"}
wolf-1  | 
wolf-1  | 0:00:20.171786163     1 0x7f52cc000a10 WARN                audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc3> error reading data -1 (reason: Success), skipping segment
wolf-1  | 16:09:26.660396783 WARN  | [RTSP] received packet from unrecognised client: 192.168.42.58
<!-- gh-comment-id:2546051393 --> @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: ``` version: "3" services: wolf: image: ghcr.io/games-on-whales/wolf:stable environment: - XDG_RUNTIME_DIR=/tmp/sockets - HOST_APPS_STATE_FOLDER=/etc/wolf - WOLF_DOCKER_FAKE_UDEV_PATH="" volumes: - /etc/wolf/:/etc/wolf - /tmp/sockets:/tmp/sockets:rw - /var/run/docker.sock:/var/run/docker.sock:rw - /dev/:/dev/:rw - /run/udev:/run/udev:rw device_cgroup_rules: - 'c 13:* rmw' devices: - /dev/dri - /dev/uinput - /dev/uhid network_mode: host restart: unless-stopped ``` And here's my full log output if it's needed: ``` wolf-1 | [2024-12-16 16:09:06] wolf-1 | [2024-12-16 16:09:06] [ /etc/cont-init.d/10-setup_user.sh: executing... ] wolf-1 | [2024-12-16 16:09:06] **** Configure default user **** wolf-1 | [2024-12-16 16:09:06] Container running as root. Nothing to do. wolf-1 | [2024-12-16 16:09:06] DONE wolf-1 | [2024-12-16 16:09:06] wolf-1 | [2024-12-16 16:09:06] [ /etc/cont-init.d/15-setup_devices.sh: executing... ] wolf-1 | [2024-12-16 16:09:06] **** Configure devices **** wolf-1 | [2024-12-16 16:09:06] Exec device groups wolf-1 | [2024-12-16 16:09:06] Adding user 'root' to groups: gow-gid-104 wolf-1 | [2024-12-16 16:09:06] DONE wolf-1 | [2024-12-16 16:09:06] wolf-1 | [2024-12-16 16:09:06] [ /etc/cont-init.d/30-nvidia.sh: executing... ] wolf-1 | [2024-12-16 16:09:06] wolf-1 | [2024-12-16 16:09:06] [ /etc/cont-init.d/init-gamescope.sh: executing... ] wolf-1 | [2024-12-16 16:09:06] Launching the container's startup script as user 'root' wolf-1 | 0:00:00.048611408 193 0x55ca3806e460 WARN adaptivedemux2 gstadaptivedemuxelement.c:41:adaptivedemux2_base_element_init: Failed to load libsoup library wolf-1 | 0:00:00.048715749 193 0x55ca3806e460 WARN adaptivedemux2 gstadaptivedemuxelement.c:41:adaptivedemux2_base_element_init: Failed to load libsoup library wolf-1 | 0:00:00.048803249 193 0x55ca3806e460 WARN adaptivedemux2 gstadaptivedemuxelement.c:41:adaptivedemux2_base_element_init: Failed to load libsoup library wolf-1 | 0:00:00.219949922 193 0x55ca3806e460 WARN default ges-meta-container.c:236:_set_value:<GESAsset@0x55ca38474f80> Could not set value on item: format-version wolf-1 | 0:00:00.219970062 193 0x55ca3806e460 WARN default ges-meta-container.c:236:_set_value:<GESAsset@0x55ca384756f0> Could not set value on item: format-version wolf-1 | 0:00:00.219985432 193 0x55ca3806e460 WARN default ges-meta-container.c:236:_set_value:<GESAsset@0x55ca38475e40> Could not set value on item: format-version wolf-1 | 0:00:00.220246443 193 0x55ca3806e460 WARN structure gststructure.c:2375:priv_gst_structure_parse_fields: Failed to find delimiter, r=mimetype wolf-1 | 0:00:00.266264172 193 0x55ca3806e460 WARN cudaloader gstcudaloader.cpp:260:gst_cuda_load_library_once_func: Could not open library libcuda.so.1, libcuda.so.1: cannot open shared object file: No such file or directory wolf-1 | amdgpu: os_same_file_description couldn't determine if two DRM fds reference the same file description. wolf-1 | If they do, bad things may happen! wolf-1 | 0:00:00.414302906 193 0x55ca3806e460 WARN vafilter gstvafilter.c:1743:gst_va_filter_has_compose:<vafilter0> VPP does not support alpha blending wolf-1 | 0:00:00.443903880 193 0x55ca3806e460 WARN default gstvaapi.c:229:plugin_init: Cannot create a VA display wolf-1 | 16:09:07.005177572 INFO | Gstreamer version: 1.25.0-1 wolf-1 | 16:09:07.005829074 INFO | Reading config file from: /etc/wolf/cfg/config.toml wolf-1 | 0:00:00.535793416 1 0x5592820ea520 WARN GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vah264lpenc"! wolf-1 | amdgpu: os_same_file_description couldn't determine if two DRM fds reference the same file description. wolf-1 | If they do, bad things may happen! wolf-1 | 0:00:00.567071538 1 0x5592820ea520 WARN vafilter gstvafilter.c:1743:gst_va_filter_has_compose:<vafilter0> VPP does not support alpha blending wolf-1 | 16:09:07.057857466 INFO | Using H264 encoder: vaapi wolf-1 | 0:00:00.580524700 1 0x5592820ea520 WARN GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vah265lpenc"! wolf-1 | 16:09:07.066066547 INFO | Using HEVC encoder: vaapi wolf-1 | 0:00:00.588726952 1 0x5592820ea520 WARN GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vaav1enc"! wolf-1 | 0:00:00.588739162 1 0x5592820ea520 WARN GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "vaav1lpenc"! wolf-1 | 16:09:07.066501009 INFO | Using AV1 encoder: aom wolf-1 | 16:09:07.066510259 WARN | Software AV1 encoder detected wolf-1 | 16:09:07.067504743 INFO | Starting API server on /tmp/wolf.sock wolf-1 | 16:09:07.067520973 INFO | Starting mDNS service wolf-1 | 16:09:07.067540773 INFO | RTSP server started on port: 48010 wolf-1 | 16:09:07.067568653 INFO | HTTP server listening on port: 47989 wolf-1 | 16:09:07.067575263 INFO | Control server started on port: 47999 wolf-1 | XDG_RUNTIME_DIR (/tmp/sockets) is not owned by us (uid 0), but by uid 1000! (This could e.g. happen if you try to connect to a non-root PulseAudio as a root user, over the native protocol. Don't do that.) wolf-1 | 16:09:07.067732404 WARN | [PULSE] Unable to connect, Access denied wolf-1 | 16:09:07.067776524 INFO | Starting PulseAudio docker container wolf-1 | 16:09:07.068444977 WARN | [DOCKER] Container WolfPulseAudio already present, removing first wolf-1 | 16:09:07.068904469 INFO | HTTPS server listening on port: 47984 wolf-1 | 16:09:10.839454254 WARN | HTTPS error during request at error code: 2 - End of file wolf-1 | 16:09:11.103662469 WARN | [HTTP] Can't find icon_png_path for app with id: 1 wolf-1 | 16:09:11.177125904 WARN | [HTTP] Can't find icon_png_path for app with id: 1 wolf-1 | 16:09:11.212713281 WARN | [HTTP] Can't find icon_png_path for app with id: 4 wolf-1 | 16:09:11.213254394 WARN | [HTTP] Can't find icon_png_path for app with id: 2 wolf-1 | 16:09:12.289008784 WARN | [HTTP] Can't find icon_png_path for app with id: 1 wolf-1 | 16:09:12.329672681 WARN | [HTTP] Can't find icon_png_path for app with id: 1 wolf-1 | 16:09:12.697998049 WARN | [HTTP] Can't find icon_png_path for app with id: 4 wolf-1 | 16:09:12.728541217 WARN | [HTTP] Can't find icon_png_path for app with id: 2 wolf-1 | 16:09:13.004705398 WARN | Moonlight requested an impossible number of channels: 2 wolf-1 | 16:09:13.005892782 INFO | RTP server started on port: 48100 wolf-1 | 16:09:13.005917523 INFO | RTP server started on port: 48200 wolf-1 | 16:09:13.068433505 WARN | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"} wolf-1 | wolf-1 | 0:00:06.591309000 1 0x7f527c000e60 WARN audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc0> error reading data -1 (reason: Success), skipping segment wolf-1 | 16:09:13.076542667 WARN | [RTSP] received packet from unrecognised client: 192.168.42.58 wolf-1 | 16:09:13.658596813 WARN | [HTTP] Can't find icon_png_path for app with id: 1 wolf-1 | 16:09:13.744568276 WARN | [HTTP] Can't find icon_png_path for app with id: 1 wolf-1 | 16:09:13.842027864 WARN | [HTTP] Can't find icon_png_path for app with id: 4 wolf-1 | 16:09:14.037075340 WARN | [HTTP] Can't find icon_png_path for app with id: 2 wolf-1 | 16:09:16.239103116 WARN | Moonlight requested an impossible number of channels: 2 wolf-1 | 16:09:16.239630198 WARN | [RTP] Unable to start RTP server on 48200: bind: Address already in use [system:98 at /usr/include/boost/asio/detail/reactive_socket_service.hpp:161:33 in function 'boost::system::error_code boost::asio::detail::reactive_socket_service<boost::asio::ip::udp>::bind(implementation_type &, const endpoint_type &, boost::system::error_code &) [Protocol = boost::asio::ip::udp]'] wolf-1 | 16:09:16.239654218 WARN | [RTP] Unable to start RTP server on 48100: bind: Address already in use [system:98 at /usr/include/boost/asio/detail/reactive_socket_service.hpp:161:33 in function 'boost::system::error_code boost::asio::detail::reactive_socket_service<boost::asio::ip::udp>::bind(implementation_type &, const endpoint_type &, boost::system::error_code &) [Protocol = boost::asio::ip::udp]'] wolf-1 | 16:09:16.296417958 WARN | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"} wolf-1 | wolf-1 | 0:00:09.819306723 1 0x7f5318067650 WARN audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc1> error reading data -1 (reason: Success), skipping segment wolf-1 | 16:09:16.298561876 WARN | [RTSP] received packet from unrecognised client: 192.168.42.58 wolf-1 | 16:09:19.369840202 WARN | Moonlight requested an impossible number of channels: 2 wolf-1 | 16:09:19.370088533 INFO | RTP server started on port: 48100 wolf-1 | 16:09:19.370114353 INFO | RTP server started on port: 48200 wolf-1 | 16:09:19.430283216 WARN | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"} wolf-1 | wolf-1 | 0:00:12.953280521 1 0x7f52d8001380 WARN audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc2> error reading data -1 (reason: Success), skipping segment wolf-1 | 16:09:19.436972322 WARN | [RTSP] received packet from unrecognised client: 192.168.42.58 wolf-1 | 16:09:21.931747033 WARN | HTTPS error during request at error code: 2 - End of file wolf-1 | 16:09:26.593724064 WARN | Moonlight requested an impossible number of channels: 2 wolf-1 | 16:09:26.594006025 INFO | RTP server started on port: 48100 wolf-1 | 16:09:26.594018655 INFO | RTP server started on port: 48200 wolf-1 | 16:09:26.648927938 WARN | [DOCKER] error 400 - {"message":"Duplicate mount point: /run/udev"} wolf-1 | wolf-1 | 0:00:20.171786163 1 0x7f52cc000a10 WARN audiosrc gstaudiosrc.c:227:audioringbuffer_thread_func:<pulsesrc3> error reading data -1 (reason: Success), skipping segment wolf-1 | 16:09:26.660396783 WARN | [RTSP] received packet from unrecognised client: 192.168.42.58 ```
Author
Owner

@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.

<!-- gh-comment-id:2546280979 --> @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.
Author
Owner

@pahaze 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.

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

<!-- gh-comment-id:2546285056 --> @pahaze 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. 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
Author
Owner

@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.

<!-- gh-comment-id:2546383778 --> @ABeltramo commented on GitHub (Dec 16, 2024): Right, I see what you mean. You can [override the joypad mapping](https://games-on-whales.github.io/wolf/stable/user/configuration.html#_override_the_default_joypad_mapping) by editing the `config.toml`, that should allow you to get around this.
Author
Owner

@pahaze 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.

That should do the job! Minus the keyboard/mouse games, but I can get a wireless combo later on for those. Thanks!

<!-- gh-comment-id:2546413952 --> @pahaze commented on GitHub (Dec 16, 2024): > Right, I see what you mean. > You can [override the joypad mapping](https://games-on-whales.github.io/wolf/stable/user/configuration.html#_override_the_default_joypad_mapping) by editing the `config.toml`, that should allow you to get around this. That should do the job! Minus the keyboard/mouse games, but I can get a wireless combo later on for those. Thanks!
Author
Owner

@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!

<!-- gh-comment-id:2546680102 --> @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!
Author
Owner

@pahaze 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!

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?

<!-- gh-comment-id:2547022834 --> @pahaze 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! 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?
Author
Owner

@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!

<!-- gh-comment-id:2547062974 --> @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](https://games-on-whales.github.io/wolf/stable/dev/fake-udev.html) 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!
Author
Owner

@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:

Screenshot_20241216-180609.png

Which, scared me, until I went past the screen and saw it was actually working as an Xbox controller (hello from work!) 😆

Screenshot_20241216-180653.png

Thanks again for the help and all info!

<!-- gh-comment-id:2547078644 --> @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: ![Screenshot_20241216-180609.png](https://github.com/user-attachments/assets/83cd19ac-6860-4a8b-a7f3-e466ad2465f3) Which, scared me, until I went past the screen and saw it was actually working as an Xbox controller (hello from work!) 😆 ![Screenshot_20241216-180653.png](https://github.com/user-attachments/assets/8fb63e2c-8cc1-4742-9ad2-d42d9460713a) Thanks again for the help and all info!
Author
Owner

@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_CREATE via LD_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/uinput is 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 all ioctl() and write() operations to a small root-running host daemon (vuinputd).

The host daemon then implements the full uinput ABI in userspace:

  • creates the actual /dev/input/eventX nodes by forwarding the requests to the real uinput
  • does mknod, permission fixes, etc.
  • sends real netlink/udev hotplug events in the container
  • manages the lifetime of virtual devices

From the container’s perspective, Steam Input just talks to “/dev/uinput” normally.

Benefits compared to the LD_PRELOAD approach

  • No LD_PRELOAD necessary → no risk of static binaries, Proton side processes, or future Steam changes breaking the interception.
  • Clean behavior with multiple containers or processes
  • Works even in rootless / unprivileged containers since the container never touches the real /dev/uinput directly.

Drawbacks

  • Requires a tiny host-side daemon and CUSE device setup (so not 100% in-container).
  • Slightly more implementation upfront because it emulates the whole uinput character device.
  • Maybe more input delay (I haven't measured the impact, yet)

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!

<!-- gh-comment-id:3591935139 --> @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](https://github.com/joleuger/vuinputd/tree/main) and is currently a working prototype implemented in rust. ## Overview of my approach Instead of intercepting only `UI_DEV_CREATE` via `LD_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/uinput` is 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 all `ioctl()` and `write()` operations to a small root-running host daemon (`vuinputd`). The host daemon then implements the full uinput ABI in userspace: * creates the actual `/dev/input/eventX` nodes by forwarding the requests to the real uinput * does `mknod`, permission fixes, etc. * sends real netlink/udev hotplug events in the container * manages the lifetime of virtual devices From the container’s perspective, Steam Input just talks to “/dev/uinput” normally. ## Benefits compared to the LD_PRELOAD approach * **No LD_PRELOAD necessary** → no risk of static binaries, Proton side processes, or future Steam changes breaking the interception. * **Clean behavior with multiple containers or processes** * **Works even in rootless / unprivileged containers** since the container never touches the real `/dev/uinput` directly. ## Drawbacks * Requires a tiny host-side daemon and CUSE device setup (so not 100% in-container). * Slightly more implementation upfront because it emulates the whole uinput character device. * Maybe more input delay (I haven't measured the impact, yet) ## 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!
Author
Owner

@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/uinput and /dev/input so 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 udev events and mknod + device lifecycle the only bit that I'm missing is the most critical one: a fake uinput device and using CUSE might 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!

<!-- gh-comment-id:3592279271 --> @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/uinput` and `/dev/input` so 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 `udev` events and `mknod` + device lifecycle the only bit that I'm missing is the most critical one: a fake `uinput` device and using `CUSE` might 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!
Author
Owner

@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 CUSE before, 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 😛

<!-- gh-comment-id:3593697685 --> @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 `CUSE` before, 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 😛
Author
Owner

@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

<!-- gh-comment-id:3703810843 --> @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
Author
Owner

@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:

  1. Input Sanitization (Kernel Protection) I am filtering dangerous key combinations directly in the vuinputd write loop (see https://github.com/joleuger/vuinputd/issues/2 ).

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.

  1. Seat Isolation (Desktop/Sandbox Protection) This is the primary defense against the "sandbox escape" scenario you described. I am introducing an architecture where virtual devices are physically isolated from the active user session.

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.

<!-- gh-comment-id:3704240758 --> @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](https://github.com/joleuger/vuinputd): 1. Input Sanitization (Kernel Protection) I am filtering dangerous key combinations directly in the vuinputd write loop (see https://github.com/joleuger/vuinputd/issues/2 ). 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. 2. Seat Isolation (Desktop/Sandbox Protection) This is the primary defense against the "sandbox escape" scenario you described. I am introducing an architecture where virtual devices are physically isolated from the active user session. 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.
Author
Owner

@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?

<!-- gh-comment-id:3705067426 --> @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?
Author
Owner

@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_PRELOAD approach (as mentioned by Drakulix via the Valve engineer) is definitely a valid path for intercepting uinput calls, there are a few "puzzle pieces" that make a CUSE-based approach like vuinputd particularly interesting for the long term:

1. Reliability across the Stack
LD_PRELOAD works 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.

  • This allows a single host-side instance to serve multiple Wolf sessions simultaneously.
  • It ensures that virtual controllers created by one container are only routed back to that specific container, preventing device "leakage" without needing complex configuration.

4. Supporting Unprivileged/Rootless Setups
Because the actual mknod and 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.

<!-- gh-comment-id:3705539958 --> @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_PRELOAD` approach (as mentioned by Drakulix via the Valve engineer) is definitely a valid path for intercepting `uinput` calls, there are a few "puzzle pieces" that make a CUSE-based approach like `vuinputd` particularly interesting for the long term: **1. Reliability across the Stack** `LD_PRELOAD` works 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. * This allows a single host-side instance to serve multiple Wolf sessions simultaneously. * It ensures that virtual controllers created by one container are only routed back to that specific container, preventing device "leakage" without needing complex configuration. **4. Supporting Unprivileged/Rootless Setups** Because the actual `mknod` and 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.
Author
Owner

@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.

<!-- gh-comment-id:3707147171 --> @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.
Author
Owner

@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.

<!-- gh-comment-id:3707417569 --> @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.
Author
Owner

@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

<!-- gh-comment-id:3942275751 --> @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
Author
Owner

@Nurgus commented on GitHub (Oct 6, 2026):

This will break isolation, but for a single user this should work:

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.

WOLF_DOCKER_FAKE_UDEV_PATH=""

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:

[[profiles.apps]]
    icon_png_path = 'https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png'
    start_virtual_compositor = true
    title = 'Steam'

        [profiles.apps.runner]
type = "docker"
name = "WolfSteam"
image = "ghcr.io/games-on-whales/steam:edge"
mounts = ["/dev/input:/dev/input:rw"]
env = [
  "PROTON_LOG=1",
  "RUN_GAMESCOPE=true",
  "GOW_REQUIRED_DEVICES=/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia*",
]
devices = ["/dev/uinput:/dev/uinput"]
ports = []
base_create_json = """
{
  "HostConfig": {
    "NetworkMode": "host",
    "IpcMode": "host",
    "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"],
    "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"],
    "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}],
    "Privileged": false,
    "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]
  }
}
\
"""

What have I done wrong?

<!-- gh-comment-id:6026426560 --> @Nurgus commented on GitHub (Oct 6, 2026): > This **will break isolation**, but for a single user this should work: 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. > WOLF_DOCKER_FAKE_UDEV_PATH="" 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: ``` [[profiles.apps]] icon_png_path = 'https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png' start_virtual_compositor = true title = 'Steam' [profiles.apps.runner] type = "docker" name = "WolfSteam" image = "ghcr.io/games-on-whales/steam:edge" mounts = ["/dev/input:/dev/input:rw"] env = [ "PROTON_LOG=1", "RUN_GAMESCOPE=true", "GOW_REQUIRED_DEVICES=/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia*", ] devices = ["/dev/uinput:/dev/uinput"] ports = [] base_create_json = """ { "HostConfig": { "NetworkMode": "host", "IpcMode": "host", "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"], "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}], "Privileged": false, "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] } } \ """ ``` What have I done wrong?
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#60
No description provided.