| package safepath | |
| import ( | |
| "context" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "strconv" | |
| "github.com/containerd/log" | |
| "github.com/docker/docker/internal/unix_noeintr" | |
| "github.com/pkg/errors" | |
| "golang.org/x/sys/unix" | |
| ) | |
| // Join makes sure that the concatenation of path and subpath doesn't | |
| // resolve to a path outside of path and returns a path to a temporary file that is | |
| // a bind mount to the exact same file/directory that was validated. | |
| // | |
| // After use, it is the caller's responsibility to call Close on the returned | |
| // SafePath object, which will unmount the temporary file/directory | |
| // and remove it. | |
| func Join(_ context.Context, path, subpath string) (*SafePath, error) { | |
| base, subpart, err := evaluatePath(path, subpath) | |
| if err != nil { | |
| return nil, err | |
| } | |
| runtime.LockOSThread() | |
| defer runtime.UnlockOSThread() | |
| fd, err := safeOpenFd(base, subpart) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer unix_noeintr.Close(fd) | |
| tmpMount, err := tempMountPoint(fd) | |
| if err != nil { | |
| return nil, errors.Wrap(err, "failed to create temporary file for safe mount") | |
| } | |
| pid := strconv.Itoa(unix.Gettid()) | |
| // Using explicit pid path, because /proc/self/fd/<fd> fails with EACCES | |
| // when running under "Enhanced Container Isolation" in Docker Desktop | |
| // which uses sysbox runtime under the hood. | |
| // TODO(vvoland): Investigate. | |
| mountSource := "/proc/" + pid + "/fd/" + strconv.Itoa(fd) | |
| if err := unix_noeintr.Mount(mountSource, tmpMount, "none", unix.MS_BIND, ""); err != nil { | |
| os.Remove(tmpMount) | |
| return nil, errors.Wrap(err, "failed to mount resolved path") | |
| } | |
| return &SafePath{ | |
| path: tmpMount, | |
| sourceBase: base, | |
| sourceSubpath: subpart, | |
| cleanup: cleanupSafePath(tmpMount), | |
| }, nil | |
| } | |
| // safeOpenFd opens the file at filepath.Join(path, subpath) in O_PATH | |
| // mode and returns the file descriptor if subpath is within the subtree | |
| // rooted at path. It is an error if any of components of path or subpath | |
| // are symbolic links. | |
| // | |
| // It is a caller's responsibility to close the returned file descriptor, if no | |
| // error was returned. | |
| func safeOpenFd(path, subpath string) (int, error) { | |
| // Open base volume path (_data directory). | |
| prevFd, err := unix_noeintr.Open(path, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) | |
| if err != nil { | |
| return -1, &ErrNotAccessible{Path: path, Cause: err} | |
| } | |
| defer unix_noeintr.Close(prevFd) | |
| // Try to use the Openat2 syscall first (available on Linux 5.6+). | |
| fd, err := unix_noeintr.Openat2(prevFd, subpath, &unix.OpenHow{ | |
| Flags: unix.O_PATH | unix.O_CLOEXEC, | |
| Mode: 0, | |
| Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_SYMLINKS, | |
| }) | |
| switch { | |
| case errors.Is(err, unix.ENOSYS): | |
| // Openat2 is not available, fallback to Openat loop. | |
| return kubernetesSafeOpen(path, subpath) | |
| case errors.Is(err, unix.EXDEV): | |
| return -1, &ErrEscapesBase{Base: path, Subpath: subpath} | |
| case errors.Is(err, unix.ENOENT), errors.Is(err, unix.ELOOP): | |
| return -1, &ErrNotAccessible{Path: filepath.Join(path, subpath), Cause: err} | |
| case err != nil: | |
| return -1, &os.PathError{Op: "openat2", Path: subpath, Err: err} | |
| } | |
| // Openat2 is available and succeeded. | |
| return fd, nil | |
| } | |
| // tempMountPoint creates a temporary file/directory to act as mount | |
| // point for the file descriptor. | |
| func tempMountPoint(sourceFd int) (string, error) { | |
| var stat unix.Stat_t | |
| err := unix_noeintr.Fstat(sourceFd, &stat) | |
| if err != nil { | |
| return "", errors.Wrap(err, "failed to Fstat mount source fd") | |
| } | |
| isDir := (stat.Mode & unix.S_IFMT) == unix.S_IFDIR | |
| if isDir { | |
| return os.MkdirTemp("", "safe-mount") | |
| } | |
| f, err := os.CreateTemp("", "safe-mount") | |
| if err != nil { | |
| return "", err | |
| } | |
| p := f.Name() | |
| if err := f.Close(); err != nil { | |
| return "", err | |
| } | |
| return p, nil | |
| } | |
| // cleanupSafePaths returns a function that unmounts the path and removes the | |
| // mountpoint. | |
| func cleanupSafePath(path string) func(context.Context) error { | |
| return func(ctx context.Context) error { | |
| log.G(ctx).WithField("path", path).Debug("removing safe temp mount") | |
| if err := unix_noeintr.Unmount(path, unix.MNT_DETACH); err != nil { | |
| if errors.Is(err, unix.EINVAL) { | |
| log.G(ctx).WithField("path", path).Warn("safe temp mount no longer exists?") | |
| return nil | |
| } | |
| return errors.Wrapf(err, "error unmounting safe mount %s", path) | |
| } | |
| if err := os.Remove(path); err != nil { | |
| if errors.Is(err, os.ErrNotExist) { | |
| log.G(ctx).WithField("path", path).Warn("safe temp mount no longer exists?") | |
| return nil | |
| } | |
| return errors.Wrapf(err, "failed to delete temporary safe mount") | |
| } | |
| return nil | |
| } | |
| } |