github.com/moby/docker@v26.1.3+incompatible/internal/safepath/join_linux.go (about)

     1  package safepath
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"runtime"
     8  	"strconv"
     9  
    10  	"github.com/containerd/log"
    11  	"github.com/docker/docker/internal/unix_noeintr"
    12  	"github.com/pkg/errors"
    13  	"golang.org/x/sys/unix"
    14  )
    15  
    16  // Join makes sure that the concatenation of path and subpath doesn't
    17  // resolve to a path outside of path and returns a path to a temporary file that is
    18  // a bind mount to the exact same file/directory that was validated.
    19  //
    20  // After use, it is the caller's responsibility to call Close on the returned
    21  // SafePath object, which will unmount the temporary file/directory
    22  // and remove it.
    23  func Join(_ context.Context, path, subpath string) (*SafePath, error) {
    24  	base, subpart, err := evaluatePath(path, subpath)
    25  	if err != nil {
    26  		return nil, err
    27  	}
    28  
    29  	runtime.LockOSThread()
    30  	defer runtime.UnlockOSThread()
    31  	fd, err := safeOpenFd(base, subpart)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  
    36  	defer unix_noeintr.Close(fd)
    37  
    38  	tmpMount, err := tempMountPoint(fd)
    39  	if err != nil {
    40  		return nil, errors.Wrap(err, "failed to create temporary file for safe mount")
    41  	}
    42  
    43  	pid := strconv.Itoa(unix.Gettid())
    44  	// Using explicit pid path, because /proc/self/fd/<fd> fails with EACCES
    45  	// when running under "Enhanced Container Isolation" in Docker Desktop
    46  	// which uses sysbox runtime under the hood.
    47  	// TODO(vvoland): Investigate.
    48  	mountSource := "/proc/" + pid + "/fd/" + strconv.Itoa(fd)
    49  
    50  	if err := unix_noeintr.Mount(mountSource, tmpMount, "none", unix.MS_BIND, ""); err != nil {
    51  		os.Remove(tmpMount)
    52  		return nil, errors.Wrap(err, "failed to mount resolved path")
    53  	}
    54  
    55  	return &SafePath{
    56  		path:          tmpMount,
    57  		sourceBase:    base,
    58  		sourceSubpath: subpart,
    59  		cleanup:       cleanupSafePath(tmpMount),
    60  	}, nil
    61  }
    62  
    63  // safeOpenFd opens the file at filepath.Join(path, subpath) in O_PATH
    64  // mode and returns the file descriptor if subpath is within the subtree
    65  // rooted at path. It is an error if any of components of path or subpath
    66  // are symbolic links.
    67  //
    68  // It is a caller's responsibility to close the returned file descriptor, if no
    69  // error was returned.
    70  func safeOpenFd(path, subpath string) (int, error) {
    71  	// Open base volume path (_data directory).
    72  	prevFd, err := unix_noeintr.Open(path, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
    73  	if err != nil {
    74  		return -1, &ErrNotAccessible{Path: path, Cause: err}
    75  	}
    76  	defer unix_noeintr.Close(prevFd)
    77  
    78  	// Try to use the Openat2 syscall first (available on Linux 5.6+).
    79  	fd, err := unix_noeintr.Openat2(prevFd, subpath, &unix.OpenHow{
    80  		Flags:   unix.O_PATH | unix.O_CLOEXEC,
    81  		Mode:    0,
    82  		Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_SYMLINKS,
    83  	})
    84  
    85  	switch {
    86  	case errors.Is(err, unix.ENOSYS):
    87  		// Openat2 is not available, fallback to Openat loop.
    88  		return kubernetesSafeOpen(path, subpath)
    89  	case errors.Is(err, unix.EXDEV):
    90  		return -1, &ErrEscapesBase{Base: path, Subpath: subpath}
    91  	case errors.Is(err, unix.ENOENT), errors.Is(err, unix.ELOOP):
    92  		return -1, &ErrNotAccessible{Path: filepath.Join(path, subpath), Cause: err}
    93  	case err != nil:
    94  		return -1, &os.PathError{Op: "openat2", Path: subpath, Err: err}
    95  	}
    96  
    97  	// Openat2 is available and succeeded.
    98  	return fd, nil
    99  }
   100  
   101  // tempMountPoint creates a temporary file/directory to act as mount
   102  // point for the file descriptor.
   103  func tempMountPoint(sourceFd int) (string, error) {
   104  	var stat unix.Stat_t
   105  	err := unix_noeintr.Fstat(sourceFd, &stat)
   106  	if err != nil {
   107  		return "", errors.Wrap(err, "failed to Fstat mount source fd")
   108  	}
   109  
   110  	isDir := (stat.Mode & unix.S_IFMT) == unix.S_IFDIR
   111  	if isDir {
   112  		return os.MkdirTemp("", "safe-mount")
   113  	}
   114  
   115  	f, err := os.CreateTemp("", "safe-mount")
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  
   120  	p := f.Name()
   121  	if err := f.Close(); err != nil {
   122  		return "", err
   123  	}
   124  	return p, nil
   125  }
   126  
   127  // cleanupSafePaths returns a function that unmounts the path and removes the
   128  // mountpoint.
   129  func cleanupSafePath(path string) func(context.Context) error {
   130  	return func(ctx context.Context) error {
   131  		log.G(ctx).WithField("path", path).Debug("removing safe temp mount")
   132  
   133  		if err := unix_noeintr.Unmount(path, unix.MNT_DETACH); err != nil {
   134  			if errors.Is(err, unix.EINVAL) {
   135  				log.G(ctx).WithField("path", path).Warn("safe temp mount no longer exists?")
   136  				return nil
   137  			}
   138  			return errors.Wrapf(err, "error unmounting safe mount %s", path)
   139  		}
   140  		if err := os.Remove(path); err != nil {
   141  			if errors.Is(err, os.ErrNotExist) {
   142  				log.G(ctx).WithField("path", path).Warn("safe temp mount no longer exists?")
   143  				return nil
   144  			}
   145  			return errors.Wrapf(err, "failed to delete temporary safe mount")
   146  		}
   147  
   148  		return nil
   149  	}
   150  }