carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "crypto/rand"
8 "encoding/hex"
9 "errors"
10 "io"
11 "os"
12 "reflect"
13
14 "github.com/carlosframework/platform/internal/hostbox"
15 "golang.org/x/sys/unix"
16)
17
18// Retain one obstruction and, if creation loses its identity write, one
19// unrecorded replacement. Further replacements require investigation.
20type servingParkQuarantine struct {
21 BootID string `json:",omitempty"`
22 Version int
23 ID string
24 Parent, Obstruction servingArtifactDirectory
25 Previous *servingParkNamespace `json:",omitempty"`
26 Moved bool
27 Unrecorded servingArtifactDirectory `json:",omitempty"`
28 UnrecordedMoved bool `json:",omitempty"`
29 Fresh servingArtifactDirectory
30 Complete bool
31}
32
33func (q servingParkQuarantine) name() string {
34 return ".serving-parked-quarantine-" + q.ID
35}
36
37func (q servingParkQuarantine) unrecordedName() string {
38 return ".serving-parked-unrecorded-" + q.ID
39}
40
41func (q servingParkQuarantine) validate() error {
42 id, err := hex.DecodeString(q.ID)
43 if err != nil || len(id) != 16 || hex.EncodeToString(id) != q.ID || q.Version != 1 || q.Parent.Inode == 0 || q.Parent.Device == 0 || q.Parent.Mount == 0 || q.Obstruction.Inode == 0 || q.Obstruction.Device != q.Parent.Device || q.Obstruction.Mount != q.Parent.Mount {
44 return hostbox.ErrChanged
45 }
46 if q.Previous != nil && (q.Previous.Version != 1 || q.Previous.BootID != q.BootID || q.Previous.Parent != q.Parent || q.Previous.Root.Inode == 0 || q.Previous.Root.Device != q.Parent.Device || q.Previous.Root.Mount != q.Parent.Mount || q.Previous.Root == q.Obstruction) {
47 return hostbox.ErrChanged
48 }
49 if q.Fresh != (servingArtifactDirectory{}) && (!q.Moved || q.Fresh.Inode == 0 || q.Fresh.Device != q.Parent.Device || q.Fresh.Mount != q.Parent.Mount || q.Fresh == q.Obstruction || q.Previous != nil && q.Fresh == q.Previous.Root) {
50 return hostbox.ErrChanged
51 }
52 if q.Unrecorded != (servingArtifactDirectory{}) && (!q.Moved || q.Unrecorded.Inode == 0 || q.Unrecorded.Device != q.Parent.Device || q.Unrecorded.Mount != q.Parent.Mount || q.Unrecorded == q.Obstruction || q.Unrecorded == q.Fresh || q.Previous != nil && q.Unrecorded == q.Previous.Root || q.Fresh.Inode != 0 && !q.UnrecordedMoved) {
53 return hostbox.ErrChanged
54 }
55 if q.UnrecordedMoved && q.Unrecorded.Inode == 0 {
56 return hostbox.ErrChanged
57 }
58 if q.Complete && (!q.Moved || q.Fresh.Inode == 0) {
59 return hostbox.ErrChanged
60 }
61 return nil
62}
63
64// Publishing namespace.json can succeed before quarantine completion. Keep
65// ordinary parking out until that final record is durable, including after a
66// lost reply, so recovery can still require an empty replacement root.
67func (f *servingParkFS) checkNamespaceQuarantine(n servingParkNamespace) error {
68 body, err := f.state.Read("namespace-quarantine.json")
69 if errors.Is(err, os.ErrNotExist) {
70 return nil
71 }
72 if err != nil {
73 return err
74 }
75 var q servingParkQuarantine
76 if servingParkDecode(body, &q) != nil || q.validate() != nil || !q.Complete || n.Parent != q.Parent || n.Root != q.Fresh || n.BootID != q.BootID || f.checkBoot(q.BootID) != nil {
77 return hostbox.ErrChanged
78 }
79 return nil
80}
81
82// This is an internal mechanism, not a request handler. The caller supplies a
83// parent identity from independent root installation evidence and holds the
84// namespace lock. No caller path, child listing or quarantine deletion exists.
85func (f *servingParkFS) recoverNamespace(ctx context.Context, expected servingArtifactDirectory) (servingParkQuarantine, error) {
86 var q servingParkQuarantine
87 if ctx == nil {
88 return q, hostbox.ErrUnsupported
89 }
90 if err := ctx.Err(); err != nil {
91 return q, err
92 }
93 boot, err := f.currentBoot()
94 if err != nil {
95 return q, err
96 }
97 parent, pid, err := f.parent()
98 if err != nil {
99 return q, err
100 }
101 defer parent.Close()
102 var st unix.Stat_t
103 if err = unix.Fstat(int(parent.Fd()), &st); err != nil {
104 return q, err
105 }
106 if pid != expected || st.Mode&0022 != 0 {
107 return q, hostbox.ErrChanged
108 }
109 ledger := servingParkLedger{state: f.state}
110 anchor, err := f.namespaceParent()
111 if err == nil && (anchor.Parent != expected || f.checkBoot(anchor.BootID) != nil) {
112 return q, hostbox.ErrChanged
113 }
114 if err != nil && !errors.Is(err, os.ErrNotExist) {
115 return q, err
116 }
117 save := func() error {
118 if err := ctx.Err(); err != nil {
119 return err
120 }
121 if err := q.validate(); err != nil {
122 return err
123 }
124 return ledger.write("namespace-quarantine.json", q)
125 }
126 body, err := f.state.Read("namespace-quarantine.json")
127 if err == nil {
128 if servingParkDecode(body, &q) != nil || q.validate() != nil || q.Parent != pid || f.checkBoot(q.BootID) != nil {
129 return q, hostbox.ErrChanged
130 }
131 } else if errors.Is(err, os.ErrNotExist) {
132 q.Version, q.Parent, q.BootID = 1, pid, boot
133 body, err = f.state.Read("namespace.json")
134 if err == nil {
135 var prior servingParkNamespace
136 if servingParkDecode(body, &prior) != nil || prior.Version != 1 || prior.Parent != pid || f.checkBoot(prior.BootID) != nil {
137 return q, hostbox.ErrChanged
138 }
139 q.Previous = &prior
140 } else if !errors.Is(err, os.ErrNotExist) {
141 return q, err
142 }
143 obstruction, err := servingParkOpenDir(parent, ".serving-parked")
144 if err != nil {
145 return q, err
146 }
147 q.Obstruction, err = servingParkDirID(obstruction)
148 obstruction.Close()
149 if err != nil {
150 return q, err
151 }
152 var nonce [16]byte
153 if _, err = rand.Read(nonce[:]); err != nil {
154 return q, err
155 }
156 q.ID = hex.EncodeToString(nonce[:])
157 if err = save(); err != nil {
158 return q, err
159 }
160 } else {
161 return q, err
162 }
163 // A private record is necessary but not sufficient: it must still describe
164 // the installed namespace, or the exact replacement recorded by this run.
165 body, err = f.state.Read("namespace.json")
166 if err == nil {
167 var current servingParkNamespace
168 if servingParkDecode(body, &current) != nil {
169 return q, hostbox.ErrChanged
170 }
171 old := q.Previous != nil && reflect.DeepEqual(current, *q.Previous)
172 fresh := q.Fresh.Inode != 0 && current.Version == 1 && current.Parent == q.Parent && current.Root == q.Fresh && current.BootID == q.BootID
173 if !old && !fresh || q.Complete && !fresh {
174 return q, hostbox.ErrChanged
175 }
176 } else if !errors.Is(err, os.ErrNotExist) || q.Previous != nil || q.Complete {
177 return q, err
178 }
179 if !q.Moved {
180 if err = f.moveQuarantine(ctx, parent, q); err != nil {
181 return q, err
182 }
183 q.Moved = true
184 if err = save(); err != nil {
185 return q, err
186 }
187 }
188 if err = f.checkQuarantine(parent, q); err != nil {
189 return q, err
190 }
191 if q.Fresh.Inode == 0 {
192 if err = f.createQuarantineReplacement(ctx, parent, &q, save); err != nil {
193 return q, err
194 }
195 }
196 if q.UnrecordedMoved {
197 if err = f.checkRetainedDirectory(parent, q.unrecordedName(), q.Unrecorded); err != nil {
198 return q, err
199 }
200 }
201 fresh, id, err := f.privateDir(parent, ".serving-parked")
202 if err != nil {
203 return q, err
204 }
205 defer fresh.Close()
206 if id != q.Fresh {
207 return q, hostbox.ErrChanged
208 }
209 // Recheck the effective mount/path before publishing the replacement.
210 check, current, err := f.parent()
211 if err != nil {
212 return q, err
213 }
214 check.Close()
215 if current != q.Parent {
216 return q, hostbox.ErrChanged
217 }
218 if q.Complete {
219 return q, ctx.Err()
220 }
221 // No retirement can use this root before publication. Even a directory
222 // with the right owner and mode is refused if it already contains data.
223 names, err := fresh.Readdirnames(1)
224 if len(names) != 0 || err != nil && !errors.Is(err, io.EOF) {
225 return q, hostbox.ErrChanged
226 }
227 if err = ctx.Err(); err != nil {
228 return q, err
229 }
230 if err = f.sync(int(parent.Fd())); err != nil {
231 return q, err
232 }
233 n := servingParkNamespace{Version: 1, Parent: q.Parent, Root: q.Fresh, BootID: q.BootID}
234 n.Topology, err = f.captureTopology(parent, fresh, n)
235 if err != nil {
236 return q, err
237 }
238 if err = ledger.write("namespace.json", n); err != nil {
239 return q, err
240 }
241 q.Complete = true
242 return q, save()
243}
244
245func (f *servingParkFS) checkQuarantine(parent *os.File, q servingParkQuarantine) error {
246 return f.checkRetainedDirectory(parent, q.name(), q.Obstruction)
247}
248
249func (f *servingParkFS) checkRetainedDirectory(parent *os.File, name string, expected servingArtifactDirectory) error {
250 d, err := servingParkOpenDir(parent, name)
251 if err != nil {
252 return err
253 }
254 defer d.Close()
255 id, err := servingParkDirID(d)
256 if err != nil || id != expected {
257 return hostbox.ErrChanged
258 }
259 return nil
260}
261
262func (f *servingParkFS) moveQuarantine(ctx context.Context, parent *os.File, q servingParkQuarantine) error {
263 return f.moveRetainedDirectory(ctx, parent, q.name(), q.Obstruction)
264}
265
266func (f *servingParkFS) moveRetainedDirectory(ctx context.Context, parent *os.File, name string, expected servingArtifactDirectory) error {
267 if err := ctx.Err(); err != nil {
268 return err
269 }
270 source, err := servingParkOpenDir(parent, ".serving-parked")
271 if errors.Is(err, os.ErrNotExist) {
272 // The rename may have succeeded before its reply or fsync was lost.
273 if err = f.checkRetainedDirectory(parent, name, expected); err != nil {
274 return err
275 }
276 return f.sync(int(parent.Fd()))
277 }
278 if err != nil {
279 return err
280 }
281 defer source.Close()
282 id, err := servingParkDirID(source)
283 if err != nil || id != expected {
284 return hostbox.ErrChanged
285 }
286 if err = f.rename(int(parent.Fd()), ".serving-parked", int(parent.Fd()), name, unix.RENAME_NOREPLACE); err != nil {
287 return err
288 }
289 if err = f.checkRetainedDirectory(parent, name, expected); err != nil {
290 return err
291 }
292 return f.sync(int(parent.Fd()))
293}
294
295// EEXIST grants retention only. Persist the unexpected identity before moving
296// it, and never inspect its contents. One spare name bounds crash recovery even
297// if the service owner keeps replacing the root or private writes keep failing.
298func (f *servingParkFS) createQuarantineReplacement(ctx context.Context, parent *os.File, q *servingParkQuarantine, save func() error) error {
299 for {
300 if err := ctx.Err(); err != nil {
301 return err
302 }
303 if q.Unrecorded.Inode != 0 {
304 if !q.UnrecordedMoved {
305 if err := f.moveRetainedDirectory(ctx, parent, q.unrecordedName(), q.Unrecorded); err != nil {
306 return err
307 }
308 q.UnrecordedMoved = true
309 if err := save(); err != nil {
310 return err
311 }
312 }
313 if err := f.checkRetainedDirectory(parent, q.unrecordedName(), q.Unrecorded); err != nil {
314 return err
315 }
316 }
317 err := unix.Mkdirat(int(parent.Fd()), ".serving-parked", 0700)
318 if errors.Is(err, unix.EEXIST) && q.Unrecorded.Inode == 0 {
319 d, err := servingParkOpenDir(parent, ".serving-parked")
320 if err != nil {
321 return err
322 }
323 id, err := servingParkDirID(d)
324 d.Close()
325 if err != nil {
326 return err
327 }
328 q.Unrecorded = id
329 if err := save(); err != nil {
330 return err
331 }
332 continue
333 }
334 if err != nil {
335 return err
336 }
337 fresh, id, err := f.privateDir(parent, ".serving-parked")
338 if err != nil {
339 return err
340 }
341 fresh.Close()
342 q.Fresh = id
343 // Directory durability precedes the private identity record. Otherwise
344 // a reboot could retain the record while losing its directory entry.
345 if err := f.sync(int(parent.Fd())); err != nil {
346 return err
347 }
348 return save()
349 }
350}
351