fix(thothctl): harden workspace result and candidate publication
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//go:build !windows
|
||||
//go:build !windows && !linux
|
||||
|
||||
package safeio
|
||||
|
||||
@@ -92,7 +92,10 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
defer unix.Close(dir)
|
||||
// Close the descriptor that owns the final retained parent. The traversal
|
||||
// closes each superseded descriptor explicitly; evaluating unix.Close(dir)
|
||||
// at defer time would close only the original root descriptor.
|
||||
defer func() { _ = unix.Close(dir) }()
|
||||
for _, component := range components[:len(components)-1] {
|
||||
next, openErr := unix.Openat(dir, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
||||
if openErr != nil {
|
||||
@@ -129,7 +132,8 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
published := false
|
||||
var publishedIdentity unix.Stat_t
|
||||
// Keep the inode identity immutable across every check and cleanup path.
|
||||
var publishedIdentity = staged
|
||||
cleanup := func() {
|
||||
if published {
|
||||
var current unix.Stat_t
|
||||
@@ -161,8 +165,12 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
if err := stageFile.Sync(); err != nil {
|
||||
return fail()
|
||||
}
|
||||
// On platforms without O_TMPFILE, a same-user actor can hard-link the
|
||||
// nameable stage. Extra links are therefore not a post-write rejection:
|
||||
// once observed, the bytes are already public and rejecting would leave the
|
||||
// attacker's link behind. Cleanup still uses the immutable inode identity.
|
||||
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)) {
|
||||
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 {
|
||||
@@ -177,16 +185,17 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
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 {
|
||||
var linked unix.Stat_t
|
||||
if err := unix.Fstatat(dir, components[len(components)-1], &linked, unix.AT_SYMLINK_NOFOLLOW); err != nil ||
|
||||
linked.Ino != staged.Ino || linked.Dev != staged.Dev || linked.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 {
|
||||
var finalIdentity unix.Stat_t
|
||||
if err := unix.Fstatat(dir, components[len(components)-1], &finalIdentity, unix.AT_SYMLINK_NOFOLLOW); err != nil || finalIdentity.Ino != publishedIdentity.Ino || finalIdentity.Dev != publishedIdentity.Dev || finalIdentity.Nlink < 1 {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Fsync(dir); err != nil || !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
|
||||
Reference in New Issue
Block a user