docker/docker

View on GitHub
daemon/containerfs_linux.go

Summary

Maintainability
C
1 day
Test Coverage
package daemon // import "github.com/docker/docker/daemon"

import (
    "context"
    "errors"
    "fmt"
    "os"
    "path/filepath"
    "runtime"
    "strings"

    "github.com/containerd/log"
    "github.com/hashicorp/go-multierror"
    "github.com/moby/sys/mount"
    "github.com/moby/sys/symlink"
    "golang.org/x/sys/unix"

    "github.com/docker/docker/api/types"
    "github.com/docker/docker/container"
    "github.com/docker/docker/internal/compatcontext"
    "github.com/docker/docker/internal/mounttree"
    "github.com/docker/docker/internal/unshare"
    "github.com/docker/docker/pkg/fileutils"
)

type future struct {
    fn  func() error
    res chan<- error
}

// containerFSView allows functions to be run in the context of a container's
// filesystem. Inside these functions, the root directory is the container root
// for all native OS filesystem APIs, including, but not limited to, the [os]
// and [golang.org/x/sys/unix] packages. The view of the container's filesystem
// is live and read-write. Each view has its own private set of tmpfs mounts.
// Any files written under a tmpfs mount are not visible to processes inside the
// container nor any other view of the container's filesystem, and vice versa.
//
// Each view has its own current working directory which is initialized to the
// root of the container filesystem and can be changed with [os.Chdir]. Changes
// to the current directory persist across successive [*containerFSView.RunInFS]
// and [*containerFSView.GoInFS] calls.
//
// Multiple views of the same container filesystem can coexist at the same time.
// Only one function can be running in a particular filesystem view at any given
// time. Calls to [*containerFSView.RunInFS] or [*containerFSView.GoInFS] will
// block while another function is running. If more than one call is blocked
// concurrently, the order they are unblocked is undefined.
type containerFSView struct {
    d    *Daemon
    ctr  *container.Container
    todo chan future
    done chan error
}

// openContainerFS opens a new view of the container's filesystem.
func (daemon *Daemon) openContainerFS(container *container.Container) (_ *containerFSView, err error) {
    ctx := context.TODO()

    if err := daemon.Mount(container); err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            _ = daemon.Unmount(container)
        }
    }()

    mounts, cleanup, err := daemon.setupMounts(ctx, container)
    if err != nil {
        return nil, err
    }
    defer func() {
        ctx := compatcontext.WithoutCancel(ctx)
        cleanup(ctx)
        if err != nil {
            _ = container.UnmountVolumes(ctx, daemon.LogVolumeEvent)
        }
    }()

    // Setup in initial mount namespace complete. We're ready to unshare the
    // mount namespace and bind the volume mounts into that private view of
    // the container FS.
    todo := make(chan future)
    done := make(chan error)
    err = unshare.Go(unix.CLONE_NEWNS,
        func() error {
            if err := mount.MakeRSlave("/"); err != nil {
                return err
            }
            for _, m := range mounts {
                dest, err := container.GetResourcePath(m.Destination)
                if err != nil {
                    return err
                }

                var stat os.FileInfo
                stat, err = os.Stat(m.Source)
                if err != nil {
                    return err
                }
                if err := fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
                    return err
                }

                bindMode := "rbind"
                if m.NonRecursive {
                    bindMode = "bind"
                }
                writeMode := "ro"
                if m.Writable {
                    writeMode = "rw"
                    if m.ReadOnlyNonRecursive {
                        return errors.New("options conflict: Writable && ReadOnlyNonRecursive")
                    }
                    if m.ReadOnlyForceRecursive {
                        return errors.New("options conflict: Writable && ReadOnlyForceRecursive")
                    }
                }
                if m.ReadOnlyNonRecursive && m.ReadOnlyForceRecursive {
                    return errors.New("options conflict: ReadOnlyNonRecursive && ReadOnlyForceRecursive")
                }

                // openContainerFS() is called for temporary mounts
                // outside the container. Soon these will be unmounted
                // with lazy unmount option and given we have mounted
                // them rbind, all the submounts will propagate if these
                // are shared. If daemon is running in host namespace
                // and has / as shared then these unmounts will
                // propagate and unmount original mount as well. So make
                // all these mounts rprivate.  Do not use propagation
                // property of volume as that should apply only when
                // mounting happens inside the container.
                opts := strings.Join([]string{bindMode, writeMode, "rprivate"}, ",")
                if err := mount.Mount(m.Source, dest, "", opts); err != nil {
                    return err
                }

                if !m.Writable && !m.ReadOnlyNonRecursive {
                    if err := makeMountRRO(dest); err != nil {
                        if m.ReadOnlyForceRecursive {
                            return err
                        } else {
                            log.G(context.TODO()).WithError(err).Debugf("Failed to make %q recursively read-only", dest)
                        }
                    }
                }
            }

            return mounttree.SwitchRoot(container.BaseFS)
        },
        func() {
            defer close(done)

            for it := range todo {
                err := it.fn()
                if it.res != nil {
                    it.res <- err
                }
            }

            // The thread will terminate when this goroutine returns, taking the
            // mount namespace and all the volume bind-mounts with it.
        },
    )
    if err != nil {
        return nil, err
    }
    vw := &containerFSView{
        d:    daemon,
        ctr:  container,
        todo: todo,
        done: done,
    }
    runtime.SetFinalizer(vw, (*containerFSView).Close)
    return vw, nil
}

// RunInFS synchronously runs fn in the context of the container filesytem and
// passes through its return value.
//
// The container filesystem is only visible to functions called in the same
// goroutine as fn. Goroutines started from fn will see the host's filesystem.
func (vw *containerFSView) RunInFS(ctx context.Context, fn func() error) error {
    res := make(chan error)
    select {
    case vw.todo <- future{fn: fn, res: res}:
    case <-ctx.Done():
        return ctx.Err()
    }
    return <-res
}

// GoInFS starts fn in the container FS. It blocks until fn is started but does
// not wait until fn returns. An error is returned if ctx is canceled before fn
// has been started.
//
// The container filesystem is only visible to functions called in the same
// goroutine as fn. Goroutines started from fn will see the host's filesystem.
func (vw *containerFSView) GoInFS(ctx context.Context, fn func()) error {
    select {
    case vw.todo <- future{fn: func() error { fn(); return nil }}:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

// Close waits until any in-flight operations complete and frees all
// resources associated with vw.
func (vw *containerFSView) Close() error {
    runtime.SetFinalizer(vw, nil)
    close(vw.todo)
    err := multierror.Append(nil, <-vw.done)
    err = multierror.Append(err, vw.ctr.UnmountVolumes(context.TODO(), vw.d.LogVolumeEvent))
    err = multierror.Append(err, vw.d.Unmount(vw.ctr))
    return err.ErrorOrNil()
}

// Stat returns the metadata for path, relative to the current working directory
// of vw inside the container filesystem view.
func (vw *containerFSView) Stat(ctx context.Context, path string) (*types.ContainerPathStat, error) {
    var stat *types.ContainerPathStat
    err := vw.RunInFS(ctx, func() error {
        lstat, err := os.Lstat(path)
        if err != nil {
            return err
        }
        var target string
        if lstat.Mode()&os.ModeSymlink != 0 {
            // Fully evaluate symlinks along path to the ultimate
            // target, or as much as possible with broken links.
            target, err = symlink.FollowSymlinkInScope(path, "/")
            if err != nil {
                return err
            }
        }
        stat = &types.ContainerPathStat{
            Name:       filepath.Base(path),
            Size:       lstat.Size(),
            Mode:       lstat.Mode(),
            Mtime:      lstat.ModTime(),
            LinkTarget: target,
        }
        return nil
    })
    return stat, err
}

// makeMountRRO makes the mount recursively read-only.
func makeMountRRO(dest string) error {
    attr := &unix.MountAttr{
        Attr_set: unix.MOUNT_ATTR_RDONLY,
    }
    var err error
    for {
        err = unix.MountSetattr(-1, dest, unix.AT_RECURSIVE, attr)
        if !errors.Is(err, unix.EINTR) {
            break
        }
    }
    if err != nil {
        err = fmt.Errorf("failed to apply MOUNT_ATTR_RDONLY with AT_RECURSIVE to %q: %w", dest, err)
    }
    return err
}