//go:build linux package host import ( "context" "errors" "io" "os" "reflect" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/provision" "golang.org/x/sys/unix" ) // This observation is not filesystem or transport authority. The eventual // transaction must retain Record and commit separate current-boot identities. type servingParkRebootRecord struct { Topology servingParkRebootObservation Record servingParkRecord Source, Event servingArtifactDirectory } // The caller holds namespace.lock. Repeating the whole inspection rejects a // source/name change during handle reads and a concurrent private-record change. func (f *servingParkFS) inspectRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) { var zero servingParkRebootRecord first, err := f.observeRebootRecord(ctx, id) if err != nil { return zero, err } again, err := f.observeRebootRecord(ctx, id) if err != nil { return zero, err } if !reflect.DeepEqual(first, again) { return zero, hostbox.ErrChanged } if err := ctx.Err(); err != nil { return zero, err } return again, nil } func (f *servingParkFS) observeRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) { var out servingParkRebootRecord topology, err := f.inspectRebootTopology(ctx) if err != nil { return out, err } r, err := (&servingParkLedger{state: f.state}).readHistorical(id) if err != nil { return out, err } n := topology.Previous // A later retirement binds its parent to namespace history with root-private // persistent evidence. Device/mount numbers constrain only the same boot. if (r.Source.BootID != n.BootID || r.Source.Parent != n.Parent) && (!r.Source.ParentPersistent.valid() || r.Source.ParentPersistent != n.Topology.Parent) { return out, hostbox.ErrChanged } if !validServingParkBootID(r.Source.BootID) || (r.Source.BootID == topology.BootID && r.Source.Parent.Device != topology.CurrentParent.Device) || !r.Source.Persistent.valid() || r.Source.Parent.Inode != n.Parent.Inode || r.Source.Persistent.Filesystem != n.Topology.Parent.Filesystem || r.Prepared && (r.Namespace.Inode != n.Root.Inode || r.Namespace.Mount != r.Source.Parent.Mount) { return out, hostbox.ErrChanged } if r.Source.BootID == topology.BootID && r.Source.Parent.Mount == topology.CurrentParent.Mount { prior, err := servingParkReadBinding(f.state, "retirement-"+id) if err != nil || prior == nil || prior.BeforeBoot != r.Source.BootID || prior.BeforeParent != r.Source.Parent { return out, hostbox.ErrChanged } } parent, pid, err := f.parent() if err != nil { return out, err } defer parent.Close() root, rid, err := f.privateDir(parent, ".serving-parked") if err != nil { return out, err } defer root.Close() if pid != topology.CurrentParent || rid != topology.CurrentRoot { return out, hostbox.ErrChanged } current := servingParkNamespace{Version: 1, Parent: pid, Root: rid, BootID: topology.BootID} handles, err := f.captureTopology(parent, root, current) if err != nil { return out, err } if *handles != *n.Topology { return out, hostbox.ErrChanged } var source *os.File // Completed parks are independent of the live installation, which may have // restored a successor or been removed. Incomplete parks still require it. if !r.Complete { source, err = servingParkOpenDir(parent, provision.InstanceID(r.Authorization.Successor.Host)) if err != nil { return out, err } defer source.Close() out.Source, err = f.rebootDirectory(source, r.Source.Directory, r.Source.Persistent, pid) if err != nil { return out, err } } event, eid, err := f.privateDir(root, r.Authorization.ID) if errors.Is(err, unix.ENOENT) { if r.Prepared && !r.EventRemoveIntent { return out, hostbox.ErrChanged } } else if err != nil { return out, err } else { defer event.Close() if !r.Prepared || r.EventRemoved { return out, hostbox.ErrChanged } out.Event, err = f.rebootDirectory(event, r.Event, r.EventPersistent, pid) if err != nil || out.Event != eid { return out, hostbox.ErrChanged } // Only four fixed artifact names can be evidence. Unknown children remain // untouched and block this event, including after an interrupted unlink. names, e := event.Readdirnames(5) if len(names) > 4 || e != nil && !errors.Is(e, io.EOF) { return out, hostbox.ErrChanged } for _, name := range names { known := false for _, item := range r.Source.Files { if item.Present && !item.Deleted && item.Name == name { known = true } } if !known { return out, hostbox.ErrChanged } } } for _, item := range r.Source.Files { src := servingParkFile{Name: item.Name} dst := servingParkFile{Name: item.Name} if source != nil { src, err = servingParkStat(source, item.Name) if err != nil { return out, err } } if event != nil { dst, err = servingParkStat(event, item.Name) if err != nil { return out, err } } if !servingParkRebootFileMatches(item, src, dst, pid, r) { return out, hostbox.ErrChanged } } if err := f.checkBoot(topology.BootID); err != nil { return out, err } if err := ctx.Err(); err != nil { return out, err } out.Topology, out.Record = topology, r return out, nil } func (f *servingParkFS) rebootDirectory(d *os.File, old servingArtifactDirectory, proof servingParkPersistentDirectory, parent servingArtifactDirectory) (servingArtifactDirectory, error) { var zero servingArtifactDirectory id, err := servingParkDirID(d) if err != nil { return zero, err } if !proof.valid() || id.Device != parent.Device || id.Inode != old.Inode || id.Mount != parent.Mount { return zero, hostbox.ErrChanged } actual, err := f.handle(d) if err != nil { return zero, err } if actual != proof { return zero, hostbox.ErrChanged } return id, nil } func servingParkRebootFileMatches(old, src, dst servingParkFile, parent servingArtifactDirectory, r servingParkRecord) bool { if !old.Present { return !src.Present && !dst.Present } if !old.Persistent.valid() { return false } expected := old expected.Device, expected.Mount = parent.Device, parent.Mount if old.Deleted || r.EventRemoved { return !src.Present && !dst.Present } if r.Deleting { return !src.Present && (sameServingParkFile(expected, dst) || !dst.Present && old.DeleteIntent) } if r.Parked || old.Moved { return !src.Present && sameServingParkFile(expected, dst) } // A rename can be durable before its progress write. Accept exactly one // recorded inode, with its handle; a second link or a replacement refuses. return sameServingParkFile(expected, src) && !dst.Present || !src.Present && sameServingParkFile(expected, dst) }