d1c0ca5260
This wonderfully large commit replaces basically everything under the `server/filesystem` package, re-implementing essentially everything. This is related to https://github.com/pterodactyl/wings/security/advisories/GHSA-494h-9924-xww9 If any vulnerabilities related to symlinks persist after this commit, I will be very upset. Signed-off-by: Matthew Penner <me@matthewp.io>
826 lines
26 KiB
Go
826 lines
26 KiB
Go
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
|
|
|
//go:build unix
|
|
|
|
package ufs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// UnixFS is a filesystem that uses the unix package to make io calls.
|
|
//
|
|
// This is used for proper sand-boxing and full control over the exact syscalls
|
|
// being performed.
|
|
type UnixFS struct {
|
|
// basePath is the base path for file operations to take place in.
|
|
basePath string
|
|
|
|
// dirfd holds the file descriptor of BasePath and is used to ensure
|
|
// operations are restricted into descendants of BasePath.
|
|
dirfd atomic.Int64
|
|
|
|
// useOpenat2 controls whether the `openat2` syscall is used instead of the
|
|
// older `openat` syscall.
|
|
useOpenat2 bool
|
|
}
|
|
|
|
// NewUnixFS creates a new sandboxed unix filesystem. BasePath is used as the
|
|
// sandbox path, operations on BasePath itself are not allowed, but any
|
|
// operations on its descendants are. Symlinks pointing outside BasePath are
|
|
// checked and prevented from enabling an escape in a non-raceable manor.
|
|
func NewUnixFS(basePath string, useOpenat2 bool) (*UnixFS, error) {
|
|
basePath = strings.TrimSuffix(basePath, "/")
|
|
// We don't need Openat2, if we are given a basePath that is already unsafe
|
|
// I give up on trying to sandbox it.
|
|
dirfd, err := unix.Openat(AT_EMPTY_PATH, basePath, O_DIRECTORY|O_RDONLY, 0)
|
|
if err != nil {
|
|
return nil, convertErrorType(err)
|
|
}
|
|
|
|
fs := &UnixFS{
|
|
basePath: basePath,
|
|
useOpenat2: useOpenat2,
|
|
}
|
|
fs.dirfd.Store(int64(dirfd))
|
|
return fs, nil
|
|
}
|
|
|
|
// BasePath returns the base path of the UnixFS sandbox, file operations
|
|
// pointing outside this path are prohibited and will be blocked by all
|
|
// operations implemented by UnixFS.
|
|
func (fs *UnixFS) BasePath() string {
|
|
return fs.basePath
|
|
}
|
|
|
|
// Close releases the file descriptor used to sandbox operations within the
|
|
// base path of the filesystem.
|
|
func (fs *UnixFS) Close() error {
|
|
// Once closed, change dirfd to something invalid to detect when it has been
|
|
// closed.
|
|
defer func() {
|
|
fs.dirfd.Store(-1)
|
|
}()
|
|
return unix.Close(int(fs.dirfd.Load()))
|
|
}
|
|
|
|
// Chmod changes the mode of the named file to mode.
|
|
//
|
|
// If the file is a symbolic link, it changes the mode of the link's target.
|
|
// If there is an error, it will be of type *PathError.
|
|
//
|
|
// A different subset of the mode bits are used, depending on the
|
|
// operating system.
|
|
//
|
|
// On Unix, the mode's permission bits, ModeSetuid, ModeSetgid, and
|
|
// ModeSticky are used.
|
|
//
|
|
// On Windows, only the 0200 bit (owner writable) of mode is used; it
|
|
// controls whether the file's read-only attribute is set or cleared.
|
|
// The other bits are currently unused. For compatibility with Go 1.12
|
|
// and earlier, use a non-zero mode. Use mode 0400 for a read-only
|
|
// file and 0600 for a readable+writable file.
|
|
//
|
|
// On Plan 9, the mode's permission bits, ModeAppend, ModeExclusive,
|
|
// and ModeTemporary are used.
|
|
func (fs *UnixFS) Chmod(name string, mode FileMode) error {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return convertErrorType(unix.Fchmodat(dirfd, name, uint32(mode), 0))
|
|
}
|
|
|
|
// Chown changes the numeric uid and gid of the named file.
|
|
//
|
|
// If the file is a symbolic link, it changes the uid and gid of the link's target.
|
|
// A uid or gid of -1 means to not change that value.
|
|
// If there is an error, it will be of type *PathError.
|
|
//
|
|
// On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or
|
|
// EPLAN9 error, wrapped in *PathError.
|
|
func (fs *UnixFS) Chown(name string, uid, gid int) error {
|
|
return fs.fchown(name, uid, gid, 0)
|
|
}
|
|
|
|
// Lchown changes the numeric uid and gid of the named file.
|
|
//
|
|
// If the file is a symbolic link, it changes the uid and gid of the link itself.
|
|
// If there is an error, it will be of type *PathError.
|
|
//
|
|
// On Windows, it always returns the syscall.EWINDOWS error, wrapped
|
|
// in *PathError.
|
|
func (fs *UnixFS) Lchown(name string, uid, gid int) error {
|
|
// With AT_SYMLINK_NOFOLLOW, Fchownat acts like Lchown but allows us to
|
|
// pass a dirfd.
|
|
return fs.fchown(name, uid, gid, AT_SYMLINK_NOFOLLOW)
|
|
}
|
|
|
|
// fchown is a re-usable Fchownat syscall used by Chown and Lchown.
|
|
func (fs *UnixFS) fchown(name string, uid, gid, flags int) error {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, flags))
|
|
}
|
|
|
|
// Chownat is like Chown but allows passing an existing directory file
|
|
// descriptor rather than needing to resolve one.
|
|
func (fs *UnixFS) Chownat(dirfd int, name string, uid, gid int) error {
|
|
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, 0))
|
|
}
|
|
|
|
// Lchownat is like Lchown but allows passing an existing directory file
|
|
// descriptor rather than needing to resolve one.
|
|
func (fs *UnixFS) Lchownat(dirfd int, name string, uid, gid int) error {
|
|
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, AT_SYMLINK_NOFOLLOW))
|
|
}
|
|
|
|
// Chtimes changes the access and modification times of the named
|
|
// file, similar to the Unix utime() or utimes() functions.
|
|
//
|
|
// The underlying filesystem may truncate or round the values to a
|
|
// less precise time unit.
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) Chtimes(name string, atime, mtime time.Time) error {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fs.Chtimesat(dirfd, name, atime, mtime)
|
|
}
|
|
|
|
// Chtimesat is like Chtimes but allows passing an existing directory file
|
|
// descriptor rather than needing to resolve one.
|
|
func (fs *UnixFS) Chtimesat(dirfd int, name string, atime, mtime time.Time) error {
|
|
var utimes [2]unix.Timespec
|
|
set := func(i int, t time.Time) {
|
|
if t.IsZero() {
|
|
utimes[i] = unix.Timespec{Sec: unix.UTIME_OMIT, Nsec: unix.UTIME_OMIT}
|
|
} else {
|
|
utimes[i] = unix.NsecToTimespec(t.UnixNano())
|
|
}
|
|
}
|
|
set(0, atime)
|
|
set(1, mtime)
|
|
// This does support `AT_SYMLINK_NOFOLLOW` as well if needed.
|
|
if err := unix.UtimesNanoAt(dirfd, name, utimes[0:], 0); err != nil {
|
|
return convertErrorType(&PathError{Op: "chtimes", Path: name, Err: err})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create creates or truncates the named file. If the file already exists,
|
|
// it is truncated.
|
|
//
|
|
// If the file does not exist, it is created with mode 0666
|
|
// (before umask). If successful, methods on the returned File can
|
|
// be used for I/O; the associated file descriptor has mode O_RDWR.
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) Create(name string) (File, error) {
|
|
return fs.OpenFile(name, O_CREATE|O_WRONLY|O_TRUNC, 0o644)
|
|
}
|
|
|
|
// Mkdir creates a new directory with the specified name and permission
|
|
// bits (before umask).
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) Mkdir(name string, mode FileMode) error {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fs.Mkdirat(dirfd, name, mode)
|
|
}
|
|
|
|
func (fs *UnixFS) Mkdirat(dirfd int, name string, mode FileMode) error {
|
|
return convertErrorType(unix.Mkdirat(dirfd, name, uint32(mode)))
|
|
}
|
|
|
|
// MkdirAll creates a directory named path, along with any necessary
|
|
// parents, and returns nil, or else returns an error.
|
|
//
|
|
// The permission bits perm (before umask) are used for all
|
|
// directories that MkdirAll creates.
|
|
// If path is already a directory, MkdirAll does nothing
|
|
// and returns nil.
|
|
func (fs *UnixFS) MkdirAll(name string, mode FileMode) error {
|
|
// Ensure name is somewhat clean before continuing.
|
|
name, err := fs.unsafePath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fs.mkdirAll(name, mode)
|
|
}
|
|
|
|
// Open opens the named file for reading.
|
|
//
|
|
// If successful, methods on the returned file can be used for reading; the
|
|
// associated file descriptor has mode O_RDONLY.
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) Open(name string) (File, error) {
|
|
return fs.OpenFile(name, O_RDONLY, 0)
|
|
}
|
|
|
|
// OpenFile is the generalized open call; most users will use Open
|
|
// or Create instead. It opens the named file with specified flag
|
|
// (O_RDONLY etc.).
|
|
//
|
|
// If the file does not exist, and the O_CREATE flag
|
|
// is passed, it is created with mode perm (before umask). If successful,
|
|
// methods on the returned File can be used for I/O.
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) OpenFile(name string, flag int, mode FileMode) (File, error) {
|
|
fd, err := fs.openFile(name, flag, mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Do not close `fd` here, it is passed to a file that needs the fd, the
|
|
// caller of this function is responsible for calling Close() on the File
|
|
// to release the file descriptor.
|
|
return os.NewFile(uintptr(fd), name), nil
|
|
}
|
|
|
|
func (fs *UnixFS) openFile(name string, flag int, mode FileMode) (int, error) {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return fs.openat(dirfd, name, flag, mode)
|
|
}
|
|
|
|
func (fs *UnixFS) OpenFileat(dirfd int, name string, flag int, mode FileMode) (File, error) {
|
|
fd, err := fs.openat(dirfd, name, flag, mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Do not close `fd` here, it is passed to a file that needs the fd, the
|
|
// caller of this function is responsible for calling Close() on the File
|
|
// to release the file descriptor.
|
|
return os.NewFile(uintptr(fd), name), nil
|
|
}
|
|
|
|
// ReadDir reads the named directory,
|
|
//
|
|
// returning all its directory entries sorted by filename.
|
|
// If an error occurs reading the directory, ReadDir returns the entries it
|
|
// was able to read before the error, along with the error.
|
|
func (fs *UnixFS) ReadDir(path string) ([]DirEntry, error) {
|
|
dirfd, name, closeFd, err := fs.safePath(path)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fd, err := fs.openat(dirfd, name, O_DIRECTORY|O_RDONLY, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer unix.Close(fd)
|
|
return fs.readDir(fd, name, nil)
|
|
}
|
|
|
|
// RemoveStat is a combination of Stat and Remove, it is used to more
|
|
// efficiently remove a file when the caller needs to stat it before
|
|
// removing it.
|
|
//
|
|
// This optimized function exists for our QuotaFS abstraction, which needs
|
|
// to track writes to a filesystem. When removing a file, the QuotaFS needs
|
|
// to know if the entry is a file and if so, how large it is. Because we
|
|
// need to Stat a file in order to get its mode and size, we will already
|
|
// know if the entry needs to be removed by using Unlink or Rmdir. The
|
|
// standard `Remove` method just tries both Unlink and Rmdir (in that order)
|
|
// as it ends up usually being faster and more efficient than calling Stat +
|
|
// the proper operation in the first place.
|
|
func (fs *UnixFS) RemoveStat(name string) (FileInfo, error) {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Lstat name, we use Lstat as Unlink doesn't care about symlinks.
|
|
s, err := fs.Lstatat(dirfd, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.IsDir() {
|
|
err = fs.unlinkat(dirfd, name, AT_REMOVEDIR) // Rmdir
|
|
} else {
|
|
err = fs.unlinkat(dirfd, name, 0)
|
|
}
|
|
if err != nil {
|
|
return s, convertErrorType(&PathError{Op: "remove", Path: name, Err: err})
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// Remove removes the named file or (empty) directory.
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) Remove(name string) error {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prevent trying to Remove the base directory.
|
|
if name == "." {
|
|
return &PathError{
|
|
Op: "remove",
|
|
Path: name,
|
|
Err: ErrBadPathResolution,
|
|
}
|
|
}
|
|
|
|
// System call interface forces us to know
|
|
// whether name is a file or directory.
|
|
// Try both: it is cheaper on average than
|
|
// doing a Stat plus the right one.
|
|
err = fs.unlinkat(dirfd, name, 0)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
err1 := fs.unlinkat(dirfd, name, AT_REMOVEDIR) // Rmdir
|
|
if err1 == nil {
|
|
return nil
|
|
}
|
|
|
|
// Both failed: figure out which error to return.
|
|
// OS X and Linux differ on whether unlink(dir)
|
|
// returns EISDIR, so can't use that. However,
|
|
// both agree that rmdir(file) returns ENOTDIR,
|
|
// so we can use that to decide which error is real.
|
|
// Rmdir might also return ENOTDIR if given a bad
|
|
// file path, like /etc/passwd/foo, but in that case,
|
|
// both errors will be ENOTDIR, so it's okay to
|
|
// use the error from unlink.
|
|
if err1 != unix.ENOTDIR {
|
|
err = err1
|
|
}
|
|
return convertErrorType(&PathError{Op: "remove", Path: name, Err: err})
|
|
}
|
|
|
|
// RemoveAll removes path and any children it contains.
|
|
//
|
|
// It removes everything it can but returns the first error
|
|
// it encounters. If the path does not exist, RemoveAll
|
|
// returns nil (no error).
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) RemoveAll(name string) error {
|
|
name, err := fs.unsafePath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// While removeAll internally checks this, I want to make sure we check it
|
|
// and return the proper error so our tests can ensure that this will never
|
|
// be a possibility.
|
|
if name == "." {
|
|
return &PathError{
|
|
Op: "removeall",
|
|
Path: name,
|
|
Err: ErrBadPathResolution,
|
|
}
|
|
}
|
|
return fs.removeAll(name)
|
|
}
|
|
|
|
func (fs *UnixFS) unlinkat(dirfd int, name string, flags int) error {
|
|
return ignoringEINTR(func() error {
|
|
return unix.Unlinkat(dirfd, name, flags)
|
|
})
|
|
}
|
|
|
|
// Rename renames (moves) oldpath to newpath.
|
|
//
|
|
// If newpath already exists and is not a directory, Rename replaces it.
|
|
// OS-specific restrictions may apply when oldpath and newpath are in different directories.
|
|
// Even within the same directory, on non-Unix platforms Rename is not an atomic operation.
|
|
//
|
|
// If there is an error, it will be of type *LinkError.
|
|
func (fs *UnixFS) Rename(oldpath, newpath string) error {
|
|
// Simple case: both paths are the same.
|
|
if oldpath == newpath {
|
|
return nil
|
|
}
|
|
|
|
olddirfd, oldname, closeFd, err := fs.safePath(oldpath)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Ensure that we are not trying to rename the base directory itself.
|
|
// While unix.Renameat ends up throwing a "device or resource busy" error,
|
|
// that doesn't mean we are protecting the system properly.
|
|
if oldname == "." {
|
|
return convertErrorType(&PathError{
|
|
Op: "rename",
|
|
Path: oldname,
|
|
Err: ErrBadPathResolution,
|
|
})
|
|
}
|
|
// Stat the old target to return proper errors.
|
|
if _, err := fs.Lstatat(olddirfd, oldname); err != nil {
|
|
return err
|
|
}
|
|
|
|
newdirfd, newname, closeFd2, err := fs.safePath(newpath)
|
|
if err != nil {
|
|
closeFd2()
|
|
if !errors.Is(err, ErrNotExist) {
|
|
return convertErrorType(err)
|
|
}
|
|
var pathErr *PathError
|
|
if !errors.As(err, &pathErr) {
|
|
return convertErrorType(err)
|
|
}
|
|
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
|
|
return err
|
|
}
|
|
newdirfd, newname, closeFd2, err = fs.safePath(newpath)
|
|
defer closeFd2()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
defer closeFd2()
|
|
}
|
|
|
|
// Ensure that we are not trying to rename the base directory itself.
|
|
// While unix.Renameat ends up throwing a "device or resource busy" error,
|
|
// that doesn't mean we are protecting the system properly.
|
|
if newname == "." {
|
|
return convertErrorType(&PathError{
|
|
Op: "rename",
|
|
Path: newname,
|
|
Err: ErrBadPathResolution,
|
|
})
|
|
}
|
|
// Stat the new target to return proper errors.
|
|
_, err = fs.Lstatat(newdirfd, newname)
|
|
switch {
|
|
case err == nil:
|
|
return convertErrorType(&PathError{
|
|
Op: "rename",
|
|
Path: newname,
|
|
Err: ErrExist,
|
|
})
|
|
case !errors.Is(err, ErrNotExist):
|
|
return err
|
|
}
|
|
return unix.Renameat(olddirfd, oldname, newdirfd, newname)
|
|
}
|
|
|
|
// Stat returns a FileInfo describing the named file.
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) Stat(name string) (FileInfo, error) {
|
|
return fs.fstat(name, 0)
|
|
}
|
|
|
|
// Statat is like Stat but allows passing an existing directory file
|
|
// descriptor rather than needing to resolve one.
|
|
func (fs *UnixFS) Statat(dirfd int, name string) (FileInfo, error) {
|
|
return fs.fstatat(dirfd, name, 0)
|
|
}
|
|
|
|
// Lstat returns a FileInfo describing the named file.
|
|
//
|
|
// If the file is a symbolic link, the returned FileInfo
|
|
// describes the symbolic link. Lstat makes no attempt to follow the link.
|
|
//
|
|
// If there is an error, it will be of type *PathError.
|
|
func (fs *UnixFS) Lstat(name string) (FileInfo, error) {
|
|
return fs.fstat(name, AT_SYMLINK_NOFOLLOW)
|
|
}
|
|
|
|
// Lstatat is like Lstat but allows passing an existing directory file
|
|
// descriptor rather than needing to resolve one.
|
|
func (fs *UnixFS) Lstatat(dirfd int, name string) (FileInfo, error) {
|
|
return fs.fstatat(dirfd, name, AT_SYMLINK_NOFOLLOW)
|
|
}
|
|
|
|
func (fs *UnixFS) fstat(name string, flags int) (FileInfo, error) {
|
|
dirfd, name, closeFd, err := fs.safePath(name)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fs.fstatat(dirfd, name, flags)
|
|
}
|
|
|
|
func (fs *UnixFS) fstatat(dirfd int, name string, flags int) (FileInfo, error) {
|
|
var s fileStat
|
|
if err := ignoringEINTR(func() error {
|
|
return unix.Fstatat(dirfd, name, &s.sys, flags)
|
|
}); err != nil {
|
|
return nil, &PathError{Op: "stat", Path: name, Err: err}
|
|
}
|
|
fillFileStatFromSys(&s, name)
|
|
return &s, nil
|
|
}
|
|
|
|
// Symlink creates newname as a symbolic link to oldname.
|
|
//
|
|
// On Windows, a symlink to a non-existent oldname creates a file symlink;
|
|
// if oldname is later created as a directory the symlink will not work.
|
|
//
|
|
// If there is an error, it will be of type *LinkError.
|
|
func (fs *UnixFS) Symlink(oldpath, newpath string) error {
|
|
dirfd, newpath, closeFd, err := fs.safePath(newpath)
|
|
defer closeFd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ignoringEINTR(func() error {
|
|
// We aren't concerned with oldpath here as a symlink can point anywhere
|
|
// it wants.
|
|
return unix.Symlinkat(oldpath, dirfd, newpath)
|
|
}); err != nil {
|
|
return &LinkError{Op: "symlink", Old: oldpath, New: newpath, Err: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Touch will attempt to open a file for reading and/or writing. If the file
|
|
// does not exist it will be created, and any missing parent directories will
|
|
// also be created. The opened file may be truncated, only if `flag` has
|
|
// O_TRUNC set.
|
|
func (fs *UnixFS) Touch(path string, flag int, mode FileMode) (File, error) {
|
|
if flag&O_CREATE == 0 {
|
|
flag |= O_CREATE
|
|
}
|
|
dirfd, name, closeFd, err := fs.safePath(path)
|
|
defer closeFd()
|
|
if err == nil {
|
|
return fs.OpenFileat(dirfd, name, flag, mode)
|
|
}
|
|
if !errors.Is(err, ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
var pathErr *PathError
|
|
if !errors.As(err, &pathErr) {
|
|
return nil, err
|
|
}
|
|
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
// Try to open the file one more time after creating its parent directories.
|
|
return fs.OpenFile(path, flag, mode)
|
|
}
|
|
|
|
// WalkDir walks the file tree rooted at root, calling fn for each file or
|
|
// directory in the tree, including root.
|
|
//
|
|
// All errors that arise visiting files and directories are filtered by fn:
|
|
// see the [WalkDirFunc] documentation for details.
|
|
//
|
|
// The files are walked in lexical order, which makes the output deterministic
|
|
// but requires WalkDir to read an entire directory into memory before proceeding
|
|
// to walk that directory.
|
|
//
|
|
// WalkDir does not follow symbolic links found in directories,
|
|
// but if root itself is a symbolic link, its target will be walked.
|
|
func (fs *UnixFS) WalkDir(root string, fn WalkDirFunc) error {
|
|
return WalkDir(fs, root, fn)
|
|
}
|
|
|
|
// openat is a wrapper around both unix.Openat and unix.Openat2. If the UnixFS
|
|
// was configured to enable openat2 support, unix.Openat2 will be used instead
|
|
// of unix.Openat due to having better security properties for our use-case.
|
|
func (fs *UnixFS) openat(dirfd int, name string, flag int, mode FileMode) (int, error) {
|
|
if flag&O_NOFOLLOW == 0 {
|
|
flag |= O_NOFOLLOW
|
|
}
|
|
|
|
var fd int
|
|
for {
|
|
var err error
|
|
if fs.useOpenat2 {
|
|
fd, err = fs._openat2(dirfd, name, uint64(flag), uint64(syscallMode(mode)))
|
|
} else {
|
|
fd, err = fs._openat(dirfd, name, flag, uint32(syscallMode(mode)))
|
|
}
|
|
if err == nil {
|
|
break
|
|
}
|
|
// We have to check EINTR here, per issues https://go.dev/issue/11180 and https://go.dev/issue/39237.
|
|
if err == unix.EINTR {
|
|
continue
|
|
}
|
|
return 0, convertErrorType(err)
|
|
}
|
|
|
|
// If we are not using openat2, do additional path checking. This assumes
|
|
// that openat2 is using `RESOLVE_BENEATH` to avoid the same security
|
|
// issue.
|
|
if !fs.useOpenat2 {
|
|
var finalPath string
|
|
finalPath, err := filepath.EvalSymlinks(filepath.Join("/proc/self/fd/", strconv.Itoa(dirfd)))
|
|
if err != nil {
|
|
return fd, convertErrorType(err)
|
|
}
|
|
if err != nil {
|
|
if !errors.Is(err, ErrNotExist) {
|
|
return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
|
|
}
|
|
|
|
// The target of one of the symlinks (EvalSymlinks is recursive)
|
|
// does not exist. So get the path that does not exist and use
|
|
// that for further validation instead.
|
|
var pErr *PathError
|
|
if ok := errors.As(err, &pErr); !ok {
|
|
return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
|
|
}
|
|
finalPath = pErr.Path
|
|
}
|
|
|
|
// Check if the path is within our root.
|
|
if !fs.unsafeIsPathInsideOfBase(finalPath) {
|
|
return fd, convertErrorType(&PathError{
|
|
Op: "openat",
|
|
Path: name,
|
|
Err: ErrBadPathResolution,
|
|
})
|
|
}
|
|
}
|
|
return fd, nil
|
|
}
|
|
|
|
// _openat is a wrapper around unix.Openat. This method should never be directly
|
|
// called, use `openat` instead.
|
|
func (fs *UnixFS) _openat(dirfd int, name string, flag int, mode uint32) (int, error) {
|
|
// Ensure the O_CLOEXEC flag is set.
|
|
// Go sets this in the os package, but since we are directly using unix
|
|
// we need to set it ourselves.
|
|
if flag&O_CLOEXEC == 0 {
|
|
flag |= O_CLOEXEC
|
|
}
|
|
// O_LARGEFILE is set by Openat for us automatically.
|
|
fd, err := unix.Openat(dirfd, name, flag, mode)
|
|
switch {
|
|
case err == nil:
|
|
return fd, nil
|
|
case err == unix.EINTR:
|
|
return 0, err
|
|
case err == unix.EAGAIN:
|
|
return 0, err
|
|
default:
|
|
return 0, &PathError{Op: "openat", Path: name, Err: err}
|
|
}
|
|
}
|
|
|
|
// _openat2 is a wonderful syscall that supersedes the `openat` syscall. It has
|
|
// improved validation and security characteristics that weren't available or
|
|
// considered when `openat` was originally implemented. As such, it is only
|
|
// present in Kernel 5.6 and above.
|
|
//
|
|
// This method should never be directly called, use `openat` instead.
|
|
func (fs *UnixFS) _openat2(dirfd int, name string, flag uint64, mode uint64) (int, error) {
|
|
// Ensure the O_CLOEXEC flag is set.
|
|
// Go sets this when using the os package, but since we are directly using
|
|
// the unix package we need to set it ourselves.
|
|
if flag&O_CLOEXEC == 0 {
|
|
flag |= O_CLOEXEC
|
|
}
|
|
// Ensure the O_LARGEFILE flag is set.
|
|
// Go sets this for unix.Open, unix.Openat, but not unix.Openat2.
|
|
if flag&O_LARGEFILE == 0 {
|
|
flag |= O_LARGEFILE
|
|
}
|
|
fd, err := unix.Openat2(dirfd, name, &unix.OpenHow{
|
|
Flags: flag,
|
|
Mode: mode,
|
|
// This is the bread and butter of preventing a symlink escape, without
|
|
// this option, we have to handle path validation fully on our own.
|
|
//
|
|
// This is why using Openat2 over Openat is preferred if available.
|
|
Resolve: unix.RESOLVE_BENEATH,
|
|
})
|
|
switch {
|
|
case err == nil:
|
|
return fd, nil
|
|
case err == unix.EINTR:
|
|
return 0, err
|
|
case err == unix.EAGAIN:
|
|
return 0, err
|
|
default:
|
|
return 0, &PathError{Op: "openat2", Path: name, Err: err}
|
|
}
|
|
}
|
|
|
|
func (fs *UnixFS) SafePath(path string) (int, string, func(), error) {
|
|
return fs.safePath(path)
|
|
}
|
|
|
|
func (fs *UnixFS) safePath(path string) (dirfd int, file string, closeFd func(), err error) {
|
|
// Default closeFd to a NO-OP.
|
|
closeFd = func() {}
|
|
|
|
// Use unsafePath to clean the path and strip BasePath if path is absolute.
|
|
var name string
|
|
name, err = fs.unsafePath(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Check if dirfd was closed, this will happen if (*UnixFS).Close()
|
|
// was called.
|
|
fsDirfd := int(fs.dirfd.Load())
|
|
if fsDirfd == -1 {
|
|
err = ErrClosed
|
|
return
|
|
}
|
|
|
|
// Split the parent from the last element in the path, this gives us the
|
|
// "file name" and the full path to its parent.
|
|
var dir string
|
|
dir, file = filepath.Split(name)
|
|
// If dir is empty then name is not nested.
|
|
if dir == "" {
|
|
// We don't need to set closeFd here as it will default to a NO-OP and
|
|
// `fs.dirfd` is re-used until the filesystem is no-longer needed.
|
|
dirfd = fsDirfd
|
|
|
|
// Return dirfd, name, an empty closeFd func, and no error
|
|
return
|
|
}
|
|
|
|
// Dir will usually contain a trailing slash as filepath.Split doesn't
|
|
// trim slashes.
|
|
dir = strings.TrimSuffix(dir, "/")
|
|
dirfd, err = fs.openat(fsDirfd, dir, O_DIRECTORY|O_RDONLY, 0)
|
|
if dirfd != 0 {
|
|
// Set closeFd to close the newly opened directory file descriptor.
|
|
closeFd = func() { _ = unix.Close(dirfd) }
|
|
}
|
|
|
|
// Return dirfd, name, the closeFd func, and err
|
|
return
|
|
}
|
|
|
|
// unsafePath prefixes the given path and prefixes it with the filesystem's
|
|
// base path, cleaning the result. The path returned by this function may not
|
|
// be inside the filesystem's base path, additional checks are required to
|
|
// safely use paths returned by this function.
|
|
func (fs *UnixFS) unsafePath(path string) (string, error) {
|
|
// Calling filepath.Clean on the joined directory will resolve it to the
|
|
// absolute path, removing any ../ type of resolution arguments, and leaving
|
|
// us with a direct path link.
|
|
//
|
|
// This will also trim the existing root path off the beginning of the path
|
|
// passed to the function since that can get a bit messy.
|
|
r := filepath.Clean(filepath.Join(fs.basePath, strings.TrimPrefix(path, fs.basePath)))
|
|
|
|
if fs.unsafeIsPathInsideOfBase(r) {
|
|
// This is kinda ironic isn't it.
|
|
// We do this as we are operating with dirfds and `*at` syscalls which
|
|
// behave differently if given an absolute path.
|
|
//
|
|
// First trim the BasePath, then trim any leading slashes.
|
|
r = strings.TrimPrefix(strings.TrimPrefix(r, fs.basePath), "/")
|
|
// If the path is empty then return "." as the path is pointing to the
|
|
// root.
|
|
if r == "" {
|
|
return ".", nil
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
return "", &PathError{
|
|
Op: "safePath",
|
|
Path: path,
|
|
Err: ErrBadPathResolution,
|
|
}
|
|
}
|
|
|
|
// unsafeIsPathInsideOfBase checks if the given path is inside the filesystem's
|
|
// base path.
|
|
func (fs *UnixFS) unsafeIsPathInsideOfBase(path string) bool {
|
|
return strings.HasPrefix(
|
|
strings.TrimSuffix(path, "/")+"/",
|
|
fs.basePath+"/",
|
|
)
|
|
}
|