-
Notifications
You must be signed in to change notification settings - Fork 0
/
join_linux.go
150 lines (130 loc) · 4.49 KB
/
join_linux.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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
}
}