package host import ( "context" "os" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type generationRetirement struct { guard hostguard.Guard journal *hostbox.LaunchJournal kernel func(context.Context, release.GenerationReservation) (string, error) retiredKernel func(context.Context, release.GenerationReservation, hostguard.FenceView) (string, error) } func (v generationRetirement) closure(ctx context.Context, r release.GenerationReservation) (bool, error) { if v.guard.Root == nil || v.journal == nil || v.journal.Remote() || v.kernel == nil { return false, os.ErrPermission } b := r.Binding t := b.Target in, err := v.guard.Root.Installation(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) if err != nil { return false, err } // Installation admits an older capture only when root retained it as // retired. Rebind preserves this identity's account, enrollment and migration. // Its old launch journal and kernel obligations still have to settle below. if in.Incarnation != t.Incarnation || in.Stamp < t.Stamp { return false, os.ErrPermission } in.Stamp = t.Stamp if in.Host != b.Binding.Host || in.Account != b.Binding.Account || in.App != b.Binding.App || in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 || in.Record != b.Record || rootHistoryInstallation(in) != b.Installation { return false, os.ErrPermission } launches, err := v.journal.GenerationLaunches(ctx) if err != nil { return false, err } matches, controllers := 0, 0 closed := false writersClosed := false for _, launch := range launches { target := launch.Target if target.Host != t.Host || target.Incarnation != t.Incarnation || target.Stamp != t.Stamp || target.Session != t.Session || target.Box != t.Box || target.Enrollment != t.Enrollment || target.BootID != t.BootID || target.ControllerID != t.ControllerID { continue } controllers++ writersClosed = launch.WritersClosed if launch.ControllerPID != b.ControllerPID || launch.ProcessStart != b.ControllerStart { return false, os.ErrPermission } for _, unit := range launch.Reservations { if unit.ID != b.Attempt { continue } if !hostbox.IsUnitReservation(unit) || (unit.State != hostbox.UnitIntended && unit.State != hostbox.UnitStarted && unit.State != hostbox.UnitSettled) || !hostbox.SameLaunchOwner(unit.Target, target) || unit.Inputs != b.Inputs || unit.InvocationID != b.Invocation || unit.Executable.Device != b.Executable.Device || unit.Executable.Inode != b.Executable.Inode || unit.Group.RelativePath != generationBindingGroup(b) { return false, os.ErrPermission } p := r.Process if p != (release.GenerationProcess{}) && (unit.PID != p.PID || unit.ProcessStart != p.Start || unit.Group.RelativePath != p.Group || unit.Group.InvocationID != p.Invocation || unit.Group.Device != p.GroupIdentity.Device || unit.Group.Inode != p.GroupIdentity.Inode) { return false, os.ErrPermission } matches++ closed = launch.WritersClosed && unit.State == hostbox.UnitSettled } } if controllers == 1 && matches == 0 && r.Process == (release.GenerationProcess{}) { if r.Closed && !writersClosed { return false, os.ErrPermission } return writersClosed, nil } if controllers != 1 || matches != 1 || (r.Closed && !closed) { return false, os.ErrPermission } return closed, nil } // closed verifies root's existing irreversible closure. It never stops a unit, // closes another controller's admission, or interprets a missing journal as join. func (v generationRetirement) closed(ctx context.Context, r release.GenerationReservation) error { closed, err := v.closure(ctx, r) if err != nil { return err } if !closed { return os.ErrPermission } return nil } func (v generationRetirement) verify(ctx context.Context, r release.GenerationReservation) (string, error) { if err := v.closed(ctx, r); err != nil { return "", err } evidence, err := v.kernel(ctx, r) if err != nil { return "", err } if err := v.closed(ctx, r); err != nil { return "", err } return evidence, nil } // The caller holds release exclusion; host exclusion remains held through both // ledger commits and the kernel check, in the same order used by launch. func (v generationRetirement) retire(ctx context.Context, g *release.RootGenerations, r release.GenerationReservation) error { if v.retiredKernel != nil { t := r.Binding.Target view, err := v.guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) if err != nil { return err } v.kernel = func(ctx context.Context, current release.GenerationReservation) (string, error) { return v.retiredKernel(ctx, current, view) } } return v.guard.WithLocal(ctx, r.Binding.Binding.Host, func() error { closed, err := v.closure(ctx, r) if err != nil || !closed { return err } return g.Retire(ctx, r, v.closed, v.verify) }) } func productionRetireGenerations(ctx context.Context, g *release.RootGenerations) error { records, err := g.Reservations(ctx) if err != nil { return err } // Absence is not completeness. The pin reader still requires its verifier. if len(records) == 0 { return nil } state, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false) if err != nil { return err } defer state.Close() journal, err := openGenerationLaunchInventory("/var/lib/carlos-claims/journals.db") if err != nil { return err } defer journal.Close() verifier := generationRetirement{guard: hostguard.Guard{DataDir: dataDir(), Root: hostguard.NewRootAuthority(state)}, journal: journal, kernel: productionGenerationAbsence, retiredKernel: productionRetiredGenerationAbsence} return verifier.retireAll(context.WithValue(ctx, generationCensusReservationsKey{}, records), g, records) } func (v generationRetirement) retireAll(ctx context.Context, g *release.RootGenerations, records []release.GenerationReservation) error { for _, r := range records { if r.Retirement != "" { continue } if err := v.retire(ctx, g, r); err != nil { return err } } return nil }