fix(auth): pin native auth storage operations
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user