| 1 | //go:build linux |
| 2 | |
| 3 | package hostbox |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "io" |
| 10 | "os" |
| 11 | "path/filepath" |
| 12 | "strings" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/rootstate" |
| 15 | "golang.org/x/sys/unix" |
| 16 | ) |
| 17 | |
| 18 | var ErrTypedParkUnsupported = errors.New("hostbox: typed park unsupported") |
| 19 | |
| 20 | // TypedParker resolves inventory root IDs only from trusted configuration. |
| 21 | // ParkedRoot is the provisioned filesystem root containing .parked. |
| 22 | type TypedParker struct { |
| 23 | CapturePersistent bool |
| 24 | Roots map[string]string |
| 25 | ParkedRoots map[string]string |
| 26 | ParkedRoot, Incarnation, Host, Retirement string |
| 27 | syncParents func(int, int) error |
| 28 | } |
| 29 | |
| 30 | func (p TypedParker) validate() error { |
| 31 | if (p.ParkedRoot == "" && len(p.ParkedRoots) == 0) || !component(p.Incarnation) || !component(p.Host) || !component(p.Retirement) || len(p.Roots) == 0 { |
| 32 | return ErrTypedInventory |
| 33 | } |
| 34 | return nil |
| 35 | } |
| 36 | |
| 37 | // CaptureArtifact records the no-follow inode identity used at retirement. |
| 38 | func (p TypedParker) CaptureArtifact(rootID, name, owner string, kind ArtifactKind) (Artifact, error) { |
| 39 | if err := p.validate(); err != nil { |
| 40 | return Artifact{}, err |
| 41 | } |
| 42 | rootPath, ok := p.Roots[rootID] |
| 43 | if !ok || !managedName(name) || owner == "" { |
| 44 | return Artifact{}, ErrTypedInventory |
| 45 | } |
| 46 | fd, err := openParent(rootPath, name, false) |
| 47 | if err != nil { |
| 48 | return Artifact{}, err |
| 49 | } |
| 50 | defer unix.Close(fd.dir) |
| 51 | var st unix.Stat_t |
| 52 | if err := unix.Fstatat(fd.dir, fd.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| 53 | return Artifact{}, err |
| 54 | } |
| 55 | identity, err := inventoryIdentityAt(fd.dir, fd.base, true) |
| 56 | if err != nil { |
| 57 | return Artifact{}, err |
| 58 | } |
| 59 | if identity.Device != uint64(st.Dev) || identity.Inode != st.Ino { |
| 60 | return Artifact{}, ErrChanged |
| 61 | } |
| 62 | return Artifact{Identity: identity, Kind: kind, Root: rootID, Name: name, Owner: owner, Dev: uint64(st.Dev), Ino: st.Ino, Type: st.Mode & unix.S_IFMT}, nil |
| 63 | } |
| 64 | |
| 65 | func (p TypedParker) Park(ctx context.Context, id string, a Artifact) error { |
| 66 | if err := ctx.Err(); err != nil { |
| 67 | return err |
| 68 | } |
| 69 | if err := p.validate(); err != nil { |
| 70 | return err |
| 71 | } |
| 72 | if p.CapturePersistent && a.Identity.Boot == "" { |
| 73 | return ErrTypedInventory |
| 74 | } |
| 75 | if id == "" || !managedName(a.Name) || a.Dev == 0 || a.Ino == 0 || a.Type == 0 { |
| 76 | return ErrTypedInventory |
| 77 | } |
| 78 | srcRoot, ok := p.Roots[a.Root] |
| 79 | if !ok { |
| 80 | return ErrTypedInventory |
| 81 | } |
| 82 | src, err := openParent(srcRoot, a.Name, false) |
| 83 | if err != nil { |
| 84 | return err |
| 85 | } |
| 86 | defer unix.Close(src.dir) |
| 87 | var st unix.Stat_t |
| 88 | if err := unix.Fstatat(src.dir, src.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| 89 | if errors.Is(err, unix.ENOENT) { |
| 90 | return p.verifyDestination(a, src.dir) |
| 91 | } |
| 92 | return err |
| 93 | } |
| 94 | if !sameArtifactAt(a, src.dir, src.base, &st) || !artifactRootChecked(a, srcRoot) { |
| 95 | return ErrChanged |
| 96 | } |
| 97 | dstRel := filepath.Join(".parked", p.Incarnation, p.Host, p.Retirement, a.Root, a.Name) |
| 98 | parkedRoot, err := p.ParkedArtifactRoot(a) |
| 99 | if err != nil { |
| 100 | return err |
| 101 | } |
| 102 | dst, err := openParent(parkedRoot, dstRel, true) |
| 103 | if err != nil { |
| 104 | return err |
| 105 | } |
| 106 | defer unix.Close(dst.dir) |
| 107 | var srcFS, dstFS unix.Stat_t |
| 108 | if err := unix.Fstat(src.dir, &srcFS); err != nil { |
| 109 | return err |
| 110 | } |
| 111 | if err := unix.Fstat(dst.dir, &dstFS); err != nil { |
| 112 | return err |
| 113 | } |
| 114 | if srcFS.Dev != dstFS.Dev { |
| 115 | return unix.EXDEV |
| 116 | } |
| 117 | if err := unix.Renameat2(src.dir, src.base, dst.dir, dst.base, unix.RENAME_NOREPLACE); err != nil { |
| 118 | if errors.Is(err, unix.EEXIST) { |
| 119 | return ErrChanged |
| 120 | } |
| 121 | return err |
| 122 | } |
| 123 | return p.sync(src.dir, dst.dir) |
| 124 | } |
| 125 | |
| 126 | func (p TypedParker) verifyDestination(a Artifact, srcParent int) error { |
| 127 | rel := filepath.Join(".parked", p.Incarnation, p.Host, p.Retirement, a.Root, a.Name) |
| 128 | parkedRoot, err := p.ParkedArtifactRoot(a) |
| 129 | if err != nil { |
| 130 | return err |
| 131 | } |
| 132 | fd, err := openParent(parkedRoot, rel, false) |
| 133 | if err != nil { |
| 134 | return err |
| 135 | } |
| 136 | defer unix.Close(fd.dir) |
| 137 | var st unix.Stat_t |
| 138 | if err := unix.Fstatat(fd.dir, fd.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| 139 | return err |
| 140 | } |
| 141 | if !sameArtifactAt(a, fd.dir, fd.base, &st) || !artifactRootChecked(a, parkedRoot) { |
| 142 | return ErrChanged |
| 143 | } |
| 144 | return p.sync(srcParent, fd.dir) |
| 145 | } |
| 146 | |
| 147 | func (p TypedParker) sync(a, b int) error { |
| 148 | if p.syncParents != nil { |
| 149 | return p.syncParents(a, b) |
| 150 | } |
| 151 | return syncFDs(a, b) |
| 152 | } |
| 153 | |
| 154 | type parentFD struct { |
| 155 | dir int |
| 156 | base string |
| 157 | } |
| 158 | |
| 159 | func openParent(rootPath, name string, create bool) (parentFD, error) { |
| 160 | root, err := unix.Openat2(unix.AT_FDCWD, rootPath, &unix.OpenHow{Flags: unix.O_RDONLY | unix.O_DIRECTORY | unix.O_CLOEXEC, Resolve: unix.RESOLVE_NO_SYMLINKS}) |
| 161 | if err != nil { |
| 162 | return parentFD{}, err |
| 163 | } |
| 164 | parts := strings.Split(filepath.Clean(name), string(filepath.Separator)) |
| 165 | cur := root |
| 166 | for _, part := range parts[:len(parts)-1] { |
| 167 | next, er := unix.Openat(cur, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) |
| 168 | if er != nil && create && errors.Is(er, unix.ENOENT) { |
| 169 | if er = unix.Mkdirat(cur, part, 0o700); er == nil || errors.Is(er, unix.EEXIST) { |
| 170 | if er == nil { |
| 171 | er = unix.Fsync(cur) |
| 172 | } |
| 173 | if er != nil { |
| 174 | if cur != root { |
| 175 | unix.Close(cur) |
| 176 | } |
| 177 | unix.Close(root) |
| 178 | return parentFD{}, er |
| 179 | } |
| 180 | next, er = unix.Openat(cur, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) |
| 181 | } |
| 182 | } |
| 183 | if cur != root { |
| 184 | unix.Close(cur) |
| 185 | } |
| 186 | if er != nil { |
| 187 | unix.Close(root) |
| 188 | return parentFD{}, er |
| 189 | } |
| 190 | cur = next |
| 191 | } |
| 192 | if cur != root { |
| 193 | unix.Close(root) |
| 194 | } |
| 195 | return parentFD{cur, parts[len(parts)-1]}, nil |
| 196 | } |
| 197 | func artifactRootChecked(a Artifact, root string) bool { |
| 198 | boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") |
| 199 | return err == nil && (a.Identity.Boot == strings.TrimSpace(string(boot)) || checkedInventoryAncestry(root) == nil) |
| 200 | } |
| 201 | |
| 202 | func sameArtifactAt(a Artifact, parent int, name string, st *unix.Stat_t) bool { |
| 203 | if a.Identity == (rootstate.FileIdentity{}) { |
| 204 | return false |
| 205 | } |
| 206 | current, err := inventoryIdentityAt(parent, name, true) |
| 207 | return err == nil && current.Device == uint64(st.Dev) && current.Inode == st.Ino && a.Identity.Device == a.Dev && a.Identity.Inode == a.Ino && a.Type == current.Mode&unix.S_IFMT && sameInventoryIdentity(a.Identity, current) |
| 208 | } |
| 209 | |
| 210 | func syncFDs(a, b int) error { |
| 211 | if err := unix.Fsync(a); err != nil { |
| 212 | return fmt.Errorf("sync source: %w", err) |
| 213 | } |
| 214 | if err := unix.Fsync(b); err != nil { |
| 215 | return fmt.Errorf("sync destination: %w", err) |
| 216 | } |
| 217 | return nil |
| 218 | } |
| 219 | |
| 220 | // VerifyEmptyDirectory reads the captured directory itself, with no symlink |
| 221 | // traversal. The caller still parks by the same recorded inode under cleanup. |
| 222 | func (p TypedParker) VerifyEmptyDirectory(a Artifact) error { |
| 223 | if err := p.validate(); err != nil { |
| 224 | return err |
| 225 | } |
| 226 | if p.CapturePersistent && a.Identity.Boot == "" { |
| 227 | return ErrTypedInventory |
| 228 | } |
| 229 | root, ok := p.Roots[a.Root] |
| 230 | if !ok || !managedName(a.Name) || a.Type != unix.S_IFDIR { |
| 231 | return ErrTypedInventory |
| 232 | } |
| 233 | parent, err := openParent(root, a.Name, false) |
| 234 | if err != nil { |
| 235 | return err |
| 236 | } |
| 237 | defer unix.Close(parent.dir) |
| 238 | fd, err := unix.Openat(parent.dir, parent.base, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| 239 | if err != nil { |
| 240 | return err |
| 241 | } |
| 242 | f := os.NewFile(uintptr(fd), a.Name) |
| 243 | defer f.Close() |
| 244 | var st unix.Stat_t |
| 245 | if err := unix.Fstat(fd, &st); err != nil { |
| 246 | return err |
| 247 | } |
| 248 | if !sameArtifactAt(a, parent.dir, parent.base, &st) || !artifactRootChecked(a, root) { |
| 249 | return ErrChanged |
| 250 | } |
| 251 | if names, err := f.Readdirnames(1); len(names) != 0 || !errors.Is(err, io.EOF) { |
| 252 | return ErrChanged |
| 253 | } |
| 254 | return nil |
| 255 | } |
| 256 | |