package host import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "path/filepath" "strings" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "golang.org/x/sys/unix" ) func (a *ClaimRoleAssembly) prepareStateRemote(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs, actor hostbox.Actor) error { if a.Claims == nil || a.Guard.FenceView == nil || o.Claim == nil || o.Claim.Holder == nil { return hostbox.ErrUnsupported } h := o.Claim.Holder _, etag, err := a.Claims.Get(ctx, h.Key) if err != nil { return err } selected := hostbox.RecordSelection{Host: o.Host, Key: h.Key, ETag: etag, Incarnation: h.Incarnation, Family: h.Family} captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp} client := hostbox.ReconcileAdapter{Reconciler: hostbox.Reconciler{Guard: a.Guard, Claims: a.Claims}, Box: actor.Box, Enrollment: actor.Enrollment} for attempt := 0; attempt < 3; attempt++ { inventory, revision, err := a.Inventory.Snapshot(ctx, o.Host, captured) if err != nil { return err } if !inventory.Complete || inventory.Host != o.Host || inventory.Incarnation != h.Incarnation || inventory.InstallationStamp != h.Stamp || inventory.Box != actor.Box || inventory.Enrollment != actor.Enrollment || inventory.DirectoryRevision != actor.Binding { return hostbox.ErrTypedInventory } var expected hostbox.InventoryExpectation var changed bool err = work.With(ctx, in.Identity, func() error { var err error expected, err = client.CaptureInventory(ctx, selected, captured, revision) if err != nil { return err } changed, err = a.prepareStateInventory(in, &inventory, actor) return err }) if err != nil { return err } if !changed { return nil } revision, err = a.Inventory.WriteExpected(ctx, inventory, expected) if err != nil { return err } err = work.With(ctx, in.Identity, func() error { return client.RevalidateInventory(ctx, selected, captured, expected, revision) }) if err != nil { return err } } return hostbox.ErrChanged } func (a *ClaimRoleAssembly) prepareStateInventory(in ClaimLaunchInputs, inventory *hostbox.StopInventory, actor hostbox.Actor) (bool, error) { root := a.ManagedRoots["data"] relative, err := filepath.Rel(root, filepath.Dir(in.DBPath)) if err != nil || !filepath.IsLocal(relative) || relative == "." { return false, hostbox.ErrChanged } ownerDigest := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", inventory.Host, inventory.Incarnation, inventory.InstallationStamp, actor.Box))) owner := hex.EncodeToString(ownerDigest[:]) index := -1 for i, e := range inventory.Entries { if e.Kind == "path" && e.ManagedRoot == "data" && e.RelativePath == relative { if index != -1 || e.Ownership != owner { return false, hostbox.ErrTypedInventory } index = i } } fd, err := unix.Open(root, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return false, err } defer func() { unix.Close(fd) }() components := strings.Split(filepath.ToSlash(relative), "/") for _, part := range components[:len(components)-1] { next, e := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if e != nil { return false, e } unix.Close(fd) fd = next } leaf := components[len(components)-1] var stat unix.Stat_t err = unix.Fstatat(fd, leaf, &stat, unix.AT_SYMLINK_NOFOLLOW) if err == nil { if index < 0 { return false, hostbox.ErrTypedInventory } e := inventory.Entries[index] if e.Materialization != "materialized" || stat.Mode&unix.S_IFMT != unix.S_IFDIR || verifyPreparedStateIdentity(root, e) != nil { return false, hostbox.ErrTypedInventory } return false, nil } if !errors.Is(err, os.ErrNotExist) { return false, err } if index >= 0 && inventory.Entries[index].Materialization == "materialized" { return false, hostbox.ErrChanged } if index < 0 { sum := sha256.Sum256([]byte(owner + "\x00state-dir\x00" + relative)) inventory.Entries = append(inventory.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: hex.EncodeToString(sum[:]), ManagedRoot: "data", RelativePath: relative, Ownership: owner, Type: unix.S_IFDIR, Materialization: "intended"}) return true, nil } if err := unix.Mkdirat(fd, leaf, 0700); err != nil { return false, err } if err := unix.Fstatat(fd, leaf, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil { return false, err } if err := unix.Fsync(fd); err != nil { return false, err } entry := &inventory.Entries[index] entry.Identity, err = hostbox.CaptureInventoryIdentity(root, relative) if err != nil { return false, err } entry.Device = entry.Identity.Device entry.Inode = entry.Identity.Inode entry.Type = unix.S_IFDIR entry.Materialization = "materialized" return true, nil } func verifyPreparedStateIdentity(root string, e hostbox.StopInventoryEntry) error { if err := hostbox.VerifyInventoryIdentity(root, e); err == nil || !errors.Is(err, hostbox.ErrChanged) { return err } current, err := hostbox.CaptureInventoryIdentity(root, e.RelativePath) if err != nil { return err } if e.Identity.Device != e.Device || e.Identity.Inode != e.Inode || e.Identity.Mode&unix.S_IFMT != e.Type || current.Boot != e.Identity.Boot || current.SameMountNamespace(e.Identity) || !current.Same(e.Identity) { return hostbox.ErrChanged } return nil }