Files
ThothII/tools/tht/internal/safeio/claim_unix.go
T

174 lines
5.5 KiB
Go

//go:build !windows
package safeio
import (
"errors"
"io"
"os"
"golang.org/x/sys/unix"
)
func lockCanonicalPrivateClaimOperation() func() {
return func() {}
}
func claimCanonicalPrivateRegular(source, claim string) (bool, error) {
directory, sourceName, err := openCanonicalParentDirectory(source)
if err != nil {
return false, ErrUnsafeFile
}
defer unix.Close(directory)
claimName := claim[len(claim)-len(claimBaseName(claim)):]
if err := requirePrivateRegularAtLinks(directory, sourceName, 1); err != nil {
if requireSamePrivatePairAt(directory, sourceName, claimName) == nil {
return false, nil
}
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
return false, nil
}
return false, ErrUnsafeFile
}
if err := unix.Linkat(directory, sourceName, directory, claimName, 0); err != nil {
if errors.Is(err, unix.EEXIST) && privateRegularAtAllowedLinks(directory, claimName, 1, 2) == nil {
return false, nil
}
return false, ErrUnsafeFile
}
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
return false, ErrUnsafeFile
}
if err := unix.Fsync(directory); err != nil {
return false, ErrUnsafeFile
}
return true, nil
}
func readCanonicalPrivateClaim(source, claim string, maximum int64) ([]byte, bool, error) {
directory, sourceName, err := openCanonicalParentDirectory(source)
if err != nil {
return nil, false, ErrUnsafeFile
}
defer unix.Close(directory)
claimName := claim[len(claim)-len(claimBaseName(claim)):]
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
return nil, false, nil
}
return nil, false, ErrUnsafeFile
}
descriptor, err := unix.Openat(directory, sourceName, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
if err != nil {
return nil, false, ErrUnsafeFile
}
file := os.NewFile(uintptr(descriptor), "tht-safeio-oidc-claim")
if file == nil {
unix.Close(descriptor)
return nil, false, ErrUnsafeFile
}
defer file.Close()
var before unix.Stat_t
if err := unix.Fstat(descriptor, &before); err != nil || !privateRegularStatWithLinks(before, 2) {
return nil, false, ErrUnsafeFile
}
contents, err := io.ReadAll(io.LimitReader(file, maximum+1))
if err != nil || int64(len(contents)) > maximum {
return nil, false, ErrUnsafeFile
}
var after unix.Stat_t
if err := unix.Fstat(descriptor, &after); err != nil || !sameUnixPrivateFile(before, after) || !privateRegularStatWithLinks(after, 2) {
return nil, false, ErrUnsafeFile
}
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
return nil, false, ErrUnsafeFile
}
return contents, true, nil
}
func removeCanonicalPrivateClaim(source, claim string) (bool, error) {
directory, sourceName, err := openCanonicalParentDirectory(source)
if err != nil {
return false, ErrUnsafeFile
}
defer unix.Close(directory)
claimName := claim[len(claim)-len(claimBaseName(claim)):]
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
return false, nil
}
return false, ErrUnsafeFile
}
if err := unix.Unlinkat(directory, sourceName, 0); err != nil {
return false, ErrUnsafeFile
}
if err := requirePrivateRegularAtLinks(directory, claimName, 1); err != nil {
return false, ErrUnsafeFile
}
if err := unix.Unlinkat(directory, claimName, 0); err != nil {
return false, ErrUnsafeFile
}
if err := unix.Fsync(directory); err != nil {
return false, ErrUnsafeFile
}
return true, nil
}
func claimBaseName(path string) string {
for index := len(path) - 1; index >= 0; index-- {
if path[index] == byte(os.PathSeparator) {
return path[index+1:]
}
}
return path
}
func privateRegularAtAllowedLinks(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 privateRegularStatWithLinks(stat, links) {
return nil
}
}
return ErrUnsafeFile
}
func requirePrivateRegularAtLinks(directory int, name string, links uint64) error {
return privateRegularAtAllowedLinks(directory, name, links)
}
func requireSamePrivatePairAt(directory int, source, claim string) error {
var left, right unix.Stat_t
if err := unix.Fstatat(directory, source, &left, unix.AT_SYMLINK_NOFOLLOW); err != nil || !privateRegularStatWithLinks(left, 2) {
return ErrUnsafeFile
}
if err := unix.Fstatat(directory, claim, &right, unix.AT_SYMLINK_NOFOLLOW); err != nil || !privateRegularStatWithLinks(right, 2) {
return ErrUnsafeFile
}
if !sameUnixPrivateFile(left, right) {
return ErrUnsafeFile
}
return nil
}
func isPrivateClaimAbsentOrOrphanAt(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 && privateRegularStatWithLinks(claimStat, 1)
}
func privateRegularStatWithLinks(stat unix.Stat_t, links uint64) bool {
return stat.Mode&unix.S_IFMT == unix.S_IFREG && uint64(stat.Nlink) == links && stat.Mode&0o7777 == 0o600
}
func sameUnixPrivateFile(left, right unix.Stat_t) bool {
return left.Dev == right.Dev && left.Ino == right.Ino && left.Size == right.Size && left.Mtim == right.Mtim
}