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/sha256"
8 "encoding/hex"
9 "errors"
10 "fmt"
11 "io"
12 "net"
13 "os"
14 "os/exec"
15 "path/filepath"
16 "reflect"
17 "strings"
18 "sync"
19
20 "github.com/carlosframework/platform/internal/activator"
21 "github.com/carlosframework/platform/internal/cgroup"
22 "github.com/carlosframework/platform/internal/hostbox"
23 "github.com/carlosframework/platform/internal/hostclaim"
24 "github.com/carlosframework/platform/internal/hostwrite"
25 "github.com/carlosframework/platform/internal/safefile"
26 "golang.org/x/sys/unix"
27)
28
29// Prelaunch prepares the session's empty parent group and journals it with
30// this controller's PID and process start before restore or materialization.
31// A failure or crash after it returns leaves the exact record root needs.
32func (a *ClaimRoleAssembly) Prelaunch(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs) (*activator.RoleLaunch, error) {
33 if a == nil || a.Manager == nil || a.Journal == nil || a.Inventory == nil || work == nil || o.Claim == nil || o.Claim.Holder == nil {
34 return nil, hostwrite.ErrUnavailable
35 }
36 target, err := a.launchTarget(ctx, o)
37 if err != nil {
38 return nil, err
39 }
40 // The installation inventory is what root verifies a stop against. Without
41 // it the record could never be settled, so refuse before any effect.
42 inventory, err := a.Inventory.ReadExactStopInventory(ctx, target)
43 if err != nil {
44 return nil, err
45 }
46 if !inventory.Complete || inventory.Host != target.Host || inventory.Incarnation != target.Incarnation || inventory.InstallationStamp != target.Stamp || inventory.Box != target.Box || inventory.Enrollment != target.Enrollment || inventory.DirectoryRevision != target.DirectoryRevision {
47 return nil, hostbox.ErrTypedInventory
48 }
49 start, err := claimConstructionProcessStart()
50 if err != nil {
51 return nil, err
52 }
53 parentName := claimContainmentName(target)
54 var parent *cgroup.Prepared
55 var group hostbox.LaunchGroup
56 err = work.With(ctx, in.Identity, func() error {
57 var e error
58 prepare := a.prepareGroup
59 if prepare == nil {
60 prepare = a.preparePrelaunchGroup
61 }
62 parent, group, e = prepare(parentName, in.Limit)
63 if e != nil {
64 return e
65 }
66 if a.Journal.Remote() {
67 return nil
68 }
69 return a.Journal.Prelaunch(ctx, hostbox.LaunchInventory{ControllerPID: os.Getpid(), ProcessStart: start, Target: target, Groups: []hostbox.LaunchGroup{group}, NoState: true})
70 })
71 if err == nil && a.Journal.Remote() {
72 err = a.Journal.Prelaunch(ctx, hostbox.LaunchInventory{ControllerPID: os.Getpid(), ProcessStart: start, Target: target, Groups: []hostbox.LaunchGroup{group}, NoState: true})
73 if err == nil {
74 err = work.With(ctx, in.Identity, func() error {
75 if parent == nil {
76 return hostbox.ErrChanged
77 }
78 id, e := parent.Identity()
79 if e != nil {
80 return e
81 }
82 if id.Device != group.Device || id.Inode != group.Inode {
83 return hostbox.ErrChanged
84 }
85 return nil
86 })
87 }
88 }
89
90 if err != nil {
91 if parent != nil {
92 err = errors.Join(err, parent.Close())
93 }
94 return nil, err
95 }
96 return &activator.RoleLaunch{Parent: parent, ParentPath: group.RelativePath, CgroupRoot: "/sys/fs/cgroup", Target: target, Work: work, Identity: in.Identity, Journal: a.Journal}, nil
97}
98
99// Launch completes the pre-launch record after restore and materialization.
100// It reuses that exact parent group; it never creates a second record.
101func (a *ClaimRoleAssembly) Launch(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs, pre *activator.RoleLaunch) (*activator.RoleLaunch, error) {
102 if a == nil || a.Manager == nil || a.Journal == nil || a.Inventory == nil || work == nil || pre == nil || pre.Parent == nil || pre.Journal != a.Journal || o.Claim == nil || o.Claim.Holder == nil || !filepath.IsAbs(in.DBPath) {
103 return nil, hostwrite.ErrUnavailable
104 }
105 target, err := a.launchTarget(ctx, o)
106 if err != nil {
107 return nil, err
108 }
109 if !hostbox.SameLaunchOwner(pre.Target, target) {
110 return nil, hostbox.ErrChanged
111 }
112 parentID, err := pre.Parent.Identity()
113 if err != nil {
114 return nil, err
115 }
116 recorded, err := a.Journal.Read(ctx, target)
117 if err != nil {
118 return nil, err
119 }
120 if recorded.ControllerPID != os.Getpid() || len(recorded.Groups) == 0 || recorded.Groups[0] != (hostbox.LaunchGroup{RelativePath: pre.ParentPath, Device: parentID.Device, Inode: parentID.Inode}) {
121 return nil, hostbox.ErrChanged
122 }
123 inventory, err := a.Inventory.ReadExactStopInventory(ctx, target)
124 if err != nil {
125 return nil, err
126 }
127 if !inventory.Complete || inventory.Host != target.Host || inventory.Incarnation != target.Incarnation || inventory.InstallationStamp != target.Stamp || inventory.Box != target.Box || inventory.Enrollment != target.Enrollment || inventory.DirectoryRevision != target.DirectoryRevision {
128 return nil, hostbox.ErrTypedInventory
129 }
130 expectedOwner := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", target.Host, target.Incarnation, target.Stamp, target.Box)))
131 owner := hex.EncodeToString(expectedOwner[:])
132 stateDir := filepath.Clean(filepath.Dir(in.DBPath))
133 owned := false
134 for _, entry := range inventory.Entries {
135 root, ok := a.ManagedRoots[entry.ManagedRoot]
136 if !ok || !filepath.IsAbs(root) || entry.Kind != "path" || entry.Materialization != "materialized" || entry.Type != unix.S_IFDIR || entry.Ownership != owner {
137 continue
138 }
139 relative := filepath.Clean(entry.RelativePath)
140 if filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
141 return nil, hostbox.ErrChanged
142 }
143 if filepath.Join(root, relative) != stateDir {
144 continue
145 }
146 var stat unix.Stat_t
147 if err = unix.Lstat(stateDir, &stat); err != nil {
148 return nil, err
149 }
150 if stat.Mode&unix.S_IFMT != unix.S_IFDIR || hostbox.VerifyInventoryIdentity(root, entry) != nil {
151 return nil, hostbox.ErrChanged
152 }
153 owned = true
154 }
155 if !owned {
156 return nil, hostwrite.ErrUnavailable
157 }
158 launch := *pre
159 launch.Unit = in.Unit
160 launch.Spec = &hostbox.LaunchSpec{Identity: in.Identity, Executable: in.Executable, Digest: in.ExecutableSHA256, Args: append([]string(nil), in.Args...), Env: append([]string(nil), in.Env...)}
161 launch.Target, launch.Work, launch.Identity = target, work, in.Identity
162 var mu sync.Mutex
163 initializeFiles := func(ctx context.Context) ([]hostbox.LaunchFile, error) {
164 mu.Lock()
165 defer mu.Unlock()
166 files := []hostbox.LaunchFile{}
167 for _, suffix := range []string{"", "-wal", "-shm"} {
168 path := in.DBPath + suffix
169 f, e := safefile.OpenFile(path, os.O_RDONLY, 0)
170 if errors.Is(e, os.ErrNotExist) && suffix != "" {
171 continue
172 }
173 if errors.Is(e, os.ErrNotExist) && suffix == "" && in.Empty {
174 f, e = safefile.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
175 if e == nil {
176 e = f.Sync()
177 if e != nil {
178 f.Close()
179 }
180 }
181 }
182 if e != nil {
183 return nil, e
184 }
185 var stat unix.Stat_t
186 e = unix.Fstat(int(f.Fd()), &stat)
187 closeErr := f.Close()
188 if e != nil || closeErr != nil {
189 return nil, errors.Join(e, closeErr)
190 }
191 if stat.Mode&unix.S_IFMT != unix.S_IFREG || stat.Nlink != 1 {
192 return nil, hostbox.ErrChanged
193 }
194 files = append(files, hostbox.LaunchFile{Device: uint64(stat.Dev), Inode: stat.Ino})
195 }
196 if len(files) == 0 {
197 return nil, hostwrite.ErrUnavailable
198 }
199 return files, nil
200 }
201 recordFiles := func(ctx context.Context, files []hostbox.LaunchFile) error {
202 for _, f := range files {
203 if err := a.Journal.RecordFile(ctx, target, f); err != nil {
204 return err
205 }
206 }
207 return nil
208 }
209 if a.Journal.Remote() {
210 launch.InitializeUnlocked = func(ctx context.Context) error {
211 var files []hostbox.LaunchFile
212 if err := work.With(ctx, in.Identity, func() error { var e error; files, e = initializeFiles(ctx); return e }); err != nil {
213 return err
214 }
215 if err := recordFiles(ctx, files); err != nil {
216 return err
217 }
218 return work.With(ctx, in.Identity, func() error {
219 again, err := initializeFiles(ctx)
220 if err != nil {
221 return err
222 }
223 if !reflect.DeepEqual(files, again) {
224 return hostbox.ErrChanged
225 }
226 return nil
227 })
228 }
229 } else {
230 launch.Initialize = func(ctx context.Context) error {
231 files, err := initializeFiles(ctx)
232 if err != nil {
233 return err
234 }
235 return recordFiles(ctx, files)
236 }
237 }
238
239 launch.Executable = func(ctx context.Context, cmd *exec.Cmd) (*os.File, error) {
240 return verifiedClaimExecutable(ctx, cmd, in.Executable, in.ExecutableSHA256)
241 }
242 if a.Root != nil {
243 paths, err := claimIsolatedPaths(a.ManagedRoots, target.Host, in)
244 if err != nil {
245 return nil, err
246 }
247 // Root runs the unit on the build behind the host's live link. The
248 // agent pins that resolved file after verifying its digest; root
249 // verifies the same identity again and binds exactly that path.
250 resolved, err := filepath.EvalSymlinks(in.Executable)
251 if err != nil {
252 return nil, err
253 }
254 launch.Root, launch.LaunchDir, launch.RunDir, launch.LogSocket, launch.MemoryMax = a.Root, paths.LaunchDir, paths.RunDir, paths.LogSocket, paths.MemoryMax
255 launch.Executable = func(ctx context.Context, cmd *exec.Cmd) (*os.File, error) {
256 if cmd == nil || cmd.Path != in.Executable {
257 return nil, hostwrite.ErrUnavailable
258 }
259 return verifiedClaimExecutable(ctx, exec.Command(resolved), resolved, in.ExecutableSHA256)
260 }
261 }
262 return &launch, nil
263}
264
265func verifiedClaimExecutable(ctx context.Context, cmd *exec.Cmd, path, digest string) (*os.File, error) {
266 if cmd == nil || cmd.Path != path || !filepath.IsAbs(path) || len(digest) != 64 {
267 return nil, hostwrite.ErrUnavailable
268 }
269 expected, err := hex.DecodeString(digest)
270 if err != nil || len(expected) != sha256.Size {
271 return nil, hostwrite.ErrUnavailable
272 }
273 f, err := safefile.OpenFile(path, os.O_RDONLY, 0)
274 if err != nil {
275 return nil, err
276 }
277 fd := int(f.Fd())
278 refuse := func(e error) (*os.File, error) { f.Close(); return nil, e }
279 var before unix.Stat_t
280 if err = unix.Fstat(fd, &before); err != nil {
281 return refuse(err)
282 }
283 if before.Uid != 0 || before.Mode&unix.S_IFMT != unix.S_IFREG || before.Nlink != 1 || before.Mode&0111 == 0 || before.Mode&0022 != 0 {
284 return refuse(hostbox.ErrChanged)
285 }
286 hash := sha256.New()
287 buffer := make([]byte, 128<<10)
288 for {
289 if err = ctx.Err(); err != nil {
290 return refuse(err)
291 }
292 n, e := f.Read(buffer)
293 if n > 0 {
294 _, _ = hash.Write(buffer[:n])
295 }
296 if e == io.EOF {
297 break
298 }
299 if e != nil {
300 return refuse(e)
301 }
302 }
303 var after unix.Stat_t
304 if err = unix.Fstat(fd, &after); err != nil {
305 return refuse(err)
306 }
307 if before.Dev != after.Dev || before.Ino != after.Ino || before.Size != after.Size || before.Mtim != after.Mtim || before.Ctim != after.Ctim || hex.EncodeToString(hash.Sum(nil)) != strings.ToLower(digest) {
308 return refuse(hostbox.ErrChanged)
309 }
310 if _, err = f.Seek(0, 0); err != nil {
311 return refuse(err)
312 }
313 return claimExecutableName(f, path)
314}
315
316func claimExecutableName(f *os.File, path string) (*os.File, error) {
317 namedFD, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
318 if err != nil {
319 f.Close()
320 return nil, err
321 }
322 if err = f.Close(); err != nil {
323 unix.Close(namedFD)
324 return nil, err
325 }
326 return os.NewFile(uintptr(namedFD), path), nil
327}
328
329func claimReadyPeer(conn net.Conn, pid int) error {
330 unixConn, ok := conn.(*net.UnixConn)
331 if !ok || pid <= 0 {
332 return hostwrite.ErrUnavailable
333 }
334 raw, err := unixConn.SyscallConn()
335 if err != nil {
336 return err
337 }
338 var peer *unix.Ucred
339 var socketErr error
340 if err = raw.Control(func(fd uintptr) { peer, socketErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil {
341 return err
342 }
343 if socketErr != nil {
344 return socketErr
345 }
346 if peer == nil || peer.Pid <= 0 {
347 return hostwrite.ErrUnavailable
348 }
349 if int(peer.Pid) != pid {
350 return hostbox.ErrChanged
351 }
352 return nil
353}
354
355func (a *ClaimRoleAssembly) PrepareState(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs) error {
356 if a == nil || a.Inventory == nil || work == nil || o.Claim == nil || o.Claim.Holder == nil || !filepath.IsAbs(in.DBPath) {
357 return hostwrite.ErrUnavailable
358 }
359 actor, err := a.Identity.Verify(ctx)
360 if err != nil {
361 return err
362 }
363 if a.Inventory.Remote() {
364 return a.prepareStateRemote(ctx, o, work, in, actor)
365 }
366 return work.With(ctx, in.Identity, func() error {
367 h := o.Claim.Holder
368 inventory, err := a.Inventory.ReadInstallation(ctx, o.Host, h.Incarnation, h.Stamp)
369 if err != nil {
370 return err
371 }
372 if !inventory.Complete || inventory.Host != o.Host || inventory.Incarnation != h.Incarnation || inventory.InstallationStamp != h.Stamp || inventory.Box != actor.Box || inventory.Enrollment != actor.Enrollment || inventory.DirectoryRevision != actor.Binding {
373 return hostbox.ErrTypedInventory
374 }
375 root := a.ManagedRoots["data"]
376 relative, err := filepath.Rel(root, filepath.Dir(in.DBPath))
377 if err != nil || !filepath.IsLocal(relative) || relative == "." {
378 return hostbox.ErrChanged
379 }
380 ownerDigest := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", o.Host, h.Incarnation, h.Stamp, actor.Box)))
381 owner := hex.EncodeToString(ownerDigest[:])
382 index := -1
383 for i, e := range inventory.Entries {
384 if e.Kind == "path" && e.ManagedRoot == "data" && e.RelativePath == relative {
385 if index != -1 || e.Ownership != owner {
386 return hostbox.ErrTypedInventory
387 }
388 index = i
389 }
390 }
391 fd, err := unix.Open(root, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
392 if err != nil {
393 return err
394 }
395 defer func() { unix.Close(fd) }()
396 components := strings.Split(filepath.ToSlash(relative), "/")
397 for _, part := range components[:len(components)-1] {
398 next, e := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
399 if e != nil {
400 return e
401 }
402 unix.Close(fd)
403 fd = next
404 }
405 leaf := components[len(components)-1]
406 var stat unix.Stat_t
407 err = unix.Fstatat(fd, leaf, &stat, unix.AT_SYMLINK_NOFOLLOW)
408 if err == nil {
409 if index < 0 {
410 return hostbox.ErrTypedInventory
411 }
412 e := inventory.Entries[index]
413 if e.Materialization != "materialized" || stat.Mode&unix.S_IFMT != unix.S_IFDIR || hostbox.VerifyInventoryIdentity(root, e) != nil {
414 return hostbox.ErrTypedInventory
415 }
416 return nil
417 }
418 if !errors.Is(err, os.ErrNotExist) {
419 return err
420 }
421 if index >= 0 && inventory.Entries[index].Materialization == "materialized" {
422 return hostbox.ErrChanged
423 }
424 if index < 0 {
425 sum := sha256.Sum256([]byte(owner + "\x00state-dir\x00" + relative))
426 inventory.Entries = append(inventory.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: hex.EncodeToString(sum[:]), ManagedRoot: "data", RelativePath: relative, Ownership: owner, Type: unix.S_IFDIR, Materialization: "intended"})
427 index = len(inventory.Entries) - 1
428 if err = a.Inventory.WriteStopInventory(ctx, inventory); err != nil {
429 return err
430 }
431 }
432 if err = unix.Mkdirat(fd, leaf, 0700); err != nil {
433 return err
434 }
435 if err = unix.Fstatat(fd, leaf, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
436 return err
437 }
438 if err = unix.Fsync(fd); err != nil {
439 return err
440 }
441 entry := &inventory.Entries[index]
442 entry.Identity, err = hostbox.CaptureInventoryIdentity(root, relative)
443 if err != nil {
444 return err
445 }
446 entry.Device = entry.Identity.Device
447 entry.Inode = entry.Identity.Inode
448 entry.Type = unix.S_IFDIR
449 entry.Materialization = "materialized"
450 return a.Inventory.WriteStopInventory(ctx, inventory)
451 })
452}
453
454func (a *ClaimRoleAssembly) preparePrelaunchGroup(name string, limit cgroup.Limit) (*cgroup.Prepared, hostbox.LaunchGroup, error) {
455 path, err := filepath.Rel("/sys/fs/cgroup", filepath.Join(a.Manager.Root(), cgroup.ExecName(name)))
456 if err != nil || !filepath.IsLocal(path) {
457 return nil, hostbox.LaunchGroup{}, hostwrite.ErrUnavailable
458 }
459 parent, err := a.Manager.PrepareStrict(name, limit)
460 if err != nil {
461 return nil, hostbox.LaunchGroup{}, err
462 }
463 id, err := parent.Identity()
464 if err != nil {
465 parent.Close()
466 return nil, hostbox.LaunchGroup{}, err
467 }
468 return parent, hostbox.LaunchGroup{RelativePath: path, Device: id.Device, Inode: id.Inode}, nil
469}
470