From 8a7af069837585ff76d3f32318ff06dd14c9c73a Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 29 Oct 2026 08:33:23 +0900 Subject: [PATCH] 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. --- README.md | 2 +- agent-container.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2072a9e..929f4e1 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ socket goes away. ## Install -You need rootless Podman on the host. On Arch Linux the packages `podman` and `passt` (or `slirp4netns`) are enough; your user also needs subuid/subgid entries, which `usermod --add-subuids 100000-165535 --add-subgids 100000-165535 ` sets up if the package did not: +You need rootless Podman on the host. On Arch Linux the packages `podman` and `passt` (or `slirp4netns`) are enough; your user also needs subuid/subgid entries, which `usermod --add-subuids 100000-165535 --add-subgids 100000-165535 ` sets up if the package did not (accounts created before shadow 4.11.1-3 get no entries automatically — see [Podman: Set subuid and subgid](https://wiki.archlinux.org/title/Podman#Set_subuid_and_subgid)): ```sh sudo pacman -S podman passt diff --git a/agent-container.py b/agent-container.py index a7312b6..1029344 100755 --- a/agent-container.py +++ b/agent-container.py @@ -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})" )