container: Fail early on missing subuid/subgid ranges

Rootless Podman falls back to a single-UID user namespace when the
invoking user has no /etc/subuid and /etc/subgid entry, and then any
chown to an unmapped UID fails with EINVAL. Newer pacman versions hit
this during builds and updates: their download sandbox chowns its
temporary download directory to the 'alpm' user, which made image
builds fail with a cryptic traceback after several minutes of output.

Accounts created manually or before shadow 4.11.1-3 (October 2022) get
no subordinate ID ranges automatically, while freshly created ones do.
Rather than working around this in the image (disabling pacman's
sandbox would silently drop a security feature), detect the missing
ranges before building and stop with the exact fix, using the first
unclaimed 65536-ID block so the printed command never collides with
other users' ranges. Link the Arch Wiki section and Podman's rootless
tutorial for background.
This commit is contained in:
Jeena 2026-10-29 08:33:23 +09:00
parent fa32cc4218
commit 8a7af06983
2 changed files with 83 additions and 1 deletions

View file

@ -363,7 +363,89 @@ class AgentContainer:
== 0
)
# Subordinate ID block size; rootless Podman needs at least 65536 IDs so
# every container UID is mappable. 100000 is the default first block the
# shadow suite allocates on Arch (and the number the Arch Wiki suggests).
SUBID_SIZE = 65536
SUBID_START = 100000
@classmethod
def _free_subid_range(cls) -> str:
"""Return the first unclaimed 65536-ID block as "start-end".
Ranges in /etc/subuid and /etc/subgid must never overlap between
users (a shared range would let one user impersonate another inside
containers), so both files' claims are considered.
"""
taken = []
for path in ("/etc/subuid", "/etc/subgid"):
try:
with open(path) as f:
for line in f:
parts = line.split(":")
if len(parts) < 3:
continue
try:
taken.append(
(int(parts[1]), int(parts[1]) + int(parts[2]))
)
except ValueError:
continue
except FileNotFoundError:
pass
taken.sort()
start = cls.SUBID_START
for lo, hi in taken:
if hi <= start:
continue
if lo - start >= cls.SUBID_SIZE:
break
start = max(start, hi)
return f"{start}-{start + cls.SUBID_SIZE - 1}"
def _subid_guard(self) -> None:
"""Warn when rootless Podman has no subuid/subgid ranges for this user.
Without a range in /etc/subuid and /etc/subgid, Podman falls back to a
single-UID user namespace: the container only has the invoking user's
UID, and chown to any other UID fails with EINVAL. This breaks e.g.
pacman's download sandbox, which chowns its download directory to the
'alpm' user. Distros like Fedora allocate ranges for every user
automatically, but accounts created manually (e.g. on Arch) may not
have one.
"""
if self.RUNTIME != "podman" or os.geteuid() == 0:
return
def has_entry(path: str) -> bool:
try:
with open(path) as f:
return any(line.split(":", 1)[0] == self.host_username for line in f)
except FileNotFoundError:
return False
missing = [
p for p in ("/etc/subuid", "/etc/subgid") if not has_entry(p)
]
if not missing:
return
range_str = self._free_subid_range()
logger.error(
f"No subuid/subgid entry for user '{self.host_username}' in"
" /etc/subuid, /etc/subgid — rootless Podman has only a single UID"
" available, so operations inside containers that chown to other"
" UIDs (e.g. pacman's alpm download sandbox) fail with 'Invalid"
" argument'. Fix once per user:\n"
f" sudo usermod --add-subuids {range_str} --add-subgids {range_str} {self.host_username}\n"
" podman system migrate\n"
" See https://wiki.archlinux.org/title/Podman#Set_subuid_and_subgid"
" or https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md"
)
sys.exit(1)
def build_image(self, no_cache: bool = False, pull: bool = False) -> None:
self._subid_guard()
logger.info(
f"Building image '{self.IMAGE}' with user {self.host_username} ({self.host_uid}:{self.host_gid})"
)