| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/rand" |
| 6 | "crypto/sha256" |
| 7 | "encoding/hex" |
| 8 | "encoding/json" |
| 9 | "errors" |
| 10 | "os" |
| 11 | "path/filepath" |
| 12 | "strings" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/rootstate" |
| 15 | ) |
| 16 | |
| 17 | const RootGenerationsDir = "/var/lib/carlos-release/private/generations" |
| 18 | |
| 19 | type GenerationOwner struct { |
| 20 | Host, Incarnation, Box, Enrollment, BootID, ControllerID string |
| 21 | Stamp, Session uint64 |
| 22 | } |
| 23 | |
| 24 | type GenerationBinding struct { |
| 25 | Unit string `json:",omitempty"` |
| 26 | Binding HistoryBinding |
| 27 | Target GenerationOwner |
| 28 | Installation, Attempt, Inputs, Invocation string |
| 29 | Record, RecordETag string |
| 30 | ControllerPID int |
| 31 | ControllerStart string |
| 32 | Version, Digest, Path, Tree string |
| 33 | Executable, TreeIdentity rootstate.FileIdentity |
| 34 | ExecutableProof, TreeProof rootstate.FileIdentity `json:",omitzero"` |
| 35 | } |
| 36 | |
| 37 | type GenerationProcess struct { |
| 38 | PID, TenantPID int |
| 39 | Start, TenantStart, Group, Invocation string |
| 40 | GroupIdentity rootstate.FileIdentity |
| 41 | LauncherIdentity rootstate.FileIdentity `json:",omitzero"` |
| 42 | Containment uint8 `json:",omitempty"` |
| 43 | } |
| 44 | |
| 45 | type GenerationReservation struct { |
| 46 | Schema int |
| 47 | Revision uint64 |
| 48 | ID string |
| 49 | Binding GenerationBinding |
| 50 | Process GenerationProcess |
| 51 | Closed bool `json:",omitempty"` |
| 52 | Retirement string `json:",omitempty"` |
| 53 | } |
| 54 | |
| 55 | // RootGenerations records additional pins under the caller's release exclusion. |
| 56 | // A reservation is never retired by unit settlement or an agent publication. |
| 57 | // These records alone cannot establish a complete generation census. |
| 58 | type RootGenerations struct{ Store rootstate.Store } |
| 59 | |
| 60 | func generationKey(b GenerationBinding) string { |
| 61 | raw, _ := json.Marshal([]any{b.Target, b.Attempt}) |
| 62 | sum := sha256.Sum256(raw) |
| 63 | return hex.EncodeToString(sum[:]) + ".json" |
| 64 | } |
| 65 | |
| 66 | func (b GenerationBinding) valid() bool { |
| 67 | t := b.Target |
| 68 | digest, err := hex.DecodeString(b.Digest) |
| 69 | 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 && |
| 70 | b.Record != "" && b.RecordETag != "" && b.Installation != "" && b.Attempt != "" && b.Inputs != "" && b.Invocation != "" && b.ControllerPID > 0 && b.ControllerStart != "" && |
| 71 | ValidVersion(b.Version) && err == nil && len(digest) == sha256.Size && hex.EncodeToString(digest) == b.Digest && |
| 72 | filepath.IsAbs(b.Path) && filepath.Clean(b.Path) == b.Path && b.Tree == filepath.Dir(b.Path) && |
| 73 | b.Executable.Device > 0 && b.Executable.Inode > 0 && b.TreeIdentity.Device > 0 && b.TreeIdentity.Inode > 0 |
| 74 | } |
| 75 | |
| 76 | func (p GenerationProcess) valid() bool { |
| 77 | 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 != "" && |
| 78 | filepath.IsLocal(p.Group) && p.Group != "." && p.Invocation != "" && p.GroupIdentity.Device > 0 && p.GroupIdentity.Inode > 0 |
| 79 | } |
| 80 | |
| 81 | func validGeneration(r GenerationReservation) bool { |
| 82 | id, err := hex.DecodeString(r.ID) |
| 83 | if r.Retirement != "" { |
| 84 | evidence, err := hex.DecodeString(r.Retirement) |
| 85 | if err != nil || len(evidence) != sha256.Size || hex.EncodeToString(evidence) != r.Retirement || !r.Closed { |
| 86 | return false |
| 87 | } |
| 88 | } |
| 89 | base := uint64(1) |
| 90 | if r.Process != (GenerationProcess{}) { |
| 91 | base = 2 |
| 92 | } |
| 93 | if r.Closed { |
| 94 | base += 2 |
| 95 | } |
| 96 | if r.Retirement != "" { |
| 97 | base += 2 |
| 98 | } |
| 99 | return r.Schema == 1 && err == nil && len(id) == 32 && hex.EncodeToString(id) == r.ID && r.Binding.valid() && |
| 100 | r.Revision == base && (r.Process == (GenerationProcess{}) || r.Process.valid()) |
| 101 | } |
| 102 | |
| 103 | func (g *RootGenerations) Read(b GenerationBinding) (GenerationReservation, error) { |
| 104 | var r GenerationReservation |
| 105 | if g == nil || g.Store == nil || !b.valid() { |
| 106 | return r, os.ErrPermission |
| 107 | } |
| 108 | raw, err := g.Store.Read(generationKey(b)) |
| 109 | if err != nil { |
| 110 | return r, err |
| 111 | } |
| 112 | if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validGeneration(r) || r.Binding != b { |
| 113 | return GenerationReservation{}, os.ErrPermission |
| 114 | } |
| 115 | // Recommit before relying on a rename a prior process may not have synced. |
| 116 | if err := g.Store.Write(generationKey(b), raw); err != nil { |
| 117 | return GenerationReservation{}, err |
| 118 | } |
| 119 | return r, nil |
| 120 | } |
| 121 | |
| 122 | func (g *RootGenerations) write(r GenerationReservation) error { |
| 123 | if g == nil || g.Store == nil || !validGeneration(r) { |
| 124 | return os.ErrPermission |
| 125 | } |
| 126 | raw, err := json.Marshal(r) |
| 127 | if err != nil { |
| 128 | return err |
| 129 | } |
| 130 | if len(raw) > rootstate.MaxRecord { |
| 131 | return os.ErrPermission |
| 132 | } |
| 133 | if err := g.invalidateCompleteness(); err != nil { |
| 134 | return err |
| 135 | } |
| 136 | return g.Store.Write(generationKey(r.Binding), raw) |
| 137 | } |
| 138 | |
| 139 | // Reserve reuses an exact pending request after a lost response. Root allocates |
| 140 | // the ID. A changed request or an already observed launch cannot reserve again. |
| 141 | func (g *RootGenerations) Reserve(b GenerationBinding) (GenerationReservation, error) { |
| 142 | r, err := g.Read(b) |
| 143 | if err == nil { |
| 144 | if r.Closed || r.Process != (GenerationProcess{}) { |
| 145 | return GenerationReservation{}, os.ErrPermission |
| 146 | } |
| 147 | return r, nil |
| 148 | } |
| 149 | if !errors.Is(err, os.ErrNotExist) || g == nil || g.Store == nil || !b.valid() { |
| 150 | return GenerationReservation{}, err |
| 151 | } |
| 152 | var id [32]byte |
| 153 | if _, err := rand.Read(id[:]); err != nil { |
| 154 | return GenerationReservation{}, err |
| 155 | } |
| 156 | r = GenerationReservation{Schema: 1, Revision: 1, ID: hex.EncodeToString(id[:]), Binding: b} |
| 157 | if err := g.write(r); err != nil { |
| 158 | return GenerationReservation{}, err |
| 159 | } |
| 160 | return r, nil |
| 161 | } |
| 162 | |
| 163 | func (g *RootGenerations) Check(r GenerationReservation) error { |
| 164 | got, err := g.Read(r.Binding) |
| 165 | if err != nil { |
| 166 | return err |
| 167 | } |
| 168 | if got != r || r.Closed || r.Process != (GenerationProcess{}) { |
| 169 | return os.ErrPermission |
| 170 | } |
| 171 | return nil |
| 172 | } |
| 173 | |
| 174 | func (g *RootGenerations) Started(r GenerationReservation, p GenerationProcess) error { |
| 175 | if !p.valid() { |
| 176 | return os.ErrPermission |
| 177 | } |
| 178 | got, err := g.Read(r.Binding) |
| 179 | if err != nil { |
| 180 | return err |
| 181 | } |
| 182 | if got.ID != r.ID || got.Closed { |
| 183 | return os.ErrPermission |
| 184 | } |
| 185 | if got.Process == p { |
| 186 | return nil |
| 187 | } |
| 188 | if got != r || got.Process != (GenerationProcess{}) { |
| 189 | return os.ErrPermission |
| 190 | } |
| 191 | got.Process, got.Revision = p, 2 |
| 192 | return g.write(got) |
| 193 | } |
| 194 | |
| 195 | // Retire runs under release exclusion. close verifies that the exact launch |
| 196 | // authority has ended; verify independently checks controller join and kernel |
| 197 | // references after the durable close. Interrupted retirement retains its pin. |
| 198 | func (g *RootGenerations) Retire(ctx context.Context, expected GenerationReservation, close func(context.Context, GenerationReservation) error, verify func(context.Context, GenerationReservation) (string, error)) error { |
| 199 | if close == nil || verify == nil { |
| 200 | return os.ErrPermission |
| 201 | } |
| 202 | r, err := g.Read(expected.Binding) |
| 203 | if err != nil { |
| 204 | return err |
| 205 | } |
| 206 | if r.ID != expected.ID || r.Process != expected.Process { |
| 207 | return os.ErrPermission |
| 208 | } |
| 209 | if r.Retirement != "" { |
| 210 | return nil |
| 211 | } |
| 212 | if err := ctx.Err(); err != nil { |
| 213 | return err |
| 214 | } |
| 215 | if !r.Closed { |
| 216 | if err := close(ctx, r); err != nil { |
| 217 | return err |
| 218 | } |
| 219 | r.Closed = true |
| 220 | r.Revision += 2 |
| 221 | if err := g.write(r); err != nil { |
| 222 | return err |
| 223 | } |
| 224 | } |
| 225 | evidence, err := verify(ctx, r) |
| 226 | if err != nil { |
| 227 | return err |
| 228 | } |
| 229 | if evidence == "" { |
| 230 | return os.ErrPermission |
| 231 | } |
| 232 | if err := ctx.Err(); err != nil { |
| 233 | return err |
| 234 | } |
| 235 | r.Retirement = evidence |
| 236 | r.Revision += 2 |
| 237 | return g.write(r) |
| 238 | } |
| 239 | |