Skip to content

chroot: RUN steps always fail with "error setting supplemental groups list: operation not permitted" when the user namespace denies setgroups (regression of #2459) #6947

Description

@michelemodolo

Description

When buildah runs rootless inside a single-ID user namespace whose
/proc/self/setgroups is deny
(the unavoidable situation when the invoking UID has
no subuid/subgid ranges and no privileged newuidmap/newgidmap helpers — e.g.
Kubernetes/OpenShift pods under a restricted SCC, or podman run --user <uid> into an
image with no subid configuration), every Dockerfile RUN step under
--isolation chroot fails
:

error setting supplemental groups list: operation not permitted
subprocess exited with status 1

The chroot runtime has a deny-tolerant branch that was added for exactly this
environment (see #2459, closed as completed in 2020), but it is unreachable today:
the spec generation unconditionally injects the primary GID into the RUN user's
supplemental groups, so the group list is never empty and setgroups() is always
attempted — and in a setgroups-denied namespace that call can only return EPERM.

Steps to reproduce

No cluster needed; rootless podman on any host reproduces it:

# 1. An environment image: buildah + a passwd entry for an arbitrary uid.
#    (The passwd row only avoids an unrelated startup failure of the re-exec'd
#    chroot child, so that the setgroups bug is reached.)
cat > /tmp/env.Containerfile <<'EOF'
FROM registry.access.redhat.com/ubi9/ubi-minimal
RUN microdnf -y install buildah \
 && echo "build:x:1000:0::/tmp/h:/sbin/nologin" >> /etc/passwd
EOF
podman build -t buildah-setgroups-repro -f /tmp/env.Containerfile

# 2. Run it as uid 1000 (no subuid/subgid ranges inside the container),
#    and do a trivial build with one RUN step.
podman run --rm --user 1000:0 buildah-setgroups-repro sh -c '
  export HOME=/tmp/h; mkdir -p /tmp/h /tmp/ctx
  printf "FROM registry.access.redhat.com/ubi9/ubi-minimal\nRUN true\n" > /tmp/ctx/Containerfile
  buildah build --isolation chroot --storage-driver vfs \
    --storage-opt vfs.ignore_chown_errors=true \
    -f /tmp/ctx/Containerfile /tmp/ctx'

(ignore_chown_errors only lets the base image extract in the single-ID namespace;
it is unrelated to the bug itself.)

Describe the results you received

STEP 2/2: RUN true
error setting supplemental groups list: operation not permitted
subprocess exited with status 1
Error: building at STEP "RUN true": exit status 1

Describe the results you expected

The RUN step executes. When /proc/self/setgroups reads deny, the kernel has
permanently frozen the supplementary-group set of every process in the namespace —
setgroups() can never succeed and can never be needed, so it should be skipped
(which is exactly what the existing else branch already does for the empty-list
case).

Analysis

Two interacting pieces (permalinks to v1.43.1):

  1. run_common.go#L285
    configureUIDGID unconditionally does g.AddProcessAdditionalGid(user.GID), so
    spec.Process.User.AdditionalGids is never empty for a RUN step.
  2. chroot/run_common.go#L699-L718
    — the non-empty branch calls syscall.Setgroups(gids) and hard-exits on error, with
    no deny check; the deny check exists only in the else (empty-list) branch, which
    (1) makes unreachable.

Because of (1), the deny-tolerance added after #2459 (see also PR #1640) no longer has
any effect — the scenario that issue was closed for is broken again. This reproduces
on every tag I checked back to v1.29 and the code is unchanged on main as of
2026-07-08.

Workarounds that do not help, for completeness:

  • --group-add keep-groups — only sets the run.oci.keep_original_groups annotation,
    which the chroot isolation code never reads; the primary-GID injection happens
    regardless.
  • --userns-uid-map 0:0:1 --userns-gid-map 0:0:1 — explicit maps route layer storage
    through id-mapping, which hard-fails extracting any base image containing files owned
    by other ids (container ID N cannot be mapped to a host ID), and
    ignore_chown_errors does not apply to that path.

Suggested fix

Check the namespace's setgroups state first and skip the doomed call (we have been
running this in production CI since 2026-07-08 without regressions; the same read is
already done in the else branch). Happy to open a PR:

--- a/chroot/run_common.go
+++ b/chroot/run_common.go
@@ -696,19 +696,27 @@
 
 	// Drop privileges.
 	user := options.Spec.Process.User
+	// When this user namespace was created without privileged id-mapping
+	// helpers, the kernel has setgroups permanently denied and ANY
+	// setgroups() call fails with EPERM. The supplementary-group set is
+	// frozen by the kernel in that case, so skipping is the only possible
+	// behavior.
+	setgroupsState, _ := os.ReadFile("/proc/self/setgroups")
+	setgroupsDenied := strings.Trim(string(setgroupsState), "\n") == "deny"
 	if len(user.AdditionalGids) > 0 {
-		gids := make([]int, len(user.AdditionalGids))
-		for i := range user.AdditionalGids {
-			gids[i] = int(user.AdditionalGids[i])
-		}
-		logrus.Debugf("setting supplemental groups")
-		if err = syscall.Setgroups(gids); err != nil {
-			fmt.Fprintf(os.Stderr, "error setting supplemental groups list: %v\n", err)
-			os.Exit(1)
+		if setgroupsDenied {
+			logrus.Debugf("setgroups is denied in this user namespace; not setting supplemental groups")
+		} else {
+			gids := make([]int, len(user.AdditionalGids))
+			for i := range user.AdditionalGids {
+				gids[i] = int(user.AdditionalGids[i])
+			}
+			logrus.Debugf("setting supplemental groups")
+			if err = syscall.Setgroups(gids); err != nil {
+				fmt.Fprintf(os.Stderr, "error setting supplemental groups list: %v\n", err)
+				os.Exit(1)
+			}
 		}
 	} else {
-		setgroups, _ := os.ReadFile("/proc/self/setgroups")
-		if strings.Trim(string(setgroups), "\n") != "deny" {
+		if !setgroupsDenied {
 			logrus.Debugf("clearing supplemental groups")
 			if err = syscall.Setgroups([]int{}); err != nil {
 				fmt.Fprintf(os.Stderr, "error clearing supplemental groups list: %v\n", err)

An alternative (possibly preferable) fix is to stop force-adding the primary GID in
configureUIDGID when it would be the only entry — that would also make the existing
deny-guard reachable again.

Version

buildah version 1.43.1 (image-spec 1.1.1, runtime-spec 1.2.1)

(RHEL 9 build buildah-1.43.1-*.el9_8; the relevant code is identical on main as of
2026-07-08.)

Environment

  • --isolation chroot, --storage-driver vfs.
  • Reproduced: rootless podman on kernel 6.6 (repro above) and Kubernetes/OpenShift
    runner pods under a restricted SCC (non-root UID, no subuid/subgid ranges,
    allowPrivilegeEscalation: false, so newuidmap/newgidmap file capabilities are
    inert and the namespace is created with setgroups = deny).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions