Skip to content

Commit 2cc3429

Browse files
committed
merge #54 into cyphar/filepath-securejoin:main
Aleksa Sarai (1): *: unify nil dir handling for *at(2) wrappers LGTMs: cyphar
2 parents 4531d11 + 8e3d619 commit 2cc3429

File tree

3 files changed

+51
-21
lines changed

3 files changed

+51
-21
lines changed

openat2_linux.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"os"
1313
"path/filepath"
14+
"runtime"
1415
"strings"
1516

1617
"golang.org/x/sys/unix"
@@ -44,12 +45,12 @@ func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
4445
const scopedLookupMaxRetries = 10
4546

4647
func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) {
47-
fullPath := dir.Name() + "/" + path
48+
dirFd, fullPath := prepareAt(dir, path)
4849
// Make sure we always set O_CLOEXEC.
4950
how.Flags |= unix.O_CLOEXEC
5051
var tries int
5152
for tries < scopedLookupMaxRetries {
52-
fd, err := unix.Openat2(int(dir.Fd()), path, how)
53+
fd, err := unix.Openat2(dirFd, path, how)
5354
if err != nil {
5455
if scopedLookupShouldRetry(how, err) {
5556
// We retry a couple of times to avoid the spurious errors, and
@@ -60,6 +61,7 @@ func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error)
6061
}
6162
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
6263
}
64+
runtime.KeepAlive(dir)
6365
// If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
6466
// NOTE: The procRoot code MUST NOT use RESOLVE_IN_ROOT, otherwise
6567
// you'll get infinite recursion here.

openat_linux.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package securejoin
99
import (
1010
"os"
1111
"path/filepath"
12+
"runtime"
1213

1314
"golang.org/x/sys/unix"
1415
)
@@ -21,35 +22,64 @@ func dupFile(f *os.File) (*os.File, error) {
2122
return os.NewFile(uintptr(fd), f.Name()), nil
2223
}
2324

25+
// prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using
26+
// the dir.Fd(). We use -EBADF because in filepath-securejoin we generally
27+
// don't want to allow relative-to-cwd paths. The returned path is an
28+
// *informational* string that describes a reasonable pathname for the given
29+
// *at(2) arguments. You must not use the full path for any actual filesystem
30+
// operations.
31+
func prepareAt(dir *os.File, path string) (dirFd int, unsafeFullPath string) {
32+
dirFd, dirPath := -int(unix.EBADF), "."
33+
if dir != nil {
34+
dirFd, dirPath = int(dir.Fd()), dir.Name()
35+
}
36+
if !filepath.IsAbs(path) {
37+
// only prepend the dirfd path for relative paths
38+
path = dirPath + "/" + path
39+
}
40+
// NOTE: If path is "." or "", the returned path won't be filepath.Clean,
41+
// but that's okay since this path is either used for errors (in which case
42+
// a trailing "/" or "/." is important information) or will be
43+
// filepath.Clean'd later (in the case of openatFile).
44+
return dirFd, path
45+
}
46+
2447
func openatFile(dir *os.File, path string, flags int, mode int) (*os.File, error) { //nolint:unparam // wrapper func
48+
dirFd, fullPath := prepareAt(dir, path)
2549
// Make sure we always set O_CLOEXEC.
2650
flags |= unix.O_CLOEXEC
27-
fd, err := unix.Openat(int(dir.Fd()), path, flags, uint32(mode))
51+
fd, err := unix.Openat(dirFd, path, flags, uint32(mode))
2852
if err != nil {
29-
return nil, &os.PathError{Op: "openat", Path: dir.Name() + "/" + path, Err: err}
53+
return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err}
3054
}
31-
// All of the paths we use with openatFile(2) are guaranteed to be
32-
// lexically safe, so we can use path.Join here.
33-
fullPath := filepath.Join(dir.Name(), path)
55+
runtime.KeepAlive(dir)
56+
// openat is only used with lexically-safe paths so we can use
57+
// filepath.Clean here, and also the path itself is not going to be used
58+
// for actual path operations.
59+
fullPath = filepath.Clean(fullPath)
3460
return os.NewFile(uintptr(fd), fullPath), nil
3561
}
3662

3763
func fstatatFile(dir *os.File, path string, flags int) (unix.Stat_t, error) {
64+
dirFd, fullPath := prepareAt(dir, path)
3865
var stat unix.Stat_t
39-
if err := unix.Fstatat(int(dir.Fd()), path, &stat, flags); err != nil {
40-
return stat, &os.PathError{Op: "fstatat", Path: dir.Name() + "/" + path, Err: err}
66+
if err := unix.Fstatat(dirFd, path, &stat, flags); err != nil {
67+
return stat, &os.PathError{Op: "fstatat", Path: fullPath, Err: err}
4168
}
69+
runtime.KeepAlive(dir)
4270
return stat, nil
4371
}
4472

4573
func readlinkatFile(dir *os.File, path string) (string, error) {
74+
dirFd, fullPath := prepareAt(dir, path)
4675
size := 4096
4776
for {
4877
linkBuf := make([]byte, size)
49-
n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf)
78+
n, err := unix.Readlinkat(dirFd, path, linkBuf)
5079
if err != nil {
51-
return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err}
80+
return "", &os.PathError{Op: "readlinkat", Path: fullPath, Err: err}
5281
}
82+
runtime.KeepAlive(dir)
5383
if n != size {
5484
return string(linkBuf[:n]), nil
5585
}

procfs_linux.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,15 @@ func newPrivateProcMount() (*os.File, error) {
113113
}
114114

115115
func openTree(dir *os.File, path string, flags uint) (*os.File, error) {
116-
dirFd := -int(unix.EBADF)
117-
dirName := "."
118-
if dir != nil {
119-
dirFd = int(dir.Fd())
120-
dirName = dir.Name()
121-
}
116+
dirFd, fullPath := prepareAt(dir, path)
122117
// Make sure we always set O_CLOEXEC.
123118
flags |= unix.OPEN_TREE_CLOEXEC
124119
fd, err := unix.OpenTree(dirFd, path, flags)
125120
if err != nil {
126-
return nil, &os.PathError{Op: "open_tree", Path: path, Err: err}
121+
return nil, &os.PathError{Op: "open_tree", Path: fullPath, Err: err}
127122
}
128-
return os.NewFile(uintptr(fd), dirName+"/"+path), nil
123+
runtime.KeepAlive(dir)
124+
return os.NewFile(uintptr(fd), fullPath), nil
129125
}
130126

131127
func clonePrivateProcMount() (_ *os.File, Err error) {
@@ -325,15 +321,17 @@ func getMountID(dir *os.File, path string) (uint64, error) {
325321
wantStxMask uint32 = STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
326322
)
327323

328-
err := unix.Statx(int(dir.Fd()), path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, int(wantStxMask), &stx)
324+
dirFd, fullPath := prepareAt(dir, path)
325+
err := unix.Statx(dirFd, path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, int(wantStxMask), &stx)
329326
if stx.Mask&wantStxMask == 0 {
330327
// It's not a kernel limitation, for some reason we couldn't get a
331328
// mount ID. Assume it's some kind of attack.
332329
err = fmt.Errorf("%w: could not get mount id", errUnsafeProcfs)
333330
}
334331
if err != nil {
335-
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: dir.Name() + "/" + path, Err: err}
332+
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err}
336333
}
334+
runtime.KeepAlive(dir)
337335
return stx.Mnt_id, nil
338336
}
339337

0 commit comments

Comments
 (0)