Skip to content

fix(daemon): accept single-uid userns overflow-owned ancestors (#1830) - #2138

Merged
DeusData merged 1 commit into
mainfrom
fix/decision-bd
Sep 9, 2026
Merged

DeusData merged 1 commit into
mainfrom
fix/decision-bd

Conversation

@DeusData

@DeusData DeusData commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Problem

Inside a single-uid user namespace (unshare -U --map-current-user, podman --userns=keep-id), the root-owned top-level ancestors (/, /tmp) appear owned by the kernel overflow uid (nobody). The daemon's directory-security walk refused every overflow-owned ancestor, so it could not create its private runtime directory and refused to start in that legitimate setup.

Fix (#1830)

Accept an overflow-owned ancestor only when /proc/self/uid_map is a single-uid map (exactly one line, count == 1, inside == euid) and /proc/sys/kernel/overflowuid is readable (the value is read from /proc, never hardcoded; unreadable ⇒ no tolerance). No environment escape hatch. Group-writable-overflow ancestors are refused (an unmapped owner's host group is unknowable); world-writable-overflow is tolerated only with the sticky bit (the /tmp shape).

Why it's safe

The tolerance is confined to the ancestor walk (posix_directory_parent_secure, posix_directory_transition_secure). The directories that actually hold data stay strict == geteuid(): the created-dir recheck (ipc.c:1687) and the final-dir gate (ipc.c:1714). These two euid gates are the security-critical backstop — a host-squatted, overflow-owned private directory is still refused there, so the overflow tolerance only widens which ancestors the walk may traverse, never the destination. In a single-uid namespace uid_map is write-once/immutable and no in-namespace principal can own or mutate an overflow-owned file, so an overflow-owned ancestor is exactly as safe as a root-owned one on the host.

Verification

  • The single-uid gate refuses a multi-uid map, count != 1, inside != euid, and any second mapping line; a mapped-out foreign uid is refused even with tolerance ON.
  • macOS 52 / 1-skip; Linux (Colima arm64, gcc) 47 / 1-skip — exercises the __linux__ /proc path, plus a real unshare(CLONE_NEWUSER) end-to-end smoke (whitelisted-skip where userns is unavailable).
  • RED-on-revert: neutering the overflow clause fails the ancestor-decision test; restoring is byte-identical and green.
  • clang-format + cppcheck clean; localized to src/daemon/ipc.c (+ test seams).

Adversarially reviewed as a security-boundary change: no privilege-boundary fail-open found; the safety rests on the two == geteuid() gates above staying strict.

Inside a 1:1 user namespace (podman --userns=keep-id, unshare -U
--map-current-user), the root-owned ancestors /, /tmp appear owned by the
kernel overflow uid; the daemon's ancestor-ownership walk refused every
overflow-owned ancestor, making the daemon unusable in that legitimate setup.

Tolerate an overflow-owned ancestor ONLY when /proc/self/uid_map is a
single-uid map whose sole inside id is our euid AND /proc/sys/kernel/overflowuid
is readable (never hardcoded; unreadable => no tolerance). No environment
escape hatch. Group-writable overflow ancestor refused (host group unknown);
world-writable overflow tolerated only with the sticky bit (the /tmp shape).
The created/final/snapshot gates stay strict == geteuid(), so a host-squatted
overflow-owned private directory is still refused; the tolerance only widens
which ancestors the walk traverses. macOS byte-identical (overflow=-1).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant