//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 }