carlos / platform Public

host: local validates the journalled prepared-manifest binding before skipping the build (Task 6 fix round 1, finding 3)

preparedManifestJournalled replaces filesManifestBuilt: the step counts
as done only with this install's binding (decodePreparedManifest: P's
key, the op, the captured stamp, a digest and a version). The legacy
"skipped" value still counts as not done and is rebuilt. An empty or
malformed value, or a binding for another key, op or stamp, is an
error: the install neither advances over it nor rebuilds over it, and
the journal keeps the value.

Test: a replay over each damaged value leaves the record at local,
writes nothing under the location's files prefix, keeps the value and
never reaches files_gen.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b9ca273fb08f67a1b2ee74451ddc41015ef9e033 parent ab629d8
2 files changed, +77 −7
  • internal/host/movesinstall.go +22 −7
  • internal/host/movesland_test.go +55 −0
diff --git a/internal/host/movesinstall.go b/internal/host/movesinstall.go
index 6a72ed6ce..72c5d5293 100644
--- a/internal/host/movesinstall.go
+++ b/internal/host/movesinstall.go
@@ -293,13 +293,24 @@ func (installAuthorityStore) DeleteVersion(context.Context, string, string, stri
return fmt.Errorf("%w: the prepared build never deletes", errLandingChanged)
}
-// filesManifestBuilt reports whether the journal records the prepared
-// files manifest as built: the step done with a binding, not the "skipped"
-// value journals written before C8 existed, which a replay builds (ruling
-// Q3 item 2; the step itself is create-only and replay-safe).
-func filesManifestBuilt(j *moves.Journal) bool {
+// preparedManifestJournalled reports whether the journal records the
+// prepared files manifest as built: the step done with this install's
+// binding (decodePreparedManifest: P's key, the op and the captured
+// stamp), not the "skipped" value journals written before C8 existed,
+// which a replay builds (ruling Q3 item 2; the step itself is create-only
+// and replay-safe). Any other value is an error (fix round 1, finding 3):
+// an empty or malformed step, or a binding for another key, op or stamp,
+// is a damaged or foreign journal, and the install neither advances over
+// it nor rebuilds over it.
+func preparedManifestJournalled(j *moves.Journal, key, op string, stamp uint64) (bool, error) {
v, ok := j.Steps[landStepFilesManifest]
- return ok && v != landingFilesManifestSkipped
+ if !ok || v == landingFilesManifestSkipped {
+ return false, nil
+ }
+ if _, err := decodePreparedManifest(v, key, op, stamp); err != nil {
+ return false, err
+ }
+ return true, nil
}
// fault is the test seam after a side effect (the landing's points).
@@ -846,7 +857,11 @@ func (ir *installRun) local_(ctx context.Context) (bool, error) {
return false, err
}
}
- if !filesManifestBuilt(j) {
+ built, err := preparedManifestJournalled(j, hostfiles.ManifestKey(movesFilesPrefix(holder.Location, gen)), id, captured.Stamp)
+ if err != nil {
+ return false, err
+ }
+ if !built {
// Spec rev 7.22 §7.6 (L17, R19-1; C8, Task 6): the prepared files
// manifest under files/g-<generation>/ in the location, bound to
// {location, g-<generation>, op, stamp}, where the generation is the
diff --git a/internal/host/movesland_test.go b/internal/host/movesland_test.go
index 0c6477db3..a93862691 100644
--- a/internal/host/movesland_test.go
+++ b/internal/host/movesland_test.go
@@ -1406,6 +1406,61 @@ func TestSkippedManifestStepIsBuiltOnReplay(t *testing.T) {
}
}
+// A journalled files_manifest value that is not this install's binding
+// (fix round 1, finding 3): empty, malformed, or a binding for another op,
+// stamp or key. The install neither skips the build over it nor rebuilds
+// over it: the replay refuses, the record stays at local, nothing is
+// written under the files prefix and the journal keeps the value for an
+// operator to see.
+func TestLocalRefusesADamagedFilesManifestJournal(t *testing.T) {
+ good := func(h *landingHarness) preparedManifest {
+ c := h.claim(t)
+ return preparedManifest{Key: hostfiles.ManifestKey(movesFilesPrefix(c.Holder.Location, p1ID)), SHA256: sha([]byte("x")), Version: "v1", Op: p1ID, Stamp: c.Holder.Stamp}
+ }
+ for name, plant := range map[string]func(h *landingHarness) string{
+ "empty": func(*landingHarness) string { return "" },
+ "malformed": func(*landingHarness) string { return `{"key":` },
+ "another op": func(h *landingHarness) string { pm := good(h); pm.Op = "mv_other"; return string(mustJSON(t, pm)) },
+ "another stamp": func(h *landingHarness) string { pm := good(h); pm.Stamp++; return string(mustJSON(t, pm)) },
+ "another key": func(h *landingHarness) string {
+ pm := good(h)
+ pm.Key = hostfiles.ManifestKey(movesFilesPrefix(h.claim(t).Holder.Location, "mv_other"))
+ return string(mustJSON(t, pm))
+ },
+ "no version": func(h *landingHarness) string { pm := good(h); pm.Version = ""; return string(mustJSON(t, pm)) },
+ } {
+ t.Run(name, func(t *testing.T) {
+ h := newLandingHarness(t)
+ h.flip(t)
+ h.landAt = "after-verify"
+ h.crashLand(t)
+ value := plant(h)
+ j := h.landJournal(t)
+ j.Mark(landStepFilesManifest, value)
+ if err := j.Commit(h.tdir); err != nil {
+ t.Fatal(err)
+ }
+ h.freshTarget()
+ h.land(t)
+ if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandLocal || m.Reason != "" {
+ t.Fatalf("the record moved over a damaged journal: %+v", m)
+ }
+ j = h.landJournal(t)
+ if got := j.Steps[landStepFilesManifest]; got != value {
+ t.Fatalf("the journal value changed: %q", got)
+ }
+ if _, done := j.Steps[landStepFilesGen]; done {
+ t.Fatal("the install went on past the damaged step")
+ }
+ for k := range h.f.buckets["data-uk"] {
+ if strings.HasPrefix(k, cutRoot+"/files/") {
+ t.Fatalf("written over a damaged journal: %s", k)
+ }
+ }
+ })
+ }
+}
+
// A cross-region landing's prepared manifest names destination-local
// objects it materialized from the sealed archive (claim P6-D3): every
// object lies under the manifest's own prefix in the target's bucket, and