421 lines
15 KiB
Go
421 lines
15 KiB
Go
//go:build !windows
|
|
|
|
package safeio
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type unixPrivateDirectory struct {
|
|
descriptor int
|
|
}
|
|
|
|
func openPrivateDirectory(path string, ensure bool) (PrivateDirectoryHandle, bool, error) {
|
|
parents, err := openCanonicalUnixParent(path)
|
|
if err != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
defer parents.Close()
|
|
return openPrivateUnixDirectoryAt(parents.parent, parents.target, ensure)
|
|
}
|
|
|
|
func openPrivateUnixDirectoryAt(parent int, name string, ensure bool) (PrivateDirectoryHandle, bool, error) {
|
|
if parent < 0 || !validPrivateLeafName(name) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
descriptor, err := unix.Openat(parent, name, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_DIRECTORY|unix.O_NOFOLLOW, 0)
|
|
if err == nil {
|
|
value := &unixPrivateDirectory{descriptor: descriptor}
|
|
if value.Validate() != nil {
|
|
_ = value.Close()
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
return value, true, nil
|
|
}
|
|
if !errors.Is(err, unix.ENOENT) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
if !ensure {
|
|
if unix.Faccessat(parent, ".", unix.W_OK|unix.X_OK, unix.AT_EACCESS) != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
return nil, false, nil
|
|
}
|
|
if err := unix.Mkdirat(parent, name, 0o700); err != nil && !errors.Is(err, unix.EEXIST) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
}
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) Close() error {
|
|
if directory == nil || directory.descriptor < 0 {
|
|
return nil
|
|
}
|
|
err := unix.Close(directory.descriptor)
|
|
directory.descriptor = -1
|
|
if err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) Validate() error {
|
|
if directory == nil || directory.descriptor < 0 {
|
|
return ErrUnsafeFile
|
|
}
|
|
var stat unix.Stat_t
|
|
if err := unix.Fstat(directory.descriptor, &stat); err != nil || !privateUnixDirectoryStat(&stat) {
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) OpenChild(name string, ensure bool) (PrivateDirectoryHandle, bool, error) {
|
|
if directory.Validate() != nil || !validPrivateLeafName(name) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
child, found, err := openPrivateUnixDirectoryAt(directory.descriptor, name, ensure)
|
|
if err != nil || directory.Validate() != nil {
|
|
if child != nil {
|
|
_ = child.Close()
|
|
}
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
return child, found, nil
|
|
}
|
|
|
|
func privateUnixRootRegular(stat *unix.Stat_t, links uint64) bool {
|
|
return stat != nil && stat.Mode&unix.S_IFMT == unix.S_IFREG && uint64(stat.Nlink) == links &&
|
|
stat.Uid == uint32(os.Geteuid()) && stat.Mode&0o7777 == 0o600
|
|
}
|
|
|
|
func sameUnixRootRegular(left, right unix.Stat_t) bool {
|
|
return left.Dev == right.Dev && left.Ino == right.Ino && left.Size == right.Size &&
|
|
left.Mtim == right.Mtim && left.Ctim == right.Ctim && left.Mode == right.Mode && left.Nlink == right.Nlink
|
|
}
|
|
|
|
func requirePrivateUnixRootRegularAt(directory int, name string, links uint64) (unix.Stat_t, error) {
|
|
var stat unix.Stat_t
|
|
if err := unix.Fstatat(directory, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil || !privateUnixRootRegular(&stat, links) {
|
|
return unix.Stat_t{}, ErrUnsafeFile
|
|
}
|
|
return stat, nil
|
|
}
|
|
|
|
func privateUnixRootRegularAtAllowedLinks(directory int, name string, allowed ...uint64) error {
|
|
var stat unix.Stat_t
|
|
if err := unix.Fstatat(directory, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
for _, links := range allowed {
|
|
if privateUnixRootRegular(&stat, links) {
|
|
return nil
|
|
}
|
|
}
|
|
return ErrUnsafeFile
|
|
}
|
|
|
|
func requireSamePrivateUnixRootPairAt(directory int, source, claim string) error {
|
|
left, err := requirePrivateUnixRootRegularAt(directory, source, 2)
|
|
if err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
right, err := requirePrivateUnixRootRegularAt(directory, claim, 2)
|
|
if err != nil || !sameUnixPrivateFile(left, right) {
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isPrivateUnixRootClaimAbsentOrOrphanAt(directory int, source, claim string) bool {
|
|
var sourceStat, claimStat unix.Stat_t
|
|
sourceErr := unix.Fstatat(directory, source, &sourceStat, unix.AT_SYMLINK_NOFOLLOW)
|
|
claimErr := unix.Fstatat(directory, claim, &claimStat, unix.AT_SYMLINK_NOFOLLOW)
|
|
if errors.Is(sourceErr, unix.ENOENT) && errors.Is(claimErr, unix.ENOENT) {
|
|
return true
|
|
}
|
|
return errors.Is(sourceErr, unix.ENOENT) && claimErr == nil && privateUnixRootRegular(&claimStat, 1)
|
|
}
|
|
|
|
func writeAllPrivateRoot(file *os.File, contents []byte) error {
|
|
for written := 0; written < len(contents); {
|
|
count, err := file.Write(contents[written:])
|
|
written += count
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return io.ErrShortWrite
|
|
}
|
|
}
|
|
return file.Sync()
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) CreateRegular(name string, contents []byte) (bool, error) {
|
|
if directory.Validate() != nil || !validPrivateLeafName(name) || len(contents) == 0 {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
descriptor, err := unix.Openat(directory.descriptor, name,
|
|
unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0o600)
|
|
if errors.Is(err, unix.EEXIST) {
|
|
if _, existingErr := requirePrivateUnixRootRegularAt(directory.descriptor, name, 1); existingErr != nil || directory.Validate() != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
file := os.NewFile(uintptr(descriptor), "tht-safeio-private-root-create")
|
|
if file == nil {
|
|
_ = unix.Close(descriptor)
|
|
_ = unix.Unlinkat(directory.descriptor, name, 0)
|
|
return false, ErrUnsafeFile
|
|
}
|
|
failed := true
|
|
defer func() {
|
|
if failed {
|
|
_ = unix.Unlinkat(directory.descriptor, name, 0)
|
|
}
|
|
}()
|
|
if unix.Fchmod(descriptor, 0o600) != nil {
|
|
_ = file.Close()
|
|
return false, ErrUnsafeFile
|
|
}
|
|
var stat unix.Stat_t
|
|
if unix.Fstat(descriptor, &stat) != nil || !privateUnixRootRegular(&stat, 1) || writeAllPrivateRoot(file, contents) != nil || file.Close() != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if directory.Validate() != nil || unix.Fsync(directory.descriptor) != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
failed = false
|
|
return true, nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) ReadRegular(name string, maximum int64) ([]byte, bool, error) {
|
|
if directory.Validate() != nil || !validPrivateLeafName(name) || maximum < 0 || maximum == int64(^uint64(0)>>1) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
before, err := requirePrivateUnixRootRegularAt(directory.descriptor, name, 1)
|
|
if err != nil {
|
|
var stat unix.Stat_t
|
|
if statErr := unix.Fstatat(directory.descriptor, name, &stat, unix.AT_SYMLINK_NOFOLLOW); errors.Is(statErr, unix.ENOENT) {
|
|
return nil, false, nil
|
|
}
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
descriptor, err := unix.Openat(directory.descriptor, name, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
|
|
if err != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
file := os.NewFile(uintptr(descriptor), "tht-safeio-private-root-read")
|
|
if file == nil {
|
|
_ = unix.Close(descriptor)
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
contents, readErr := io.ReadAll(io.LimitReader(file, maximum+1))
|
|
var opened unix.Stat_t
|
|
statErr := unix.Fstat(descriptor, &opened)
|
|
closeErr := file.Close()
|
|
var current unix.Stat_t
|
|
currentErr := unix.Fstatat(directory.descriptor, name, ¤t, unix.AT_SYMLINK_NOFOLLOW)
|
|
if readErr != nil || statErr != nil || closeErr != nil || currentErr != nil || int64(len(contents)) > maximum ||
|
|
!privateUnixRootRegular(&opened, 1) || !privateUnixRootRegular(¤t, 1) ||
|
|
!sameUnixRootRegular(before, opened) || !sameUnixRootRegular(opened, current) || directory.Validate() != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
return contents, true, nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) ReplaceRegular(name string, contents []byte) error {
|
|
if directory.Validate() != nil || !validPrivateLeafName(name) || len(contents) == 0 {
|
|
return ErrUnsafeFile
|
|
}
|
|
if _, err := requirePrivateUnixRootRegularAt(directory.descriptor, name, 1); err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
temporary, err := writePrivateTemporaryAt(directory.descriptor, contents)
|
|
if err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
defer func() { _ = unix.Unlinkat(directory.descriptor, temporary, 0) }()
|
|
if _, err := requirePrivateUnixRootRegularAt(directory.descriptor, name, 1); err != nil || directory.Validate() != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
if err := unix.Renameat(directory.descriptor, temporary, directory.descriptor, name); err != nil || unix.Fsync(directory.descriptor) != nil || directory.Validate() != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) RemoveRegular(name string) (bool, error) {
|
|
if directory.Validate() != nil || !validPrivateLeafName(name) {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if _, err := requirePrivateUnixRootRegularAt(directory.descriptor, name, 1); err != nil {
|
|
var stat unix.Stat_t
|
|
if statErr := unix.Fstatat(directory.descriptor, name, &stat, unix.AT_SYMLINK_NOFOLLOW); errors.Is(statErr, unix.ENOENT) {
|
|
return false, nil
|
|
}
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Unlinkat(directory.descriptor, name, 0); err != nil || unix.Fsync(directory.descriptor) != nil || directory.Validate() != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) ListPage(
|
|
maximumEntries int,
|
|
afterName string,
|
|
validName func(string) bool,
|
|
validLinks func(string, uint64) bool,
|
|
) (PrivateDirectoryPage, error) {
|
|
if directory.Validate() != nil || maximumEntries < 1 || maximumEntries > 4096 || validName == nil || validLinks == nil || (afterName != "" && !validName(afterName)) {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
var before unix.Stat_t
|
|
if unix.Fstat(directory.descriptor, &before) != nil || !privateUnixDirectoryStat(&before) {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
duplicate, err := unix.Dup(directory.descriptor)
|
|
if err != nil {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
file := os.NewFile(uintptr(duplicate), "tht-safeio-private-root-list")
|
|
if file == nil {
|
|
_ = unix.Close(duplicate)
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
defer file.Close()
|
|
seen := make(map[string]struct{}, maximumEntries+1)
|
|
selected := make([]PrivateDirectoryEntry, 0, maximumEntries+1)
|
|
scanned := 0
|
|
for {
|
|
entries, readErr := file.ReadDir(1)
|
|
if readErr != nil && !errors.Is(readErr, io.EOF) {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
if len(entries) == 0 {
|
|
break
|
|
}
|
|
if len(entries) != 1 {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
scanned++
|
|
if scanned > maximumPrivateDirectoryPageScanEntries {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
name := entries[0].Name()
|
|
if !validPrivateLeafName(name) || !validName(name) {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
if _, duplicate := seen[name]; duplicate {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
seen[name] = struct{}{}
|
|
var stat unix.Stat_t
|
|
if unix.Fstatat(directory.descriptor, name, &stat, unix.AT_SYMLINK_NOFOLLOW) != nil ||
|
|
!privateUnixRootRegular(&stat, uint64(stat.Nlink)) || !validLinks(name, uint64(stat.Nlink)) {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
if name > afterName {
|
|
selected = appendBoundedPrivateDirectoryEntry(selected, PrivateDirectoryEntry{
|
|
Name: name, ModifiedUnixMs: time.Unix(stat.Mtim.Sec, stat.Mtim.Nsec).UnixMilli(),
|
|
}, maximumEntries+1)
|
|
}
|
|
if errors.Is(readErr, io.EOF) {
|
|
break
|
|
}
|
|
}
|
|
var after unix.Stat_t
|
|
if unix.Fstat(directory.descriptor, &after) != nil || !privateUnixDirectoryStat(&after) ||
|
|
before.Dev != after.Dev || before.Ino != after.Ino || before.Mode != after.Mode || before.Mtim != after.Mtim || before.Ctim != after.Ctim || directory.Validate() != nil {
|
|
return PrivateDirectoryPage{}, ErrUnsafeFile
|
|
}
|
|
sort.Slice(selected, func(left, right int) bool { return selected[left].Name < selected[right].Name })
|
|
more := len(selected) > maximumEntries
|
|
if more {
|
|
selected = selected[:maximumEntries]
|
|
}
|
|
return PrivateDirectoryPage{Entries: selected, More: more}, nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) ClaimRegular(source, claim string) (bool, error) {
|
|
if directory.Validate() != nil || !validPrivateLeafName(source) || !validPrivateLeafName(claim) {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := privateUnixRootRegularAtAllowedLinks(directory.descriptor, source, 1); err != nil {
|
|
if requireSamePrivateUnixRootPairAt(directory.descriptor, source, claim) == nil || isPrivateUnixRootClaimAbsentOrOrphanAt(directory.descriptor, source, claim) {
|
|
return false, nil
|
|
}
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Linkat(directory.descriptor, source, directory.descriptor, claim, 0); err != nil {
|
|
if errors.Is(err, unix.EEXIST) && privateUnixRootRegularAtAllowedLinks(directory.descriptor, claim, 1, 2) == nil {
|
|
return false, nil
|
|
}
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := requireSamePrivateUnixRootPairAt(directory.descriptor, source, claim); err != nil || unix.Fsync(directory.descriptor) != nil || directory.Validate() != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) ReadClaim(source, claim string, maximum int64) ([]byte, bool, error) {
|
|
if directory.Validate() != nil || !validPrivateLeafName(source) || !validPrivateLeafName(claim) || maximum < 0 || maximum == int64(^uint64(0)>>1) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
if err := requireSamePrivateUnixRootPairAt(directory.descriptor, source, claim); err != nil {
|
|
if isPrivateUnixRootClaimAbsentOrOrphanAt(directory.descriptor, source, claim) {
|
|
return nil, false, nil
|
|
}
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
descriptor, err := unix.Openat(directory.descriptor, source, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
|
|
if err != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
file := os.NewFile(uintptr(descriptor), "tht-safeio-private-root-claim")
|
|
if file == nil {
|
|
_ = unix.Close(descriptor)
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
contents, readErr := io.ReadAll(io.LimitReader(file, maximum+1))
|
|
var after unix.Stat_t
|
|
statErr := unix.Fstat(descriptor, &after)
|
|
closeErr := file.Close()
|
|
if readErr != nil || statErr != nil || closeErr != nil || int64(len(contents)) > maximum || !privateUnixRootRegular(&after, 2) ||
|
|
requireSamePrivateUnixRootPairAt(directory.descriptor, source, claim) != nil || directory.Validate() != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
return contents, true, nil
|
|
}
|
|
|
|
func (directory *unixPrivateDirectory) RemoveClaim(source, claim string) (bool, error) {
|
|
if directory.Validate() != nil || !validPrivateLeafName(source) || !validPrivateLeafName(claim) {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := requireSamePrivateUnixRootPairAt(directory.descriptor, source, claim); err != nil {
|
|
if isPrivateUnixRootClaimAbsentOrOrphanAt(directory.descriptor, source, claim) {
|
|
return false, nil
|
|
}
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Unlinkat(directory.descriptor, source, 0); err != nil || privateUnixRootRegularAtAllowedLinks(directory.descriptor, claim, 1) != nil ||
|
|
unix.Unlinkat(directory.descriptor, claim, 0) != nil || unix.Fsync(directory.descriptor) != nil || directory.Validate() != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
return true, nil
|
|
}
|