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):
run_common.go#L285
— configureUIDGID unconditionally does g.AddProcessAdditionalGid(user.GID), so
spec.Process.User.AdditionalGids is never empty for a RUN step.
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).
Description
When buildah runs rootless inside a single-ID user namespace whose
/proc/self/setgroupsisdeny(the unavoidable situation when the invoking UID hasno subuid/subgid ranges and no privileged
newuidmap/newgidmaphelpers — e.g.Kubernetes/OpenShift pods under a restricted SCC, or
podman run --user <uid>into animage with no subid configuration), every Dockerfile
RUNstep under--isolation chrootfails: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 alwaysattempted — 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:
(
ignore_chown_errorsonly lets the base image extract in the single-ID namespace;it is unrelated to the bug itself.)
Describe the results you received
Describe the results you expected
The
RUNstep executes. When/proc/self/setgroupsreadsdeny, the kernel haspermanently 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
elsebranch already does for the empty-listcase).
Analysis
Two interacting pieces (permalinks to v1.43.1):
run_common.go#L285—
configureUIDGIDunconditionally doesg.AddProcessAdditionalGid(user.GID), sospec.Process.User.AdditionalGidsis never empty for a RUN step.chroot/run_common.go#L699-L718— the non-empty branch calls
syscall.Setgroups(gids)and hard-exits on error, withno 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
mainas of2026-07-08.
Workarounds that do not help, for completeness:
--group-add keep-groups— only sets therun.oci.keep_original_groupsannotation,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 storagethrough 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), andignore_chown_errorsdoes 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
elsebranch). Happy to open a PR:An alternative (possibly preferable) fix is to stop force-adding the primary GID in
configureUIDGIDwhen it would be the only entry — that would also make the existingdeny-guard reachable again.
Version
(RHEL 9 build
buildah-1.43.1-*.el9_8; the relevant code is identical onmainas of2026-07-08.)
Environment
--isolation chroot,--storage-driver vfs.runner pods under a restricted SCC (non-root UID, no subuid/subgid ranges,
allowPrivilegeEscalation: false, sonewuidmap/newgidmapfile capabilities areinert and the namespace is created with
setgroups = deny).