carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
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
23type 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
33func 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}
36func (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}
95func 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}
106func scanStopCgroups(ctx context.Context, root string, groups []hostbox.StopCgroupIdentity) ([]string, error) {
107 return scanStopCgroupsMode(ctx, root, groups, true)
108}
109func 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}
200func 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
284func 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
292func (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
370func (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
392func (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}
464func 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
513func 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
527func (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
540func (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
551func (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.
586func (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
598func 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