activator: a seed proof on a kernel with no cgroup.kill keeps the leader-only proof
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+25
−2
internal/activator/backend_exec.go+10 −2internal/activator/containment_test.go+15 −0
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go| index 017ea974f..b95208e75 100644 |
| --- a/internal/activator/backend_exec.go |
| +++ b/internal/activator/backend_exec.go |
| @@ -1124,8 +1124,9 @@ func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe stri |
| // lease. An absent cgroup is empty. A box with no delegated cgroup |
| // (cgroup.ErrNoDelegation) ran the seed unaccounted, with nothing that |
| // could reach a descendant, so the leader's exit is the only proof there |
| -// is (ledger 127): it logs and answers nil. Any other failure is returned, |
| -// and the caller keeps the database (ErrSeedUnproven). |
| +// is (ledger 127): it logs and answers nil. So is a kernel with no |
| +// cgroup.kill (cgroup.ErrNoKill). Any other failure is returned, and the |
| +// caller keeps the database (ErrSeedUnproven). |
| func (b *ExecBackend) seedDescendantsGone(ctx context.Context, rt registry.Route, child string) error { |
| var err error |
| switch { |
| @@ -1140,6 +1141,13 @@ func (b *ExecBackend) seedDescendantsGone(ctx context.Context, rt registry.Route |
| log.Printf("activator: %s: the seed ran with no delegated cgroup; its leader's exit is the only proof there is", rt.Host) |
| return nil |
| } |
| + if errors.Is(err, cgroup.ErrNoKill) { |
| + // A kernel before 5.14 can kill nothing in a cgroup (EmptyHost's |
| + // rule): read as unproven, an obligation could never be discharged |
| + // on such a box and the row would stay down for good. |
| + log.Printf("activator: %s: this kernel has no cgroup.kill; the seed's leader's exit is the only proof there is", rt.Host) |
| + return nil |
| + } |
| return err |
| } |
diff --git a/internal/activator/containment_test.go b/internal/activator/containment_test.go| index 12e03f2ad..a6680b4e4 100644 |
| --- a/internal/activator/containment_test.go |
| +++ b/internal/activator/containment_test.go |
| @@ -639,6 +639,21 @@ func TestAPreexistingDBBesideAnUnprovenSeedIsRefusedThenKept(t *testing.T) { |
| } |
| } |
| +// Finding 5, on the seed's own proof: a kernel with no cgroup.kill can kill |
| +// nothing, so the leader's exit is the only proof there is, as with no |
| +// delegation. Read as unproven instead, an obligation could never be |
| +// discharged on such a box and the row would stay down for good. |
| +func TestASeedProofWithNoCgroupKillIsTheLeaderOnlyProof(t *testing.T) { |
| + const host = "old-kernel-seed.example.com" |
| + b, _ := delegatedBackend(t, host) |
| + b.cgroupKill = func(context.Context, registry.Route, string) error { |
| + return fmt.Errorf("%w: open cgroup.kill: no such file or directory", cgroup.ErrNoKill) |
| + } |
| + if err := b.seedDescendantsGone(context.Background(), registry.Route{Host: host}, ""); err != nil { |
| + t.Fatalf("seedDescendantsGone = %v, want the leader-only proof (nil)", err) |
| + } |
| +} |
| + |
| // Flag on, a failed seed's role child is proven empty before the database |
| // it built is removed. |
| func TestAFlagOnFailedSeedProvesItsRoleChildBeforeRemovingTheDB(t *testing.T) { |