fix: harden task1 workspace output and publication
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
package safeio
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -90,51 +92,117 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
defer func() { unix.Close(dir) }()
|
||||
defer unix.Close(dir)
|
||||
for _, component := range components[:len(components)-1] {
|
||||
next, err := unix.Openat(dir, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
||||
if err != nil {
|
||||
next, openErr := unix.Openat(dir, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
||||
if openErr != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
unix.Close(dir)
|
||||
dir = next
|
||||
}
|
||||
var parentBefore unix.Stat_t
|
||||
if err := unix.Fstat(dir, &parentBefore); err != nil {
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
fd, err := unix.Openat(dir, components[len(components)-1], unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC|unix.O_NOFOLLOW, uint32(mode.Perm()))
|
||||
|
||||
// Build the candidate under a private, same-parent name. Only after it is fully
|
||||
// written, synced, and identity-checked do we link it into the requested leaf.
|
||||
stage, err := privateStageName()
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
f := os.NewFile(uintptr(fd), "thothctl-safeio-output")
|
||||
if f == nil {
|
||||
unix.Close(fd)
|
||||
stageFD, err := unix.Openat(dir, stage, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC|unix.O_NOFOLLOW, uint32(mode.Perm()))
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
defer f.Close()
|
||||
if err := f.Chmod(mode); err != nil {
|
||||
stageFile := os.NewFile(uintptr(stageFD), "thothctl-safeio-stage")
|
||||
if stageFile == nil {
|
||||
unix.Close(stageFD)
|
||||
_ = unix.Unlinkat(dir, stage, 0)
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
if _, err := f.Write(contents); err != nil {
|
||||
stageCreated := true
|
||||
var staged unix.Stat_t
|
||||
if err := unix.Fstat(stageFD, &staged); err != nil || staged.Nlink != 1 || staged.Mode&unix.S_IFMT != unix.S_IFREG {
|
||||
_ = stageFile.Close()
|
||||
_ = unix.Unlinkat(dir, stage, 0)
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
published := false
|
||||
var publishedIdentity unix.Stat_t
|
||||
cleanup := func() {
|
||||
if published {
|
||||
var current unix.Stat_t
|
||||
if unix.Fstatat(dir, components[len(components)-1], ¤t, unix.AT_SYMLINK_NOFOLLOW) == nil &&
|
||||
current.Ino == publishedIdentity.Ino && current.Dev == publishedIdentity.Dev {
|
||||
_ = unix.Unlinkat(dir, components[len(components)-1], 0)
|
||||
}
|
||||
}
|
||||
if stageCreated {
|
||||
var current unix.Stat_t
|
||||
if unix.Fstatat(dir, stage, ¤t, unix.AT_SYMLINK_NOFOLLOW) == nil &&
|
||||
current.Ino == staged.Ino && current.Dev == staged.Dev {
|
||||
_ = unix.Unlinkat(dir, stage, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
fail := func() error {
|
||||
_ = stageFile.Close()
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
var opened, named, parentAfter unix.Stat_t
|
||||
if err := unix.Fstat(fd, &opened); err != nil || opened.Nlink != 1 {
|
||||
if err := stageFile.Chmod(mode); err != nil {
|
||||
return fail()
|
||||
}
|
||||
n, err := stageFile.Write(contents)
|
||||
if err != nil || n != len(contents) {
|
||||
return fail()
|
||||
}
|
||||
if err := stageFile.Sync(); err != nil {
|
||||
return fail()
|
||||
}
|
||||
var afterWrite unix.Stat_t
|
||||
if err := unix.Fstat(stageFD, &afterWrite); err != nil || afterWrite.Nlink != 1 || afterWrite.Mode&unix.S_IFMT != unix.S_IFREG || afterWrite.Size != int64(len(contents)) {
|
||||
return fail()
|
||||
}
|
||||
if err := stageFile.Close(); err != nil {
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
if err := unix.Fstatat(dir, components[len(components)-1], &named, unix.AT_SYMLINK_NOFOLLOW); err != nil || named.Nlink != 1 || named.Ino != opened.Ino || named.Dev != opened.Dev {
|
||||
return ErrUnsafeFile
|
||||
stageCreated = true
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Fstat(dir, &parentAfter); err != nil || parentAfter.Ino != parentBefore.Ino || parentAfter.Dev != parentBefore.Dev || !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return ErrUnsafeFile
|
||||
if err := unix.Linkat(dir, stage, dir, components[len(components)-1], 0); err != nil {
|
||||
return fail()
|
||||
}
|
||||
published = true
|
||||
publishedIdentity = staged
|
||||
if err := unix.Fstatat(dir, components[len(components)-1], &publishedIdentity, unix.AT_SYMLINK_NOFOLLOW); err != nil ||
|
||||
publishedIdentity.Ino != staged.Ino || publishedIdentity.Dev != staged.Dev || publishedIdentity.Nlink != 2 {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Unlinkat(dir, stage, 0); err != nil {
|
||||
return fail()
|
||||
}
|
||||
stageCreated = false
|
||||
if err := unix.Fstatat(dir, components[len(components)-1], &publishedIdentity, unix.AT_SYMLINK_NOFOLLOW); err != nil || publishedIdentity.Nlink != 1 {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Fsync(dir); err != nil || !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return fail()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func privateStageName() (string, error) {
|
||||
var random [16]byte
|
||||
if _, err := rand.Read(random[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ".thothctl-candidate-" + hex.EncodeToString(random[:]), nil
|
||||
}
|
||||
|
||||
func validateCanonicalOutputPath(path string) error {
|
||||
if err := ValidateCanonicalPath(path); err != nil {
|
||||
return err
|
||||
@@ -207,3 +275,5 @@ func recheckUnixParentPath(components []string, retained int) bool {
|
||||
var got, want unix.Stat_t
|
||||
return unix.Fstat(dir, &got) == nil && unix.Fstat(retained, &want) == nil && got.Ino == want.Ino && got.Dev == want.Dev
|
||||
}
|
||||
|
||||
func validatePlatformPathSyntax(path string) error { return nil }
|
||||
|
||||
Reference in New Issue
Block a user