package release import ( "context" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "path/filepath" "strings" "github.com/carlosframework/platform/internal/rootstate" ) const RootGenerationsDir = "/var/lib/carlos-release/private/generations" type GenerationOwner struct { Host, Incarnation, Box, Enrollment, BootID, ControllerID string Stamp, Session uint64 } type GenerationBinding struct { Unit string `json:",omitempty"` Binding HistoryBinding Target GenerationOwner Installation, Attempt, Inputs, Invocation string Record, RecordETag string ControllerPID int ControllerStart string Version, Digest, Path, Tree string Executable, TreeIdentity rootstate.FileIdentity ExecutableProof, TreeProof rootstate.FileIdentity `json:",omitzero"` } type GenerationProcess struct { PID, TenantPID int Start, TenantStart, Group, Invocation string GroupIdentity rootstate.FileIdentity LauncherIdentity rootstate.FileIdentity `json:",omitzero"` Containment uint8 `json:",omitempty"` } type GenerationReservation struct { Schema int Revision uint64 ID string Binding GenerationBinding Process GenerationProcess Closed bool `json:",omitempty"` Retirement string `json:",omitempty"` } // RootGenerations records additional pins under the caller's release exclusion. // A reservation is never retired by unit settlement or an agent publication. // These records alone cannot establish a complete generation census. type RootGenerations struct{ Store rootstate.Store } func generationKey(b GenerationBinding) string { raw, _ := json.Marshal([]any{b.Target, b.Attempt}) sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]) + ".json" } func (b GenerationBinding) valid() bool { t := b.Target digest, err := hex.DecodeString(b.Digest) return (b.Unit == "" || (filepath.Base(b.Unit) == b.Unit && strings.HasSuffix(b.Unit, ".service") && !strings.ContainsAny(b.Unit, "\x00\r\n/\\"))) && b.Binding.valid() && t.Host == b.Binding.Host && t.Incarnation != "" && t.Box != "" && t.Enrollment != "" && t.BootID != "" && t.ControllerID != "" && t.Stamp > 0 && t.Session > 0 && b.Record != "" && b.RecordETag != "" && b.Installation != "" && b.Attempt != "" && b.Inputs != "" && b.Invocation != "" && b.ControllerPID > 0 && b.ControllerStart != "" && ValidVersion(b.Version) && err == nil && len(digest) == sha256.Size && hex.EncodeToString(digest) == b.Digest && filepath.IsAbs(b.Path) && filepath.Clean(b.Path) == b.Path && b.Tree == filepath.Dir(b.Path) && b.Executable.Device > 0 && b.Executable.Inode > 0 && b.TreeIdentity.Device > 0 && b.TreeIdentity.Inode > 0 } func (p GenerationProcess) valid() bool { return p.Containment <= 1 && (p.Containment == 0 || (p.LauncherIdentity.Device > 0 && p.LauncherIdentity.Inode > 0)) && p.PID > 0 && p.TenantPID > 0 && p.PID != p.TenantPID && p.Start != "" && p.TenantStart != "" && filepath.IsLocal(p.Group) && p.Group != "." && p.Invocation != "" && p.GroupIdentity.Device > 0 && p.GroupIdentity.Inode > 0 } func validGeneration(r GenerationReservation) bool { id, err := hex.DecodeString(r.ID) if r.Retirement != "" { evidence, err := hex.DecodeString(r.Retirement) if err != nil || len(evidence) != sha256.Size || hex.EncodeToString(evidence) != r.Retirement || !r.Closed { return false } } base := uint64(1) if r.Process != (GenerationProcess{}) { base = 2 } if r.Closed { base += 2 } if r.Retirement != "" { base += 2 } return r.Schema == 1 && err == nil && len(id) == 32 && hex.EncodeToString(id) == r.ID && r.Binding.valid() && r.Revision == base && (r.Process == (GenerationProcess{}) || r.Process.valid()) } func (g *RootGenerations) Read(b GenerationBinding) (GenerationReservation, error) { var r GenerationReservation if g == nil || g.Store == nil || !b.valid() { return r, os.ErrPermission } raw, err := g.Store.Read(generationKey(b)) if err != nil { return r, err } if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validGeneration(r) || r.Binding != b { return GenerationReservation{}, os.ErrPermission } // Recommit before relying on a rename a prior process may not have synced. if err := g.Store.Write(generationKey(b), raw); err != nil { return GenerationReservation{}, err } return r, nil } func (g *RootGenerations) write(r GenerationReservation) error { if g == nil || g.Store == nil || !validGeneration(r) { return os.ErrPermission } raw, err := json.Marshal(r) if err != nil { return err } if len(raw) > rootstate.MaxRecord { return os.ErrPermission } if err := g.invalidateCompleteness(); err != nil { return err } return g.Store.Write(generationKey(r.Binding), raw) } // Reserve reuses an exact pending request after a lost response. Root allocates // the ID. A changed request or an already observed launch cannot reserve again. func (g *RootGenerations) Reserve(b GenerationBinding) (GenerationReservation, error) { r, err := g.Read(b) if err == nil { if r.Closed || r.Process != (GenerationProcess{}) { return GenerationReservation{}, os.ErrPermission } return r, nil } if !errors.Is(err, os.ErrNotExist) || g == nil || g.Store == nil || !b.valid() { return GenerationReservation{}, err } var id [32]byte if _, err := rand.Read(id[:]); err != nil { return GenerationReservation{}, err } r = GenerationReservation{Schema: 1, Revision: 1, ID: hex.EncodeToString(id[:]), Binding: b} if err := g.write(r); err != nil { return GenerationReservation{}, err } return r, nil } func (g *RootGenerations) Check(r GenerationReservation) error { got, err := g.Read(r.Binding) if err != nil { return err } if got != r || r.Closed || r.Process != (GenerationProcess{}) { return os.ErrPermission } return nil } func (g *RootGenerations) Started(r GenerationReservation, p GenerationProcess) error { if !p.valid() { return os.ErrPermission } got, err := g.Read(r.Binding) if err != nil { return err } if got.ID != r.ID || got.Closed { return os.ErrPermission } if got.Process == p { return nil } if got != r || got.Process != (GenerationProcess{}) { return os.ErrPermission } got.Process, got.Revision = p, 2 return g.write(got) } // Retire runs under release exclusion. close verifies that the exact launch // authority has ended; verify independently checks controller join and kernel // references after the durable close. Interrupted retirement retains its pin. func (g *RootGenerations) Retire(ctx context.Context, expected GenerationReservation, close func(context.Context, GenerationReservation) error, verify func(context.Context, GenerationReservation) (string, error)) error { if close == nil || verify == nil { return os.ErrPermission } r, err := g.Read(expected.Binding) if err != nil { return err } if r.ID != expected.ID || r.Process != expected.Process { return os.ErrPermission } if r.Retirement != "" { return nil } if err := ctx.Err(); err != nil { return err } if !r.Closed { if err := close(ctx, r); err != nil { return err } r.Closed = true r.Revision += 2 if err := g.write(r); err != nil { return err } } evidence, err := verify(ctx, r) if err != nil { return err } if evidence == "" { return os.ErrPermission } if err := ctx.Err(); err != nil { return err } r.Retirement = evidence r.Revision += 2 return g.write(r) }