| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "net" |
| 9 | "net/http" |
| 10 | "os" |
| 11 | "sort" |
| 12 | "strconv" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/carlosframework/platform/internal/activator" |
| 16 | "github.com/carlosframework/platform/internal/cgroup" |
| 17 | "github.com/carlosframework/platform/internal/hostbox" |
| 18 | "github.com/carlosframework/platform/internal/hostclaim" |
| 19 | execlease "github.com/carlosframework/platform/internal/hostexec" |
| 20 | "github.com/carlosframework/platform/internal/hostguard" |
| 21 | "github.com/carlosframework/platform/internal/instancectl" |
| 22 | "github.com/carlosframework/platform/internal/release" |
| 23 | "github.com/carlosframework/platform/internal/rootstate" |
| 24 | ) |
| 25 | |
| 26 | // ErrClaimLaunchUnavailable refuses every launch while a root provider of |
| 27 | // the isolated launch protocol is missing. |
| 28 | var ErrClaimLaunchUnavailable = fmt.Errorf("%w: claim launch root provider missing", activator.ErrClaimIsolationUnavailable) |
| 29 | |
| 30 | // ClaimLaunchUnits starts, stops and observes the host's isolated run unit. |
| 31 | // Production is instancectl.ClaimUnits. |
| 32 | type ClaimLaunchUnits interface { |
| 33 | StartReserved(context.Context, instancectl.ClaimUnit, instancectl.Launch, instancectl.ClaimExecPermit, func() error) (instancectl.ClaimUnitStarted, error) |
| 34 | Same(context.Context, instancectl.ClaimUnitStarted) error |
| 35 | Stop(context.Context, string) error |
| 36 | Gone(context.Context, string, string) error |
| 37 | } |
| 38 | |
| 39 | // ClaimLaunchKernel is root's own view of the build and of the started unit. |
| 40 | type ClaimLaunchKernel interface { |
| 41 | // Verify accepts only a root-owned, immutable build under the release |
| 42 | // tree with exactly the pinned identity. |
| 43 | Verify(string, hostbox.LaunchFile) error |
| 44 | VerifyDigest(context.Context, string, hostbox.LaunchFile, string) error |
| 45 | // Observe reads the main process lifetime and cgroup, and its tenant |
| 46 | // child's lifetime, namespace-bound build and running executable. |
| 47 | Observe(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) |
| 48 | } |
| 49 | |
| 50 | type ClaimLaunchObserved struct { |
| 51 | ProcessStart string |
| 52 | TenantPID int |
| 53 | TenantStart string |
| 54 | Group, Executable hostbox.LaunchFile |
| 55 | Running hostbox.LaunchFile |
| 56 | Launcher hostbox.LaunchFile |
| 57 | } |
| 58 | |
| 59 | // ClaimLaunchRoot is root's side of the reservation-bearing isolated launch |
| 60 | // protocol (claim Plan 11, R10). Every authority it checks comes from root's |
| 61 | // own providers: the authenticated peer, its guard and claim reader, its |
| 62 | // roster identity, its local launch journal and its own view of the kernel. |
| 63 | // A request only names which reservation to run. |
| 64 | type ClaimLaunchRoot struct { |
| 65 | boundReady func(context.Context, string, instancectl.ClaimUnitStarted) error |
| 66 | unitProgram func(claimUnitShape) (claimUnitProgram, error) |
| 67 | unitBindings *claimUnitBindings |
| 68 | history rootHistoryAccess |
| 69 | generations *rootGenerationSource |
| 70 | installedProgram func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error) |
| 71 | SeedController func(context.Context, StopPeer) error |
| 72 | executionState rootstate.Store |
| 73 | ValidateExecutable func(host, path, digest string) error |
| 74 | Local hostbox.LocalAuthority |
| 75 | Clock execlease.Clock |
| 76 | DataRoot, ExecLockDir string |
| 77 | runRoot string |
| 78 | Attempts map[string]claimLaunchAttemptReader |
| 79 | ReadyBudget, ReadyPoll time.Duration |
| 80 | Authenticate func(context.Context, net.Conn) (StopPeer, error) |
| 81 | Guard hostguard.Guard |
| 82 | Actor func(context.Context) (hostbox.Actor, error) |
| 83 | Journal *hostbox.LaunchJournal |
| 84 | Units ClaimLaunchUnits |
| 85 | Kernel ClaimLaunchKernel |
| 86 | } |
| 87 | |
| 88 | func (h *ClaimLaunchRoot) complete() bool { |
| 89 | return h != nil && h.Guard.DataDir != "" && h.Guard.Claims != nil && h.Actor != nil && h.Local != nil && h.Clock != nil && h.DataRoot != "" && h.Journal != nil && h.Units != nil && h.Kernel != nil && h.ValidateExecutable != nil |
| 90 | } |
| 91 | |
| 92 | // Serve answers one validated request for an authenticated peer. |
| 93 | func (h *ClaimLaunchRoot) Serve(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchResponse, error) { |
| 94 | if !h.complete() { |
| 95 | return hostbox.RoleLaunchResponse{}, ErrClaimLaunchUnavailable |
| 96 | } |
| 97 | if err := q.Validate(); err != nil { |
| 98 | return hostbox.RoleLaunchResponse{}, err |
| 99 | } |
| 100 | if peer.UID == 0 || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID == "" || peer.ControllerID == "" { |
| 101 | return hostbox.RoleLaunchResponse{}, ErrClaimStopIdentity |
| 102 | } |
| 103 | out := hostbox.RoleLaunchResponse{V: hostbox.RoleLaunchV, Protocol: hostbox.RoleLaunchProtocol} |
| 104 | switch q.Op { |
| 105 | case hostbox.RoleLaunchInputs: |
| 106 | inputs, err := h.boundInputs(ctx, *q.Route) |
| 107 | if err != nil { |
| 108 | return hostbox.RoleLaunchResponse{}, err |
| 109 | } |
| 110 | out.Inputs = inputs |
| 111 | case hostbox.RoleLaunchCapability: |
| 112 | if q.Kind == "files-seed" { |
| 113 | if h.SeedController == nil { |
| 114 | return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported |
| 115 | } |
| 116 | if err := h.SeedController(ctx, peer); err != nil { |
| 117 | return hostbox.RoleLaunchResponse{}, err |
| 118 | } |
| 119 | return out, nil |
| 120 | } |
| 121 | if q.Kind != "" && q.Kind != hostclaim.KindServing { |
| 122 | if (q.Kind != hostclaim.KindLanding && q.Kind != hostclaim.KindRelocating) || h.Attempts[q.Kind] == nil { |
| 123 | return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported |
| 124 | } |
| 125 | } |
| 126 | case hostbox.RoleLaunchStart: |
| 127 | grant, err := h.start(ctx, peer, q) |
| 128 | if err != nil { |
| 129 | return hostbox.RoleLaunchResponse{}, err |
| 130 | } |
| 131 | out.Grant = &grant |
| 132 | case hostbox.RoleLaunchStop: |
| 133 | if err := h.stop(ctx, peer, *q.Reservation); err != nil { |
| 134 | return hostbox.RoleLaunchResponse{}, err |
| 135 | } |
| 136 | } |
| 137 | return out, nil |
| 138 | } |
| 139 | |
| 140 | // claimLaunchAuthority is the fresh claim check root makes under the host |
| 141 | // guard: the reservation's exact holder session and operation, this box as |
| 142 | // the executor of a phase that admits a writer (main's wake policy), this |
| 143 | // box as a holder's admitted writer, and an install stamp pin equal to the |
| 144 | // reservation's stamp. A relocation must carry that pin. |
| 145 | func claimLaunchAuthority(c *hostclaim.Claim, t hostguard.FenceTarget, box string) error { |
| 146 | if c == nil || c.Holder == nil || c.Holder.Incarnation != t.Incarnation || c.Holder.Stamp != t.Stamp || c.Holder.Session != t.Session || c.Op.ID != t.Op || c.Op.Phase != t.Phase { |
| 147 | return hostbox.ErrChanged |
| 148 | } |
| 149 | probe := c.Op.Kind != hostclaim.KindServing && c.Op.Phase == hostclaim.PhaseLandingProbing |
| 150 | if err := hostbox.WakePhase(c, box, probe); err != nil { |
| 151 | return err |
| 152 | } |
| 153 | if c.Holder.WriterBox != box { |
| 154 | return hostbox.ErrChanged |
| 155 | } |
| 156 | pin, pinned := c.Op.Journal["install_stamp"] |
| 157 | if (c.Op.Kind == hostclaim.KindRelocating && !pinned) || (pinned && pin != strconv.FormatUint(t.Stamp, 10)) { |
| 158 | return hostbox.ErrChanged |
| 159 | } |
| 160 | return nil |
| 161 | } |
| 162 | |
| 163 | func claimLaunchTier(n int64) bool { |
| 164 | return n == cgroup.TierDefault.MemoryMax || n == cgroup.TierChildren.MemoryMax |
| 165 | } |
| 166 | |
| 167 | // launchRecord is the prelaunch record of exactly t, written for the live |
| 168 | // controller that is asking. |
| 169 | func (h *ClaimLaunchRoot) launchRecord(ctx context.Context, peer StopPeer, t hostguard.FenceTarget) error { |
| 170 | rec, err := h.Journal.Read(ctx, t) |
| 171 | if err != nil { |
| 172 | return err |
| 173 | } |
| 174 | if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil { |
| 175 | return err |
| 176 | } |
| 177 | if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart { |
| 178 | return ErrClaimStopIdentity |
| 179 | } |
| 180 | if rec.Target.Region != t.Region || rec.Target.Op != t.Op || rec.Target.Phase != t.Phase || rec.Target.DirectoryRevision != t.DirectoryRevision { |
| 181 | return hostbox.ErrChanged |
| 182 | } |
| 183 | return nil |
| 184 | } |
| 185 | |
| 186 | func (h *ClaimLaunchRoot) start(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) { |
| 187 | if h.history == nil { |
| 188 | if h.generations != nil { |
| 189 | return hostbox.RoleLaunchGrant{}, os.ErrPermission |
| 190 | } |
| 191 | return h.startRecorded(ctx, peer, q, nil) |
| 192 | } |
| 193 | var out hostbox.RoleLaunchGrant |
| 194 | err := h.history(ctx, func(history *release.RootHistory) error { |
| 195 | var err error |
| 196 | out, err = h.startRecorded(ctx, peer, q, history) |
| 197 | return err |
| 198 | }) |
| 199 | return out, err |
| 200 | } |
| 201 | |
| 202 | func (h *ClaimLaunchRoot) startRecorded(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, history *release.RootHistory) (hostbox.RoleLaunchGrant, error) { |
| 203 | r := *q.Reservation |
| 204 | t := r.Target |
| 205 | if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID { |
| 206 | return hostbox.RoleLaunchGrant{}, ErrClaimStopIdentity |
| 207 | } |
| 208 | _, group, placementErr := claimLaunchPlacement(h.Units, t.Host) |
| 209 | if placementErr != nil || r.Group.RelativePath != group || !claimLaunchTier(q.MemoryMax) { |
| 210 | return hostbox.RoleLaunchGrant{}, hostbox.ErrRoleLaunchShape |
| 211 | } |
| 212 | actor, err := h.Actor(ctx) |
| 213 | if err != nil { |
| 214 | return hostbox.RoleLaunchGrant{}, err |
| 215 | } |
| 216 | if actor.Quarantined || actor.Box != t.Box || actor.Enrollment != t.Enrollment || actor.Region != t.Region || actor.Binding == "" || actor.Binding != t.DirectoryRevision { |
| 217 | return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged |
| 218 | } |
| 219 | if err = h.Kernel.Verify(q.Executable, r.Executable); err != nil { |
| 220 | return hostbox.RoleLaunchGrant{}, err |
| 221 | } |
| 222 | var grant hostbox.RoleLaunchGrant |
| 223 | var binding release.HistoryBinding |
| 224 | if history != nil { |
| 225 | if h.Guard.Root == nil || h.installedProgram == nil { |
| 226 | return grant, ErrClaimLaunchUnavailable |
| 227 | } |
| 228 | view, err := h.Guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) |
| 229 | if err != nil { |
| 230 | return grant, err |
| 231 | } |
| 232 | in := view.Installation |
| 233 | if in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 { |
| 234 | return grant, hostbox.ErrChanged |
| 235 | } |
| 236 | binding = release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App} |
| 237 | } |
| 238 | check := func(c *hostclaim.Claim) error { return h.authorize(ctx, peer, q, c) } |
| 239 | // The fence, the claim check, the journal intention, the unit start and |
| 240 | // the started record all happen under the host guard a root fence also |
| 241 | // takes, so no fence or second launch interleaves with them. |
| 242 | err = h.Guard.With(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}, check, func() (outcome error) { |
| 243 | if history != nil { |
| 244 | if err := history.ConsumeCreation(binding); err != nil { |
| 245 | return err |
| 246 | } |
| 247 | } |
| 248 | var pin *generationLaunchPermit |
| 249 | if h.generations != nil { |
| 250 | var err error |
| 251 | pin, err = h.generations.reserve(ctx, h, peer, q) |
| 252 | if err != nil { |
| 253 | return err |
| 254 | } |
| 255 | defer pin.ledger.Store.Close() |
| 256 | if pin.closeUpdate != nil { |
| 257 | defer pin.closeUpdate() |
| 258 | } |
| 259 | if pin.finishUpdate != nil { |
| 260 | defer func() { |
| 261 | finished := pin.finishUpdate(grant.UnitInvocationID, outcome, func() error { |
| 262 | started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath} |
| 263 | observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable) |
| 264 | if err != nil { |
| 265 | return err |
| 266 | } |
| 267 | if observed.ProcessStart != grant.ProcessStart || observed.Group.Device != grant.Group.Device || observed.Group.Inode != grant.Group.Inode { |
| 268 | return hostbox.ErrChanged |
| 269 | } |
| 270 | return h.Units.Same(ctx, started) |
| 271 | }) |
| 272 | if finished != nil && outcome == nil && grant.UnitInvocationID != "" { |
| 273 | cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) |
| 274 | defer cancel() |
| 275 | stopped := claimStopInvocation(cleanup, h.Units, t.Host, grant.UnitInvocationID) |
| 276 | if stopped == nil { |
| 277 | stopped = h.settleGone(cleanup, t, r.ID, grant.UnitInvocationID) |
| 278 | } |
| 279 | finished = errors.Join(finished, stopped) |
| 280 | } |
| 281 | outcome = errors.Join(outcome, finished) |
| 282 | }() |
| 283 | } |
| 284 | } |
| 285 | var e error |
| 286 | grant, e = h.launchLocked(ctx, peer, q, r, pin) |
| 287 | if e == nil && history != nil { |
| 288 | started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath} |
| 289 | observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable) |
| 290 | if err != nil { |
| 291 | return err |
| 292 | } |
| 293 | if observed.ProcessStart != grant.ProcessStart { |
| 294 | return hostbox.ErrChanged |
| 295 | } |
| 296 | program, err := h.installedProgram(ctx, q, started, observed) |
| 297 | if err != nil { |
| 298 | return err |
| 299 | } |
| 300 | fresh, err := h.Guard.Claims.Read(ctx, t.Host) |
| 301 | if err != nil { |
| 302 | return err |
| 303 | } |
| 304 | if fresh.Absent { |
| 305 | return hostbox.ErrChanged |
| 306 | } |
| 307 | if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil { |
| 308 | return err |
| 309 | } |
| 310 | if err := history.RecordInstalledProgram(binding, program); err != nil { |
| 311 | return err |
| 312 | } |
| 313 | } |
| 314 | return e |
| 315 | }) |
| 316 | return grant, err |
| 317 | } |
| 318 | |
| 319 | func (h *ClaimLaunchRoot) launchLocked(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, r hostbox.LaunchReservation, pin *generationLaunchPermit) (hostbox.RoleLaunchGrant, error) { |
| 320 | t := r.Target |
| 321 | if err := h.launchRecord(ctx, peer, t); err != nil { |
| 322 | return hostbox.RoleLaunchGrant{}, err |
| 323 | } |
| 324 | // Root has proved there is no run unit, while holding the host guard. |
| 325 | // Spend old intentions now, before a replacement can acquire the unit name. |
| 326 | // Otherwise a late stop of an intention with no invocation ID could stop it. |
| 327 | intentions, err := h.Journal.UnitIntentions(ctx, t.Host) |
| 328 | if err != nil { |
| 329 | return hostbox.RoleLaunchGrant{}, err |
| 330 | } |
| 331 | for _, old := range intentions { |
| 332 | record, err := h.Journal.Read(ctx, old.Target) |
| 333 | if err != nil { |
| 334 | return hostbox.RoleLaunchGrant{}, err |
| 335 | } |
| 336 | if err := validatePersistedUnitContainment(ctx, record, h.Units); err != nil { |
| 337 | return hostbox.RoleLaunchGrant{}, err |
| 338 | } |
| 339 | if old.Group.RelativePath != r.Group.RelativePath { |
| 340 | return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged |
| 341 | } |
| 342 | } |
| 343 | // Refuse a busy unit before journaling, so a refusal leaves nothing |
| 344 | // pending in the session's stop record. |
| 345 | if err := h.Units.Gone(ctx, t.Host, ""); err != nil { |
| 346 | return hostbox.RoleLaunchGrant{}, errors.Join(hostbox.ErrChanged, err) |
| 347 | } |
| 348 | for _, old := range intentions { |
| 349 | if old.Group.RelativePath != r.Group.RelativePath { |
| 350 | return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged |
| 351 | } |
| 352 | if err := h.Journal.SettleUnit(ctx, old.Target, old.ID); err != nil { |
| 353 | return hostbox.RoleLaunchGrant{}, err |
| 354 | } |
| 355 | } |
| 356 | if err := h.Journal.IntendUnit(ctx, r); err != nil { |
| 357 | return hostbox.RoleLaunchGrant{}, err |
| 358 | } |
| 359 | cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) |
| 360 | defer cancel() |
| 361 | launch, err := h.launchSpec(q) |
| 362 | if err != nil { |
| 363 | return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, "")) |
| 364 | } |
| 365 | permit, err := h.execPermit(ctx, q) |
| 366 | if err != nil { |
| 367 | return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, "")) |
| 368 | } |
| 369 | started, err := h.Units.StartReserved(ctx, instancectl.ClaimUnit{Host: t.Host, Executable: q.Executable, MemoryMax: q.MemoryMax}, launch, permit, func() error { |
| 370 | // Check before starting the unit and again when its authenticated |
| 371 | // trampoline reaches ForkExec, under this same host guard. |
| 372 | fresh, err := h.Guard.Claims.Read(ctx, t.Host) |
| 373 | if err != nil { |
| 374 | return err |
| 375 | } |
| 376 | if fresh.Absent || fresh.Host != t.Host { |
| 377 | return hostbox.ErrChanged |
| 378 | } |
| 379 | in, err := h.Journal.Read(ctx, t) |
| 380 | if err != nil { |
| 381 | return err |
| 382 | } |
| 383 | if in.WritersClosed { |
| 384 | return hostbox.ErrChanged |
| 385 | } |
| 386 | rr, err := h.Journal.UnitReservation(ctx, t, r.ID) |
| 387 | if err != nil { |
| 388 | return err |
| 389 | } |
| 390 | if rr.State != hostbox.UnitIntended || !sameUnitRequest(rr, r) { |
| 391 | return hostbox.ErrChanged |
| 392 | } |
| 393 | if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil { |
| 394 | return err |
| 395 | } |
| 396 | if err := pin.check(); err != nil { |
| 397 | return err |
| 398 | } |
| 399 | _, boot, err := h.Clock.Now() |
| 400 | if err != nil { |
| 401 | return err |
| 402 | } |
| 403 | if boot >= permit.SafeUntil { |
| 404 | return execlease.ErrLost |
| 405 | } |
| 406 | return nil |
| 407 | }) |
| 408 | if err != nil { |
| 409 | return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, "")) |
| 410 | } |
| 411 | observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable) |
| 412 | if err == nil && (started.ControlGroup != r.Group.RelativePath || observed.Executable != r.Executable || observed.ProcessStart == "" || observed.Group.Device == 0 || observed.Group.Inode == 0) { |
| 413 | err = hostbox.ErrChanged |
| 414 | } |
| 415 | group := hostbox.LaunchGroup{RelativePath: r.Group.RelativePath, Device: observed.Group.Device, Inode: observed.Group.Inode, InvocationID: started.InvocationID} |
| 416 | if err == nil { |
| 417 | err = h.Journal.StartedUnit(ctx, r, group, started.MainPID, observed.ProcessStart) |
| 418 | } |
| 419 | if err == nil { |
| 420 | err = pin.started(hostbox.RoleLaunchGrant{PID: started.MainPID, ProcessStart: observed.ProcessStart, Group: group, UnitInvocationID: started.InvocationID}, observed) |
| 421 | } |
| 422 | if err != nil { |
| 423 | // A replacement detected while waiting is never stopped by the old |
| 424 | // reservation. Settle only if root can prove the old invocation gone. |
| 425 | var stopErr error |
| 426 | if sameErr := h.Units.Same(cleanup, started); sameErr != nil { |
| 427 | stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID) |
| 428 | } else { |
| 429 | stopErr = claimStopInvocation(cleanup, h.Units, t.Host, started.InvocationID) |
| 430 | if stopErr == nil { |
| 431 | stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID) |
| 432 | } |
| 433 | } |
| 434 | return hostbox.RoleLaunchGrant{}, errors.Join(err, stopErr) |
| 435 | } |
| 436 | return hostbox.RoleLaunchGrant{ReservationID: r.ID, Unit: started.Unit, UnitInvocationID: started.InvocationID, PID: started.MainPID, ProcessStart: observed.ProcessStart, Group: group}, nil |
| 437 | } |
| 438 | |
| 439 | // settleGone settles a unit reservation only once root proved its |
| 440 | // invocation ended. Otherwise it stays pending and the stop record keeps |
| 441 | // refusing a receipt. |
| 442 | func (h *ClaimLaunchRoot) settleGone(ctx context.Context, t hostguard.FenceTarget, id, invocation string) error { |
| 443 | if err := h.Units.Gone(ctx, t.Host, invocation); err != nil { |
| 444 | return err |
| 445 | } |
| 446 | return h.Journal.SettleUnit(ctx, t, id) |
| 447 | } |
| 448 | |
| 449 | func sameUnitRequest(old, r hostbox.LaunchReservation) bool { |
| 450 | return old.Role == r.Role && old.InvocationID == r.InvocationID && old.Inputs == r.Inputs && old.Executable == r.Executable && |
| 451 | old.Group.RelativePath == r.Group.RelativePath && hostbox.SameLaunchOwner(old.Target, r.Target) |
| 452 | } |
| 453 | |
| 454 | // stop ends one reservation for the live controller that owns it. Stopping |
| 455 | // is always allowed, fenced or not; it needs only the host lock, so it |
| 456 | // serializes with a launch. A reservation root never saw is spent. |
| 457 | func (h *ClaimLaunchRoot) stop(ctx context.Context, peer StopPeer, r hostbox.LaunchReservation) error { |
| 458 | t := r.Target |
| 459 | if err := h.checkUnitBinding(ctx, t, true); err != nil { |
| 460 | return err |
| 461 | } |
| 462 | _, group, err := claimLaunchPlacement(h.Units, t.Host) |
| 463 | if err != nil || r.Group.RelativePath != group { |
| 464 | return hostbox.ErrRoleLaunchShape |
| 465 | } |
| 466 | if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID { |
| 467 | return ErrClaimStopIdentity |
| 468 | } |
| 469 | return h.Guard.WithLocal(ctx, t.Host, func() error { |
| 470 | rec, err := h.Journal.Read(ctx, t) |
| 471 | if err != nil { |
| 472 | return err |
| 473 | } |
| 474 | if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil { |
| 475 | return err |
| 476 | } |
| 477 | if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart { |
| 478 | return ErrClaimStopIdentity |
| 479 | } |
| 480 | for _, old := range rec.Reservations { |
| 481 | if old.ID != r.ID { |
| 482 | continue |
| 483 | } |
| 484 | if !hostbox.IsUnitReservation(old) || !sameUnitRequest(old, r) { |
| 485 | return hostbox.ErrChanged |
| 486 | } |
| 487 | if old.State == hostbox.UnitSettled { |
| 488 | return nil |
| 489 | } |
| 490 | if err := h.Units.Gone(ctx, t.Host, old.Group.InvocationID); err == nil { |
| 491 | return h.Journal.SettleUnit(ctx, t, r.ID) |
| 492 | } else if !errors.Is(err, instancectl.ErrClaimUnitBusy) { |
| 493 | return err |
| 494 | } |
| 495 | if err := claimStopInvocation(ctx, h.Units, t.Host, old.Group.InvocationID); err != nil { |
| 496 | return err |
| 497 | } |
| 498 | return h.settleGone(ctx, t, r.ID, old.Group.InvocationID) |
| 499 | } |
| 500 | return h.Journal.TombstoneUnit(ctx, r) |
| 501 | }) |
| 502 | } |
| 503 | |
| 504 | func claimLaunchStatus(err error) int { |
| 505 | switch { |
| 506 | case errors.Is(err, activator.ErrClaimIsolationUnavailable): |
| 507 | return http.StatusServiceUnavailable |
| 508 | case errors.Is(err, hostbox.ErrRoleLaunchShape): |
| 509 | return http.StatusBadRequest |
| 510 | case errors.Is(err, ErrClaimStopIdentity): |
| 511 | return http.StatusForbidden |
| 512 | } |
| 513 | return http.StatusConflict |
| 514 | } |
| 515 | |
| 516 | // ServeHTTP is POST /claim/launch on the root instance-control socket, |
| 517 | // mounted only in claim mode. The peer is authenticated before a byte of the |
| 518 | // body is read. |
| 519 | func (h *ClaimLaunchRoot) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 520 | if h == nil || h.Authenticate == nil || r.Method != http.MethodPost || r.URL.Path != "/claim/launch" { |
| 521 | http.Error(w, "refused", http.StatusServiceUnavailable) |
| 522 | return |
| 523 | } |
| 524 | conn := instancectl.RequestConn(r.Context()) |
| 525 | if conn == nil { |
| 526 | http.Error(w, "refused", http.StatusForbidden) |
| 527 | return |
| 528 | } |
| 529 | peer, err := h.Authenticate(r.Context(), conn) |
| 530 | if err != nil { |
| 531 | http.Error(w, "refused", http.StatusForbidden) |
| 532 | return |
| 533 | } |
| 534 | q, err := hostbox.DecodeRoleLaunchRequest(r.Body) |
| 535 | if err != nil { |
| 536 | http.Error(w, "refused", http.StatusBadRequest) |
| 537 | return |
| 538 | } |
| 539 | out, err := h.Serve(r.Context(), peer, q) |
| 540 | if err != nil { |
| 541 | http.Error(w, "refused", claimLaunchStatus(err)) |
| 542 | return |
| 543 | } |
| 544 | w.Header().Set("Content-Type", "application/json") |
| 545 | w.WriteHeader(http.StatusOK) |
| 546 | _ = json.NewEncoder(w).Encode(out) |
| 547 | } |
| 548 | |
| 549 | // claimUnitStopKernel puts the isolated units in root's stop evidence. It |
| 550 | // settles a unit reservation only once root proves its invocation ended, |
| 551 | // before the wrapped verifier scans the session, and reports the settled |
| 552 | // invocations. A unit it cannot prove gone refuses the receipt. |
| 553 | type claimUnitStopKernel struct { |
| 554 | Inner StopKernelVerifier |
| 555 | Journal *hostbox.LaunchJournal |
| 556 | Units ClaimLaunchUnits |
| 557 | } |
| 558 | |
| 559 | func (k claimUnitStopKernel) settleUnits(ctx context.Context, t hostguard.FenceTarget) ([]string, error) { |
| 560 | if k.Inner == nil || k.Journal == nil || k.Units == nil { |
| 561 | return nil, ErrClaimStopUnavailable |
| 562 | } |
| 563 | in, err := k.Journal.Read(ctx, t) |
| 564 | if err != nil { |
| 565 | return nil, err |
| 566 | } |
| 567 | if err := validatePersistedUnitContainment(ctx, in, k.Units); err != nil { |
| 568 | return nil, err |
| 569 | } |
| 570 | ids := []string{} |
| 571 | for _, r := range in.Reservations { |
| 572 | if !hostbox.IsUnitReservation(r) { |
| 573 | continue |
| 574 | } |
| 575 | if r.State != hostbox.UnitSettled { |
| 576 | if err := k.Units.Gone(ctx, t.Host, r.Group.InvocationID); err != nil { |
| 577 | return nil, err |
| 578 | } |
| 579 | if err := k.Journal.SettleUnit(ctx, t, r.ID); err != nil { |
| 580 | return nil, err |
| 581 | } |
| 582 | } |
| 583 | if r.Group.InvocationID != "" { |
| 584 | ids = append(ids, r.Group.InvocationID) |
| 585 | } |
| 586 | } |
| 587 | sort.Strings(ids) |
| 588 | return ids, nil |
| 589 | } |
| 590 | |
| 591 | func (k claimUnitStopKernel) VerifyStopped(ctx context.Context, t hostguard.FenceTarget, inv StopInventory, ack DrainAcknowledgement) (StopKernelEvidence, error) { |
| 592 | ids, err := k.settleUnits(ctx, t) |
| 593 | if err != nil { |
| 594 | return StopKernelEvidence{}, err |
| 595 | } |
| 596 | evidence, err := k.Inner.VerifyStopped(ctx, t, inv, ack) |
| 597 | if err != nil { |
| 598 | return StopKernelEvidence{}, err |
| 599 | } |
| 600 | evidence.UnitInvocationIDs = append(evidence.UnitInvocationIDs, ids...) |
| 601 | return evidence, nil |
| 602 | } |
| 603 | |
| 604 | func (k claimUnitStopKernel) VerifyUnjournaled(ctx context.Context, t hostguard.FenceTarget, inv StopInventory) (StopKernelEvidence, error) { |
| 605 | kernel, ok := k.Inner.(interface { |
| 606 | VerifyUnjournaled(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error) |
| 607 | }) |
| 608 | if !ok { |
| 609 | return StopKernelEvidence{}, ErrClaimStopUnavailable |
| 610 | } |
| 611 | return kernel.VerifyUnjournaled(ctx, t, inv) |
| 612 | } |
| 613 | |
| 614 | func (k claimUnitStopKernel) RecoverStopped(ctx context.Context, t hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) { |
| 615 | recovery, ok := k.Inner.(stopRecoveryKernel) |
| 616 | if !ok { |
| 617 | return StopKernelEvidence{}, DrainAcknowledgement{}, ErrClaimStopUnavailable |
| 618 | } |
| 619 | if controller, ok := k.Inner.(interface { |
| 620 | CheckRecoveryController(context.Context, hostguard.FenceTarget) error |
| 621 | }); ok { |
| 622 | if err := controller.CheckRecoveryController(ctx, t); err != nil { |
| 623 | return StopKernelEvidence{}, DrainAcknowledgement{}, err |
| 624 | } |
| 625 | } |
| 626 | ids, err := k.settleUnits(ctx, t) |
| 627 | if err != nil { |
| 628 | return StopKernelEvidence{}, DrainAcknowledgement{}, err |
| 629 | } |
| 630 | evidence, ack, err := recovery.RecoverStopped(ctx, t, in) |
| 631 | if err != nil { |
| 632 | return StopKernelEvidence{}, DrainAcknowledgement{}, err |
| 633 | } |
| 634 | evidence.UnitInvocationIDs = append(evidence.UnitInvocationIDs, ids...) |
| 635 | return evidence, ack, nil |
| 636 | } |
| 637 | |