Files
Tonis Tiigi 6ed70f8651 executor: restore mount points removed for rootless
The rootless spec conversion removes the /sys mount, so the runtime never
creates its mount point in the rootfs. A rootful build gets that empty
directory, which meant the same LLB left different mount points behind
depending on whether the worker was rootless.

Recreate the mount points of the removed mounts after the container has
exited, rather than cleaning them up from the rootful result, which would
change existing output and need a compatibility version bump. Creating
them up front would instead turn a mount point that the image does not
ship into a directory the build can write to. Nested destinations are
skipped: the runtime creates those inside the parent mount, where they
never reach the rootfs.

Fixes #6686

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-08-13 16:04:16 +03:00

194 lines
5.4 KiB
Go

//go:build !windows
package containerdexecutor
import (
"context"
"os"
"path/filepath"
"runtime"
"slices"
ctd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/mount"
containerdoci "github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/continuity/fs"
"github.com/moby/buildkit/executor"
"github.com/moby/buildkit/executor/oci"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/network"
rootlessspecconv "github.com/moby/buildkit/util/rootless/specconv"
"github.com/moby/sys/user"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
func getUserSpec(user, rootfsPath string) (specs.User, error) {
var err error
var uid, gid uint32
var sgids []uint32
if rootfsPath != "" {
uid, gid, sgids, err = oci.GetUser(rootfsPath, user)
} else {
uid, gid, err = oci.ParseUIDGID(user)
}
if err != nil {
return specs.User{}, errors.WithStack(err)
}
return specs.User{
UID: uid,
GID: gid,
AdditionalGids: sgids,
}, nil
}
func (w *containerdExecutor) prepareExecutionEnv(ctx context.Context, rootMount executor.Mount, mounts []executor.Mount, meta executor.Meta, details *containerState, netMode pb.NetMode) (string, string, func(), error) {
var releasers []func()
releaseAll := func() {
for _, release := range slices.Backward(releasers) {
release()
}
}
stateDirRoot, err := os.OpenRoot(w.root)
if err != nil {
releaseAll()
return "", "", nil, err
}
defer stateDirRoot.Close()
resolvConfName, err := oci.GetResolvConf(ctx, stateDirRoot, nil, w.dnsConfig, netMode)
if err != nil {
releaseAll()
return "", "", nil, err
}
resolvConf := filepath.Join(w.root, resolvConfName)
hostsName, clean, err := oci.GetHostsFile(ctx, stateDirRoot, meta.ExtraHosts, nil, meta.Hostname)
if err != nil {
releaseAll()
return "", "", nil, err
}
hostsFile := filepath.Join(w.root, hostsName)
if clean != nil {
releasers = append(releasers, clean)
}
mountable, err := rootMount.Src.Mount(ctx, false)
if err != nil {
releaseAll()
return "", "", nil, err
}
rootMounts, release, err := mountable.Mount()
if err != nil {
releaseAll()
return "", "", nil, err
}
details.rootMounts = rootMounts
if release != nil {
releasers = append(releasers, func() {
if err := release(); err != nil {
bklog.G(ctx).WithError(err).Error("failed to release root mount")
}
})
}
lm := snapshot.LocalMounterWithMounts(rootMounts)
rootfsPath, err := lm.Mount()
if err != nil {
releaseAll()
return "", "", nil, err
}
details.rootfsPath = rootfsPath
releasers = append(releasers, func() {
if err := lm.Unmount(); err != nil {
bklog.G(ctx).WithError(err).Error("failed to unmount rootfs")
}
})
releasers = append(releasers, executor.MountStubsCleaner(ctx, details.rootfsPath, mounts, meta.RemoveMountStubsRecursive))
return resolvConf, hostsFile, releaseAll, nil
}
func (w *containerdExecutor) ensureCWD(details *containerState, meta executor.Meta) error {
newp, err := fs.RootPath(details.rootfsPath, meta.Cwd)
if err != nil {
return errors.Wrapf(err, "working dir %s points to invalid target", newp)
}
uid, gid, _, err := oci.GetUser(details.rootfsPath, meta.User)
if err != nil {
return err
}
if _, err := os.Stat(newp); err != nil {
if err := user.MkdirAllAndChown(newp, 0755, int(uid), int(gid)); err != nil {
return errors.Wrapf(err, "failed to create working directory %s", newp)
}
}
return nil
}
func (w *containerdExecutor) createOCISpec(ctx context.Context, id, resolvConf, hostsFile string, namespace network.Namespace, mounts []executor.Mount, meta executor.Meta, details *containerState) (*specs.Spec, func(), error) {
var releasers []func()
releaseAll := func() {
for _, release := range slices.Backward(releasers) {
release()
}
}
uid, gid, sgids, err := oci.GetUser(details.rootfsPath, meta.User)
if err != nil {
releaseAll()
return nil, nil, err
}
opts := []containerdoci.SpecOpts{oci.WithUIDGID(uid, gid, sgids)}
if meta.ReadonlyRootFS {
opts = append(opts, containerdoci.WithRootFSReadonly())
}
processMode := oci.ProcessSandbox // FIXME(AkihiroSuda)
spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, resolvConf, hostsFile, namespace, w.cgroupParent, processMode, nil, w.apparmorProfile, w.selinux, w.traceSocket, w.cdiManager, opts...)
if err != nil {
releaseAll()
return nil, nil, err
}
releasers = append(releasers, cleanup)
spec.Process.Terminal = meta.Tty
if w.rootless {
removedMounts, err := rootlessspecconv.ToRootless(spec)
if err != nil {
releaseAll()
return nil, nil, err
}
// The runtime no longer sets these mounts up, but a rootful build still gets
// their mount points left in the rootfs. The caller recreates them once the
// container is gone. moby/buildkit#6686
details.removedMounts = removedMounts
}
return spec, releaseAll, nil
}
func (d *containerState) getTaskOpts() ([]ctd.NewTaskOpts, error) {
rootfs := ctd.WithRootFS([]mount.Mount{{
Source: d.rootfsPath,
Type: "bind",
Options: []string{"rbind"},
}})
if runtime.GOOS == "freebsd" {
rootfs = ctd.WithRootFS([]mount.Mount{{
Source: d.rootfsPath,
Type: "nullfs",
Options: []string{},
}})
}
return []ctd.NewTaskOpts{rootfs}, nil
}
func setArgs(spec *specs.Process, args []string) {
spec.Args = args
}