//go:build linux package host import ( "context" "errors" "os" "os/exec" "path/filepath" "strconv" "strings" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/instancectl" "golang.org/x/sys/unix" ) // linuxClaimLaunchKernel is root's production view of a claim launch. Owner // is the uid the release tree belongs to: root in production. type linuxClaimLaunchKernel struct { OptRoot, CgroupRoot, ProcRoot string Owner uint32 // BoundApp is the build the unit binds, relative to the unit's root; // empty is opt/carlos/app. BoundApp string } // rootOnly is a directory or file only Owner can change. func (k linuxClaimLaunchKernel) rootOnly(st unix.Stat_t, kind uint32) bool { return st.Mode&unix.S_IFMT == kind && st.Uid == k.Owner && st.Mode&0o022 == 0 } // Verify accepts a build only inside the release tree, outside its // retargetable live links and the platform binary, reached with no symlink, // in directories and a file only Owner can change, with the pinned identity. func (k linuxClaimLaunchKernel) Verify(path string, want hostbox.LaunchFile) error { if !claimLaunchAbs(k.OptRoot) || !claimLaunchAbs(path) || !strings.HasPrefix(path, k.OptRoot+"/") || strings.HasPrefix(path, filepath.Join(k.OptRoot, "live")+"/") || path == filepath.Join(k.OptRoot, "carlos") || want.Device == 0 || want.Inode == 0 { return hostbox.ErrRoleLaunchShape } rel, err := filepath.Rel(k.OptRoot, filepath.Dir(path)) if err != nil || !filepath.IsLocal(rel) { return hostbox.ErrRoleLaunchShape } dir := k.OptRoot parts := []string{} if rel != "." { parts = strings.Split(rel, "/") } for i := -1; i < len(parts); i++ { if i >= 0 { dir = filepath.Join(dir, parts[i]) } var st unix.Stat_t if err := unix.Lstat(dir, &st); err != nil { return err } if !k.rootOnly(st, unix.S_IFDIR) { return hostbox.ErrChanged } } fd, err := unix.Openat2(unix.AT_FDCWD, path, &unix.OpenHow{Flags: unix.O_RDONLY | unix.O_CLOEXEC | unix.O_NOFOLLOW, Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS}) if err != nil { return err } defer unix.Close(fd) var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil { return err } if !k.rootOnly(st, unix.S_IFREG) || st.Mode&0o111 == 0 || uint64(st.Dev) != want.Device || st.Ino != want.Inode { return hostbox.ErrChanged } return nil } func claimLaunchProcessIdentity(procRoot string, pid int) (string, int, error) { raw, err := os.ReadFile(filepath.Join(procRoot, strconv.Itoa(pid), "stat")) if err != nil { return "", 0, err } i := strings.LastIndexByte(string(raw), ')') if i < 0 { return "", 0, hostbox.ErrChanged } fields := strings.Fields(string(raw)[i+1:]) if len(fields) < 20 || fields[0] == "Z" || fields[0] == "X" { return "", 0, hostbox.ErrChanged } parent, err := strconv.Atoi(fields[1]) if err != nil { return "", 0, hostbox.ErrChanged } return fields[19], parent, nil } func claimLaunchProcessStart(procRoot string, pid int) (string, error) { start, _, err := claimLaunchProcessIdentity(procRoot, pid) return start, err } func (k linuxClaimLaunchKernel) member(pid int, group string) error { raw, err := os.ReadFile(filepath.Join(k.ProcRoot, strconv.Itoa(pid), "cgroup")) if err != nil { return err } for _, line := range strings.Split(string(raw), "\n") { if line == "0::/"+group { return nil } } return hostbox.ErrChanged } func (k linuxClaimLaunchKernel) tenant(main int) (int, error) { tasks := filepath.Join(k.ProcRoot, strconv.Itoa(main), "task") entries, err := os.ReadDir(tasks) if err != nil { return 0, err } children := map[int]bool{} for _, entry := range entries { raw, err := os.ReadFile(filepath.Join(tasks, entry.Name(), "children")) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { return 0, err } for _, word := range strings.Fields(string(raw)) { pid, err := strconv.Atoi(word) if err != nil || pid <= 0 || pid == main { return 0, hostbox.ErrChanged } children[pid] = true } } if len(children) == 0 { return 0, errClaimLaunchPending } if len(children) != 1 { return 0, hostbox.ErrChanged } for pid := range children { return pid, nil } return 0, hostbox.ErrChanged } // Observe keeps the main process as the unit identity and verifies its one // direct tenant child. runInit forks on a locked thread, so every task's // children file participates; the leader's children file alone is incomplete. func (k linuxClaimLaunchKernel) Observe(s instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) { if s.MainPID <= 0 || !filepath.IsLocal(s.ControlGroup) || !claimLaunchAbs(k.CgroupRoot) || !claimLaunchAbs(k.ProcRoot) { return ClaimLaunchObserved{}, hostbox.ErrChanged } start, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID) if err != nil { return ClaimLaunchObserved{}, err } var group unix.Stat_t if err = unix.Lstat(filepath.Join(k.CgroupRoot, s.ControlGroup), &group); err != nil { return ClaimLaunchObserved{}, err } if group.Mode&unix.S_IFMT != unix.S_IFDIR { return ClaimLaunchObserved{}, hostbox.ErrChanged } var launcher unix.Stat_t if err := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "exe"), &launcher); err != nil { return ClaimLaunchObserved{}, err } observed := ClaimLaunchObserved{Launcher: hostbox.LaunchFile{Device: uint64(launcher.Dev), Inode: launcher.Ino}, ProcessStart: start, Group: hostbox.LaunchFile{Device: uint64(group.Dev), Inode: group.Ino}} if err := k.member(s.MainPID, s.ControlGroup); err != nil { return observed, err } child, childErr := k.tenant(s.MainPID) if childErr != nil { again, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID) if err != nil || again != start { return observed, hostbox.ErrChanged } return observed, childErr } childStart, parent, err := claimLaunchProcessIdentity(k.ProcRoot, child) if err != nil || parent != s.MainPID { return observed, hostbox.ErrChanged } observed.TenantPID, observed.TenantStart = child, childStart if err := k.member(child, s.ControlGroup); err != nil { return observed, err } bound := k.BoundApp if bound == "" { bound = "opt/carlos/app" } var app, mainApp, running unix.Stat_t boundErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(child), "root", bound), &app) mainBoundErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "root", bound), &mainApp) execErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(child), "exe"), &running) againChild, parent, err := claimLaunchProcessIdentity(k.ProcRoot, child) if err != nil || againChild != childStart || parent != s.MainPID { return observed, hostbox.ErrChanged } again, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID) if err != nil || again != start { return observed, hostbox.ErrChanged } if err := k.member(s.MainPID, s.ControlGroup); err != nil { return observed, err } if err := k.member(child, s.ControlGroup); err != nil { return observed, err } againPID, err := k.tenant(s.MainPID) if err != nil || againPID != child { return observed, hostbox.ErrChanged } for _, err := range []error{boundErr, mainBoundErr} { if errors.Is(err, os.ErrNotExist) { return observed, errClaimLaunchPending } if err != nil { return observed, err } } if app.Dev != mainApp.Dev || app.Ino != mainApp.Ino { return observed, hostbox.ErrChanged } if execErr != nil { return observed, execErr } observed.Executable = hostbox.LaunchFile{Device: uint64(app.Dev), Inode: app.Ino} observed.Running = hostbox.LaunchFile{Device: uint64(running.Dev), Inode: running.Ino} return observed, nil } func (k linuxClaimLaunchKernel) VerifyDigest(ctx context.Context, path string, want hostbox.LaunchFile, digest string) error { if err := k.Verify(path, want); err != nil { return err } f, err := verifiedClaimExecutable(ctx, exec.Command(path), path, digest) if err != nil { return err } defer f.Close() var st unix.Stat_t if err := unix.Fstat(int(f.Fd()), &st); err != nil { return err } if uint64(st.Dev) != want.Device || st.Ino != want.Inode { return hostbox.ErrChanged } return nil }