//go:build linux package hostbox import ( "context" "errors" "io" "os" "path/filepath" "strings" "github.com/carlosframework/platform/internal/hostguard" "golang.org/x/sys/unix" ) // ResolveRoleIntentions is called by root only after a durable fence and drain, // or proof that the recorded controller is dead. It captures only an empty // direct child of the recorded parent. Unknown descendants are never adopted. func (j *LaunchJournal) ResolveRoleIntentions(ctx context.Context, target hostguard.FenceTarget, root string, reboot bool) error { in, err := j.Read(ctx, target) if err != nil { return err } for _, r := range in.Reservations { if r.State != "intended" { continue } group, err := resolveRoleIntention(root, r, reboot) if err != nil { return err } if err := j.mutate(ctx, target, func(current *LaunchInventory) error { for i, old := range current.Reservations { if old.ID != r.ID { continue } if old != r { return ErrChanged } current.Reservations[i].State = "settled" if group != nil { current.Reservations[i].Group = *group current.Groups = append(current.Groups, *group) } return nil } return ErrChanged }); err != nil { return err } } return nil } func resolveRoleIntention(root string, r LaunchReservation, reboot bool) (*LaunchGroup, error) { if !filepath.IsAbs(root) || !managedName(r.Parent.RelativePath) || !managedName(r.Group.RelativePath) || filepath.Dir(r.Group.RelativePath) != r.Parent.RelativePath || r.Parent.Device == 0 || r.Parent.Inode == 0 || r.Group.Device != 0 || r.Group.Inode != 0 { return nil, ErrTypedInventory } parentEntry, err := openParent(root, r.Parent.RelativePath, false) if reboot && errors.Is(err, unix.ENOENT) { return nil, nil } if err != nil { return nil, err } defer unix.Close(parentEntry.dir) fd, err := unix.Openat(parentEntry.dir, parentEntry.base, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if reboot { if errors.Is(err, unix.ENOENT) { return nil, nil } if err == nil { unix.Close(fd) } return nil, ErrTypedInventory } if err != nil { return nil, err } defer unix.Close(fd) var parent unix.Stat_t if err = unix.Fstat(fd, &parent); err != nil { return nil, err } if uint64(parent.Dev) != r.Parent.Device || parent.Ino != r.Parent.Inode { return nil, ErrChanged } name := filepath.Base(r.Group.RelativePath) childFD, err := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if errors.Is(err, unix.ENOENT) { return nil, nil } if err != nil { return nil, err } child := os.NewFile(uintptr(childFD), name) defer child.Close() var st unix.Stat_t if err = unix.Fstat(childFD, &st); err != nil { return nil, err } eventsFD, err := unix.Openat(childFD, "cgroup.events", unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return nil, err } events := os.NewFile(uintptr(eventsFD), "cgroup.events") data, err := io.ReadAll(io.LimitReader(events, 4097)) events.Close() if err != nil || len(data) > 4096 { return nil, ErrTypedInventory } seen := false for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { fields := strings.Fields(line) if len(fields) != 2 { return nil, ErrTypedInventory } if fields[0] == "populated" { if seen || fields[1] != "0" { return nil, ErrTypedInventory } seen = true } } if !seen { return nil, ErrTypedInventory } entries, err := child.ReadDir(-1) if err != nil { return nil, err } for _, e := range entries { if e.IsDir() || e.Type()&os.ModeSymlink != 0 { return nil, ErrTypedInventory } } var again unix.Stat_t if err = unix.Fstatat(fd, name, &again, unix.AT_SYMLINK_NOFOLLOW); err != nil { return nil, err } if again.Dev != st.Dev || again.Ino != st.Ino || again.Mode&unix.S_IFMT != unix.S_IFDIR { return nil, ErrChanged } if err = unix.Fstatat(parentEntry.dir, parentEntry.base, &again, unix.AT_SYMLINK_NOFOLLOW); err != nil { return nil, err } if again.Dev != parent.Dev || again.Ino != parent.Ino || again.Mode&unix.S_IFMT != unix.S_IFDIR { return nil, ErrChanged } group := r.Group group.Device = uint64(st.Dev) group.Inode = st.Ino return &group, nil }