fix(backup): retain staging cleanup capability

This commit is contained in:
2026-08-18 10:50:56 +02:00
parent d43738eeae
commit 5f9a3ae066
9 changed files with 349 additions and 27 deletions
+62
View File
@@ -149,6 +149,68 @@ func TestCreateCanonicalNewPrivateFileProvidesPrivateStreamingWriter(t *testing.
}
}
func TestPrivateDirectoryCreatesAndRemovesStreamingRegularFile(t *testing.T) {
temporaryRoot, err := filepath.EvalSymlinks(os.TempDir())
if err != nil {
t.Fatal(err)
}
root, err := os.MkdirTemp(temporaryRoot, "tht-safeio-stream-")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(root) })
if err := ProtectPrivateDirectory(root); err != nil {
t.Fatal(err)
}
directory, found, err := OpenPrivateDirectory(root, true)
if err != nil || !found {
t.Fatalf("OpenPrivateDirectory() = found %v, err %v", found, err)
}
defer directory.Close()
file, created, err := directory.CreateRegularFile("archive-stream.zip")
if err != nil || !created || file == nil {
t.Fatalf("CreateRegularFile() = file %v, created %v, err %v", file, created, err)
}
if _, err := file.Write([]byte("private archive")); err != nil {
t.Fatal(err)
}
if err := file.Sync(); err != nil {
t.Fatal(err)
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
streamed := make([]byte, len("private archive"))
if _, err := io.ReadFull(file, streamed); err != nil {
t.Fatal(err)
}
if string(streamed) != "private archive" {
t.Fatalf("streamed contents = %q, want %q", streamed, "private archive")
}
if err := file.Close(); err != nil {
t.Fatal(err)
}
removed, err := directory.RemoveRegular("archive-stream.zip")
if err != nil || !removed {
t.Fatalf("RemoveRegular() = removed %v, err %v", removed, err)
}
if created, err := directory.CreateRegular("existing.zip", []byte("existing")); err != nil || !created {
t.Fatalf("CreateRegular(existing.zip) = created %v, err %v", created, err)
}
if file, created, err := directory.CreateRegularFile("existing.zip"); err != nil || created || file != nil {
t.Fatalf("CreateRegularFile(existing.zip) = file %v, created %v, err %v", file, created, err)
}
if created, err := directory.CreateRegular("target.zip", []byte("target")); err != nil || !created {
t.Fatalf("CreateRegular(target.zip) = created %v, err %v", created, err)
}
testsupport.SymlinkOrSkip(t, filepath.Join(root, "target.zip"), filepath.Join(root, "linked.zip"))
if file, created, err := directory.CreateRegularFile("linked.zip"); !errors.Is(err, ErrUnsafeFile) || created || file != nil {
t.Fatalf("CreateRegularFile(linked.zip) = file %v, created %v, err %v", file, created, err)
}
}
func TestPreflightPrivateDirectoryAllowsOnlyAMissingFinalComponentWithoutMutation(t *testing.T) {
temporaryRoot, err := filepath.EvalSymlinks(os.TempDir())
if err != nil {
@@ -1,6 +1,7 @@
package safeio
import (
"os"
"strings"
"sync"
)
@@ -13,6 +14,7 @@ type PrivateDirectoryHandle interface {
Validate() error
OpenChild(name string, ensure bool) (PrivateDirectoryHandle, bool, error)
CreateRegular(name string, contents []byte) (bool, error)
CreateRegularFile(name string) (*os.File, bool, error)
ReadRegular(name string, maximum int64) ([]byte, bool, error)
ReplaceRegular(name string, contents []byte) error
RemoveRegular(name string) (bool, error)
@@ -201,6 +201,48 @@ func (directory *unixPrivateDirectory) CreateRegular(name string, contents []byt
return true, nil
}
func (directory *unixPrivateDirectory) CreateRegularFile(name string) (*os.File, bool, error) {
if directory.Validate() != nil || !validPrivateLeafName(name) {
return nil, false, ErrUnsafeFile
}
descriptor, err := unix.Openat(directory.descriptor, name,
unix.O_RDWR|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 nil, false, ErrUnsafeFile
}
return nil, false, nil
}
if err != nil {
return nil, false, ErrUnsafeFile
}
file := os.NewFile(uintptr(descriptor), "tht-safeio-private-root-stream")
if file == nil {
_ = unix.Close(descriptor)
_ = unix.Unlinkat(directory.descriptor, name, 0)
return nil, false, ErrUnsafeFile
}
failed := true
defer func() {
if failed {
_ = file.Close()
_ = unix.Unlinkat(directory.descriptor, name, 0)
}
}()
if unix.Fchmod(descriptor, 0o600) != nil {
return nil, false, ErrUnsafeFile
}
var stat unix.Stat_t
if unix.Fstat(descriptor, &stat) != nil || !privateUnixRootRegular(&stat, 1) {
return nil, false, ErrUnsafeFile
}
if directory.Validate() != nil || unix.Fsync(directory.descriptor) != nil {
return nil, false, ErrUnsafeFile
}
failed = false
return file, 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
@@ -538,6 +538,41 @@ func (directory *windowsPrivateDirectory) CreateRegular(name string, contents []
return true, nil
}
func (directory *windowsPrivateDirectory) CreateRegularFile(name string) (*os.File, bool, error) {
if directory.Validate() != nil || !validPrivateLeafName(name) {
return nil, false, ErrUnsafeFile
}
value, err := createWindowsPrivateRegularAt(directory.handle, name)
if err != nil {
existing, existingErr := openWindowsPrivateRegularAt(directory.handle, name, windows.FILE_GENERIC_READ, 1)
if existingErr == nil {
if existing.Close() != nil || directory.Validate() != nil {
return nil, false, ErrUnsafeFile
}
return nil, false, nil
}
return nil, false, ErrUnsafeFile
}
failed := true
defer func() {
if failed {
_ = closeAndDeleteWindowsPrivateRegular(value)
}
}()
info, err := privateWindowsRegularInfo(value.handle, 1)
if err != nil || directory.Validate() != nil {
return nil, false, ErrUnsafeFile
}
value.info = info
file := os.NewFile(uintptr(value.handle), "tht-safeio-private-root-stream")
if file == nil {
return nil, false, ErrUnsafeFile
}
value.handle = 0
failed = false
return file, true, nil
}
func (directory *windowsPrivateDirectory) 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