| index 5cf68faaf..08b1f75aa 100644 |
| --- a/internal/release/proofgate_test.go |
| +++ b/internal/release/proofgate_test.go |
| @@ -118,6 +118,46 @@ func (e *proofEnv) putAt(t *testing.T, sha string, p *Proof) { |
| } |
| } |
| |
| +// manifestDownStore answers every other key from the wrapped store but fails |
| +// reads of one manifest with an error that is not a not-found, as a bucket |
| +// that is unreachable for that object would. |
| +type manifestDownStore struct { |
| + Store |
| + key string |
| +} |
| + |
| +func (m manifestDownStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + if key == m.key { |
| + return nil, "", errors.New("bucket unreachable for the manifest") |
| + } |
| + return m.Store.Get(ctx, key) |
| +} |
| + |
| +// stageStaleCopyKept stages stale as the binary of acct/app/ver, with the |
| +// canonical manifest (the store's exact bytes) as its sibling, and returns s |
| +// wrapped so that manifest cannot be read from the bucket. |
| +// |
| +// This is how stale bytes reach the binding check since staging hashes a copy |
| +// it reuses (spec section 4.2): an unreferenced copy that differs from its |
| +// release is restaged, so the check only still sees different bytes where the |
| +// canonical manifest cannot be read, a case ensureStagedChecked leaves as |
| +// staged. The sibling lets the proof gate still learn the manifest's sha256. |
| +func stageStaleCopyKept(t *testing.T, ctx context.Context, s Store, opt, acct, app, ver string, stale []byte) Store { |
| + t.Helper() |
| + key := ManifestKey(acct, app, ver) |
| + raw, _, err := s.Get(ctx, key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := StageArtifact(opt, acct, ver, app, stale); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := StageManifest(opt, acct, ver, app, raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + return manifestDownStore{Store: s, key: key} |
| +} |
| + |
| type proofReadCountingStore struct { |
| Store |
| mu sync.Mutex |
| @@ -232,8 +272,10 @@ func TestProofHealthyHistoryAdmitsWithoutProof(t *testing.T) { |
| } |
| } |
| |
| -// Review focus 2: the manifest's sha256 is proven, but the staged file (a |
| -// cache hit, which ensureStaged does not re-hash) holds other bytes. |
| +// Review focus 2: the manifest's sha256 is proven, but the staged file holds |
| +// other bytes. They reach the binding check through a kept stale copy: the |
| +// bucket cannot answer for the manifest, so staging leaves the copy as staged |
| +// (stageStaleCopyKept). |
| // |
| // Ruling 3: the record is its own kind, recovered and still on the old |
| // build, because nothing was restarted. The next pass's gate line says the |
| @@ -243,11 +285,9 @@ func TestProofBindingRefusesStagedBytesThatDiffer(t *testing.T) { |
| e := newProofEnv(t) |
| e.requireDev(t) |
| stale := []byte("not the shipped bytes") |
| - if err := StageArtifact(e.opt, "bab", "new", "console", stale); err != nil { |
| - t.Fatal(err) |
| - } |
| + down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "new", stale) |
| e.putAt(t, e.newSHA, e.signed(t, e.prover, "console", e.newSHA, "dev")) |
| - out := e.pass(t, e.store) |
| + out := e.pass(t, down) |
| if e.live() != "old" || !strings.Contains(out, "not pointed live") { |
| t.Fatalf("live = %q, printed %q; want old and the binding refusal", e.live(), out) |
| } |
| @@ -272,7 +312,8 @@ func TestProofBindingRefusesStagedBytesThatDiffer(t *testing.T) { |
| } |
| |
| // Final review triage (Task 4): the healthy history admits the manifest's |
| -// sha256, but the staged file holds other bytes. bind must ask the history |
| +// sha256, but the staged file holds other bytes (a kept stale copy, as in |
| +// TestProofBindingRefusesStagedBytesThatDiffer). bind must ask the history |
| // about the staged hash, not the manifest's, so the build is refused. |
| func TestProofHealthyManifestWithOtherStagedBytesIsRefused(t *testing.T) { |
| e := newProofEnv(t) |
| @@ -281,10 +322,8 @@ func TestProofHealthyManifestWithOtherStagedBytesIsRefused(t *testing.T) { |
| t.Fatal(err) |
| } |
| stale := []byte("not the shipped bytes") |
| - if err := StageArtifact(e.opt, "bab", "new", "console", stale); err != nil { |
| - t.Fatal(err) |
| - } |
| - out := e.pass(t, e.store) |
| + down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "new", stale) |
| + out := e.pass(t, down) |
| if e.live() != "old" || !strings.Contains(out, "not pointed live") { |
| t.Fatalf("live = %q, printed %q; want old and the binding refusal", e.live(), out) |
| } |
| @@ -517,16 +556,15 @@ func TestProofGateNilIsOff(t *testing.T) { |
| } |
| |
| // bindingRefusal is TestProofBindingRefusesStagedBytesThatDiffer's first |
| -// pass: a proven manifest, other bytes staged, a RefusalNotProven written. |
| +// pass: a proven manifest, other bytes staged (kept, see stageStaleCopyKept), |
| +// a RefusalNotProven written. |
| func bindingRefusal(t *testing.T) *proofEnv { |
| t.Helper() |
| e := newProofEnv(t) |
| e.requireDev(t) |
| - if err := StageArtifact(e.opt, "bab", "new", "console", []byte("not the shipped bytes")); err != nil { |
| - t.Fatal(err) |
| - } |
| + down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "new", []byte("not the shipped bytes")) |
| e.putAt(t, e.newSHA, e.signed(t, e.prover, "console", e.newSHA, "dev")) |
| - e.pass(t, e.store) |
| + e.pass(t, down) |
| if r, _ := ReadRefusal(e.bake, proofHost); r == nil || r.Kind != RefusalNotProven || e.live() != "old" { |
| t.Fatalf("refusal = %+v, live = %q; want a not-proven refusal and old", r, e.live()) |
| } |