[PR #59] [MERGED] fix(mounts): host-back XDG_RUNTIME_DIR + stop tmpfs tripping the disk guardrail (#53) #58

Closed
opened 2026-10-15 12:51:42 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/games-on-whales/LXC2Docker/pull/59
Author: @JBailes
Created: 6/5/2026
Status: ✅ Merged
Merged: 6/5/2026
Merged by: @JBailes

Base: main ← Head: fix/wolf-pulse-runtimedir-hostbacked


📝 Commits (1)

  • 4976822 fix(mounts): host-back XDG_RUNTIME_DIR; stop tmpfs tripping disk guardrail

📊 Changes

4 files changed (+161 additions, -1 deletions)

View changed files

📝 internal/api/containers.go (+65 -0)
📝 internal/api/sibling_mount_test.go (+54 -0)
📝 internal/lxc/diskspace.go (+20 -1)
📝 internal/lxc/diskspace_test.go (+22 -0)

📄 Description

Follow-up to #54 / #55 for the remaining Wolf PulseAudio failures on Proxmox reported in #53. Two distinct issues blocked the wolfpulseaudio sibling from starting.

1. Sibling runtime-dir bind failed with EINVAL

Wolf passes its own XDG_RUNTIME_DIR (/run/user/wolf) as a bind source to its PulseAudio/app siblings so they can reach its audio socket. Wolf creates that directory at runtime inside its own mount namespace, so nothing on the host backs it. After #55, translateSiblingBindSource could only resolve it through the owner's live namespace as /proc/<pid>/root/run/user/wolf:

safe_mount: Invalid argument - Failed to mount "/proc/2219714/root/run/user/wolf" onto ".../tmp/pulse"

Bind-mounting a path that lives in another container's mount namespace is refused by the kernel with EINVAL. (This is why the symptom changed from the earlier ENOENT: once Pass 2 resolved the path via /proc/<pid>/root, os.Stat passed but mount(2) will always reject it.) Reaching into a live namespace is also fragile: the PID is captured at create time and the tmpfs only exists while the owner runs.

Fix: host-back the runtime dir. When a container declares an absolute XDG_RUNTIME_DIR that no user mount already covers, create a real host directory (<state-dir>/runtime/<id>) and bind it there. Wolf's /run/user/wolf then becomes an ordinary host bind, so when the PulseAudio sibling is created sharing that path, translateViaBindDest (the stable host-path case) maps it to the same host directory. Both containers bind the same real directory, exactly as sharing through a Docker volume would, and it survives the owner restarting. Guarded so explicit user binds are never overridden.

2. Disk-pressure guardrail false positive

The create pre-flight added in #57 statfs'd the shared runtime dir, a small tmpfs, and refused the create:

manager: refusing create: bind source /proc/.../run/user/wolf has only 1.9GiB free (below 2.0GiB threshold)

(The reporter worked around it with --storage-opt size=8G.) tmpfs free space reflects available RAM, not the host filesystem the guardrail protects: a container filling a tmpfs hits ENOSPC on the tmpfs itself, it cannot eat the host disk. Fix: skip tmpfs (and ramfs) bind sources in both the create pre-flight and the background watcher.

Tests

  • TestEnsureRuntimeDirMount — host-backing produces the bind, creates the dir, lets a sibling resolve to the host path, and never overrides a user mount or fires on a missing/relative value.
  • TestCheckCreateDiskPressureSkipsTmpfs — a tmpfs bind source (/dev/shm) does not trip the guardrail even with an impossibly high threshold.
  • go build ./..., go vet, full go test ./..., and gofmt all clean.

Validation caveat

I could not reproduce the live Wolf-on-Proxmox audio path here, so this needs validation on the reporter's box. Two things to watch:

  • If Wolf's image mounts a tmpfs over /run at runtime, it could shadow the host-backed /run/user/wolf bind.
  • Socket ownership: the dir is created 0777 so both the owner (root) and the app-uid siblings can create their sockets; PulseAudio's own runtime-dir permission expectations on /tmp/pulse may need a follow-up.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/games-on-whales/LXC2Docker/pull/59 **Author:** [@JBailes](https://github.com/JBailes) **Created:** 6/5/2026 **Status:** ✅ Merged **Merged:** 6/5/2026 **Merged by:** [@JBailes](https://github.com/JBailes) **Base:** `main` ← **Head:** `fix/wolf-pulse-runtimedir-hostbacked` --- ### 📝 Commits (1) - [`4976822`](https://github.com/games-on-whales/LXC2Docker/commit/497682298a9a5b2dc68bc7ef6631ae525c9eb09a) fix(mounts): host-back XDG_RUNTIME_DIR; stop tmpfs tripping disk guardrail ### 📊 Changes **4 files changed** (+161 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `internal/api/containers.go` (+65 -0) 📝 `internal/api/sibling_mount_test.go` (+54 -0) 📝 `internal/lxc/diskspace.go` (+20 -1) 📝 `internal/lxc/diskspace_test.go` (+22 -0) </details> ### 📄 Description Follow-up to #54 / #55 for the remaining Wolf PulseAudio failures on Proxmox reported in #53. Two distinct issues blocked the `wolfpulseaudio` sibling from starting. ## 1. Sibling runtime-dir bind failed with `EINVAL` Wolf passes its own `XDG_RUNTIME_DIR` (`/run/user/wolf`) as a bind source to its PulseAudio/app siblings so they can reach its audio socket. Wolf creates that directory at runtime *inside its own mount namespace*, so nothing on the host backs it. After #55, `translateSiblingBindSource` could only resolve it through the owner's live namespace as `/proc/<pid>/root/run/user/wolf`: ``` safe_mount: Invalid argument - Failed to mount "/proc/2219714/root/run/user/wolf" onto ".../tmp/pulse" ``` Bind-mounting a path that lives in another container's mount namespace is refused by the kernel with `EINVAL`. (This is why the symptom changed from the earlier `ENOENT`: once Pass 2 resolved the path via `/proc/<pid>/root`, `os.Stat` passed but `mount(2)` will always reject it.) Reaching into a live namespace is also fragile: the PID is captured at create time and the tmpfs only exists while the owner runs. **Fix:** host-back the runtime dir. When a container declares an absolute `XDG_RUNTIME_DIR` that no user mount already covers, create a real host directory (`<state-dir>/runtime/<id>`) and bind it there. Wolf's `/run/user/wolf` then becomes an ordinary host bind, so when the PulseAudio sibling is created sharing that path, `translateViaBindDest` (the stable host-path case) maps it to the same host directory. Both containers bind the same real directory, exactly as sharing through a Docker volume would, and it survives the owner restarting. Guarded so explicit user binds are never overridden. ## 2. Disk-pressure guardrail false positive The create pre-flight added in #57 statfs'd the shared runtime dir, a small tmpfs, and refused the create: ``` manager: refusing create: bind source /proc/.../run/user/wolf has only 1.9GiB free (below 2.0GiB threshold) ``` (The reporter worked around it with `--storage-opt size=8G`.) tmpfs free space reflects available RAM, not the host filesystem the guardrail protects: a container filling a tmpfs hits `ENOSPC` on the tmpfs itself, it cannot eat the host disk. **Fix:** skip tmpfs (and ramfs) bind sources in both the create pre-flight and the background watcher. ## Tests - `TestEnsureRuntimeDirMount` — host-backing produces the bind, creates the dir, lets a sibling resolve to the host path, and never overrides a user mount or fires on a missing/relative value. - `TestCheckCreateDiskPressureSkipsTmpfs` — a tmpfs bind source (`/dev/shm`) does not trip the guardrail even with an impossibly high threshold. - `go build ./...`, `go vet`, full `go test ./...`, and `gofmt` all clean. ## Validation caveat I could not reproduce the live Wolf-on-Proxmox audio path here, so this needs validation on the reporter's box. Two things to watch: - If Wolf's image mounts a tmpfs over `/run` at runtime, it could shadow the host-backed `/run/user/wolf` bind. - Socket ownership: the dir is created `0777` so both the owner (root) and the app-uid siblings can create their sockets; PulseAudio's own runtime-dir permission expectations on `/tmp/pulse` may need a follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-15 12:51:42 +00:00
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
games-on-whales/LXC2Docker#58
No description provided.