host: the three claims-on record reads are judged readers, and the root restart source pin requires the caller's box judge
Fix round 1. channelRelease, rootGenerationSource.resolve and generationRequiredInventory read through the judged readers with the context's judge (nil on a claims-on box: today's reads exactly). TestRootRestartReadersAreJudged now requires each source literal's judge to be its caller's and each production call of the three constructors to be handed newBoxJudge's judge.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4 files changed,
+59
−3
internal/host/lifetime_gating_test.go+56 −0internal/host/relocatepass.go+1 −1internal/host/rootgeneration_requirements.go+1 −1internal/host/rootgenerations_launch.go+1 −1
diff --git a/internal/host/lifetime_gating_test.go b/internal/host/lifetime_gating_test.go| index 7924d1e02..ea91b4847 100644 |
| --- a/internal/host/lifetime_gating_test.go |
| +++ b/internal/host/lifetime_gating_test.go |
| @@ -639,6 +639,7 @@ func TestRootRestartReadersAreJudged(t *testing.T) { |
| } |
| fset := token.NewFileSet() |
| takes := map[string]bool{} |
| + calls := map[string]int{} |
| literals := 0 |
| for _, name := range files { |
| if strings.HasSuffix(name, "_test.go") { |
| @@ -671,6 +672,10 @@ func TestRootRestartReadersAreJudged(t *testing.T) { |
| for _, e := range lit.Elts { |
| if kv, ok := e.(*ast.KeyValueExpr); ok { |
| if k, ok := kv.Key.(*ast.Ident); ok && k.Name == "judge" { |
| + // The caller's judge, never a literal nil. |
| + if v, ok := kv.Value.(*ast.Ident); !ok || v.Name != "judge" { |
| + t.Errorf("%s: a rootRestartRequestSource whose judge is not its caller's", fset.Position(lit.Pos())) |
| + } |
| return true |
| } |
| } |
| @@ -678,6 +683,47 @@ func TestRootRestartReadersAreJudged(t *testing.T) { |
| t.Errorf("%s: a rootRestartRequestSource with no judge", fset.Position(lit.Pos())) |
| return true |
| }) |
| + // Every production call of the three hands them the box's judge: |
| + // newBoxJudge(...) itself, or a local assigned from it. |
| + fromBoxJudge := map[string]bool{} |
| + ast.Inspect(file, func(n ast.Node) bool { |
| + if as, ok := n.(*ast.AssignStmt); ok && len(as.Lhs) == len(as.Rhs) { |
| + for i, r := range as.Rhs { |
| + if isNewBoxJudge(r) { |
| + if id, ok := as.Lhs[i].(*ast.Ident); ok { |
| + fromBoxJudge[id.Name] = true |
| + } |
| + } |
| + } |
| + } |
| + return true |
| + }) |
| + ast.Inspect(file, func(n ast.Node) bool { |
| + call, ok := n.(*ast.CallExpr) |
| + if !ok { |
| + return true |
| + } |
| + id, ok := call.Fun.(*ast.Ident) |
| + if !ok { |
| + return true |
| + } |
| + at, ok := map[string]int{"productionRestartRequestPass": 3, "rootHistoryCreation": 1, "productionRootUpdatesHeld": 2}[id.Name] |
| + if !ok || len(call.Args) <= at { |
| + return true |
| + } |
| + calls[id.Name]++ |
| + arg := call.Args[at] |
| + if a, ok := arg.(*ast.Ident); ok && fromBoxJudge[a.Name] || isNewBoxJudge(arg) { |
| + return true |
| + } |
| + t.Errorf("%s: %s is not handed newBoxJudge's judge", fset.Position(call.Pos()), id.Name) |
| + return true |
| + }) |
| + } |
| + for _, fn := range []string{"productionRestartRequestPass", "rootHistoryCreation", "productionRootUpdatesHeld"} { |
| + if calls[fn] == 0 { |
| + t.Errorf("found no production call of %s", fn) |
| + } |
| } |
| if literals < 3 { |
| t.Fatalf("found %d production constructions of the source, want at least 3", literals) |
| @@ -691,3 +737,13 @@ func TestRootRestartReadersAreJudged(t *testing.T) { |
| t.Error("runAdoptUnderRootRelease does not hand productionRestartRequestPass root's judge") |
| } |
| } |
| + |
| +// isNewBoxJudge reports whether e is a call of newBoxJudge. |
| +func isNewBoxJudge(e ast.Expr) bool { |
| + c, ok := e.(*ast.CallExpr) |
| + if !ok { |
| + return false |
| + } |
| + id, ok := c.Fun.(*ast.Ident) |
| + return ok && id.Name == "newBoxJudge" |
| +} |
diff --git a/internal/host/relocatepass.go b/internal/host/relocatepass.go| index cb27db9b6..dc9cfa71f 100644 |
| --- a/internal/host/relocatepass.go |
| +++ b/internal/host/relocatepass.go |
| @@ -167,7 +167,7 @@ func channelRelease(store release.Store, pub ed25519.PublicKey) func(context.Con |
| if err != nil || obs.Absent || obs.Claim.Holder == nil { |
| return "", errors.Join(err, hostrelocate.ErrRefused) |
| } |
| - rec, err := provision.GetRecord(ctx, store, obs.Claim.Holder.Account, obs.Claim.Holder.App, host) |
| + rec, err := judgedRecord(ctx, store, provision.JudgeFrom(ctx), obs.Claim.Holder.Account, obs.Claim.Holder.App, host) |
| if err != nil { |
| return "", err |
| } |
diff --git a/internal/host/rootgeneration_requirements.go b/internal/host/rootgeneration_requirements.go| index e9887e9a9..37209675f 100644 |
| --- a/internal/host/rootgeneration_requirements.go |
| +++ b/internal/host/rootgeneration_requirements.go |
| @@ -52,7 +52,7 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps |
| } |
| continue |
| } |
| - rec, etag, err := provision.GetRecordETag(ctx, store, in.Account, in.App, in.Host) |
| + rec, etag, err := provision.ReadRecordJudged(ctx, store, provision.JudgeFrom(ctx), in.Account, in.App, in.Host) |
| if err != nil { |
| return zero, err |
| } |
diff --git a/internal/host/rootgenerations_launch.go b/internal/host/rootgenerations_launch.go| index f9fde5a8b..b1b1e5f85 100644 |
| --- a/internal/host/rootgenerations_launch.go |
| +++ b/internal/host/rootgenerations_launch.go |
| @@ -53,7 +53,7 @@ func (s *rootGenerationSource) resolve(ctx context.Context, h *ClaimLaunchRoot, |
| if in.Host != t.Host || in.Incarnation != t.Incarnation || in.Stamp != t.Stamp || in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 || in.Record != provision.RecordKey(in.Account, in.App, in.Host) { |
| return b, os.ErrPermission |
| } |
| - rec, etag, err := provision.GetRecordETag(ctx, s.store, in.Account, in.App, in.Host) |
| + rec, etag, err := provision.ReadRecordJudged(ctx, s.store, provision.JudgeFrom(ctx), in.Account, in.App, in.Host) |
| if err != nil { |
| return b, err |
| } |