package release import ( "crypto/ed25519" "encoding/json" "errors" "os" "os/exec" "path/filepath" "strings" "testing" ) type retirementCrashStore struct { *healthyRetryStore boundary string } func (s *retirementCrashStore) Write(name string, raw []byte) error { if !strings.HasSuffix(name, ".retired") { return s.healthyRetryStore.Write(name, raw) } if s.boundary == "before" { os.Exit(75) } if s.boundary == "visible" { s.fail = 1 } err := s.healthyRetryStore.Write(name, raw) if err != nil && s.boundary != "visible" { return err } os.Exit(75) return nil } func TestPlan14HistoryRetirementProcessCrash(t *testing.T) { if boundary := os.Getenv("PLAN14_RETIRE_CRASH"); boundary != "" { dir := os.Getenv("PLAN14_RETIRE_DIR") wire, err := os.ReadFile(filepath.Join(dir, "signed")) if err != nil { t.Fatal(err) } pub, err := os.ReadFile(filepath.Join(dir, "public")) if err != nil { t.Fatal(err) } var g HistoryRecoveryRetirement if err := VerifyRootEnvelope(ed25519.PublicKey(pub), wire, &g); err != nil { t.Fatal(err) } h := &RootHistory{Store: &retirementCrashStore{healthyRetryStore: &healthyRetryStore{dir: dir, fault: os.ErrPermission}, boundary: boundary}} if err := h.RetireRecovery(pub, wire, g.Binding, g.IssuedAt, func(HistoryBinding) (string, error) { return "canonical", nil }); err != nil { t.Fatal(err) } t.Fatal("child missed crash boundary") } for _, boundary := range []string{"before", "durable", "visible"} { t.Run(boundary, func(t *testing.T) { _, s, old, _, g, pub, key := retirementFixture(t) wire, err := SignRootEnvelope(RootSigner(key), g) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(s.dir, "signed"), wire, 0600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(s.dir, "public"), pub, 0600); err != nil { t.Fatal(err) } cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14HistoryRetirementProcessCrash$", "-test.count=1") cmd.Dir = t.TempDir() cmd.Env = append(os.Environ(), "PLAN14_RETIRE_CRASH="+boundary, "PLAN14_RETIRE_DIR="+s.dir) out, err := cmd.CombinedOutput() var exit *exec.ExitError if !errors.As(err, &exit) || exit.ExitCode() != 75 { t.Fatalf("child: %v\n%s", err, out) } fault := &retirementSyncStore{Store: s, fail: boundary == "visible"} h := &RootHistory{Store: fault} view, statusErr := h.MigrationStatus(old.Binding, func(HistoryBinding) (string, error) { return "canonical", nil }) if boundary == "visible" { if statusErr == nil { t.Fatal("unsynced retirement completion after crash") } } else { expected := "" if boundary == "durable" { accepted, _ := json.Marshal(g) expected = reconstructionDigest(accepted) } if statusErr != nil || view.Hosts[0].RetirementDigests[old.Attempt.ID] != expected { t.Fatalf("crash completion: %+v %v", view, statusErr) } } refs, err := h.Refusals(old.Binding) if boundary == "visible" { if err == nil { t.Fatal("unsynced retirement granted after crash") } fault.fail = false refs, err = h.Refusals(old.Binding) } view, statusErr = h.MigrationStatus(old.Binding, func(HistoryBinding) (string, error) { return "canonical", nil }) expected := "" if boundary != "before" { accepted, _ := json.Marshal(g) expected = reconstructionDigest(accepted) } if statusErr != nil || view.Hosts[0].RetirementDigests[old.Attempt.ID] != expected { t.Fatalf("repaired completion: %+v %v", view, statusErr) } want := 0 if boundary == "before" { want = 1 } if err != nil || len(refs) != want { t.Fatalf("reopened retirement: %+v %v", refs, err) } raw, err := s.Read(historyRecoveryKey(old)) if err != nil { t.Fatal(err) } var retained historyRecoveryRecord if err := json.Unmarshal(raw, &retained); err != nil { t.Fatal(err) } if !retained.Failed || retained.Attempt.SHA256 != old.Attempt.SHA256 { t.Fatal("retirement erased evidence") } }) } }