wings/internal/ufs/fs_quota.go
Matthew Penner d1c0ca5260
server(filesystem): rebuild everything imaginable
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>
2024-03-13 12:27:29 -06:00

160 lines
3.7 KiB
Go

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
package ufs
import (
"sync/atomic"
)
type Quota struct {
// fs is the underlying filesystem that runs the actual I/O operations.
*UnixFS
// limit is the size limit of the filesystem.
//
// limit is atomic to allow the limit to be safely changed after the
// filesystem was created.
//
// A limit of `-1` disables any write operation from being performed.
// A limit of `0` disables any limit checking.
limit atomic.Int64
// usage is the current usage of the filesystem.
//
// If usage is set to `-1`, it hasn't been calculated yet.
usage atomic.Int64
}
func NewQuota(fs *UnixFS, limit int64) *Quota {
qfs := Quota{UnixFS: fs}
qfs.limit.Store(limit)
return &qfs
}
// Close closes the filesystem.
func (fs *Quota) Close() (err error) {
err = fs.UnixFS.Close()
return
}
// Limit returns the limit of the filesystem.
func (fs *Quota) Limit() int64 {
return fs.limit.Load()
}
// SetLimit returns the limit of the filesystem.
func (fs *Quota) SetLimit(newLimit int64) int64 {
return fs.limit.Swap(newLimit)
}
// Usage returns the current usage of the filesystem.
func (fs *Quota) Usage() int64 {
return fs.usage.Load()
}
// SetUsage updates the total usage of the filesystem.
func (fs *Quota) SetUsage(newUsage int64) int64 {
return fs.usage.Swap(newUsage)
}
// Add adds `i` to the tracked usage total.
func (fs *Quota) Add(i int64) int64 {
usage := fs.Usage()
// If adding `i` to the usage will put us below 0, cap it. (`i` can be negative)
if usage+i < 0 {
fs.usage.Store(0)
return 0
}
return fs.usage.Add(i)
}
// CanFit checks if the given size can fit in the filesystem without exceeding
// the limit of the filesystem.
func (fs *Quota) CanFit(size int64) bool {
// Get the size limit of the filesystem.
limit := fs.Limit()
switch limit {
case -1:
// A limit of -1 means no write operations are allowed.
return false
case 0:
// A limit of 0 means unlimited.
return true
}
// Any other limit is a value we need to check.
usage := fs.Usage()
if usage == -1 {
// We don't know what the current usage is yet.
return true
}
// If the current usage + the requested size are under the limit of the
// filesystem, allow it.
if usage+size <= limit {
return true
}
// Welp, the size would exceed the limit of the filesystem, deny it.
return false
}
func (fs *Quota) Remove(name string) error {
// For information on why this interface is used here, check its
// documentation.
s, err := fs.RemoveStat(name)
if err != nil {
return err
}
// Don't reduce the quota's usage as `name` is not a regular file.
if !s.Mode().IsRegular() {
return nil
}
// Remove the size of the deleted file from the quota usage.
fs.Add(-s.Size())
return nil
}
// 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 *Quota) 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 *Quota) removeAll(path string) error {
return removeAll(fs, path)
}
func (fs *Quota) unlinkat(dirfd int, name string, flags int) error {
if flags == 0 {
s, err := fs.Lstatat(dirfd, name)
if err == nil && s.Mode().IsRegular() {
fs.Add(-s.Size())
}
}
return fs.UnixFS.unlinkat(dirfd, name, flags)
}