carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
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
17const RootGenerationsDir = "/var/lib/carlos-release/private/generations"
18
19type GenerationOwner struct {
20 Host, Incarnation, Box, Enrollment, BootID, ControllerID string
21 Stamp, Session uint64
22}
23
24type 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
37type 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
45type 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.
58type RootGenerations struct{ Store rootstate.Store }
59
60func 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
66func (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
76func (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
81func 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
103func (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
122func (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.
141func (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
163func (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
174func (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.
198func (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