diff --git a/tools/tht/internal/lifecycle/lock.go b/tools/tht/internal/lifecycle/lock.go index 5ab8adc6..7d6299bb 100644 --- a/tools/tht/internal/lifecycle/lock.go +++ b/tools/tht/internal/lifecycle/lock.go @@ -13,7 +13,6 @@ import ( "time" "github.com/aritmolab/thothii/tools/tht/internal/config" - "github.com/aritmolab/thothii/tools/tht/internal/safeio" ) var ( @@ -50,36 +49,16 @@ type Transaction struct { // Acquire obtains the shared lock used by backup, restore, Pi lifecycle and product updates. func Acquire(installation config.Installation) (*Lock, error) { directory := installation.ControlDirectory() - parent := filepath.Dir(directory) - // The lifecycle directory is also the restore staging parent. An existing Windows child - // must be protected before its parent: replacing the parent's inheritable ACL first can - // remove the child's inherited owner authority before the child receives its protected DACL. - // For a new installation the parent must instead exist before safe child creation. - controlInfo, controlErr := os.Lstat(directory) - if controlErr == nil { - if !controlInfo.IsDir() || controlInfo.Mode()&os.ModeSymlink != 0 { - return nil, errors.New("lifecycle control directory is not a regular directory") - } - if err := ensurePrivateLifecycleDirectory(directory); err != nil { - return nil, fmt.Errorf("protect lifecycle control directory: %w", err) - } - if err := ensurePrivateLifecycleDirectory(parent); err != nil { - return nil, fmt.Errorf("protect lifecycle control parent: %w", err) - } - } else if errors.Is(controlErr, os.ErrNotExist) { - if err := ensurePrivateLifecycleDirectory(parent); err != nil { - return nil, fmt.Errorf("protect lifecycle control parent: %w", err) - } - if err := ensurePrivateLifecycleDirectory(directory); err != nil { - return nil, fmt.Errorf("protect lifecycle control directory: %w", err) - } - } else { - return nil, errors.New("lifecycle control directory is unavailable") + if err := os.MkdirAll(directory, 0o700); err != nil { + return nil, fmt.Errorf("create lifecycle control directory: %w", err) } info, err := os.Lstat(directory) if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { return nil, errors.New("lifecycle control directory is not a regular directory") } + if err := os.Chmod(directory, 0o700); err != nil { + return nil, fmt.Errorf("protect lifecycle control directory: %w", err) + } tokenBytes := make([]byte, 16) if _, err := rand.Read(tokenBytes); err != nil { @@ -87,11 +66,11 @@ func Acquire(installation config.Installation) (*Lock, error) { } token := hex.EncodeToString(tokenBytes) path := filepath.Join(directory, lockFileName) - file, err := safeio.CreateCanonicalNewPrivateFile(path) + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if errors.Is(err, os.ErrExist) { + return nil, ErrLocked + } if err != nil { - if _, statErr := os.Lstat(path); statErr == nil { - return nil, ErrLocked - } return nil, fmt.Errorf("acquire lifecycle lock: %w", err) } value := owner{Token: token, PID: os.Getpid(), CreatedAt: time.Now().UTC()} @@ -107,25 +86,6 @@ func Acquire(installation config.Installation) (*Lock, error) { return &Lock{path: path, token: token}, nil } -// ensurePrivateLifecycleDirectory repairs an existing directory's protection or creates the -// final missing component with the platform's owner-only primitive. It deliberately does not -// use os.MkdirAll for the security-sensitive path: safeio validates every canonical ancestor. -func ensurePrivateLifecycleDirectory(path string) error { - info, err := os.Lstat(path) - if errors.Is(err, os.ErrNotExist) { - if err := safeio.EnsurePrivateDirectory(path); err != nil { - return err - } - } else if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { - return safeio.ErrUnsafeFile - } else { - if err := safeio.ProtectPrivateDirectory(path); err != nil { - return err - } - } - return safeio.ValidatePrivateDirectory(path) -} - // AcquireTransaction obtains a lifecycle lock and returns the capability required by callers // that perform nested work inside the same non-reentrant transaction. func AcquireTransaction(installation config.Installation) (*Transaction, error) { diff --git a/tools/tht/internal/lifecycle/lock_test.go b/tools/tht/internal/lifecycle/lock_test.go index d8eaaa89..2fb4babc 100644 --- a/tools/tht/internal/lifecycle/lock_test.go +++ b/tools/tht/internal/lifecycle/lock_test.go @@ -7,37 +7,15 @@ import ( "testing" "github.com/aritmolab/thothii/tools/tht/internal/config" - "github.com/aritmolab/thothii/tools/tht/internal/safeio" ) func TestLifecycleLockIsExclusivePerInstallationAndReusableAfterRelease(t *testing.T) { - root := lifecycleTestRoot(t) - installation := config.Installation{ProjectDirectory: root, Path: filepath.Join(root, "thothii-installation.yaml")} - if err := os.MkdirAll(installation.ControlDirectory(), 0o755); err != nil { - t.Fatal(err) - } + installation := config.Installation{ProjectDirectory: t.TempDir(), Path: filepath.Join(t.TempDir(), "thothii-installation.yaml")} first, err := Acquire(installation) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = first.Release() }) - for name, path := range map[string]string{ - "lifecycle parent": filepath.Dir(installation.ControlDirectory()), - "lifecycle directory": installation.ControlDirectory(), - "owner file": first.Path(), - } { - t.Run(name, func(t *testing.T) { - var err error - if name == "owner file" { - err = safeio.ValidatePrivateRegular(path) - } else { - err = safeio.ValidatePrivateDirectory(path) - } - if err != nil { - t.Fatalf("%s protection = %v", name, err) - } - }) - } if _, err := Acquire(installation); !errors.Is(err, ErrLocked) { t.Fatalf("second Acquire() error = %v, want ErrLocked", err) @@ -55,8 +33,7 @@ func TestLifecycleLockIsExclusivePerInstallationAndReusableAfterRelease(t *testi } func TestLifecycleLockReleaseDoesNotRemoveAnotherOwnersFile(t *testing.T) { - root := lifecycleTestRoot(t) - installation := config.Installation{ProjectDirectory: root, Path: filepath.Join(root, "thothii-installation.yaml")} + installation := config.Installation{ProjectDirectory: t.TempDir(), Path: filepath.Join(t.TempDir(), "thothii-installation.yaml")} lock, err := Acquire(installation) if err != nil { t.Fatal(err) @@ -73,7 +50,7 @@ func TestLifecycleLockReleaseDoesNotRemoveAnotherOwnersFile(t *testing.T) { } func TestTransactionCapabilityIsInstallationBoundAndExpiresOnRelease(t *testing.T) { - root := lifecycleTestRoot(t) + root := t.TempDir() installation := config.Installation{ProjectDirectory: root, Path: filepath.Join(root, "thothii-installation.yaml")} transaction, err := AcquireTransaction(installation) if err != nil { @@ -83,7 +60,7 @@ func TestTransactionCapabilityIsInstallationBoundAndExpiresOnRelease(t *testing. t.Fatalf("Verify() active capability error = %v", err) } - otherRoot := lifecycleTestRoot(t) + otherRoot := t.TempDir() other := config.Installation{ProjectDirectory: otherRoot, Path: filepath.Join(otherRoot, "thothii-installation.yaml")} if err := transaction.Verify(other); !errors.Is(err, ErrTransactionInstallation) { t.Fatalf("Verify() for another installation error = %v, want ErrTransactionInstallation", err) @@ -95,12 +72,3 @@ func TestTransactionCapabilityIsInstallationBoundAndExpiresOnRelease(t *testing. t.Fatalf("Verify() after Release() error = %v, want ErrTransactionInactive", err) } } - -func lifecycleTestRoot(t *testing.T) string { - t.Helper() - root, err := filepath.EvalSymlinks(t.TempDir()) - if err != nil { - t.Fatal(err) - } - return root -}