Skip to content

daemon/command: disable c8d snapshotter when userns remapping enabled#51042

Merged
thaJeztah merged 1 commit intomoby:masterfrom
akerouanton:userns-remapping-disable-snapshotter
Oct 17, 2025
Merged

daemon/command: disable c8d snapshotter when userns remapping enabled#51042
thaJeztah merged 1 commit intomoby:masterfrom
akerouanton:userns-remapping-disable-snapshotter

Conversation

@akerouanton
Copy link
Member

@akerouanton akerouanton commented Sep 25, 2025

- What I did

Buildkit fails when userns remapping is enabled and c8d snapshotter is used. As a temporary workaround, disable c8d snapshotter when userns remapping is enabled. This will need a proper fix in the future.

- How to verify it

# Should return nothing
$ docker info | grep snapshotter

# Should build successfully
$ echo -e 'FROM alpine\nRUN /bin/true' >Dockerfile.test
$ docker build -f Dockerfile.test --no-cache .

- Human readable description for the release notes

- containerd image store is temporarily not available when userns remapping is enabled as a workaround for [moby#47377](https://github.com/moby/moby/issues/47377)

@akerouanton akerouanton self-assigned this Sep 25, 2025
@akerouanton akerouanton added area/builder Build area/security/userns kind/bugfix PR's that fix bugs area/builder/buildkit Build containerd-integration Issues and PRs related to containerd integration release-blocker PRs we want to block a release on impact/changelog labels Sep 25, 2025
func disableC8dSnapshotterOnUsernsRemap(cfg *config.Config) {
if cfg.RemappedRoot != "" {
if enabled, ok := cfg.Features["containerd-snapshotter"]; !ok || enabled {
log.G(context.TODO()).Warn("userns remapping enabled, disabling containerd snapshotter")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead make this a hard failure and return an error (validate the combination of userns-remapping and snapshotter, then invalidate the config and error)?

cli, err := newDaemonCLI(opts)
if err != nil {
return err
}
if opts.Validate {
// If config wasn't OK we wouldn't have made it this far.
_, _ = fmt.Fprintln(stderr, "configuration OK")
return nil

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead make this a hard failure and return an error (validate the combination of userns-remapping and snapshotter, then invalidate the config and error)?

Not sure if that makes sense. In v29.0, we're making the feature containerd-snapshotter an opt-out. Users who have userns-remap set and that'd upgrade to v29.0 would find their Engine is now failing to start and would need to explicitly opt-out of that feature.

I think it'd be best to return an error if containerd-snapshotter is explicitly opted-in, and userns-remap is enabled. If it's not explicitly set, we shouldn't enable it if userns-remap is set. Is this what you're suggesting?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for not opting in to containerd when userns-remap is enabled. And if both containerd AND userns-remap is enabled then return a hard error.

@thaJeztah
Copy link
Member

Looking at the issue;

0.027 runc run failed: unable to start container process: 
    error during container init: 
    error mounting "/var/lib/docker/165536.165536/buildkit/executor/resolv.conf" to rootfs at "/etc/resolv.conf": 
    open /var/lib/docker/165536.165536/buildkit/executor/30g5og94pc1it3ymc8ymjdpd8/rootfs/etc/resolv.conf: permission denied

Who creates that resolv.conf is this one created by Docker or by BuildKit? If it's by docker; we know what the mapping is; can we chown the resolv.conf when we create it, based on the daemons userns-mapping?

@thaJeztah
Copy link
Member

For the remapped files; I think this is the part in the daemon that sets up the storage to be chowned;

func setupRemappedRoot(config *config.Config) (user.IdentityMapping, error) {

which returns the uid/gid to use as "root" for container filesystems;

moby/daemon/daemon.go

Lines 818 to 822 in dcf5db2

idMapping, err := setupRemappedRoot(config)
if err != nil {
return nil, err
}
uid, gid := idMapping.RootPair()

@akerouanton
Copy link
Member Author

Who creates that resolv.conf is this one created by Docker or by BuildKit? If it's by docker; we know what the mapping is; can we chown the resolv.conf when we create it, based on the daemons userns-mapping?

The original resolv.conf file that gets bind-mounted (e.g. /var/lib/docker/100000.100000/buildkit/executor/resolv.conf) is written by buildkit's oci.GetResolvConf. This function is correctly called with idmap *user.IdentityMapping set, and the file is chowned to the remapped uid/gid. Then, buildkit adds a bind-mount to the OCI spec (in oci.generateMountOpts):

    {
      "destination": "/etc/resolv.conf",
      "type": "bind",
      "source": "/var/lib/docker/100000.100000/buildkit/executor/resolv.conf",
      "options": [
        "nosuid",
        "noexec",
        "nodev",
        "rbind",
        "ro"
      ]
    },

The original issue:

0.027 runc run failed: unable to start container process: error during container init: error mounting "/var/lib/docker/165536.165536/buildkit/executor/resolv.conf" to rootfs at "/etc/resolv.conf": open /var/lib/docker/165536.165536/buildkit/executor/30g5og94pc1it3ymc8ymjdpd8/rootfs/etc/resolv.conf: permission denied

comes from this runc code path: https://github.com/opencontainers/runc/blob/00aec12c711a4d1562578e1d5e99d77610a169d9/libcontainer/rootfs_linux.go#L160.

The rootfs bind-mounted by buildkit's runcexecutor isn't id-mapped (https://github.com/moby/buildkit/blob/1cff4fb6f7938128c24eacae7c064b589423cb0e/executor/runcexecutor/executor.go#L239) — the rootMount passed to mount.All() looks like this:

rootMount := mount.Mount{
    Type: "bind",
    Source: "/var/lib/docker/100000.100000/buildkit/containerd-overlayfs/cachemounts/buildkit1070698249",
    Options: []string{"rw", "rbind"},
}

That 'Source' itself comes from buildkit's (*fromContainerd).Mounts(). The call to (*fromContainerd).Prepare() made for that snapshot key has no opts — i.e. no client. WithRemapperLabels(), nor client.WithUserNSRemapperLabels() is set.

These two client opts add labels containerd.io/snapshot/[u|g]idmapping but the mount returned by the snapshotter doesn't have idmap / gidmap options set. I'm a bit surprised by this as it seems WithRemapperLabels() was added a long time ago.

If I add this to buildkit's (*fromContainerd).Mounts() (granted s.idmap is plumbed, which is not the case right now — see here):

	if s.idmap != nil {
		for i := range mounts {
			if mounts[i].Type != "bind" && mounts[i].Type != "overlay" {
				continue
			}

			for _, m := range s.idmap.UIDMaps {
				mounts[i].Options = append(mounts[i].Options, fmt.Sprintf("uidmap=%d:%d:%d", m.ID, m.ParentID, m.Count))
			}
			for _, m := range s.idmap.GIDMaps {
				mounts[i].Options = append(mounts[i].Options, fmt.Sprintf("gidmap=%d:%d:%d", m.ID, m.ParentID, m.Count))
			}
		}
	}

Then the rootfs is correctly id-mapped, and buildkit's withROBind() needs the following to get /etc/resolv.conf and /etc/hosts to be correctly id-mapped (values hardcoded for test purpose):

			UIDMappings: []specs.LinuxIDMapping{{ContainerID: 0, HostID: 100000, Size: 65536}},
			GIDMappings: []specs.LinuxIDMapping{{ContainerID: 0, HostID: 100000, Size: 65536}},

After that, everything works correctly.

I'm not sure if that's the proper fix though (Tõnis seemed to suggest it wasn't), and it'd require changes in buildkit which won't receive a new minor release before current target date for v29.0.

@akerouanton akerouanton force-pushed the userns-remapping-disable-snapshotter branch 2 times, most recently from 2ef4c9a to ceac428 Compare October 2, 2025 16:07
@thaJeztah
Copy link
Member

If I add this to buildkit's (*fromContainerd).Mounts() (granted s.idmap is plumbed, which is not the case right now — see here):

Thought I left a comment, but probably forgot to press the button; if wiring up is the issue, given that we (currently) only provide a single mapping ... wondering if we could (but it's a bit of a hack) just have a global var that's set to the right mapping for this (until things are wired up) 🤔

@akerouanton akerouanton force-pushed the userns-remapping-disable-snapshotter branch 5 times, most recently from 03463dd to f3ed627 Compare October 8, 2025 10:04

ctx := testutil.GetContext(c)
s.d.StartWithBusybox(ctx, c, "--userns-remap", "default")
s.d.StartWithBusybox(ctx, c, "--userns-remap", "default", "--storage-driver", "vfs")
Copy link
Member Author

@akerouanton akerouanton Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely sure why but the daemon fails to start in this test when overlay2 is used — it's running an error from here:

opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lowerDir, path.Join(td, "upper"), path.Join(td, "work"))
if err := unix.Mount("overlay", mnt, "overlay", 0, opts); err != nil {
return errors.Wrap(err, "failed to mount overlay")
}

ISTR the kernel limits the size of the mount option string, so I think that's due to that (it's ~1800 chars long).

Also, I think this test was working correctly before ead007f as DOCKER_GRAPHDRIVER was defaulting to vfs (see ead007f#diff-3e42eb9b4a639f89ff1487b88be1de1111e14d83fe672e3cf69b87bcf86b48b0R43).

ERRO[2025-10-08T09:50:35.932904673Z] Failed to mount overlayfs filesystem          error="invalid argument" lowerDir="/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/lower2:/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/lower1" mnt=/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/merged opts="lowerdir=/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/lower2:/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/lower1,upperdir=/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/upper,workdir=/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/work" upperDir=/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/upper workDir=/go/src/github.com/docker/docker/bundles/test-integration/TestDockerDaemonSuite/TestDaemonUserNamespaceRootSetting/de382817ce451/root/100000.100000/check-overlayfs-support3140787799/work

Comment on lines +60 to +80
// Buildkit breaks when userns remapping is enabled and containerd snapshotter is used. As a temporary workaround,
// if containerd snapshotter is explicitly enabled, and userns remapping is enabled too, return an error. If userns
// remapping is enabled, but containerd-snapshotter is enabled by default, disable it. See https://github.com/moby/moby/issues/47377.
func disableC8dSnapshotterOnUsernsRemap(cfg *config.Config) error {
if cfg.RemappedRoot == "" {
return nil
}

enabled := cfg.Features["containerd-snapshotter"]
if enabled {
return errors.New("containerd-snapshotter is explicitly enabled, but is not comptatible with userns remapping. Please disable userns remapping or containerd-snapshotter")
}

log.G(context.TODO()).Warn("userns remapping enabled, disabling containerd snapshotter")
if cfg.Features == nil {
cfg.Features = make(map[string]bool)
}
cfg.Features["containerd-snapshotter"] = false

return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps not a strict blocker, but I just noticed that this possible could be rolled into setPlatformOptions, because it's specific to Linux, and which .. looks to be dealing with userns-remapping already;

// setPlatformOptions applies platform-specific CLI configuration options.
func setPlatformOptions(conf *config.Config) error {
if conf.RemappedRoot == "" {
return nil
}
containerdNamespace, containerdPluginNamespace, err := daemon.RemapContainerdNamespaces(conf)
if err != nil {
return err
}
conf.ContainerdNamespace = containerdNamespace
conf.ContainerdPluginNamespace = containerdPluginNamespace

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out! I've merged disableC8dSnapshotterOnUsernsRemap into setPlatformOptions.

@akerouanton akerouanton force-pushed the userns-remapping-disable-snapshotter branch 3 times, most recently from 3f8398a to 4460a72 Compare October 13, 2025 14:49
@github-actions github-actions bot added area/daemon Core Engine and removed impact/changelog area/builder/buildkit Build containerd-integration Issues and PRs related to containerd integration labels Oct 13, 2025
@akerouanton akerouanton force-pushed the userns-remapping-disable-snapshotter branch from 4460a72 to 4c8111d Compare October 13, 2025 21:24
Buildkit fails when userns remapping is enabled and c8d snapshotter is
used. As a temporary workaround, disable c8d snapshotter when userns
remapping is enabled. This will need a proper fix in the future.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
@akerouanton akerouanton force-pushed the userns-remapping-disable-snapshotter branch from 4c8111d to e1722eb Compare October 14, 2025 08:14
Comment on lines +31 to +34
enabled := conf.Features["containerd-snapshotter"]
if enabled {
return errors.New("containerd-snapshotter is explicitly enabled, but is not compatible with userns remapping. Please disable userns remapping or containerd-snapshotter")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

containerd-snapshotter is no longer an opt-in for fresh engine installations.

So if you have the userns enabled in your daemon.json on a fresh install then it would miss this.

We need to handle it somewhere around here:

log.G(ctx).Info("Starting daemon with containerd snapshotter integration enabled")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition only tests if the feature flag was explicitly set (e.g. when upgrading the daemon), and returns an error if so.

If it's not set, the feature flag is automatically disabled a few lines below: https://github.com/akerouanton/docker/blob/e1722eb8d8013019524c3ff56e45374b629d6e68/daemon/command/daemon_linux.go#L40

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but that will break the default opt-in, because containerd-snapshotter is set to explicit false (instead of no value present).

Copy link
Contributor

@vvoland vvoland Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, I missed this line:

if conf.RemappedRoot == "" {
return nil
}

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@thaJeztah thaJeztah merged commit d1a720c into moby:master Oct 17, 2025
189 of 190 checks passed
@akerouanton akerouanton deleted the userns-remapping-disable-snapshotter branch October 20, 2025 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/builder Build area/daemon Core Engine area/security/userns impact/changelog kind/bugfix PR's that fix bugs release-blocker PRs we want to block a release on

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants