hostclaim: a relocating probe with no P journaled may not certify its predecessor
relocationProbeOpening added nothing when no prepared generation was journaled, so a cold relocating probe with PredecessorCertified was admitted with no release, no pin and no writes-lost declaration. It is now refused; main's uncertified cold probe is unchanged. Merge review fix round 1, finding 6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+16
−1
internal/hostclaim/generation.go+6 −1internal/hostclaim/relocation_test.go+10 −0
diff --git a/internal/hostclaim/generation.go b/internal/hostclaim/generation.go| index 3229f98bf..d0d10fa7f 100644 |
| --- a/internal/hostclaim/generation.go |
| +++ b/internal/hostclaim/generation.go |
| @@ -308,7 +308,12 @@ func relocationProbeOpening(c *Claim, in Admission) error { |
| p, opened := c.Op.Journal["prepared"], preparedOpened(c) |
| if p == "" { |
| // No prepared generation journaled: main's cold probe as claim Plan |
| - // 11 accepted it. Region HA's ToProbing always journals P first. |
| + // 11 accepted it (region HA's ToProbing always journals P first). |
| + // With no P there is no release to back a certification, so a |
| + // certified relocating probe is refused. |
| + if in.PredecessorCertified { |
| + return fmt.Errorf("%w: a relocating probe certifies only P's opening", ErrWrongState) |
| + } |
| return nil |
| } |
| if (!opened && in.NewGeneration != p) || (opened && in.NewGeneration == p) { |
diff --git a/internal/hostclaim/relocation_test.go b/internal/hostclaim/relocation_test.go| index e0f76845e..b7ae71060 100644 |
| --- a/internal/hostclaim/relocation_test.go |
| +++ b/internal/hostclaim/relocation_test.go |
| @@ -697,6 +697,16 @@ func TestRelocationAdmissionRules(t *testing.T) { |
| t.Fatalf("admitted: %v", err) |
| } |
| }) |
| + t.Run("with no P journaled a probe certifies nothing", func(t *testing.T) { |
| + s, o := relAtPhase(t, PhaseRelocatingProbing) |
| + o = step(t, s, o, func(c *Claim) error { delete(c.Op.Journal, "prepared"); return nil }) |
| + if _, err := Update(ctx, s, o, AdmitWriter(probe(o, "P9", true))); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("a certified probe with no P was admitted: %v", err) |
| + } |
| + if _, err := Update(ctx, s, o, AdmitWriter(probe(o, "P9", false))); err != nil { |
| + t.Fatalf("main's uncertified cold probe: %v", err) |
| + } |
| + }) |
| } |
| func TestRelocationEmptyPinNeedsItsEvidence(t *testing.T) { |