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>
208 lines
5.3 KiB
Go
208 lines
5.3 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Code in this file was derived from `go/src/os/removeall_at.go`.
|
|
|
|
// Copyright 2009 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the `go.LICENSE` file.
|
|
|
|
//go:build unix
|
|
|
|
package ufs
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type unixFS interface {
|
|
Open(name string) (File, error)
|
|
Remove(name string) error
|
|
unlinkat(dirfd int, path string, flags int) error
|
|
}
|
|
|
|
func (fs *UnixFS) removeAll(path string) error {
|
|
return removeAll(fs, path)
|
|
}
|
|
|
|
func removeAll(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
|
|
}
|
|
|
|
// The rmdir system call does not permit removing ".",
|
|
// so we don't permit it either.
|
|
if endsWithDot(path) {
|
|
return &PathError{Op: "removeall", Path: path, Err: unix.EINVAL}
|
|
}
|
|
|
|
// Simple case: if Remove works, we're done.
|
|
err := fs.Remove(path)
|
|
if err == nil || errors.Is(err, ErrNotExist) {
|
|
return nil
|
|
}
|
|
|
|
// RemoveAll recurses by deleting the path base from
|
|
// its parent directory
|
|
parentDir, base := splitPath(path)
|
|
|
|
parent, err := fs.Open(parentDir)
|
|
if errors.Is(err, ErrNotExist) {
|
|
// If parent does not exist, base cannot exist. Fail silently
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer parent.Close()
|
|
|
|
if err := removeAllFrom(fs, parent, base); err != nil {
|
|
if pathErr, ok := err.(*PathError); ok {
|
|
pathErr.Path = parentDir + string(os.PathSeparator) + pathErr.Path
|
|
err = pathErr
|
|
}
|
|
return convertErrorType(err)
|
|
}
|
|
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 the directory's entries.
|
|
var recurseErr error
|
|
for {
|
|
const reqSize = 1024
|
|
var respSize int
|
|
|
|
// Open the directory to recurse into
|
|
file, err := openFdAt(parentFd, base)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotExist) {
|
|
return nil
|
|
}
|
|
recurseErr = &PathError{Op: "openfdat", Path: base, Err: err}
|
|
break
|
|
}
|
|
|
|
for {
|
|
numErr := 0
|
|
|
|
names, readErr := file.Readdirnames(reqSize)
|
|
// Errors other than EOF should stop us from continuing.
|
|
if readErr != nil && readErr != io.EOF {
|
|
_ = file.Close()
|
|
if errors.Is(readErr, ErrNotExist) {
|
|
return nil
|
|
}
|
|
return &PathError{Op: "readdirnames", Path: base, Err: readErr}
|
|
}
|
|
|
|
respSize = len(names)
|
|
for _, name := range names {
|
|
err := removeAllFrom(fs, file, name)
|
|
if err != nil {
|
|
if pathErr, ok := err.(*PathError); ok {
|
|
pathErr.Path = base + string(os.PathSeparator) + pathErr.Path
|
|
}
|
|
numErr++
|
|
if recurseErr == nil {
|
|
recurseErr = err
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we can delete any entry, break to start new iteration.
|
|
// Otherwise, we discard current names, get next entries and try deleting them.
|
|
if numErr != reqSize {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Removing files from the directory may have caused
|
|
// the OS to reshuffle it. Simply calling Readdirnames
|
|
// again may skip some entries. The only reliable way
|
|
// to avoid this is to close and re-open the
|
|
// directory. See issue https://go.dev/issue/20841.
|
|
_ = file.Close()
|
|
|
|
// Finish when the end of the directory is reached
|
|
if respSize < reqSize {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove the directory itself.
|
|
unlinkErr := fs.unlinkat(parentFd, base, AT_REMOVEDIR)
|
|
if unlinkErr == nil || errors.Is(unlinkErr, ErrNotExist) {
|
|
return nil
|
|
}
|
|
|
|
if recurseErr != nil {
|
|
return recurseErr
|
|
}
|
|
return &PathError{Op: "unlinkat", Path: base, Err: unlinkErr}
|
|
}
|
|
|
|
// openFdAt opens path relative to the directory in fd.
|
|
// Other than that this should act like openFileNolog.
|
|
// This acts like openFileNolog rather than OpenFile because
|
|
// we are going to (try to) remove the file.
|
|
// The contents of this file are not relevant for test caching.
|
|
func openFdAt(dirfd int, name string) (File, error) {
|
|
var fd int
|
|
for {
|
|
var err error
|
|
fd, err = unix.Openat(dirfd, name, O_RDONLY|O_CLOEXEC|O_NOFOLLOW, 0)
|
|
if err == nil {
|
|
break
|
|
}
|
|
|
|
// See comment in openFileNolog.
|
|
if err == unix.EINTR {
|
|
continue
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
// This is stupid, os.NewFile immediately casts `fd` to an `int`, but wants
|
|
// it to be passed as a `uintptr`.
|
|
return os.NewFile(uintptr(fd), name), nil
|
|
}
|