| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "io/fs" |
| 10 | "os" |
| 11 | "path/filepath" |
| 12 | "reflect" |
| 13 | "sort" |
| 14 | "strconv" |
| 15 | "strings" |
| 16 | "syscall" |
| 17 | "time" |
| 18 | |
| 19 | "github.com/carlosframework/platform/internal/hostbox" |
| 20 | "github.com/carlosframework/platform/internal/hostguard" |
| 21 | ) |
| 22 | |
| 23 | type LinuxStopKernel struct { |
| 24 | rootAuthority bool |
| 25 | Journal hostbox.StopKernelJournalReader |
| 26 | ManagedRoots map[string]string |
| 27 | CgroupRoot, ProcRoot string |
| 28 | // AgentCgroup returns the agent unit's delegated cgroup directory, for |
| 29 | // the session-zero witness (VerifyNoProcess). Nil refuses the witness. |
| 30 | AgentCgroup func(context.Context) (string, error) |
| 31 | } |
| 32 | |
| 33 | func sameStopSession(a, b hostguard.FenceTarget) bool { |
| 34 | return a.Host == b.Host && a.Incarnation == b.Incarnation && a.Stamp == b.Stamp && a.Session == b.Session && a.Box == b.Box && a.Enrollment == b.Enrollment && a.BootID == b.BootID && a.ControllerID == b.ControllerID |
| 35 | } |
| 36 | func (k LinuxStopKernel) VerifyStopped(ctx context.Context, t hostguard.FenceTarget, inv StopInventory, ack DrainAcknowledgement) (StopKernelEvidence, error) { |
| 37 | if k.Journal == nil || k.CgroupRoot != "/sys/fs/cgroup" || k.ProcRoot != "/proc" || os.Geteuid() != 0 { |
| 38 | return StopKernelEvidence{}, ErrClaimStopUnavailable |
| 39 | } |
| 40 | if err := verifyStopKernelMounts(k.CgroupRoot, k.ProcRoot); err != nil { |
| 41 | return StopKernelEvidence{}, err |
| 42 | } |
| 43 | j, err := k.readContainedJournal(ctx, t) |
| 44 | if err != nil { |
| 45 | return StopKernelEvidence{}, err |
| 46 | } |
| 47 | if !inv.Complete || j.V != 1 || !sameStopSession(j.Target, t) || len(j.Cgroups) == 0 || ack.FenceID != t.FenceID || ack.ControllerID != t.ControllerID { |
| 48 | return StopKernelEvidence{}, ErrClaimStopInventory |
| 49 | } |
| 50 | if err := validateClaimDrainProducers(j.ProducerIDs, ack, j.Completed); err != nil { |
| 51 | return StopKernelEvidence{}, err |
| 52 | } |
| 53 | if j.PendingRoles { |
| 54 | if err := k.resolveRoleIntentions(ctx, t, false); err != nil { |
| 55 | return StopKernelEvidence{}, err |
| 56 | } |
| 57 | j, err = k.readContainedJournal(ctx, t) |
| 58 | if err != nil { |
| 59 | return StopKernelEvidence{}, err |
| 60 | } |
| 61 | } |
| 62 | if !j.Settled || j.PendingRoles { |
| 63 | return StopKernelEvidence{}, ErrClaimStopInventory |
| 64 | } |
| 65 | groups, err := scanStopCgroups(ctx, k.CgroupRoot, j.Cgroups) |
| 66 | if err != nil { |
| 67 | return StopKernelEvidence{}, err |
| 68 | } |
| 69 | paths, err := k.stopDescriptorPaths(inv) |
| 70 | if err != nil { |
| 71 | return StopKernelEvidence{}, err |
| 72 | } |
| 73 | if err := scanStopDescriptors(ctx, k.ProcRoot, j.Files, paths); err != nil { |
| 74 | return StopKernelEvidence{}, err |
| 75 | } |
| 76 | again, err := k.Journal.ReadStopKernelJournal(ctx, t) |
| 77 | if err != nil || !reflect.DeepEqual(j, again) { |
| 78 | return StopKernelEvidence{}, ErrClaimStopInventory |
| 79 | } |
| 80 | if _, err := scanStopCgroups(ctx, k.CgroupRoot, j.Cgroups); err != nil { |
| 81 | return StopKernelEvidence{}, err |
| 82 | } |
| 83 | if raw, ok := k.Journal.(stopLaunchHosts); ok && k.rootAuthority { |
| 84 | records, err := raw.HostLaunches(ctx, t.Host) |
| 85 | if err != nil { |
| 86 | return StopKernelEvidence{}, err |
| 87 | } |
| 88 | if stopNeverIntended(records, t) { |
| 89 | groups = append(groups, "unlaunched:"+digestStop(t)) |
| 90 | } |
| 91 | } |
| 92 | units := []string{} |
| 93 | return StopKernelEvidence{ObservedBootID: t.BootID, InventorySHA256: digestStop(inv), LocalJournalSHA256: digestStop(j), CgroupIdentities: groups, UnitInvocationIDs: units}, nil |
| 94 | } |
| 95 | func stopIdentity(path string) (hostbox.StopFileIdentity, error) { |
| 96 | f, err := os.Stat(path) |
| 97 | if err != nil { |
| 98 | return hostbox.StopFileIdentity{}, err |
| 99 | } |
| 100 | s, ok := f.Sys().(*syscall.Stat_t) |
| 101 | if !ok { |
| 102 | return hostbox.StopFileIdentity{}, ErrClaimStopUnavailable |
| 103 | } |
| 104 | return hostbox.StopFileIdentity{Device: uint64(s.Dev), Inode: s.Ino}, nil |
| 105 | } |
| 106 | func scanStopCgroups(ctx context.Context, root string, groups []hostbox.StopCgroupIdentity) ([]string, error) { |
| 107 | return scanStopCgroupsMode(ctx, root, groups, true) |
| 108 | } |
| 109 | func scanStopCgroupsMode(ctx context.Context, root string, groups []hostbox.StopCgroupIdentity, empty bool) ([]string, error) { |
| 110 | if len(groups) == 0 { |
| 111 | return nil, ErrClaimStopInventory |
| 112 | } |
| 113 | byPath := map[string]hostbox.StopCgroupIdentity{} |
| 114 | for _, g := range groups { |
| 115 | if !filepath.IsLocal(g.RelativePath) || g.RelativePath == "." || filepath.Clean(g.RelativePath) != g.RelativePath || g.Inode == 0 || g.Device == 0 { |
| 116 | return nil, ErrClaimStopInventory |
| 117 | } |
| 118 | if _, ok := byPath[g.RelativePath]; ok { |
| 119 | return nil, ErrClaimStopInventory |
| 120 | } |
| 121 | byPath[g.RelativePath] = g |
| 122 | } |
| 123 | parent := "" |
| 124 | for path := range byPath { |
| 125 | if _, ok := byPath[filepath.Dir(path)]; !ok { |
| 126 | if parent != "" { |
| 127 | return nil, ErrClaimStopInventory |
| 128 | } |
| 129 | parent = path |
| 130 | } |
| 131 | } |
| 132 | if parent == "" { |
| 133 | return nil, ErrClaimStopInventory |
| 134 | } |
| 135 | for path := range byPath { |
| 136 | if path != parent && !strings.HasPrefix(path, parent+string(filepath.Separator)) { |
| 137 | return nil, ErrClaimStopInventory |
| 138 | } |
| 139 | } |
| 140 | seen := map[string]bool{} |
| 141 | out := []string{} |
| 142 | err := filepath.WalkDir(filepath.Join(root, parent), func(path string, d fs.DirEntry, walkErr error) error { |
| 143 | if walkErr != nil { |
| 144 | return walkErr |
| 145 | } |
| 146 | if err := ctx.Err(); err != nil { |
| 147 | return err |
| 148 | } |
| 149 | if d.Type()&os.ModeSymlink != 0 { |
| 150 | return ErrClaimStopInventory |
| 151 | } |
| 152 | if !d.IsDir() { |
| 153 | return nil |
| 154 | } |
| 155 | rel, err := filepath.Rel(root, path) |
| 156 | if err != nil { |
| 157 | return err |
| 158 | } |
| 159 | g, ok := byPath[rel] |
| 160 | if !ok { |
| 161 | return ErrClaimStopInventory |
| 162 | } |
| 163 | id, err := stopIdentity(path) |
| 164 | if err != nil { |
| 165 | return err |
| 166 | } |
| 167 | if id.Device != g.Device || id.Inode != g.Inode { |
| 168 | return ErrClaimStopInventory |
| 169 | } |
| 170 | events, err := os.ReadFile(filepath.Join(path, "cgroup.events")) |
| 171 | if err != nil { |
| 172 | return err |
| 173 | } |
| 174 | populated := "" |
| 175 | for _, line := range strings.Split(string(events), "\n") { |
| 176 | f := strings.Fields(line) |
| 177 | if len(f) == 2 && f[0] == "populated" { |
| 178 | if populated != "" { |
| 179 | return ErrClaimStopInventory |
| 180 | } |
| 181 | populated = f[1] |
| 182 | } |
| 183 | } |
| 184 | if populated != "0" && (empty || populated != "1") { |
| 185 | return ErrClaimStopInventory |
| 186 | } |
| 187 | seen[rel] = true |
| 188 | out = append(out, fmt.Sprintf("%s:%d:%d", rel, id.Device, id.Inode)) |
| 189 | return nil |
| 190 | }) |
| 191 | if err != nil { |
| 192 | return nil, err |
| 193 | } |
| 194 | if len(seen) != len(byPath) { |
| 195 | return nil, ErrClaimStopInventory |
| 196 | } |
| 197 | sort.Strings(out) |
| 198 | return out, nil |
| 199 | } |
| 200 | func scanStopDescriptors(ctx context.Context, proc string, files []hostbox.StopFileIdentity, paths ...[]string) error { |
| 201 | wanted := map[hostbox.StopFileIdentity]bool{} |
| 202 | for _, id := range files { |
| 203 | if id.Device == 0 || id.Inode == 0 { |
| 204 | return ErrClaimStopInventory |
| 205 | } |
| 206 | wanted[id] = true |
| 207 | } |
| 208 | entries, err := os.ReadDir(proc) |
| 209 | if err != nil { |
| 210 | return err |
| 211 | } |
| 212 | for _, entry := range entries { |
| 213 | if _, err := strconv.ParseUint(entry.Name(), 10, 32); err != nil { |
| 214 | continue |
| 215 | } |
| 216 | if err := ctx.Err(); err != nil { |
| 217 | return err |
| 218 | } |
| 219 | pidDir := filepath.Join(proc, entry.Name()) |
| 220 | before, err := os.ReadFile(filepath.Join(pidDir, "stat")) |
| 221 | if err != nil { |
| 222 | if errors.Is(err, os.ErrNotExist) { |
| 223 | continue |
| 224 | } |
| 225 | return err |
| 226 | } |
| 227 | descriptors, err := os.ReadDir(filepath.Join(pidDir, "fd")) |
| 228 | if err != nil { |
| 229 | if _, gone := os.Stat(pidDir); errors.Is(gone, os.ErrNotExist) { |
| 230 | continue |
| 231 | } |
| 232 | return err |
| 233 | } |
| 234 | for _, fd := range descriptors { |
| 235 | descriptor := filepath.Join(pidDir, "fd", fd.Name()) |
| 236 | name, linkErr := os.Readlink(descriptor) |
| 237 | if linkErr != nil && !errors.Is(linkErr, os.ErrNotExist) { |
| 238 | return linkErr |
| 239 | } |
| 240 | name = strings.TrimSuffix(name, " (deleted)") |
| 241 | for _, list := range paths { |
| 242 | for _, owned := range list { |
| 243 | if name == owned || (strings.HasSuffix(owned, "/") && strings.HasPrefix(name, owned)) { |
| 244 | return fmt.Errorf("claim stop: managed state descriptor remains at %s", descriptor) |
| 245 | } |
| 246 | } |
| 247 | } |
| 248 | id, err := stopIdentity(descriptor) |
| 249 | if err != nil { |
| 250 | if errors.Is(err, os.ErrNotExist) { |
| 251 | continue |
| 252 | } |
| 253 | return err |
| 254 | } |
| 255 | if wanted[id] { |
| 256 | return fmt.Errorf("claim stop: database descriptor remains at %s/fd/%s", entry.Name(), fd.Name()) |
| 257 | } |
| 258 | } |
| 259 | after, err := os.ReadFile(filepath.Join(pidDir, "stat")) |
| 260 | if err != nil { |
| 261 | if errors.Is(err, os.ErrNotExist) { |
| 262 | continue |
| 263 | } |
| 264 | return err |
| 265 | } |
| 266 | start := func(b []byte) string { |
| 267 | i := strings.LastIndexByte(string(b), ')') |
| 268 | if i < 0 { |
| 269 | return "" |
| 270 | } |
| 271 | f := strings.Fields(string(b[i+1:])) |
| 272 | if len(f) <= 19 { |
| 273 | return "" |
| 274 | } |
| 275 | return f[19] |
| 276 | } |
| 277 | if start(before) == "" || start(before) != start(after) { |
| 278 | return ErrClaimStopIdentity |
| 279 | } |
| 280 | } |
| 281 | return nil |
| 282 | } |
| 283 | |
| 284 | func newProductionStopKernel(j hostbox.StopKernelJournalReader, roots map[string]string, rootAuthority ...bool) StopKernelVerifier { |
| 285 | agent := agentUnitCgroup |
| 286 | if len(rootAuthority) == 1 && rootAuthority[0] { |
| 287 | agent = rootStopAgentCgroup |
| 288 | } |
| 289 | return LinuxStopKernel{rootAuthority: len(rootAuthority) == 1 && rootAuthority[0], Journal: j, ManagedRoots: roots, CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc", AgentCgroup: agent} |
| 290 | } |
| 291 | |
| 292 | func (k LinuxStopKernel) RecoverStopped(ctx context.Context, t hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) { |
| 293 | refuse := func(err error) (StopKernelEvidence, DrainAcknowledgement, error) { |
| 294 | return StopKernelEvidence{}, DrainAcknowledgement{}, err |
| 295 | } |
| 296 | if os.Geteuid() != 0 || k.CgroupRoot != "/sys/fs/cgroup" || k.ProcRoot != "/proc" || k.Journal == nil { |
| 297 | return refuse(ErrClaimStopUnavailable) |
| 298 | } |
| 299 | if err := verifyStopKernelMounts(k.CgroupRoot, k.ProcRoot); err != nil { |
| 300 | return refuse(err) |
| 301 | } |
| 302 | journal, ok := k.Journal.(interface { |
| 303 | SettleRecovered(context.Context, hostguard.FenceTarget) error |
| 304 | }) |
| 305 | if !ok { |
| 306 | return refuse(ErrClaimStopUnavailable) |
| 307 | } |
| 308 | j, err := k.readContainedJournal(ctx, t) |
| 309 | if err != nil { |
| 310 | return refuse(err) |
| 311 | } |
| 312 | if j.ControllerPID <= 0 || j.ProcessStart == "" || !sameStopSession(j.Target, t) { |
| 313 | return refuse(ErrClaimStopIdentity) |
| 314 | } |
| 315 | observedBoot, err := recoveryControllerAt(k.ProcRoot, t, j) |
| 316 | if err != nil { |
| 317 | return refuse(err) |
| 318 | } |
| 319 | if observedBoot != t.BootID { |
| 320 | return k.recoverAfterReboot(ctx, t, in, j, observedBoot, journal) |
| 321 | } |
| 322 | if j.PendingRoles { |
| 323 | if err := k.resolveRoleIntentions(ctx, t, false); err != nil { |
| 324 | return refuse(err) |
| 325 | } |
| 326 | j, err = k.readContainedJournal(ctx, t) |
| 327 | if err != nil { |
| 328 | return refuse(err) |
| 329 | } |
| 330 | } |
| 331 | if _, err := scanStopCgroupsMode(ctx, k.CgroupRoot, j.Cgroups, false); err != nil { |
| 332 | if errors.Is(err, os.ErrNotExist) { |
| 333 | return k.recoverUnlaunchedAbsent(ctx, t, in, j, journal) |
| 334 | } |
| 335 | return refuse(err) |
| 336 | } |
| 337 | groups := append([]hostbox.StopCgroupIdentity(nil), j.Cgroups...) |
| 338 | sort.Slice(groups, func(i, j int) bool { return len(groups[i].RelativePath) < len(groups[j].RelativePath) }) |
| 339 | if len(groups) == 0 { |
| 340 | return refuse(ErrClaimStopInventory) |
| 341 | } |
| 342 | if err := os.WriteFile(filepath.Join(k.CgroupRoot, groups[0].RelativePath, "cgroup.kill"), []byte("1"), 0600); err != nil { |
| 343 | return refuse(err) |
| 344 | } |
| 345 | for { |
| 346 | if _, err := scanStopCgroups(ctx, k.CgroupRoot, j.Cgroups); err == nil { |
| 347 | break |
| 348 | } |
| 349 | select { |
| 350 | case <-ctx.Done(): |
| 351 | return refuse(ctx.Err()) |
| 352 | case <-time.After(20 * time.Millisecond): |
| 353 | } |
| 354 | } |
| 355 | paths, err := k.stopDescriptorPaths(in) |
| 356 | if err != nil { |
| 357 | return refuse(err) |
| 358 | } |
| 359 | if err := scanStopDescriptors(ctx, k.ProcRoot, j.Files, paths); err != nil { |
| 360 | return refuse(err) |
| 361 | } |
| 362 | if err := journal.SettleRecovered(ctx, t); err != nil { |
| 363 | return refuse(err) |
| 364 | } |
| 365 | ack := DrainClaimAcknowledgement(t.FenceID, t.ControllerID, j.ProcessStart, j.ProducerIDs, j.Completed) |
| 366 | evidence, err := k.VerifyStopped(ctx, t, in, ack) |
| 367 | return evidence, ack, err |
| 368 | } |
| 369 | |
| 370 | func (k LinuxStopKernel) stopDescriptorPaths(in StopInventory) ([]string, error) { |
| 371 | if !in.Complete || len(k.ManagedRoots) == 0 { |
| 372 | return nil, ErrClaimStopInventory |
| 373 | } |
| 374 | out := []string{} |
| 375 | for _, a := range in.Entries { |
| 376 | if a.Kind != "path" && a.Kind != "live-link" { |
| 377 | continue |
| 378 | } |
| 379 | root := k.ManagedRoots[a.ManagedRoot] |
| 380 | if !filepath.IsAbs(root) || !filepath.IsLocal(a.RelativePath) { |
| 381 | return nil, ErrClaimStopInventory |
| 382 | } |
| 383 | path := filepath.Join(root, a.RelativePath) |
| 384 | out = append(out, path, path+"-wal", path+"-shm") |
| 385 | if a.Type&syscall.S_IFMT == syscall.S_IFDIR { |
| 386 | out = append(out, path+"/") |
| 387 | } |
| 388 | } |
| 389 | return out, nil |
| 390 | } |
| 391 | |
| 392 | func (k LinuxStopKernel) recoverAfterReboot(ctx context.Context, t hostguard.FenceTarget, in StopInventory, j hostbox.StopKernelJournal, observedBoot string, journal interface { |
| 393 | SettleRecovered(context.Context, hostguard.FenceTarget) error |
| 394 | }) (StopKernelEvidence, DrainAcknowledgement, error) { |
| 395 | refuse := func(err error) (StopKernelEvidence, DrainAcknowledgement, error) { |
| 396 | return StopKernelEvidence{}, DrainAcknowledgement{}, err |
| 397 | } |
| 398 | if observedBoot == "" || t.BootID == "" || observedBoot == t.BootID || !sameStopSession(t, j.Target) || j.ControllerPID <= 0 || j.ProcessStart == "" || j.V != 1 { |
| 399 | return refuse(ErrClaimStopIdentity) |
| 400 | } |
| 401 | if err := k.validateContainment(ctx, t, j); err != nil { |
| 402 | return refuse(err) |
| 403 | } |
| 404 | bootPath := filepath.Join(k.ProcRoot, "sys/kernel/random/boot_id") |
| 405 | checkBoot := func() error { |
| 406 | b, err := os.ReadFile(bootPath) |
| 407 | if err != nil { |
| 408 | return err |
| 409 | } |
| 410 | if strings.TrimSpace(string(b)) != observedBoot { |
| 411 | return ErrClaimStopIdentity |
| 412 | } |
| 413 | return nil |
| 414 | } |
| 415 | if err := checkBoot(); err != nil { |
| 416 | return refuse(err) |
| 417 | } |
| 418 | if j.PendingRoles { |
| 419 | if err := k.resolveRoleIntentions(ctx, t, true); err != nil { |
| 420 | return refuse(err) |
| 421 | } |
| 422 | var err error |
| 423 | j, err = k.readContainedJournal(ctx, t) |
| 424 | if err != nil { |
| 425 | return refuse(err) |
| 426 | } |
| 427 | } |
| 428 | groups, err := scanRebootCgroups(ctx, k.CgroupRoot, j.Cgroups, t.BootID, observedBoot) |
| 429 | if err != nil { |
| 430 | return refuse(err) |
| 431 | } |
| 432 | paths, err := k.stopDescriptorPaths(in) |
| 433 | if err != nil { |
| 434 | return refuse(err) |
| 435 | } |
| 436 | files, err := k.rebootStopFiles(ctx, in, j.Files) |
| 437 | if err != nil { |
| 438 | return refuse(err) |
| 439 | } |
| 440 | if err := scanStopDescriptors(ctx, k.ProcRoot, files, paths); err != nil { |
| 441 | return refuse(err) |
| 442 | } |
| 443 | if err := journal.SettleRecovered(ctx, t); err != nil { |
| 444 | return refuse(err) |
| 445 | } |
| 446 | fresh, err := k.readContainedJournal(ctx, t) |
| 447 | if err != nil { |
| 448 | return refuse(err) |
| 449 | } |
| 450 | settled := j |
| 451 | settled.Settled = true |
| 452 | if !reflect.DeepEqual(settled, fresh) { |
| 453 | return refuse(ErrClaimStopInventory) |
| 454 | } |
| 455 | if _, err := scanRebootCgroups(ctx, k.CgroupRoot, j.Cgroups, t.BootID, observedBoot); err != nil { |
| 456 | return refuse(err) |
| 457 | } |
| 458 | if err := checkBoot(); err != nil { |
| 459 | return refuse(err) |
| 460 | } |
| 461 | ack := DrainClaimAcknowledgement(t.FenceID, t.ControllerID, j.ProcessStart, j.ProducerIDs, j.Completed) |
| 462 | return StopKernelEvidence{ObservedBootID: observedBoot, InventorySHA256: digestStop(in), LocalJournalSHA256: digestStop(fresh), CgroupIdentities: groups, UnitInvocationIDs: []string{}}, ack, nil |
| 463 | } |
| 464 | func scanRebootCgroups(ctx context.Context, root string, groups []hostbox.StopCgroupIdentity, previousBoot, observedBoot string) ([]string, error) { |
| 465 | if previousBoot == "" || observedBoot == "" || previousBoot == observedBoot || len(groups) == 0 { |
| 466 | return nil, ErrClaimStopIdentity |
| 467 | } |
| 468 | info, err := os.Stat(root) |
| 469 | if err != nil { |
| 470 | return nil, err |
| 471 | } |
| 472 | if !info.IsDir() { |
| 473 | return nil, ErrClaimStopInventory |
| 474 | } |
| 475 | seen := map[string]bool{} |
| 476 | out := []string{} |
| 477 | for _, g := range groups { |
| 478 | if err := ctx.Err(); err != nil { |
| 479 | return nil, err |
| 480 | } |
| 481 | if !filepath.IsLocal(g.RelativePath) || g.RelativePath == "." || filepath.Clean(g.RelativePath) != g.RelativePath || g.Device == 0 || g.Inode == 0 || seen[g.RelativePath] { |
| 482 | return nil, ErrClaimStopInventory |
| 483 | } |
| 484 | seen[g.RelativePath] = true |
| 485 | if _, err := os.Lstat(filepath.Join(root, g.RelativePath)); !errors.Is(err, os.ErrNotExist) { |
| 486 | if err != nil { |
| 487 | return nil, err |
| 488 | } |
| 489 | return nil, ErrClaimStopInventory |
| 490 | } |
| 491 | out = append(out, fmt.Sprintf("boot=%s;previous_boot=%s;absent=%s;previous_identity=%d:%d", observedBoot, previousBoot, g.RelativePath, g.Device, g.Inode)) |
| 492 | } |
| 493 | parents := 0 |
| 494 | parent := "" |
| 495 | for path := range seen { |
| 496 | if !seen[filepath.Dir(path)] { |
| 497 | parents++ |
| 498 | parent = path |
| 499 | } |
| 500 | } |
| 501 | if parents != 1 { |
| 502 | return nil, ErrClaimStopInventory |
| 503 | } |
| 504 | for path := range seen { |
| 505 | if path != parent && !strings.HasPrefix(path, parent+"/") { |
| 506 | return nil, ErrClaimStopInventory |
| 507 | } |
| 508 | } |
| 509 | sort.Strings(out) |
| 510 | return out, nil |
| 511 | } |
| 512 | |
| 513 | func verifyStopKernelMounts(cgroupRoot, procRoot string) error { |
| 514 | var control, process syscall.Statfs_t |
| 515 | if err := syscall.Statfs(cgroupRoot, &control); err != nil { |
| 516 | return err |
| 517 | } |
| 518 | if err := syscall.Statfs(procRoot, &process); err != nil { |
| 519 | return err |
| 520 | } |
| 521 | if uint64(control.Type) != 0x63677270 || uint64(process.Type) != 0x9fa0 { |
| 522 | return ErrClaimStopUnavailable |
| 523 | } |
| 524 | return nil |
| 525 | } |
| 526 | |
| 527 | func (k LinuxStopKernel) resolveRoleIntentions(ctx context.Context, t hostguard.FenceTarget, reboot bool) error { |
| 528 | if _, err := k.readContainedJournal(ctx, t); err != nil { |
| 529 | return err |
| 530 | } |
| 531 | resolver, ok := k.Journal.(interface { |
| 532 | ResolveRoleIntentions(context.Context, hostguard.FenceTarget, string, bool) error |
| 533 | }) |
| 534 | if !ok { |
| 535 | return ErrClaimStopUnavailable |
| 536 | } |
| 537 | return resolver.ResolveRoleIntentions(ctx, t, k.CgroupRoot, reboot) |
| 538 | } |
| 539 | |
| 540 | func (k LinuxStopKernel) validateContainment(ctx context.Context, t hostguard.FenceTarget, j hostbox.StopKernelJournal) error { |
| 541 | if !sameStopSession(j.Target, t) || k.AgentCgroup == nil { |
| 542 | return ErrClaimStopInventory |
| 543 | } |
| 544 | agent, err := k.AgentCgroup(ctx) |
| 545 | if err != nil { |
| 546 | return err |
| 547 | } |
| 548 | return validateStopClaimGroups(t, j, agent) |
| 549 | } |
| 550 | |
| 551 | func (k LinuxStopKernel) readContainedJournal(ctx context.Context, t hostguard.FenceTarget) (hostbox.StopKernelJournal, error) { |
| 552 | j, err := k.Journal.ReadStopKernelJournal(ctx, t) |
| 553 | if err != nil { |
| 554 | return j, err |
| 555 | } |
| 556 | if err = k.validateContainment(ctx, t, j); err != nil { |
| 557 | return hostbox.StopKernelJournal{}, err |
| 558 | } |
| 559 | // Intentions are persisted separately from the concrete groups and must be |
| 560 | // checked before their resolver opens any privileged recovery target. |
| 561 | if raw, ok := k.Journal.(interface { |
| 562 | Read(context.Context, hostguard.FenceTarget) (hostbox.LaunchInventory, error) |
| 563 | }); ok { |
| 564 | in, e := raw.Read(ctx, t) |
| 565 | if e != nil { |
| 566 | return hostbox.StopKernelJournal{}, e |
| 567 | } |
| 568 | agent, e := k.AgentCgroup(ctx) |
| 569 | if e != nil { |
| 570 | return hostbox.StopKernelJournal{}, e |
| 571 | } |
| 572 | parent, e := claimContainmentParent(agent, t) |
| 573 | if e != nil { |
| 574 | return hostbox.StopKernelJournal{}, e |
| 575 | } |
| 576 | if e = validateClaimContainment(in, parent); e != nil { |
| 577 | return hostbox.StopKernelJournal{}, e |
| 578 | } |
| 579 | } else if j.PendingRoles { |
| 580 | return hostbox.StopKernelJournal{}, ErrClaimStopInventory |
| 581 | } |
| 582 | return j, nil |
| 583 | } |
| 584 | |
| 585 | // CheckRecoveryController lets root restart while the live edge finishes its fence. |
| 586 | func (k LinuxStopKernel) CheckRecoveryController(ctx context.Context, t hostguard.FenceTarget) error { |
| 587 | if _, err := k.realRoots(); err != nil { |
| 588 | return err |
| 589 | } |
| 590 | j, err := k.readContainedJournal(ctx, t) |
| 591 | if err != nil { |
| 592 | return err |
| 593 | } |
| 594 | _, err = recoveryControllerAt(k.ProcRoot, t, j) |
| 595 | return err |
| 596 | } |
| 597 | |
| 598 | func recoveryControllerAt(proc string, t hostguard.FenceTarget, j hostbox.StopKernelJournal) (string, error) { |
| 599 | if j.ControllerPID <= 0 || j.ProcessStart == "" || !sameStopSession(j.Target, t) { |
| 600 | return "", ErrClaimStopIdentity |
| 601 | } |
| 602 | boot, err := os.ReadFile(filepath.Join(proc, "sys/kernel/random/boot_id")) |
| 603 | if err != nil { |
| 604 | return "", err |
| 605 | } |
| 606 | observed := strings.TrimSpace(string(boot)) |
| 607 | if observed == "" { |
| 608 | return "", ErrClaimStopIdentity |
| 609 | } |
| 610 | if observed != t.BootID { |
| 611 | return observed, nil |
| 612 | } |
| 613 | raw, err := os.ReadFile(filepath.Join(proc, fmt.Sprint(j.ControllerPID), "stat")) |
| 614 | if errors.Is(err, os.ErrNotExist) { |
| 615 | return observed, nil |
| 616 | } |
| 617 | if err != nil { |
| 618 | return "", err |
| 619 | } |
| 620 | i := strings.LastIndexByte(string(raw), ')') |
| 621 | if i < 0 { |
| 622 | return "", ErrClaimStopIdentity |
| 623 | } |
| 624 | fields := strings.Fields(string(raw)[i+1:]) |
| 625 | if len(fields) <= 19 { |
| 626 | return "", ErrClaimStopIdentity |
| 627 | } |
| 628 | if fields[19] == j.ProcessStart { |
| 629 | return "", ErrClaimStopControllerAlive |
| 630 | } |
| 631 | return observed, nil |
| 632 | } |
| 633 | |