ufs: improve error handling

Signed-off-by: Matthew Penner <me@matthewp.io>
This commit is contained in:
Matthew Penner 2025-03-09 19:19:29 -06:00
parent 25966e7838
commit 407b783aa5
No known key found for this signature in database
6 changed files with 347 additions and 165 deletions

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
iofs "io/fs" iofs "io/fs"
"os" "os"
"syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@ -48,9 +49,9 @@ type PathError = iofs.PathError
// SyscallError records an error from a specific system call. // SyscallError records an error from a specific system call.
type SyscallError = os.SyscallError type SyscallError = os.SyscallError
// NewSyscallError returns, as an error, a new SyscallError // NewSyscallError returns, as an error, a new [*os.SyscallError] with the
// with the given system call name and error details. // given system call name and error details. As a convenience, if err is nil,
// As a convenience, if err is nil, NewSyscallError returns nil. // [NewSyscallError] returns nil.
func NewSyscallError(syscall string, err error) error { func NewSyscallError(syscall string, err error) error {
return os.NewSyscallError(syscall, err) return os.NewSyscallError(syscall, err)
} }
@ -61,62 +62,122 @@ func convertErrorType(err error) error {
if err == nil { if err == nil {
return nil return nil
} }
var pErr *PathError var pErr *PathError
switch { if errors.As(err, &pErr) {
case errors.As(err, &pErr): if errno, ok := pErr.Err.(syscall.Errno); ok {
switch { return errnoToPathError(errno, pErr.Op, pErr.Path)
// File exists }
case errors.Is(pErr.Err, unix.EEXIST): return pErr
return &PathError{ }
Op: pErr.Op,
Path: pErr.Path, // If the error wasn't already a path error and is a errno, wrap it with
Err: ErrExist, // details that we can use to know there is something wrong with our
} // error wrapping somewhere.
// Is a directory var errno syscall.Errno
case errors.Is(pErr.Err, unix.EISDIR): if errors.As(err, &errno) {
return &PathError{ return &PathError{
Op: pErr.Op, Op: "!(UNKNOWN)",
Path: pErr.Path, Path: "!(UNKNOWN)",
Err: ErrIsDirectory, Err: err,
}
// Not a directory
case errors.Is(pErr.Err, unix.ENOTDIR):
return &PathError{
Op: pErr.Op,
Path: pErr.Path,
Err: ErrNotDirectory,
}
// No such file or directory
case errors.Is(pErr.Err, unix.ENOENT):
return &PathError{
Op: pErr.Op,
Path: pErr.Path,
Err: ErrNotExist,
}
// Operation not permitted
case errors.Is(pErr.Err, unix.EPERM):
return &PathError{
Op: pErr.Op,
Path: pErr.Path,
Err: ErrPermission,
}
// Invalid cross-device link
case errors.Is(pErr.Err, unix.EXDEV):
return &PathError{
Op: pErr.Op,
Path: pErr.Path,
Err: ErrBadPathResolution,
}
// Too many levels of symbolic links
case errors.Is(pErr.Err, unix.ELOOP):
return &PathError{
Op: pErr.Op,
Path: pErr.Path,
Err: ErrBadPathResolution,
}
// TODO: EROFS
// TODO: ENOTEMPTY
} }
} }
return err return err
} }
// ensurePathError ensures that err is a PathError. The op and path arguments
// are only used of the error isn't already a PathError.
func ensurePathError(err error, op, path string) error {
if err == nil {
return nil
}
// Check if the error is already a PathError.
var pErr *PathError
if errors.As(err, &pErr) {
// If underlying error is a errno, convert it.
//
// DO NOT USE `errors.As` or whatever here, the error will either be
// an errno, or it will be wrapped already.
if errno, ok := pErr.Err.(syscall.Errno); ok {
return errnoToPathError(errno, pErr.Op, pErr.Path)
}
// Return the PathError as-is without modification.
return pErr
}
// If the error is directly an errno, convert it to a PathError.
var errno syscall.Errno
if errors.As(err, &errno) {
return errnoToPathError(errno, op, path)
}
// Otherwise just wrap it as a PathError without any additional changes.
return &PathError{
Op: op,
Path: path,
Err: err,
}
}
// errnoToPathError converts an errno into a proper path error.
func errnoToPathError(err syscall.Errno, op, path string) error {
switch err {
// File exists
case unix.EEXIST:
return &PathError{
Op: op,
Path: path,
Err: ErrExist,
}
// Is a directory
case unix.EISDIR:
return &PathError{
Op: op,
Path: path,
Err: ErrIsDirectory,
}
// Not a directory
case unix.ENOTDIR:
return &PathError{
Op: op,
Path: path,
Err: ErrNotDirectory,
}
// No such file or directory
case unix.ENOENT:
return &PathError{
Op: op,
Path: path,
Err: ErrNotExist,
}
// Operation not permitted
case unix.EPERM:
return &PathError{
Op: op,
Path: path,
Err: ErrPermission,
}
// Invalid cross-device link
case unix.EXDEV:
return &PathError{
Op: op,
Path: path,
Err: ErrBadPathResolution,
}
// Too many levels of symbolic links
case unix.ELOOP:
return &PathError{
Op: op,
Path: path,
Err: ErrBadPathResolution,
}
default:
return &PathError{
Op: op,
Path: path,
Err: err,
}
}
}

View File

@ -7,8 +7,12 @@ import (
"sync/atomic" "sync/atomic"
) )
// Quota . // Quota is a wrapper around [*UnixFS] that provides the ability to limit the
// TODO: document // disk usage of the filesystem.
//
// NOTE: this is not a full complete quota filesystem, it provides utilities for
// tracking and checking the usage of the filesystem. The only operation that is
// automatically accounted against the quota are file deletions.
type Quota struct { type Quota struct {
// fs is the underlying filesystem that runs the actual I/O operations. // fs is the underlying filesystem that runs the actual I/O operations.
*UnixFS *UnixFS
@ -28,8 +32,7 @@ type Quota struct {
usage atomic.Int64 usage atomic.Int64
} }
// NewQuota . // NewQuota creates a new Quota filesystem using an existing UnixFS and a limit.
// TODO: document
func NewQuota(fs *UnixFS, limit int64) *Quota { func NewQuota(fs *UnixFS, limit int64) *Quota {
qfs := Quota{UnixFS: fs} qfs := Quota{UnixFS: fs}
qfs.limit.Store(limit) qfs.limit.Store(limit)
@ -105,6 +108,9 @@ func (fs *Quota) CanFit(size int64) bool {
return false return false
} }
// Remove removes the named file or (empty) directory.
//
// If there is an error, it will be of type [*PathError].
func (fs *Quota) Remove(name string) error { func (fs *Quota) Remove(name string) error {
// For information on why this interface is used here, check its // For information on why this interface is used here, check its
// documentation. // documentation.
@ -129,7 +135,7 @@ func (fs *Quota) Remove(name string) error {
// it encounters. If the path does not exist, RemoveAll // it encounters. If the path does not exist, RemoveAll
// returns nil (no error). // returns nil (no error).
// //
// If there is an error, it will be of type *PathError. // If there is an error, it will be of type [*PathError].
func (fs *Quota) RemoveAll(name string) error { func (fs *Quota) RemoveAll(name string) error {
name, err := fs.unsafePath(name) name, err := fs.unsafePath(name)
if err != nil { if err != nil {

View File

@ -81,7 +81,16 @@ func (fs *UnixFS) Chmod(name string, mode FileMode) error {
if err != nil { if err != nil {
return err return err
} }
return convertErrorType(unix.Fchmodat(dirfd, name, uint32(mode), 0)) return fs.fchmodat("chmod", dirfd, name, mode)
}
// Chmodat is like Chmod but it takes a dirfd and name instead of a full path.
func (fs *UnixFS) Chmodat(dirfd int, name string, mode FileMode) error {
return fs.fchmodat("chmodat", dirfd, name, mode)
}
func (fs *UnixFS) fchmodat(op string, dirfd int, name string, mode FileMode) error {
return ensurePathError(unix.Fchmodat(dirfd, name, uint32(mode), 0), op, name)
} }
// Chown changes the numeric uid and gid of the named file. // Chown changes the numeric uid and gid of the named file.
@ -93,7 +102,7 @@ func (fs *UnixFS) Chmod(name string, mode FileMode) error {
// On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or // On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or
// EPLAN9 error, wrapped in *PathError. // EPLAN9 error, wrapped in *PathError.
func (fs *UnixFS) Chown(name string, uid, gid int) error { func (fs *UnixFS) Chown(name string, uid, gid int) error {
return fs.fchown(name, uid, gid, 0) return ensurePathError(fs.fchown(name, uid, gid, 0), "chown", name)
} }
// Lchown changes the numeric uid and gid of the named file. // Lchown changes the numeric uid and gid of the named file.
@ -106,7 +115,7 @@ func (fs *UnixFS) Chown(name string, uid, gid int) error {
func (fs *UnixFS) Lchown(name string, uid, gid int) error { func (fs *UnixFS) Lchown(name string, uid, gid int) error {
// With AT_SYMLINK_NOFOLLOW, Fchownat acts like Lchown but allows us to // With AT_SYMLINK_NOFOLLOW, Fchownat acts like Lchown but allows us to
// pass a dirfd. // pass a dirfd.
return fs.fchown(name, uid, gid, AT_SYMLINK_NOFOLLOW) return ensurePathError(fs.fchown(name, uid, gid, AT_SYMLINK_NOFOLLOW), "lchown", name)
} }
// fchown is a re-usable Fchownat syscall used by Chown and Lchown. // fchown is a re-usable Fchownat syscall used by Chown and Lchown.
@ -116,19 +125,19 @@ func (fs *UnixFS) fchown(name string, uid, gid, flags int) error {
if err != nil { if err != nil {
return err return err
} }
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, flags)) return unix.Fchownat(dirfd, name, uid, gid, flags)
} }
// Chownat is like Chown but allows passing an existing directory file // Chownat is like Chown but allows passing an existing directory file
// descriptor rather than needing to resolve one. // descriptor rather than needing to resolve one.
func (fs *UnixFS) Chownat(dirfd int, name string, uid, gid int) error { func (fs *UnixFS) Chownat(dirfd int, name string, uid, gid int) error {
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, 0)) return ensurePathError(unix.Fchownat(dirfd, name, uid, gid, 0), "chownat", name)
} }
// Lchownat is like Lchown but allows passing an existing directory file // Lchownat is like Lchown but allows passing an existing directory file
// descriptor rather than needing to resolve one. // descriptor rather than needing to resolve one.
func (fs *UnixFS) Lchownat(dirfd int, name string, uid, gid int) error { func (fs *UnixFS) Lchownat(dirfd int, name string, uid, gid int) error {
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, AT_SYMLINK_NOFOLLOW)) return ensurePathError(unix.Fchownat(dirfd, name, uid, gid, AT_SYMLINK_NOFOLLOW), "lchownat", name)
} }
// Chtimes changes the access and modification times of the named // Chtimes changes the access and modification times of the named
@ -160,11 +169,9 @@ func (fs *UnixFS) Chtimesat(dirfd int, name string, atime, mtime time.Time) erro
} }
set(0, atime) set(0, atime)
set(1, mtime) set(1, mtime)
// This does support `AT_SYMLINK_NOFOLLOW` as well if needed. // This does support `AT_SYMLINK_NOFOLLOW` as well if needed.
if err := unix.UtimesNanoAt(dirfd, name, utimes[0:], 0); err != nil { return ensurePathError(unix.UtimesNanoAt(dirfd, name, utimes[0:], 0), "chtimes", name)
return convertErrorType(&PathError{Op: "chtimes", Path: name, Err: err})
}
return nil
} }
// Create creates or truncates the named file. If the file already exists, // Create creates or truncates the named file. If the file already exists,
@ -188,11 +195,15 @@ func (fs *UnixFS) Mkdir(name string, mode FileMode) error {
if err != nil { if err != nil {
return err return err
} }
return fs.Mkdirat(dirfd, name, mode) return fs.mkdirat("mkdir", dirfd, name, mode)
} }
func (fs *UnixFS) Mkdirat(dirfd int, name string, mode FileMode) error { func (fs *UnixFS) Mkdirat(dirfd int, name string, mode FileMode) error {
return convertErrorType(unix.Mkdirat(dirfd, name, uint32(mode))) return fs.mkdirat("mkdirat", dirfd, name, mode)
}
func (fs *UnixFS) mkdirat(op string, dirfd int, name string, mode FileMode) error {
return ensurePathError(unix.Mkdirat(dirfd, name, uint32(mode)), op, name)
} }
// MkdirAll creates a directory named path, along with any necessary // MkdirAll creates a directory named path, along with any necessary
@ -313,7 +324,7 @@ func (fs *UnixFS) RemoveStat(name string) (FileInfo, error) {
err = fs.unlinkat(dirfd, name, 0) err = fs.unlinkat(dirfd, name, 0)
} }
if err != nil { if err != nil {
return s, convertErrorType(&PathError{Op: "remove", Path: name, Err: err}) return s, ensurePathError(err, "rename", name)
} }
return s, nil return s, nil
} }
@ -362,7 +373,7 @@ func (fs *UnixFS) Remove(name string) error {
if err1 != unix.ENOTDIR { if err1 != unix.ENOTDIR {
err = err1 err = err1
} }
return convertErrorType(&PathError{Op: "remove", Path: name, Err: err}) return ensurePathError(err, "remove", name)
} }
// RemoveAll removes path and any children it contains. // RemoveAll removes path and any children it contains.
@ -377,6 +388,7 @@ func (fs *UnixFS) RemoveAll(name string) error {
if err != nil { if err != nil {
return err return err
} }
// While removeAll internally checks this, I want to make sure we check it // 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 // and return the proper error so our tests can ensure that this will never
// be a possibility. // be a possibility.
@ -387,9 +399,29 @@ func (fs *UnixFS) RemoveAll(name string) error {
Err: ErrBadPathResolution, Err: ErrBadPathResolution,
} }
} }
return fs.removeAll(name) return fs.removeAll(name)
} }
// RemoveContents recursively removes the contents of name.
//
// It removes everything it can but returns the first error
// it encounters. If the path does not exist, RemoveContents
// returns nil (no error).
//
// If there is an error, it will be of type [*PathError].
func (fs *UnixFS) RemoveContents(name string) error {
name, err := fs.unsafePath(name)
if err != nil {
return err
}
// Unlike RemoveAll, we don't remove `name` itself, only it's contents.
// So there is no need to check for a name of `.` here.
return fs.removeContents(name)
}
func (fs *UnixFS) unlinkat(dirfd int, name string, flags int) error { func (fs *UnixFS) unlinkat(dirfd int, name string, flags int) error {
return ignoringEINTR(func() error { return ignoringEINTR(func() error {
return unix.Unlinkat(dirfd, name, flags) return unix.Unlinkat(dirfd, name, flags)
@ -418,11 +450,11 @@ func (fs *UnixFS) Rename(oldpath, newpath string) error {
// While unix.Renameat ends up throwing a "device or resource busy" error, // While unix.Renameat ends up throwing a "device or resource busy" error,
// that doesn't mean we are protecting the system properly. // that doesn't mean we are protecting the system properly.
if oldname == "." { if oldname == "." {
return convertErrorType(&PathError{ return &PathError{
Op: "rename", Op: "rename",
Path: oldname, Path: oldname,
Err: ErrBadPathResolution, Err: ErrBadPathResolution,
}) }
} }
// Stat the old target to return proper errors. // Stat the old target to return proper errors.
if _, err := fs.Lstatat(olddirfd, oldname); err != nil { if _, err := fs.Lstatat(olddirfd, oldname); err != nil {
@ -433,11 +465,11 @@ func (fs *UnixFS) Rename(oldpath, newpath string) error {
if err != nil { if err != nil {
closeFd2() closeFd2()
if !errors.Is(err, ErrNotExist) { if !errors.Is(err, ErrNotExist) {
return convertErrorType(err) return err
} }
var pathErr *PathError var pathErr *PathError
if !errors.As(err, &pathErr) { if !errors.As(err, &pathErr) {
return convertErrorType(err) return err
} }
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil { if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
return err return err
@ -455,25 +487,28 @@ func (fs *UnixFS) Rename(oldpath, newpath string) error {
// While unix.Renameat ends up throwing a "device or resource busy" error, // While unix.Renameat ends up throwing a "device or resource busy" error,
// that doesn't mean we are protecting the system properly. // that doesn't mean we are protecting the system properly.
if newname == "." { if newname == "." {
return convertErrorType(&PathError{ return &PathError{
Op: "rename", Op: "rename",
Path: newname, Path: newname,
Err: ErrBadPathResolution, Err: ErrBadPathResolution,
}) }
} }
// Stat the new target to return proper errors. // Stat the new target to return proper errors.
_, err = fs.Lstatat(newdirfd, newname) _, err = fs.Lstatat(newdirfd, newname)
switch { switch {
case err == nil: case err == nil:
return convertErrorType(&PathError{ return &PathError{
Op: "rename", Op: "rename",
Path: newname, Path: newname,
Err: ErrExist, Err: ErrExist,
}) }
case !errors.Is(err, ErrNotExist): case !errors.Is(err, ErrNotExist):
return err return err
} }
return unix.Renameat(olddirfd, oldname, newdirfd, newname) if err := unix.Renameat(olddirfd, oldname, newdirfd, newname); err != nil {
return &LinkError{Op: "rename", Old: oldpath, New: newpath, Err: err}
}
return nil
} }
// Stat returns a FileInfo describing the named file. // Stat returns a FileInfo describing the named file.
@ -527,7 +562,7 @@ func (fs *UnixFS) _fstatat(op string, dirfd int, name string, flags int) (FileIn
if err := ignoringEINTR(func() error { if err := ignoringEINTR(func() error {
return unix.Fstatat(dirfd, name, &s.sys, flags) return unix.Fstatat(dirfd, name, &s.sys, flags)
}); err != nil { }); err != nil {
return nil, &PathError{Op: op, Path: name, Err: err} return nil, ensurePathError(err, op, name)
} }
fillFileStatFromSys(&s, name) fillFileStatFromSys(&s, name)
return &s, nil return &s, nil
@ -563,23 +598,42 @@ func (fs *UnixFS) Touch(path string, flag int, mode FileMode) (File, error) {
if flag&O_CREATE == 0 { if flag&O_CREATE == 0 {
flag |= O_CREATE flag |= O_CREATE
} }
dirfd, name, closeFd, err := fs.safePath(path) dirfd, name, closeFd, err, _ := fs.TouchPath(path)
defer closeFd() defer closeFd()
if err == nil { if err != nil {
return fs.OpenFileat(dirfd, name, flag, mode)
}
if !errors.Is(err, ErrNotExist) {
return nil, err return nil, err
} }
return fs.OpenFileat(dirfd, name, flag, mode)
}
// TouchPath is like SafePath except that it will create any missing directories
// in the path. Unlike SafePath, TouchPath returns an additional boolean which
// indicates whether the parent directories already existed, this is intended to
// be used as a way to know if the final destination could already exist.
func (fs *UnixFS) TouchPath(path string) (int, string, func(), error, bool) {
dirfd, name, closeFd, err := fs.safePath(path)
switch {
case err == nil:
return dirfd, name, closeFd, nil, true
case !errors.Is(err, ErrNotExist):
return dirfd, name, closeFd, err, false
}
var pathErr *PathError var pathErr *PathError
if !errors.As(err, &pathErr) { if !errors.As(err, &pathErr) {
return nil, err return dirfd, name, closeFd, err, false
} }
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil { if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
return nil, err return dirfd, name, closeFd, err, false
} }
// Try to open the file one more time after creating its parent directories.
return fs.OpenFile(path, flag, mode) // Close the previous file descriptor since we are going to be opening
// a new one.
closeFd()
// Run safe path again now that the parent directories have been created.
dirfd, name, closeFd, err = fs.safePath(path)
return dirfd, name, closeFd, err, false
} }
// WalkDir walks the file tree rooted at root, calling fn for each file or // WalkDir walks the file tree rooted at root, calling fn for each file or
@ -621,7 +675,7 @@ func (fs *UnixFS) openat(dirfd int, name string, flag int, mode FileMode) (int,
if err == unix.EINTR { if err == unix.EINTR {
continue continue
} }
return -1, convertErrorType(err) return 0, err
} }
// If we are using openat2, we don't need the additional security checks. // If we are using openat2, we don't need the additional security checks.
@ -646,24 +700,29 @@ func (fs *UnixFS) openat(dirfd int, name string, flag int, mode FileMode) (int,
if !errors.As(err, &pErr) { if !errors.As(err, &pErr) {
return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err)) return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
} }
// Update the final path to whatever directory or path didn't exist while
// recursing any symlinks.
finalPath = pErr.Path finalPath = pErr.Path
// Ensure the error is wrapped correctly.
err = convertErrorType(err)
} }
// Check if the path is within our root. // Check if the path is within our root.
if !fs.unsafeIsPathInsideOfBase(finalPath) { if !fs.unsafeIsPathInsideOfBase(finalPath) {
return fd, convertErrorType(&PathError{ op := "openat"
Op: "openat", if fs.useOpenat2 {
op = "openat2"
}
return fd, &PathError{
Op: op,
Path: name, Path: name,
Err: ErrBadPathResolution, Err: ErrBadPathResolution,
}) }
} }
// This handles any hanging errors. // Return the file descriptor and any potential error.
if err != nil { return fd, err
return fd, convertErrorType(err)
}
return fd, nil
} }
// _openat is a wrapper around unix.Openat. This method should never be directly // _openat is a wrapper around unix.Openat. This method should never be directly
@ -681,11 +740,11 @@ func (fs *UnixFS) _openat(dirfd int, name string, flag int, mode uint32) (int, e
case err == nil: case err == nil:
return fd, nil return fd, nil
case err == unix.EINTR: case err == unix.EINTR:
return -1, err return fd, err
case err == unix.EAGAIN: case err == unix.EAGAIN:
return -1, err return fd, err
default: default:
return -1, &PathError{Op: "openat", Path: name, Err: err} return fd, ensurePathError(err, "openat", name)
} }
} }
@ -720,11 +779,11 @@ func (fs *UnixFS) _openat2(dirfd int, name string, flag, mode uint64) (int, erro
case err == nil: case err == nil:
return fd, nil return fd, nil
case err == unix.EINTR: case err == unix.EINTR:
return -1, err return fd, err
case err == unix.EAGAIN: case err == unix.EAGAIN:
return -1, err return fd, err
default: default:
return -1, &PathError{Op: "openat2", Path: name, Err: err} return fd, ensurePathError(err, "openat2", name)
} }
} }
@ -746,7 +805,7 @@ func (fs *UnixFS) safePath(path string) (dirfd int, file string, closeFd func(),
// Open the base path. We use this as the sandbox root for any further // Open the base path. We use this as the sandbox root for any further
// operations. // operations.
var fsDirfd int var fsDirfd int
fsDirfd, err = unix.Openat(AT_EMPTY_PATH, fs.basePath, O_DIRECTORY|O_RDONLY, 0) fsDirfd, err = fs._openat(AT_EMPTY_PATH, fs.basePath, O_DIRECTORY|O_RDONLY, 0)
if err != nil { if err != nil {
return return
} }
@ -772,7 +831,7 @@ func (fs *UnixFS) safePath(path string) (dirfd int, file string, closeFd func(),
// An error occurred while opening the directory, but we already opened // An error occurred while opening the directory, but we already opened
// the filesystem root, so we still need to ensure it gets closed. // the filesystem root, so we still need to ensure it gets closed.
closeFd = func() { _ = unix.Close(fsDirfd) } closeFd = func() { _ = unix.Close(fsDirfd) }
} else { } else {
// Set closeFd to close the newly opened directory file descriptor. // Set closeFd to close the newly opened directory file descriptor.
closeFd = func() { closeFd = func() {
_ = unix.Close(dirfd) _ = unix.Close(dirfd)
@ -780,22 +839,24 @@ func (fs *UnixFS) safePath(path string) (dirfd int, file string, closeFd func(),
} }
} }
// Return dirfd, name, the closeFd func, and err // Return dirfd, name, the closeFd func, and err
return return
} }
// unsafePath prefixes the given path and prefixes it with the filesystem's // unsafePath strips and joins the given path with the filesystem's base path,
// base path, cleaning the result. The path returned by this function may not // cleaning the result. The cleaned path is then checked if it starts with the
// be inside the filesystem's base path, additional checks are required to // filesystem's base path to obvious any obvious path traversal escapes. The
// safely use paths returned by this function. // fully resolved path (if symlinks are followed) may not be within 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) { func (fs *UnixFS) unsafePath(path string) (string, error) {
// Calling filepath.Clean on the joined directory will resolve it to the // Calling filepath.Clean on the path will resolve it to it's absolute path,
// absolute path, removing any ../ type of resolution arguments, and leaving // removing any path traversal arguments (such as ..), leaving us with an
// us with a direct path link. // absolute path we can then use.
// //
// This will also trim the existing root path off the beginning of the path // This will also trim the filesystem's base path from the given path and
// passed to the function since that can get a bit messy. // join the base path back on to ensure the path starts with the base path
// without appending it twice.
r := filepath.Clean(filepath.Join(fs.basePath, strings.TrimPrefix(path, fs.basePath))) r := filepath.Clean(filepath.Join(fs.basePath, strings.TrimPrefix(path, fs.basePath)))
if fs.unsafeIsPathInsideOfBase(r) { if fs.unsafeIsPathInsideOfBase(r) {
@ -822,6 +883,10 @@ func (fs *UnixFS) unsafePath(path string) (string, error) {
// unsafeIsPathInsideOfBase checks if the given path is inside the filesystem's // unsafeIsPathInsideOfBase checks if the given path is inside the filesystem's
// base path. // base path.
//
// NOTE: this method doesn't clean the given path or attempt to join the
// filesystem's base path. This is purely a basic prefix check against the
// given path.
func (fs *UnixFS) unsafeIsPathInsideOfBase(path string) bool { func (fs *UnixFS) unsafeIsPathInsideOfBase(path string) bool {
return strings.HasPrefix( return strings.HasPrefix(
strings.TrimSuffix(path, "/")+"/", strings.TrimSuffix(path, "/")+"/",

View File

@ -10,10 +10,6 @@
package ufs package ufs
import (
"golang.org/x/sys/unix"
)
// mkdirAll is a recursive Mkdir implementation that properly handles symlinks. // mkdirAll is a recursive Mkdir implementation that properly handles symlinks.
func (fs *UnixFS) mkdirAll(name string, mode FileMode) error { func (fs *UnixFS) mkdirAll(name string, mode FileMode) error {
// Fast path: if we can tell whether path is a directory or file, stop with success or error. // Fast path: if we can tell whether path is a directory or file, stop with success or error.
@ -30,7 +26,7 @@ func (fs *UnixFS) mkdirAll(name string, mode FileMode) error {
if dir.IsDir() { if dir.IsDir() {
return nil return nil
} }
return convertErrorType(&PathError{Op: "mkdir", Path: name, Err: unix.ENOTDIR}) return &PathError{Op: "mkdir", Path: name, Err: ErrNotDirectory}
} }
// Slow path: make sure parent exists and then call Mkdir for path. // Slow path: make sure parent exists and then call Mkdir for path.

View File

@ -52,60 +52,69 @@ func removeAll(fs unixFS, path string) error {
parentDir, base := splitPath(path) parentDir, base := splitPath(path)
parent, err := fs.Open(parentDir) parent, err := fs.Open(parentDir)
if errors.Is(err, ErrNotExist) { if err != nil {
if !errors.Is(err, ErrNotExist) {
return err
}
// If parent does not exist, base cannot exist. Fail silently // If parent does not exist, base cannot exist. Fail silently
return nil return nil
} }
if err != nil {
return err
}
defer parent.Close() defer parent.Close()
if err := removeAllFrom(fs, parent, base); err != nil { if err := removeAllFrom(fs, parent, base); err != nil {
if pathErr, ok := err.(*PathError); ok { if pathErr, ok := err.(*PathError); ok {
pathErr.Path = parentDir + string(os.PathSeparator) + pathErr.Path pathErr.Path = parentDir + string(os.PathSeparator) + pathErr.Path
err = pathErr err = convertErrorType(pathErr)
} else {
err = ensurePathError(err, "removeallfrom", base)
} }
return convertErrorType(err) return err
} }
return nil return nil
} }
func removeAllFrom(fs unixFS, parent File, base string) error { func (fs *UnixFS) removeContents(path string) error {
parentFd := int(parent.Fd()) return removeContents(fs, path)
// Simple case: if Unlink (aka remove) works, we're done. }
err := fs.unlinkat(parentFd, base, 0)
if err == nil || errors.Is(err, ErrNotExist) { func removeContents(fs unixFS, path string) error {
if path == "" {
// fail silently to retain compatibility with previous behavior
// of RemoveAll. See issue https://go.dev/issue/28830.
return nil return nil
} }
// EISDIR means that we have a directory, and we need to // RemoveAll recurses by deleting the path base from
// remove its contents. // its parent directory
// EPERM or EACCES means that we don't have write permission on parentDir, base := splitPath(path)
// the parent directory, but this entry might still be a directory
// whose contents need to be removed.
// Otherwise, just return the error.
if err != unix.EISDIR && err != unix.EPERM && err != unix.EACCES {
return &PathError{Op: "unlinkat", Path: base, Err: err}
}
// Is this a directory we need to recurse into? parent, err := fs.Open(parentDir)
var statInfo unix.Stat_t if err != nil {
statErr := ignoringEINTR(func() error { if !errors.Is(err, ErrNotExist) {
return unix.Fstatat(parentFd, base, &statInfo, AT_SYMLINK_NOFOLLOW) return err
})
if statErr != nil {
if errors.Is(statErr, ErrNotExist) {
return nil
} }
return &PathError{Op: "fstatat", Path: base, Err: statErr} // If parent does not exist, base cannot exist. Fail silently
} return nil
if statInfo.Mode&unix.S_IFMT != unix.S_IFDIR {
// Not a directory; return the error from the unix.Unlinkat.
return &PathError{Op: "unlinkat", Path: base, Err: err}
} }
defer parent.Close()
if err := removeContentsFrom(fs, parent, base); err != nil {
if pathErr, ok := err.(*PathError); ok {
pathErr.Path = parentDir + string(os.PathSeparator) + pathErr.Path
err = convertErrorType(pathErr)
} else {
err = ensurePathError(err, "removecontentsfrom", base)
}
return err
}
return nil
}
// removeContentsFrom recursively removes all descendants of parent without
// removing parent itself. Parent must be a directory.
func removeContentsFrom(fs unixFS, parent File, base string) error {
parentFd := int(parent.Fd())
// Remove the directory's entries.
var recurseErr error var recurseErr error
for { for {
const reqSize = 1024 const reqSize = 1024
@ -168,6 +177,50 @@ func removeAllFrom(fs unixFS, parent File, base string) error {
} }
} }
return nil
}
func removeAllFrom(fs unixFS, parent File, base string) error {
parentFd := int(parent.Fd())
// Simple case: if Unlink (aka remove) works, we're done.
err := fs.unlinkat(parentFd, base, 0)
if err == nil || errors.Is(err, ErrNotExist) {
return nil
}
// EISDIR means that we have a directory, and we need to
// remove its contents.
// EPERM or EACCES means that we don't have write permission on
// the parent directory, but this entry might still be a directory
// whose contents need to be removed.
// Otherwise, just return the error.
if err != unix.EISDIR && err != unix.EPERM && err != unix.EACCES {
return &PathError{Op: "unlinkat", Path: base, Err: err}
}
// Is this a directory we need to recurse into?
var statInfo unix.Stat_t
statErr := ignoringEINTR(func() error {
return unix.Fstatat(parentFd, base, &statInfo, AT_SYMLINK_NOFOLLOW)
})
if statErr != nil {
if errors.Is(statErr, ErrNotExist) {
return nil
}
return &PathError{Op: "fstatat", Path: base, Err: statErr}
}
if statInfo.Mode&unix.S_IFMT != unix.S_IFDIR {
// Not a directory; return the error from the unix.Unlinkat.
return &PathError{Op: "unlinkat", Path: base, Err: err}
}
// Remove all contents will remove the contents of the directory.
//
// It was split out of this function to allow the deletion of the
// contents of a directory, without deleting the directory itself.
recurseErr := removeContentsFrom(fs, parent, base)
// Remove the directory itself. // Remove the directory itself.
unlinkErr := fs.unlinkat(parentFd, base, AT_REMOVEDIR) unlinkErr := fs.unlinkat(parentFd, base, AT_REMOVEDIR)
if unlinkErr == nil || errors.Is(unlinkErr, ErrNotExist) { if unlinkErr == nil || errors.Is(unlinkErr, ErrNotExist) {
@ -177,7 +230,8 @@ func removeAllFrom(fs unixFS, parent File, base string) error {
if recurseErr != nil { if recurseErr != nil {
return recurseErr return recurseErr
} }
return &PathError{Op: "unlinkat", Path: base, Err: unlinkErr}
return ensurePathError(err, "unlinkat", base)
} }
// openFdAt opens path relative to the directory in fd. // openFdAt opens path relative to the directory in fd.

View File

@ -199,7 +199,7 @@ func (fs *UnixFS) modeTypeFromDirent(de *unix.Dirent, fd int, name string) (File
func (fs *UnixFS) modeType(dirfd int, name string) (FileMode, error) { func (fs *UnixFS) modeType(dirfd int, name string) (FileMode, error) {
fi, err := fs.Lstatat(dirfd, name) fi, err := fs.Lstatat(dirfd, name)
if err != nil { if err != nil {
return 0, fmt.Errorf("ufs: error finding mode type for %s during readDir: %w", name, convertErrorType(err)) return 0, fmt.Errorf("ufs: error finding mode type for %s during readDir: %w", name, err)
} }
return fi.Mode() & ModeType, nil return fi.Mode() & ModeType, nil
} }
@ -227,7 +227,7 @@ func (fs *UnixFS) readDir(fd int, name, relative string, b []byte) ([]DirEntry,
if err == unix.EINTR { if err == unix.EINTR {
continue continue
} }
return nil, fmt.Errorf("ufs: error with getdents during readDir: %w", convertErrorType(err)) return nil, ensurePathError(err, "getdents", name)
} }
if n <= 0 { if n <= 0 {
// end of directory: normal exit // end of directory: normal exit