| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/hostbox" |
| 8 | "github.com/carlosframework/platform/internal/hostguard" |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | "github.com/carlosframework/platform/internal/rootstate" |
| 11 | ) |
| 12 | |
| 13 | type generationRetirement struct { |
| 14 | guard hostguard.Guard |
| 15 | journal *hostbox.LaunchJournal |
| 16 | kernel func(context.Context, release.GenerationReservation) (string, error) |
| 17 | retiredKernel func(context.Context, release.GenerationReservation, hostguard.FenceView) (string, error) |
| 18 | } |
| 19 | |
| 20 | func (v generationRetirement) closure(ctx context.Context, r release.GenerationReservation) (bool, error) { |
| 21 | if v.guard.Root == nil || v.journal == nil || v.journal.Remote() || v.kernel == nil { |
| 22 | return false, os.ErrPermission |
| 23 | } |
| 24 | b := r.Binding |
| 25 | t := b.Target |
| 26 | in, err := v.guard.Root.Installation(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) |
| 27 | if err != nil { |
| 28 | return false, err |
| 29 | } |
| 30 | // Installation admits an older capture only when root retained it as |
| 31 | // retired. Rebind preserves this identity's account, enrollment and migration. |
| 32 | // Its old launch journal and kernel obligations still have to settle below. |
| 33 | if in.Incarnation != t.Incarnation || in.Stamp < t.Stamp { |
| 34 | return false, os.ErrPermission |
| 35 | } |
| 36 | in.Stamp = t.Stamp |
| 37 | 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 { |
| 38 | return false, os.ErrPermission |
| 39 | } |
| 40 | launches, err := v.journal.GenerationLaunches(ctx) |
| 41 | if err != nil { |
| 42 | return false, err |
| 43 | } |
| 44 | matches, controllers := 0, 0 |
| 45 | closed := false |
| 46 | writersClosed := false |
| 47 | for _, launch := range launches { |
| 48 | target := launch.Target |
| 49 | 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 { |
| 50 | continue |
| 51 | } |
| 52 | controllers++ |
| 53 | writersClosed = launch.WritersClosed |
| 54 | if launch.ControllerPID != b.ControllerPID || launch.ProcessStart != b.ControllerStart { |
| 55 | return false, os.ErrPermission |
| 56 | } |
| 57 | for _, unit := range launch.Reservations { |
| 58 | if unit.ID != b.Attempt { |
| 59 | continue |
| 60 | } |
| 61 | 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) { |
| 62 | return false, os.ErrPermission |
| 63 | } |
| 64 | p := r.Process |
| 65 | 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) { |
| 66 | return false, os.ErrPermission |
| 67 | } |
| 68 | matches++ |
| 69 | closed = launch.WritersClosed && unit.State == hostbox.UnitSettled |
| 70 | } |
| 71 | } |
| 72 | if controllers == 1 && matches == 0 && r.Process == (release.GenerationProcess{}) { |
| 73 | if r.Closed && !writersClosed { |
| 74 | return false, os.ErrPermission |
| 75 | } |
| 76 | return writersClosed, nil |
| 77 | } |
| 78 | if controllers != 1 || matches != 1 || (r.Closed && !closed) { |
| 79 | return false, os.ErrPermission |
| 80 | } |
| 81 | return closed, nil |
| 82 | } |
| 83 | |
| 84 | // closed verifies root's existing irreversible closure. It never stops a unit, |
| 85 | // closes another controller's admission, or interprets a missing journal as join. |
| 86 | func (v generationRetirement) closed(ctx context.Context, r release.GenerationReservation) error { |
| 87 | closed, err := v.closure(ctx, r) |
| 88 | if err != nil { |
| 89 | return err |
| 90 | } |
| 91 | if !closed { |
| 92 | return os.ErrPermission |
| 93 | } |
| 94 | return nil |
| 95 | } |
| 96 | func (v generationRetirement) verify(ctx context.Context, r release.GenerationReservation) (string, error) { |
| 97 | if err := v.closed(ctx, r); err != nil { |
| 98 | return "", err |
| 99 | } |
| 100 | evidence, err := v.kernel(ctx, r) |
| 101 | if err != nil { |
| 102 | return "", err |
| 103 | } |
| 104 | if err := v.closed(ctx, r); err != nil { |
| 105 | return "", err |
| 106 | } |
| 107 | return evidence, nil |
| 108 | } |
| 109 | |
| 110 | // The caller holds release exclusion; host exclusion remains held through both |
| 111 | // ledger commits and the kernel check, in the same order used by launch. |
| 112 | func (v generationRetirement) retire(ctx context.Context, g *release.RootGenerations, r release.GenerationReservation) error { |
| 113 | if v.retiredKernel != nil { |
| 114 | t := r.Binding.Target |
| 115 | view, err := v.guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) |
| 116 | if err != nil { |
| 117 | return err |
| 118 | } |
| 119 | v.kernel = func(ctx context.Context, current release.GenerationReservation) (string, error) { |
| 120 | return v.retiredKernel(ctx, current, view) |
| 121 | } |
| 122 | } |
| 123 | return v.guard.WithLocal(ctx, r.Binding.Binding.Host, func() error { |
| 124 | closed, err := v.closure(ctx, r) |
| 125 | if err != nil || !closed { |
| 126 | return err |
| 127 | } |
| 128 | return g.Retire(ctx, r, v.closed, v.verify) |
| 129 | }) |
| 130 | } |
| 131 | |
| 132 | func productionRetireGenerations(ctx context.Context, g *release.RootGenerations) error { |
| 133 | records, err := g.Reservations(ctx) |
| 134 | if err != nil { |
| 135 | return err |
| 136 | } |
| 137 | // Absence is not completeness. The pin reader still requires its verifier. |
| 138 | if len(records) == 0 { |
| 139 | return nil |
| 140 | } |
| 141 | state, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false) |
| 142 | if err != nil { |
| 143 | return err |
| 144 | } |
| 145 | defer state.Close() |
| 146 | journal, err := openGenerationLaunchInventory("/var/lib/carlos-claims/journals.db") |
| 147 | if err != nil { |
| 148 | return err |
| 149 | } |
| 150 | defer journal.Close() |
| 151 | verifier := generationRetirement{guard: hostguard.Guard{DataDir: dataDir(), Root: hostguard.NewRootAuthority(state)}, journal: journal, kernel: productionGenerationAbsence, retiredKernel: productionRetiredGenerationAbsence} |
| 152 | return verifier.retireAll(context.WithValue(ctx, generationCensusReservationsKey{}, records), g, records) |
| 153 | } |
| 154 | |
| 155 | func (v generationRetirement) retireAll(ctx context.Context, g *release.RootGenerations, records []release.GenerationReservation) error { |
| 156 | for _, r := range records { |
| 157 | if r.Retirement != "" { |
| 158 | continue |
| 159 | } |
| 160 | if err := v.retire(ctx, g, r); err != nil { |
| 161 | return err |
| 162 | } |
| 163 | } |
| 164 | return nil |
| 165 | } |
| 166 | |