package host import ( "context" "encoding/json" "errors" "fmt" "net" "net/http" "os" "sort" "strconv" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) // ErrClaimLaunchUnavailable refuses every launch while a root provider of // the isolated launch protocol is missing. var ErrClaimLaunchUnavailable = fmt.Errorf("%w: claim launch root provider missing", activator.ErrClaimIsolationUnavailable) // ClaimLaunchUnits starts, stops and observes the host's isolated run unit. // Production is instancectl.ClaimUnits. type ClaimLaunchUnits interface { StartReserved(context.Context, instancectl.ClaimUnit, instancectl.Launch, instancectl.ClaimExecPermit, func() error) (instancectl.ClaimUnitStarted, error) Same(context.Context, instancectl.ClaimUnitStarted) error Stop(context.Context, string) error Gone(context.Context, string, string) error } // ClaimLaunchKernel is root's own view of the build and of the started unit. type ClaimLaunchKernel interface { // Verify accepts only a root-owned, immutable build under the release // tree with exactly the pinned identity. Verify(string, hostbox.LaunchFile) error VerifyDigest(context.Context, string, hostbox.LaunchFile, string) error // Observe reads the main process lifetime and cgroup, and its tenant // child's lifetime, namespace-bound build and running executable. Observe(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) } type ClaimLaunchObserved struct { ProcessStart string TenantPID int TenantStart string Group, Executable hostbox.LaunchFile Running hostbox.LaunchFile Launcher hostbox.LaunchFile } // ClaimLaunchRoot is root's side of the reservation-bearing isolated launch // protocol (claim Plan 11, R10). Every authority it checks comes from root's // own providers: the authenticated peer, its guard and claim reader, its // roster identity, its local launch journal and its own view of the kernel. // A request only names which reservation to run. type ClaimLaunchRoot struct { boundReady func(context.Context, string, instancectl.ClaimUnitStarted) error unitProgram func(claimUnitShape) (claimUnitProgram, error) unitBindings *claimUnitBindings history rootHistoryAccess generations *rootGenerationSource installedProgram func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error) SeedController func(context.Context, StopPeer) error executionState rootstate.Store ValidateExecutable func(host, path, digest string) error Local hostbox.LocalAuthority Clock execlease.Clock DataRoot, ExecLockDir string runRoot string Attempts map[string]claimLaunchAttemptReader ReadyBudget, ReadyPoll time.Duration Authenticate func(context.Context, net.Conn) (StopPeer, error) Guard hostguard.Guard Actor func(context.Context) (hostbox.Actor, error) Journal *hostbox.LaunchJournal Units ClaimLaunchUnits Kernel ClaimLaunchKernel } func (h *ClaimLaunchRoot) complete() bool { 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 } // Serve answers one validated request for an authenticated peer. func (h *ClaimLaunchRoot) Serve(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchResponse, error) { if !h.complete() { return hostbox.RoleLaunchResponse{}, ErrClaimLaunchUnavailable } if err := q.Validate(); err != nil { return hostbox.RoleLaunchResponse{}, err } if peer.UID == 0 || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID == "" || peer.ControllerID == "" { return hostbox.RoleLaunchResponse{}, ErrClaimStopIdentity } out := hostbox.RoleLaunchResponse{V: hostbox.RoleLaunchV, Protocol: hostbox.RoleLaunchProtocol} switch q.Op { case hostbox.RoleLaunchInputs: inputs, err := h.boundInputs(ctx, *q.Route) if err != nil { return hostbox.RoleLaunchResponse{}, err } out.Inputs = inputs case hostbox.RoleLaunchCapability: if q.Kind == "files-seed" { if h.SeedController == nil { return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported } if err := h.SeedController(ctx, peer); err != nil { return hostbox.RoleLaunchResponse{}, err } return out, nil } if q.Kind != "" && q.Kind != hostclaim.KindServing { if (q.Kind != hostclaim.KindLanding && q.Kind != hostclaim.KindRelocating) || h.Attempts[q.Kind] == nil { return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported } } case hostbox.RoleLaunchStart: grant, err := h.start(ctx, peer, q) if err != nil { return hostbox.RoleLaunchResponse{}, err } out.Grant = &grant case hostbox.RoleLaunchStop: if err := h.stop(ctx, peer, *q.Reservation); err != nil { return hostbox.RoleLaunchResponse{}, err } } return out, nil } // claimLaunchAuthority is the fresh claim check root makes under the host // guard: the reservation's exact holder session and operation, this box as // the executor of a phase that admits a writer (main's wake policy), this // box as a holder's admitted writer, and an install stamp pin equal to the // reservation's stamp. A relocation must carry that pin. func claimLaunchAuthority(c *hostclaim.Claim, t hostguard.FenceTarget, box string) error { 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 { return hostbox.ErrChanged } probe := c.Op.Kind != hostclaim.KindServing && c.Op.Phase == hostclaim.PhaseLandingProbing if err := hostbox.WakePhase(c, box, probe); err != nil { return err } if c.Holder.WriterBox != box { return hostbox.ErrChanged } pin, pinned := c.Op.Journal["install_stamp"] if (c.Op.Kind == hostclaim.KindRelocating && !pinned) || (pinned && pin != strconv.FormatUint(t.Stamp, 10)) { return hostbox.ErrChanged } return nil } func claimLaunchTier(n int64) bool { return n == cgroup.TierDefault.MemoryMax || n == cgroup.TierChildren.MemoryMax } // launchRecord is the prelaunch record of exactly t, written for the live // controller that is asking. func (h *ClaimLaunchRoot) launchRecord(ctx context.Context, peer StopPeer, t hostguard.FenceTarget) error { rec, err := h.Journal.Read(ctx, t) if err != nil { return err } if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil { return err } if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart { return ErrClaimStopIdentity } if rec.Target.Region != t.Region || rec.Target.Op != t.Op || rec.Target.Phase != t.Phase || rec.Target.DirectoryRevision != t.DirectoryRevision { return hostbox.ErrChanged } return nil } func (h *ClaimLaunchRoot) start(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) { if h.history == nil { if h.generations != nil { return hostbox.RoleLaunchGrant{}, os.ErrPermission } return h.startRecorded(ctx, peer, q, nil) } var out hostbox.RoleLaunchGrant err := h.history(ctx, func(history *release.RootHistory) error { var err error out, err = h.startRecorded(ctx, peer, q, history) return err }) return out, err } func (h *ClaimLaunchRoot) startRecorded(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, history *release.RootHistory) (hostbox.RoleLaunchGrant, error) { r := *q.Reservation t := r.Target if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID { return hostbox.RoleLaunchGrant{}, ErrClaimStopIdentity } _, group, placementErr := claimLaunchPlacement(h.Units, t.Host) if placementErr != nil || r.Group.RelativePath != group || !claimLaunchTier(q.MemoryMax) { return hostbox.RoleLaunchGrant{}, hostbox.ErrRoleLaunchShape } actor, err := h.Actor(ctx) if err != nil { return hostbox.RoleLaunchGrant{}, err } if actor.Quarantined || actor.Box != t.Box || actor.Enrollment != t.Enrollment || actor.Region != t.Region || actor.Binding == "" || actor.Binding != t.DirectoryRevision { return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged } if err = h.Kernel.Verify(q.Executable, r.Executable); err != nil { return hostbox.RoleLaunchGrant{}, err } var grant hostbox.RoleLaunchGrant var binding release.HistoryBinding if history != nil { if h.Guard.Root == nil || h.installedProgram == nil { return grant, ErrClaimLaunchUnavailable } view, err := h.Guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) if err != nil { return grant, err } in := view.Installation if in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 { return grant, hostbox.ErrChanged } binding = release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App} } check := func(c *hostclaim.Claim) error { return h.authorize(ctx, peer, q, c) } // The fence, the claim check, the journal intention, the unit start and // the started record all happen under the host guard a root fence also // takes, so no fence or second launch interleaves with them. err = h.Guard.With(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}, check, func() (outcome error) { if history != nil { if err := history.ConsumeCreation(binding); err != nil { return err } } var pin *generationLaunchPermit if h.generations != nil { var err error pin, err = h.generations.reserve(ctx, h, peer, q) if err != nil { return err } defer pin.ledger.Store.Close() if pin.closeUpdate != nil { defer pin.closeUpdate() } if pin.finishUpdate != nil { defer func() { finished := pin.finishUpdate(grant.UnitInvocationID, outcome, func() error { started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath} observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable) if err != nil { return err } if observed.ProcessStart != grant.ProcessStart || observed.Group.Device != grant.Group.Device || observed.Group.Inode != grant.Group.Inode { return hostbox.ErrChanged } return h.Units.Same(ctx, started) }) if finished != nil && outcome == nil && grant.UnitInvocationID != "" { cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) defer cancel() stopped := claimStopInvocation(cleanup, h.Units, t.Host, grant.UnitInvocationID) if stopped == nil { stopped = h.settleGone(cleanup, t, r.ID, grant.UnitInvocationID) } finished = errors.Join(finished, stopped) } outcome = errors.Join(outcome, finished) }() } } var e error grant, e = h.launchLocked(ctx, peer, q, r, pin) if e == nil && history != nil { started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath} observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable) if err != nil { return err } if observed.ProcessStart != grant.ProcessStart { return hostbox.ErrChanged } program, err := h.installedProgram(ctx, q, started, observed) if err != nil { return err } fresh, err := h.Guard.Claims.Read(ctx, t.Host) if err != nil { return err } if fresh.Absent { return hostbox.ErrChanged } if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil { return err } if err := history.RecordInstalledProgram(binding, program); err != nil { return err } } return e }) return grant, err } func (h *ClaimLaunchRoot) launchLocked(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, r hostbox.LaunchReservation, pin *generationLaunchPermit) (hostbox.RoleLaunchGrant, error) { t := r.Target if err := h.launchRecord(ctx, peer, t); err != nil { return hostbox.RoleLaunchGrant{}, err } // Root has proved there is no run unit, while holding the host guard. // Spend old intentions now, before a replacement can acquire the unit name. // Otherwise a late stop of an intention with no invocation ID could stop it. intentions, err := h.Journal.UnitIntentions(ctx, t.Host) if err != nil { return hostbox.RoleLaunchGrant{}, err } for _, old := range intentions { record, err := h.Journal.Read(ctx, old.Target) if err != nil { return hostbox.RoleLaunchGrant{}, err } if err := validatePersistedUnitContainment(ctx, record, h.Units); err != nil { return hostbox.RoleLaunchGrant{}, err } if old.Group.RelativePath != r.Group.RelativePath { return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged } } // Refuse a busy unit before journaling, so a refusal leaves nothing // pending in the session's stop record. if err := h.Units.Gone(ctx, t.Host, ""); err != nil { return hostbox.RoleLaunchGrant{}, errors.Join(hostbox.ErrChanged, err) } for _, old := range intentions { if old.Group.RelativePath != r.Group.RelativePath { return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged } if err := h.Journal.SettleUnit(ctx, old.Target, old.ID); err != nil { return hostbox.RoleLaunchGrant{}, err } } if err := h.Journal.IntendUnit(ctx, r); err != nil { return hostbox.RoleLaunchGrant{}, err } cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) defer cancel() launch, err := h.launchSpec(q) if err != nil { return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, "")) } permit, err := h.execPermit(ctx, q) if err != nil { return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, "")) } started, err := h.Units.StartReserved(ctx, instancectl.ClaimUnit{Host: t.Host, Executable: q.Executable, MemoryMax: q.MemoryMax}, launch, permit, func() error { // Check before starting the unit and again when its authenticated // trampoline reaches ForkExec, under this same host guard. fresh, err := h.Guard.Claims.Read(ctx, t.Host) if err != nil { return err } if fresh.Absent || fresh.Host != t.Host { return hostbox.ErrChanged } in, err := h.Journal.Read(ctx, t) if err != nil { return err } if in.WritersClosed { return hostbox.ErrChanged } rr, err := h.Journal.UnitReservation(ctx, t, r.ID) if err != nil { return err } if rr.State != hostbox.UnitIntended || !sameUnitRequest(rr, r) { return hostbox.ErrChanged } if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil { return err } if err := pin.check(); err != nil { return err } _, boot, err := h.Clock.Now() if err != nil { return err } if boot >= permit.SafeUntil { return execlease.ErrLost } return nil }) if err != nil { return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, "")) } observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable) if err == nil && (started.ControlGroup != r.Group.RelativePath || observed.Executable != r.Executable || observed.ProcessStart == "" || observed.Group.Device == 0 || observed.Group.Inode == 0) { err = hostbox.ErrChanged } group := hostbox.LaunchGroup{RelativePath: r.Group.RelativePath, Device: observed.Group.Device, Inode: observed.Group.Inode, InvocationID: started.InvocationID} if err == nil { err = h.Journal.StartedUnit(ctx, r, group, started.MainPID, observed.ProcessStart) } if err == nil { err = pin.started(hostbox.RoleLaunchGrant{PID: started.MainPID, ProcessStart: observed.ProcessStart, Group: group, UnitInvocationID: started.InvocationID}, observed) } if err != nil { // A replacement detected while waiting is never stopped by the old // reservation. Settle only if root can prove the old invocation gone. var stopErr error if sameErr := h.Units.Same(cleanup, started); sameErr != nil { stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID) } else { stopErr = claimStopInvocation(cleanup, h.Units, t.Host, started.InvocationID) if stopErr == nil { stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID) } } return hostbox.RoleLaunchGrant{}, errors.Join(err, stopErr) } return hostbox.RoleLaunchGrant{ReservationID: r.ID, Unit: started.Unit, UnitInvocationID: started.InvocationID, PID: started.MainPID, ProcessStart: observed.ProcessStart, Group: group}, nil } // settleGone settles a unit reservation only once root proved its // invocation ended. Otherwise it stays pending and the stop record keeps // refusing a receipt. func (h *ClaimLaunchRoot) settleGone(ctx context.Context, t hostguard.FenceTarget, id, invocation string) error { if err := h.Units.Gone(ctx, t.Host, invocation); err != nil { return err } return h.Journal.SettleUnit(ctx, t, id) } func sameUnitRequest(old, r hostbox.LaunchReservation) bool { return old.Role == r.Role && old.InvocationID == r.InvocationID && old.Inputs == r.Inputs && old.Executable == r.Executable && old.Group.RelativePath == r.Group.RelativePath && hostbox.SameLaunchOwner(old.Target, r.Target) } // stop ends one reservation for the live controller that owns it. Stopping // is always allowed, fenced or not; it needs only the host lock, so it // serializes with a launch. A reservation root never saw is spent. func (h *ClaimLaunchRoot) stop(ctx context.Context, peer StopPeer, r hostbox.LaunchReservation) error { t := r.Target if err := h.checkUnitBinding(ctx, t, true); err != nil { return err } _, group, err := claimLaunchPlacement(h.Units, t.Host) if err != nil || r.Group.RelativePath != group { return hostbox.ErrRoleLaunchShape } if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID { return ErrClaimStopIdentity } return h.Guard.WithLocal(ctx, t.Host, func() error { rec, err := h.Journal.Read(ctx, t) if err != nil { return err } if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil { return err } if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart { return ErrClaimStopIdentity } for _, old := range rec.Reservations { if old.ID != r.ID { continue } if !hostbox.IsUnitReservation(old) || !sameUnitRequest(old, r) { return hostbox.ErrChanged } if old.State == hostbox.UnitSettled { return nil } if err := h.Units.Gone(ctx, t.Host, old.Group.InvocationID); err == nil { return h.Journal.SettleUnit(ctx, t, r.ID) } else if !errors.Is(err, instancectl.ErrClaimUnitBusy) { return err } if err := claimStopInvocation(ctx, h.Units, t.Host, old.Group.InvocationID); err != nil { return err } return h.settleGone(ctx, t, r.ID, old.Group.InvocationID) } return h.Journal.TombstoneUnit(ctx, r) }) } func claimLaunchStatus(err error) int { switch { case errors.Is(err, activator.ErrClaimIsolationUnavailable): return http.StatusServiceUnavailable case errors.Is(err, hostbox.ErrRoleLaunchShape): return http.StatusBadRequest case errors.Is(err, ErrClaimStopIdentity): return http.StatusForbidden } return http.StatusConflict } // ServeHTTP is POST /claim/launch on the root instance-control socket, // mounted only in claim mode. The peer is authenticated before a byte of the // body is read. func (h *ClaimLaunchRoot) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h == nil || h.Authenticate == nil || r.Method != http.MethodPost || r.URL.Path != "/claim/launch" { http.Error(w, "refused", http.StatusServiceUnavailable) return } conn := instancectl.RequestConn(r.Context()) if conn == nil { http.Error(w, "refused", http.StatusForbidden) return } peer, err := h.Authenticate(r.Context(), conn) if err != nil { http.Error(w, "refused", http.StatusForbidden) return } q, err := hostbox.DecodeRoleLaunchRequest(r.Body) if err != nil { http.Error(w, "refused", http.StatusBadRequest) return } out, err := h.Serve(r.Context(), peer, q) if err != nil { http.Error(w, "refused", claimLaunchStatus(err)) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(out) } // claimUnitStopKernel puts the isolated units in root's stop evidence. It // settles a unit reservation only once root proves its invocation ended, // before the wrapped verifier scans the session, and reports the settled // invocations. A unit it cannot prove gone refuses the receipt. type claimUnitStopKernel struct { Inner StopKernelVerifier Journal *hostbox.LaunchJournal Units ClaimLaunchUnits } func (k claimUnitStopKernel) settleUnits(ctx context.Context, t hostguard.FenceTarget) ([]string, error) { if k.Inner == nil || k.Journal == nil || k.Units == nil { return nil, ErrClaimStopUnavailable } in, err := k.Journal.Read(ctx, t) if err != nil { return nil, err } if err := validatePersistedUnitContainment(ctx, in, k.Units); err != nil { return nil, err } ids := []string{} for _, r := range in.Reservations { if !hostbox.IsUnitReservation(r) { continue } if r.State != hostbox.UnitSettled { if err := k.Units.Gone(ctx, t.Host, r.Group.InvocationID); err != nil { return nil, err } if err := k.Journal.SettleUnit(ctx, t, r.ID); err != nil { return nil, err } } if r.Group.InvocationID != "" { ids = append(ids, r.Group.InvocationID) } } sort.Strings(ids) return ids, nil } func (k claimUnitStopKernel) VerifyStopped(ctx context.Context, t hostguard.FenceTarget, inv StopInventory, ack DrainAcknowledgement) (StopKernelEvidence, error) { ids, err := k.settleUnits(ctx, t) if err != nil { return StopKernelEvidence{}, err } evidence, err := k.Inner.VerifyStopped(ctx, t, inv, ack) if err != nil { return StopKernelEvidence{}, err } evidence.UnitInvocationIDs = append(evidence.UnitInvocationIDs, ids...) return evidence, nil } func (k claimUnitStopKernel) VerifyUnjournaled(ctx context.Context, t hostguard.FenceTarget, inv StopInventory) (StopKernelEvidence, error) { kernel, ok := k.Inner.(interface { VerifyUnjournaled(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error) }) if !ok { return StopKernelEvidence{}, ErrClaimStopUnavailable } return kernel.VerifyUnjournaled(ctx, t, inv) } func (k claimUnitStopKernel) RecoverStopped(ctx context.Context, t hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) { recovery, ok := k.Inner.(stopRecoveryKernel) if !ok { return StopKernelEvidence{}, DrainAcknowledgement{}, ErrClaimStopUnavailable } if controller, ok := k.Inner.(interface { CheckRecoveryController(context.Context, hostguard.FenceTarget) error }); ok { if err := controller.CheckRecoveryController(ctx, t); err != nil { return StopKernelEvidence{}, DrainAcknowledgement{}, err } } ids, err := k.settleUnits(ctx, t) if err != nil { return StopKernelEvidence{}, DrainAcknowledgement{}, err } evidence, ack, err := recovery.RecoverStopped(ctx, t, in) if err != nil { return StopKernelEvidence{}, DrainAcknowledgement{}, err } evidence.UnitInvocationIDs = append(evidence.UnitInvocationIDs, ids...) return evidence, ack, nil }