//go:build linux package host import ( "context" "crypto/rand" "encoding/hex" "errors" "io" "os" "reflect" "github.com/carlosframework/platform/internal/hostbox" "golang.org/x/sys/unix" ) // Retain one obstruction and, if creation loses its identity write, one // unrecorded replacement. Further replacements require investigation. type servingParkQuarantine struct { BootID string `json:",omitempty"` Version int ID string Parent, Obstruction servingArtifactDirectory Previous *servingParkNamespace `json:",omitempty"` Moved bool Unrecorded servingArtifactDirectory `json:",omitempty"` UnrecordedMoved bool `json:",omitempty"` Fresh servingArtifactDirectory Complete bool } func (q servingParkQuarantine) name() string { return ".serving-parked-quarantine-" + q.ID } func (q servingParkQuarantine) unrecordedName() string { return ".serving-parked-unrecorded-" + q.ID } func (q servingParkQuarantine) validate() error { id, err := hex.DecodeString(q.ID) if err != nil || len(id) != 16 || hex.EncodeToString(id) != q.ID || q.Version != 1 || q.Parent.Inode == 0 || q.Parent.Device == 0 || q.Parent.Mount == 0 || q.Obstruction.Inode == 0 || q.Obstruction.Device != q.Parent.Device || q.Obstruction.Mount != q.Parent.Mount { return hostbox.ErrChanged } if q.Previous != nil && (q.Previous.Version != 1 || q.Previous.BootID != q.BootID || q.Previous.Parent != q.Parent || q.Previous.Root.Inode == 0 || q.Previous.Root.Device != q.Parent.Device || q.Previous.Root.Mount != q.Parent.Mount || q.Previous.Root == q.Obstruction) { return hostbox.ErrChanged } if q.Fresh != (servingArtifactDirectory{}) && (!q.Moved || q.Fresh.Inode == 0 || q.Fresh.Device != q.Parent.Device || q.Fresh.Mount != q.Parent.Mount || q.Fresh == q.Obstruction || q.Previous != nil && q.Fresh == q.Previous.Root) { return hostbox.ErrChanged } if q.Unrecorded != (servingArtifactDirectory{}) && (!q.Moved || q.Unrecorded.Inode == 0 || q.Unrecorded.Device != q.Parent.Device || q.Unrecorded.Mount != q.Parent.Mount || q.Unrecorded == q.Obstruction || q.Unrecorded == q.Fresh || q.Previous != nil && q.Unrecorded == q.Previous.Root || q.Fresh.Inode != 0 && !q.UnrecordedMoved) { return hostbox.ErrChanged } if q.UnrecordedMoved && q.Unrecorded.Inode == 0 { return hostbox.ErrChanged } if q.Complete && (!q.Moved || q.Fresh.Inode == 0) { return hostbox.ErrChanged } return nil } // Publishing namespace.json can succeed before quarantine completion. Keep // ordinary parking out until that final record is durable, including after a // lost reply, so recovery can still require an empty replacement root. func (f *servingParkFS) checkNamespaceQuarantine(n servingParkNamespace) error { body, err := f.state.Read("namespace-quarantine.json") if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } var q servingParkQuarantine if servingParkDecode(body, &q) != nil || q.validate() != nil || !q.Complete || n.Parent != q.Parent || n.Root != q.Fresh || n.BootID != q.BootID || f.checkBoot(q.BootID) != nil { return hostbox.ErrChanged } return nil } // This is an internal mechanism, not a request handler. The caller supplies a // parent identity from independent root installation evidence and holds the // namespace lock. No caller path, child listing or quarantine deletion exists. func (f *servingParkFS) recoverNamespace(ctx context.Context, expected servingArtifactDirectory) (servingParkQuarantine, error) { var q servingParkQuarantine if ctx == nil { return q, hostbox.ErrUnsupported } if err := ctx.Err(); err != nil { return q, err } boot, err := f.currentBoot() if err != nil { return q, err } parent, pid, err := f.parent() if err != nil { return q, err } defer parent.Close() var st unix.Stat_t if err = unix.Fstat(int(parent.Fd()), &st); err != nil { return q, err } if pid != expected || st.Mode&0022 != 0 { return q, hostbox.ErrChanged } ledger := servingParkLedger{state: f.state} anchor, err := f.namespaceParent() if err == nil && (anchor.Parent != expected || f.checkBoot(anchor.BootID) != nil) { return q, hostbox.ErrChanged } if err != nil && !errors.Is(err, os.ErrNotExist) { return q, err } save := func() error { if err := ctx.Err(); err != nil { return err } if err := q.validate(); err != nil { return err } return ledger.write("namespace-quarantine.json", q) } body, err := f.state.Read("namespace-quarantine.json") if err == nil { if servingParkDecode(body, &q) != nil || q.validate() != nil || q.Parent != pid || f.checkBoot(q.BootID) != nil { return q, hostbox.ErrChanged } } else if errors.Is(err, os.ErrNotExist) { q.Version, q.Parent, q.BootID = 1, pid, boot body, err = f.state.Read("namespace.json") if err == nil { var prior servingParkNamespace if servingParkDecode(body, &prior) != nil || prior.Version != 1 || prior.Parent != pid || f.checkBoot(prior.BootID) != nil { return q, hostbox.ErrChanged } q.Previous = &prior } else if !errors.Is(err, os.ErrNotExist) { return q, err } obstruction, err := servingParkOpenDir(parent, ".serving-parked") if err != nil { return q, err } q.Obstruction, err = servingParkDirID(obstruction) obstruction.Close() if err != nil { return q, err } var nonce [16]byte if _, err = rand.Read(nonce[:]); err != nil { return q, err } q.ID = hex.EncodeToString(nonce[:]) if err = save(); err != nil { return q, err } } else { return q, err } // A private record is necessary but not sufficient: it must still describe // the installed namespace, or the exact replacement recorded by this run. body, err = f.state.Read("namespace.json") if err == nil { var current servingParkNamespace if servingParkDecode(body, ¤t) != nil { return q, hostbox.ErrChanged } old := q.Previous != nil && reflect.DeepEqual(current, *q.Previous) fresh := q.Fresh.Inode != 0 && current.Version == 1 && current.Parent == q.Parent && current.Root == q.Fresh && current.BootID == q.BootID if !old && !fresh || q.Complete && !fresh { return q, hostbox.ErrChanged } } else if !errors.Is(err, os.ErrNotExist) || q.Previous != nil || q.Complete { return q, err } if !q.Moved { if err = f.moveQuarantine(ctx, parent, q); err != nil { return q, err } q.Moved = true if err = save(); err != nil { return q, err } } if err = f.checkQuarantine(parent, q); err != nil { return q, err } if q.Fresh.Inode == 0 { if err = f.createQuarantineReplacement(ctx, parent, &q, save); err != nil { return q, err } } if q.UnrecordedMoved { if err = f.checkRetainedDirectory(parent, q.unrecordedName(), q.Unrecorded); err != nil { return q, err } } fresh, id, err := f.privateDir(parent, ".serving-parked") if err != nil { return q, err } defer fresh.Close() if id != q.Fresh { return q, hostbox.ErrChanged } // Recheck the effective mount/path before publishing the replacement. check, current, err := f.parent() if err != nil { return q, err } check.Close() if current != q.Parent { return q, hostbox.ErrChanged } if q.Complete { return q, ctx.Err() } // No retirement can use this root before publication. Even a directory // with the right owner and mode is refused if it already contains data. names, err := fresh.Readdirnames(1) if len(names) != 0 || err != nil && !errors.Is(err, io.EOF) { return q, hostbox.ErrChanged } if err = ctx.Err(); err != nil { return q, err } if err = f.sync(int(parent.Fd())); err != nil { return q, err } n := servingParkNamespace{Version: 1, Parent: q.Parent, Root: q.Fresh, BootID: q.BootID} n.Topology, err = f.captureTopology(parent, fresh, n) if err != nil { return q, err } if err = ledger.write("namespace.json", n); err != nil { return q, err } q.Complete = true return q, save() } func (f *servingParkFS) checkQuarantine(parent *os.File, q servingParkQuarantine) error { return f.checkRetainedDirectory(parent, q.name(), q.Obstruction) } func (f *servingParkFS) checkRetainedDirectory(parent *os.File, name string, expected servingArtifactDirectory) error { d, err := servingParkOpenDir(parent, name) if err != nil { return err } defer d.Close() id, err := servingParkDirID(d) if err != nil || id != expected { return hostbox.ErrChanged } return nil } func (f *servingParkFS) moveQuarantine(ctx context.Context, parent *os.File, q servingParkQuarantine) error { return f.moveRetainedDirectory(ctx, parent, q.name(), q.Obstruction) } func (f *servingParkFS) moveRetainedDirectory(ctx context.Context, parent *os.File, name string, expected servingArtifactDirectory) error { if err := ctx.Err(); err != nil { return err } source, err := servingParkOpenDir(parent, ".serving-parked") if errors.Is(err, os.ErrNotExist) { // The rename may have succeeded before its reply or fsync was lost. if err = f.checkRetainedDirectory(parent, name, expected); err != nil { return err } return f.sync(int(parent.Fd())) } if err != nil { return err } defer source.Close() id, err := servingParkDirID(source) if err != nil || id != expected { return hostbox.ErrChanged } if err = f.rename(int(parent.Fd()), ".serving-parked", int(parent.Fd()), name, unix.RENAME_NOREPLACE); err != nil { return err } if err = f.checkRetainedDirectory(parent, name, expected); err != nil { return err } return f.sync(int(parent.Fd())) } // EEXIST grants retention only. Persist the unexpected identity before moving // it, and never inspect its contents. One spare name bounds crash recovery even // if the service owner keeps replacing the root or private writes keep failing. func (f *servingParkFS) createQuarantineReplacement(ctx context.Context, parent *os.File, q *servingParkQuarantine, save func() error) error { for { if err := ctx.Err(); err != nil { return err } if q.Unrecorded.Inode != 0 { if !q.UnrecordedMoved { if err := f.moveRetainedDirectory(ctx, parent, q.unrecordedName(), q.Unrecorded); err != nil { return err } q.UnrecordedMoved = true if err := save(); err != nil { return err } } if err := f.checkRetainedDirectory(parent, q.unrecordedName(), q.Unrecorded); err != nil { return err } } err := unix.Mkdirat(int(parent.Fd()), ".serving-parked", 0700) if errors.Is(err, unix.EEXIST) && q.Unrecorded.Inode == 0 { d, err := servingParkOpenDir(parent, ".serving-parked") if err != nil { return err } id, err := servingParkDirID(d) d.Close() if err != nil { return err } q.Unrecorded = id if err := save(); err != nil { return err } continue } if err != nil { return err } fresh, id, err := f.privateDir(parent, ".serving-parked") if err != nil { return err } fresh.Close() q.Fresh = id // Directory durability precedes the private identity record. Otherwise // a reboot could retain the record while losing its directory entry. if err := f.sync(int(parent.Fd())); err != nil { return err } return save() } }