| 1 | //go:build linux |
| 2 | |
| 3 | package hostbox |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "io" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostguard" |
| 14 | "golang.org/x/sys/unix" |
| 15 | ) |
| 16 | |
| 17 | // ResolveRoleIntentions is called by root only after a durable fence and drain, |
| 18 | // or proof that the recorded controller is dead. It captures only an empty |
| 19 | // direct child of the recorded parent. Unknown descendants are never adopted. |
| 20 | func (j *LaunchJournal) ResolveRoleIntentions(ctx context.Context, target hostguard.FenceTarget, root string, reboot bool) error { |
| 21 | in, err := j.Read(ctx, target) |
| 22 | if err != nil { |
| 23 | return err |
| 24 | } |
| 25 | for _, r := range in.Reservations { |
| 26 | if r.State != "intended" { |
| 27 | continue |
| 28 | } |
| 29 | group, err := resolveRoleIntention(root, r, reboot) |
| 30 | if err != nil { |
| 31 | return err |
| 32 | } |
| 33 | if err := j.mutate(ctx, target, func(current *LaunchInventory) error { |
| 34 | for i, old := range current.Reservations { |
| 35 | if old.ID != r.ID { |
| 36 | continue |
| 37 | } |
| 38 | if old != r { |
| 39 | return ErrChanged |
| 40 | } |
| 41 | current.Reservations[i].State = "settled" |
| 42 | if group != nil { |
| 43 | current.Reservations[i].Group = *group |
| 44 | current.Groups = append(current.Groups, *group) |
| 45 | } |
| 46 | return nil |
| 47 | } |
| 48 | return ErrChanged |
| 49 | }); err != nil { |
| 50 | return err |
| 51 | } |
| 52 | } |
| 53 | return nil |
| 54 | } |
| 55 | |
| 56 | func resolveRoleIntention(root string, r LaunchReservation, reboot bool) (*LaunchGroup, error) { |
| 57 | 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 { |
| 58 | return nil, ErrTypedInventory |
| 59 | } |
| 60 | parentEntry, err := openParent(root, r.Parent.RelativePath, false) |
| 61 | if reboot && errors.Is(err, unix.ENOENT) { |
| 62 | return nil, nil |
| 63 | } |
| 64 | if err != nil { |
| 65 | return nil, err |
| 66 | } |
| 67 | defer unix.Close(parentEntry.dir) |
| 68 | fd, err := unix.Openat(parentEntry.dir, parentEntry.base, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| 69 | if reboot { |
| 70 | if errors.Is(err, unix.ENOENT) { |
| 71 | return nil, nil |
| 72 | } |
| 73 | if err == nil { |
| 74 | unix.Close(fd) |
| 75 | } |
| 76 | return nil, ErrTypedInventory |
| 77 | } |
| 78 | if err != nil { |
| 79 | return nil, err |
| 80 | } |
| 81 | defer unix.Close(fd) |
| 82 | var parent unix.Stat_t |
| 83 | if err = unix.Fstat(fd, &parent); err != nil { |
| 84 | return nil, err |
| 85 | } |
| 86 | if uint64(parent.Dev) != r.Parent.Device || parent.Ino != r.Parent.Inode { |
| 87 | return nil, ErrChanged |
| 88 | } |
| 89 | name := filepath.Base(r.Group.RelativePath) |
| 90 | childFD, err := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| 91 | if errors.Is(err, unix.ENOENT) { |
| 92 | return nil, nil |
| 93 | } |
| 94 | if err != nil { |
| 95 | return nil, err |
| 96 | } |
| 97 | child := os.NewFile(uintptr(childFD), name) |
| 98 | defer child.Close() |
| 99 | var st unix.Stat_t |
| 100 | if err = unix.Fstat(childFD, &st); err != nil { |
| 101 | return nil, err |
| 102 | } |
| 103 | eventsFD, err := unix.Openat(childFD, "cgroup.events", unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| 104 | if err != nil { |
| 105 | return nil, err |
| 106 | } |
| 107 | events := os.NewFile(uintptr(eventsFD), "cgroup.events") |
| 108 | data, err := io.ReadAll(io.LimitReader(events, 4097)) |
| 109 | events.Close() |
| 110 | if err != nil || len(data) > 4096 { |
| 111 | return nil, ErrTypedInventory |
| 112 | } |
| 113 | seen := false |
| 114 | for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { |
| 115 | fields := strings.Fields(line) |
| 116 | if len(fields) != 2 { |
| 117 | return nil, ErrTypedInventory |
| 118 | } |
| 119 | if fields[0] == "populated" { |
| 120 | if seen || fields[1] != "0" { |
| 121 | return nil, ErrTypedInventory |
| 122 | } |
| 123 | seen = true |
| 124 | } |
| 125 | } |
| 126 | if !seen { |
| 127 | return nil, ErrTypedInventory |
| 128 | } |
| 129 | entries, err := child.ReadDir(-1) |
| 130 | if err != nil { |
| 131 | return nil, err |
| 132 | } |
| 133 | for _, e := range entries { |
| 134 | if e.IsDir() || e.Type()&os.ModeSymlink != 0 { |
| 135 | return nil, ErrTypedInventory |
| 136 | } |
| 137 | } |
| 138 | var again unix.Stat_t |
| 139 | if err = unix.Fstatat(fd, name, &again, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| 140 | return nil, err |
| 141 | } |
| 142 | if again.Dev != st.Dev || again.Ino != st.Ino || again.Mode&unix.S_IFMT != unix.S_IFDIR { |
| 143 | return nil, ErrChanged |
| 144 | } |
| 145 | if err = unix.Fstatat(parentEntry.dir, parentEntry.base, &again, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| 146 | return nil, err |
| 147 | } |
| 148 | if again.Dev != parent.Dev || again.Ino != parent.Ino || again.Mode&unix.S_IFMT != unix.S_IFDIR { |
| 149 | return nil, ErrChanged |
| 150 | } |
| 151 | group := r.Group |
| 152 | group.Device = uint64(st.Dev) |
| 153 | group.Inode = st.Ino |
| 154 | return &group, nil |
| 155 | } |
| 156 | |