[GH-ISSUE #1] [FEATURE} Add support for rootless containers by executing privileged tasks on the host #3

Closed
opened 2026-10-30 21:32:19 +00:00 by bsakaguchi · 6 comments
Owner

Originally created by @ShaneTRS on GitHub (Dec 23, 2025).
Original GitHub issue: https://github.com/joleuger/vuinputd/issues/1

Permission errors occur when MKNOD is attempted from within the container. I got around this error by dropping the --target-namespace arguments for Action::MknodDevice.
I also ran into permission errors when trying to create files in /run/udev/data for some reason, but I assume that's a configuration issue on my end.
I'm hoping I don't have a fundamental misunderstanding here, but it seems like it'll be possible to do this entirely unprivileged (outside of the daemon running with CAP_MKNOD and other relevant capabilities)

Originally created by @ShaneTRS on GitHub (Dec 23, 2025). Original GitHub issue: https://github.com/joleuger/vuinputd/issues/1 Permission errors occur when `MKNOD` is attempted from within the container. I got around this error by dropping the --target-namespace arguments for `Action::MknodDevice`. I also ran into permission errors when trying to create files in `/run/udev/data` for some reason, but I assume that's a configuration issue on my end. I'm hoping I don't have a fundamental misunderstanding here, but it seems like it'll be possible to do this entirely unprivileged (outside of the daemon running with CAP_MKNOD and other relevant capabilities)
Author
Owner

@joleuger commented on GitHub (Dec 27, 2025):

Thank you for trying out vuinputd. As it is in a prototype stage, any comments or issues or input is valuable.

Permission errors occur when MKNOD is attempted from within the container. I got around this error by dropping the --target-namespace arguments for Action::MknodDevice.

The --target option was designed to enter the mount namespace of a container process in cases where the container has its own private /dev/input, different from the host. In this mode, mknod is executed as root but inside the target namespace, so that the device node is created in the container’s /dev.

Dropping --target is only correct if the daemon if the host and container share the same /dev/input, in which case no mknod is required at all because the device node is already present.

If the host and container do not share /dev/input, then entering the container’s namespace (via --target) is required for mknod to have the intended effect, and the host namespace of vuinputd must have CAP_MKNOD.

(Edit: made the paragraph more technical precise)

/run/udev/data issue

This might have a lot of reasons. You can try to make sure that a writable /run is available in the container by creating a tmpfs there. Can you describe your detailed setup? Then, I'll try to write an integration test for that kind of setup and provide more detailed debugging messages. I haven't finished it yet, but I have documented my setup on https://github.com/joleuger/containerized-headless-sunshine-howto. Just one note for now: Actually calling

mkdir -p /run/udev/data
/usr/bin/touch /run/udev/control

is important for libinput to read the udev messages. That is a known libinput quirk.

I'm hoping I don't have a fundamental misunderstanding here, but it seems like it'll be possible to do this entirely unprivileged (outside of the daemon running with CAP_MKNOD and other relevant capabilities)
That is exactly my goal.

In the next days, I'll add some code that makes it easier to debug with strace.

<!-- gh-comment-id:3693863766 --> @joleuger commented on GitHub (Dec 27, 2025): Thank you for trying out vuinputd. As it is in a prototype stage, any comments or issues or input is valuable. > Permission errors occur when MKNOD is attempted from within the container. I got around this error by dropping the --target-namespace arguments for Action::MknodDevice. The `--target` option was designed to enter the mount namespace of a container process in cases where the container has its own private `/dev/input`, different from the host. In this mode, `mknod` is executed as root but *inside the target namespace*, so that the device node is created in the container’s `/dev`. Dropping `--target` is only correct if the daemon if the host and container share the same `/dev/input`, in which case no `mknod` is required at all because the device node is already present. If the host and container do *not* share `/dev/input`, then entering the container’s namespace (via `--target`) is required for `mknod` to have the intended effect, and the host namespace of vuinputd must have `CAP_MKNOD`. (Edit: made the paragraph more technical precise) > /run/udev/data issue This might have a lot of reasons. You can try to make sure that a writable /run is available in the container by creating a tmpfs there. Can you describe your detailed setup? Then, I'll try to write an integration test for that kind of setup and provide more detailed debugging messages. I haven't finished it yet, but I have documented my setup on https://github.com/joleuger/containerized-headless-sunshine-howto. Just one note for now: Actually calling >mkdir -p /run/udev/data /usr/bin/touch /run/udev/control is important for libinput to read the udev messages. That is a known libinput quirk. > I'm hoping I don't have a fundamental misunderstanding here, but it seems like it'll be possible to do this entirely unprivileged (outside of the daemon running with CAP_MKNOD and other relevant capabilities) That is exactly my goal. In the next days, I'll add some code that makes it easier to debug with strace.
Author
Owner

@joleuger commented on GitHub (Dec 28, 2025):

have added some additional debugging support to the current HEAD.
Whenever mknod is supposed to be executed inside a container, vuinputd now emits additional debug output, for example:

[2025-12-27T23:45:06Z DEBUG vuinputd::job_engine::job] Executing job: mknod input device in container
[2025-12-27T23:45:06Z DEBUG vuinputd::process_tools] In case you need to debug the system calls, call strace vuinputd --target-namespace /proc/103086/ns --action-base64 eyJhY3Rpb24iOiJta25vZC1kZXZpY2UiLCJwYXRoIjoiL2Rldi9pbnB1dC9ldmVudDEyIiwibWFqb3IiOjEzLCJtaW5vciI6NzZ9

If you run the suggested strace command, the output might look like this:

strace target/debug/vuinputd --target-namespace /proc/102044/ns --action-base64 eyJhY3Rpb24iOiJta25vZC1kZXZpY2UiLCJwYXRoIjoiL2Rldi9pbnB1dC9ldmVudDEyIiwibWFqb3IiOjEzLCJtaW5vciI6NzZ9
...
statx(AT_FDCWD, "/proc/102044/ns", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7ffeb6bea3c0) = -1 ENOENT (No such file or directory)
...
thread 'main' (103610) panicked at vuinputd/src/main.rs:170:84:
called Result::unwrap() on an Err value: the root process of the container whose namespaces we want to enter does not exist anymore
...
+++ exited with 101 +++

In this example, the output clearly shows that the container process no longer exists at the time vuinputd tries to enter its namespace.
This kind of output should make it easier to distinguish between:

  • missing or terminated container processes,
  • permission issues caused by seccomp, SELinux, or AppArmor,
  • general misconfigurations,
  • or other kernel-level restrictions (e.g. eBPF-based policies).
<!-- gh-comment-id:3694309092 --> @joleuger commented on GitHub (Dec 28, 2025): have added some additional debugging support to the current HEAD. Whenever mknod is supposed to be executed inside a container, vuinputd now emits additional debug output, for example: >[2025-12-27T23:45:06Z DEBUG vuinputd::job_engine::job] Executing job: mknod input device in container [2025-12-27T23:45:06Z DEBUG vuinputd::process_tools] In case you need to debug the system calls, call `strace vuinputd --target-namespace /proc/103086/ns --action-base64 eyJhY3Rpb24iOiJta25vZC1kZXZpY2UiLCJwYXRoIjoiL2Rldi9pbnB1dC9ldmVudDEyIiwibWFqb3IiOjEzLCJtaW5vciI6NzZ9` If you run the suggested strace command, the output might look like this: >strace target/debug/vuinputd --target-namespace /proc/102044/ns --action-base64 eyJhY3Rpb24iOiJta25vZC1kZXZpY2UiLCJwYXRoIjoiL2Rldi9pbnB1dC9ldmVudDEyIiwibWFqb3IiOjEzLCJtaW5vciI6NzZ9 ... statx(AT_FDCWD, "/proc/102044/ns", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7ffeb6bea3c0) = -1 ENOENT (No such file or directory) ... thread 'main' (103610) panicked at vuinputd/src/main.rs:170:84: called `Result::unwrap()` on an `Err` value: the root process of the container whose namespaces we want to enter does not exist anymore ... +++ exited with 101 +++ In this example, the output clearly shows that the container process no longer exists at the time vuinputd tries to enter its namespace. This kind of output should make it easier to distinguish between: - missing or terminated container processes, - permission issues caused by seccomp, SELinux, or AppArmor, - general misconfigurations, - or other kernel-level restrictions (e.g. eBPF-based policies).
Author
Owner

@ShaneTRS commented on GitHub (Dec 29, 2025):

The added debug information was extremely helpful, thank you! I managed to get it working. Though I did have some more troubles with /run/data, I managed to find a workaround via -v /tmp/stardust-run/$PORT:/run:rslave,rw

I thought maybe it was a UID mapping problem, but even something like --tmpfs /run:mode=777 didn't work

strace.txt

podman run --rm --detach "$@"  \
--name /stardust-$PORT \
--tty \
--interactive \
--device /dev/vuinput:/dev/uinput \
--device /dev/dri \
-v /dev/input:/dev/input:rslave,rw \
-v /tmp/stardust-run/$PORT:/run:rslave,rw \
--publish "$RANGE_A:$RANGE_A/tcp" \
--publish "$RANGE_B:$RANGE_B" \
--publish "$RANGE_C:$RANGE_C/udp" \
-v ./data:/data:ro \
-v ./flatpak:/home/app/.var:rw \
-v ./flatpak/share:/home/app/.local/share/flatpak:rw \
-v ./config/sunshine:/home/app/.config/sunshine:rw \
-v ./config/wireplumber:/home/app/.config/wireplumber:ro \
-e PORT=$PORT \
--group-add 1 \
--security-opt unmask=ALL \
stardust:latest
<!-- gh-comment-id:3696286053 --> @ShaneTRS commented on GitHub (Dec 29, 2025): The added debug information was extremely helpful, thank you! I managed to get it working. Though I did have some more troubles with /run/data, I managed to find a workaround via `-v /tmp/stardust-run/$PORT:/run:rslave,rw` I thought maybe it was a UID mapping problem, but even something like `--tmpfs /run:mode=777` didn't work [strace.txt](https://github.com/user-attachments/files/24367532/strace.txt) ``` podman run --rm --detach "$@" \ --name /stardust-$PORT \ --tty \ --interactive \ --device /dev/vuinput:/dev/uinput \ --device /dev/dri \ -v /dev/input:/dev/input:rslave,rw \ -v /tmp/stardust-run/$PORT:/run:rslave,rw \ --publish "$RANGE_A:$RANGE_A/tcp" \ --publish "$RANGE_B:$RANGE_B" \ --publish "$RANGE_C:$RANGE_C/udp" \ -v ./data:/data:ro \ -v ./flatpak:/home/app/.var:rw \ -v ./flatpak/share:/home/app/.local/share/flatpak:rw \ -v ./config/sunshine:/home/app/.config/sunshine:rw \ -v ./config/wireplumber:/home/app/.config/wireplumber:ro \ -e PORT=$PORT \ --group-add 1 \ --security-opt unmask=ALL \ stardust:latest ```
Author
Owner

@joleuger commented on GitHub (Dec 29, 2025):

Thank you for the detailed report and the strace logs. The investigation clearly shows that the EOVERFLOW (Value too large for defined data type) is caused by a User Namespace ID mapping mismatch.

The Technical Cause (my current hypothesis)

When the tool enters the container’s Mount Namespace but remains in the Host’s User Namespace, it creates a "cross-namespace" conflict:

  1. The process is Host Root (UID 0).
  2. The target filesystem (/run on tmpfs) is owned by the Container's Root (e.g., Host UID 100000).
  3. Because Host UID 0 is typically not mapped in a rootless or remapped Podman container, the kernel cannot represent the caller as a valid owner/creator for new entries on that filesystem, triggering the EOVERFLOW.

Proposed Solution: "Ownership Alignment"

To fix this without losing the CAP_MKNOD privileges required for device creation, we need to align our filesystem identity with the container's expectations. I am considering two implementation paths:

  • Option A: setfsuid / setfsgid (Surgical)
    We temporarily adopt the target filesystem's UID/GID for IO operations only. This is the traditional way systems tools (like NFS servers or nsenter) handle this without dropping process capabilities.
  • Option B: setresuid + SECBIT_KEEP_CAPS (Modern/Strict)
    A more modern approach that changes the Effective UID. This is more verbose as it requires managing prctl securebits and manually restoring the effective capability set via capset, but it avoids the "deprecated" feel of setfsuid.

The Plan

I will proceed as follows:

  1. Integration Test: I am writing a reproduction test using Podman to simulate a remapped UID environment. This will confirm the EOVERFLOW in our CI pipeline.
  2. Implementation: Once the failure is reproduced, I will implement one of the alignment methods above (leaning toward the most robust and maintainable one for our specific capability requirements).
  3. Validation: Verify that the tool can successfully mkdir and mknod within the container's /run without requiring the host-volume workaround.

Thanks again to @ShaneTRS for the excellent investigation!

<!-- gh-comment-id:3697722162 --> @joleuger commented on GitHub (Dec 29, 2025): Thank you for the detailed report and the `strace` logs. The investigation clearly shows that the `EOVERFLOW` (Value too large for defined data type) is caused by a **User Namespace ID mapping mismatch**. ### The Technical Cause (my current hypothesis) When the tool enters the container’s **Mount Namespace** but remains in the **Host’s User Namespace**, it creates a "cross-namespace" conflict: 1. The process is Host Root (UID 0). 2. The target filesystem (`/run` on `tmpfs`) is owned by the Container's Root (e.g., Host UID 100000). 3. Because Host UID 0 is typically not mapped in a rootless or remapped Podman container, the kernel cannot represent the caller as a valid owner/creator for new entries on that filesystem, triggering the `EOVERFLOW`. ### Proposed Solution: "Ownership Alignment" To fix this without losing the `CAP_MKNOD` privileges required for device creation, we need to align our filesystem identity with the container's expectations. I am considering two implementation paths: * **Option A: `setfsuid` / `setfsgid` (Surgical)** We temporarily adopt the target filesystem's UID/GID for IO operations only. This is the traditional way systems tools (like NFS servers or `nsenter`) handle this without dropping process capabilities. * **Option B: `setresuid` + `SECBIT_KEEP_CAPS` (Modern/Strict)** A more modern approach that changes the Effective UID. This is more verbose as it requires managing `prctl` securebits and manually restoring the effective capability set via `capset`, but it avoids the "deprecated" feel of `setfsuid`. ### The Plan I will proceed as follows: 1. **Integration Test:** I am writing a reproduction test using Podman to simulate a remapped UID environment. This will confirm the `EOVERFLOW` in our CI pipeline. 2. **Implementation:** Once the failure is reproduced, I will implement one of the alignment methods above (leaning toward the most robust and maintainable one for our specific capability requirements). 3. **Validation:** Verify that the tool can successfully `mkdir` and `mknod` within the container's `/run` without requiring the host-volume workaround. Thanks again to @ShaneTRS for the excellent investigation!
Author
Owner

@joleuger commented on GitHub (Jan 16, 2026):

I am doing some tests, I am just writing down some notes here:

  • podman with runc as root is working as expected. For podman to be able to access the created devices without -v /dev/input:/dev/input:rslave,rw , I need the flag --device-cgroup-rule="c 13:* rwm".
  • When I run this in rootless mode, I get the error Error: device cgroup rules are not supported in rootless mode or in a user namespace => -v /dev/input:/dev/input:rslave,rw is currently required

Next: more rootless tests

<!-- gh-comment-id:3762193280 --> @joleuger commented on GitHub (Jan 16, 2026): I am doing some tests, I am just writing down some notes here: - podman with runc as root is working as expected. For podman to be able to access the created devices without `-v /dev/input:/dev/input:rslave,rw` , I need the flag `--device-cgroup-rule="c 13:* rwm"`. - When I run this in rootless mode, I get the error `Error: device cgroup rules are not supported in rootless mode or in a user namespace` => `-v /dev/input:/dev/input:rslave,rw` is currently required Next: more rootless tests
Author
Owner

@joleuger commented on GitHub (Jan 20, 2026):

The original request should be complete. It is now possible to set vuinputd into a mode in which vuinputd does not try to get into the container and create the devices and udev files, but creates them on the host. Then, the user needs to bind-mount the directories on the host. See https://github.com/joleuger/vuinputd/blob/main/docs/USAGE.md

mkdir -p /run/vuinputd/vuinput
mount -t tmpfs -o mode=755,size=1M tmpfs /run/vuinputd/vuinput
mkdir -p /run/vuinputd/vuinput/dev-input
mkdir -p /run/vuinputd/vuinput/udev

vuinputd --placement on-host

podman run --rm 
--device /dev/vuinput:/dev/uinput \
-v /run/vuinputd/vuinput/dev-input:/dev/input:rslave,rw \
-v /run/vuinputd/vuinput/udev:/run/udev:rslave,rw

Some tests need to be completed, before I create a release with this new functionality. But an automatic integration test has been setup. Also, I am unsure if mknod the character device files into /run/vuinputd-folder is actually a good idea. Maybe bind-mounting them is better.

<!-- gh-comment-id:3775380266 --> @joleuger commented on GitHub (Jan 20, 2026): The original request should be complete. It is now possible to set vuinputd into a mode in which vuinputd does not try to get into the container and create the devices and udev files, but creates them on the host. Then, the user needs to bind-mount the directories on the host. See https://github.com/joleuger/vuinputd/blob/main/docs/USAGE.md ``` mkdir -p /run/vuinputd/vuinput mount -t tmpfs -o mode=755,size=1M tmpfs /run/vuinputd/vuinput mkdir -p /run/vuinputd/vuinput/dev-input mkdir -p /run/vuinputd/vuinput/udev vuinputd --placement on-host podman run --rm --device /dev/vuinput:/dev/uinput \ -v /run/vuinputd/vuinput/dev-input:/dev/input:rslave,rw \ -v /run/vuinputd/vuinput/udev:/run/udev:rslave,rw ``` Some tests need to be completed, before I create a release with this new functionality. But an automatic integration test has been setup. Also, I am unsure if mknod the character device files into /run/vuinputd-folder is actually a good idea. Maybe bind-mounting them is better.
Sign in to join this conversation.
No labels
pull-request
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
joleuger/vuinputd#3
No description provided.