carlos / platform Public

release: randomized G1/G2 check for the Authorize hook (#229)

Mutation checks, run from committed HEAD in throwaway copies:
- mutant 1 (hook moved before the pointer read): fails G1 at seed 4 of 400.
- mutant 2 (DirStore compare-and-swap disabled): fails G2 at seed 28 of 2000.
Both replay with DEPLOY_SIM_SEED=<n>. The unmutated run passes 2000 seeds.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 99dac9bd193683ff5bfa5c88937dd3a4a71325ec parent 69952eb
1 file changed, +189 −0
  • internal/release/authorize_sim_test.go +189 −0
diff --git a/internal/release/authorize_sim_test.go b/internal/release/authorize_sim_test.go
new file mode 100644
index 000000000..933fbf499
--- /dev/null
+++ b/internal/release/authorize_sim_test.go
@@ -0,0 +1,189 @@
+package release
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "errors"
+ "fmt"
+ "math/rand"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+// TestAuthorizeInterleavings runs random schedules of one deploy request
+// against an owner who installs and removes pipelines, promotes and rolls
+// back, at random points inside the request. It checks G1 and G2 and
+// nothing else (spec §7.2, §12):
+//
+// G1: the last hook call before the write came after the pointer read the
+// write was conditioned on, and the pipeline it was given permits it.
+// G2: the bytes the write replaced equal the bytes that read returned.
+//
+// Seeds: DEPLOY_SIM_SEEDS (default 400). A failure prints the seed and the
+// event log; DEPLOY_SIM_SEED=<n> replays exactly that seed.
+func TestAuthorizeInterleavings(t *testing.T) {
+ first, last := 0, 400
+ if v := os.Getenv("DEPLOY_SIM_SEEDS"); v != "" {
+ n, err := strconv.Atoi(v)
+ if err != nil {
+ t.Fatal(err)
+ }
+ last = n
+ }
+ if v := os.Getenv("DEPLOY_SIM_SEED"); v != "" {
+ n, err := strconv.Atoi(v)
+ if err != nil {
+ t.Fatal(err)
+ }
+ first, last = n, n+1
+ }
+ priv, _ := testKeys(t)
+ for seed := first; seed < last; seed++ {
+ simulateOnce(t, seed, priv)
+ }
+}
+
+type simRead struct {
+ seq int
+ body []byte
+ etag string
+ absent bool
+}
+
+type simHook struct {
+ seq int
+ pipelined bool
+}
+
+func simulateOnce(t *testing.T, seed int, priv ed25519.PrivateKey) {
+ t.Helper()
+ ctx := context.Background()
+ rng := rand.New(rand.NewSource(int64(seed)))
+ inner := NewDirStore(t.TempDir())
+ for _, v := range []string{"v1", "v2", "v3", "v4"} {
+ shipRelease(t, inner, "bab", "myapp", v, "linux-arm64", []byte("bin "+v))
+ }
+ for _, v := range []string{"v1", "v2"} {
+ if _, err := PromoteOnLadder(ctx, inner, priv, "bab", "myapp", "edge", v, PromoteOptions{Hotfix: true}); err != nil {
+ t.Fatalf("seed %d setup: %v", seed, err)
+ }
+ }
+ var (
+ events []string
+ reads []simRead
+ hooks []simHook
+ put *storeCall
+ replaced []byte
+ replacedAbsent bool
+ )
+ owner := func() {
+ var err error
+ var what string
+ switch rng.Intn(4) {
+ case 0:
+ what = "install"
+ _, err = SetPipeline(ctx, inner, priv, "bab", "myapp", []PipelineChannel{{Name: "edge"}, {Name: "stable"}}, "owner")
+ case 1:
+ what = "remove pipeline"
+ err = inner.Delete(ctx, PipelineKey("bab", "myapp"))
+ case 2:
+ v := fmt.Sprintf("v%d", 1+rng.Intn(4))
+ what = "promote " + v
+ _, err = PromoteOnLadder(ctx, inner, priv, "bab", "myapp", "edge", v, PromoteOptions{Hotfix: true})
+ case 3:
+ what = "rollback"
+ err = Rollback(ctx, inner, priv, "bab", "myapp", "edge")
+ }
+ events = append(events, fmt.Sprintf(" owner %s: %v", what, err))
+ }
+ h := &hookStore{inner: inner}
+ h.before = func(c storeCall) {
+ if rng.Float64() < 0.3 {
+ for i := 0; i <= rng.Intn(2); i++ {
+ owner()
+ }
+ }
+ events = append(events, fmt.Sprintf("%d %s %s %s", c.seq, c.op, c.key, c.ifMatch))
+ if c.op == "put" && c.key == edgeKey {
+ cc := c
+ put = &cc
+ body, _, err := inner.Get(ctx, edgeKey)
+ replaced, replacedAbsent = body, errors.Is(err, os.ErrNotExist)
+ }
+ }
+ h.after = func(c storeCall, body []byte, etag string, err error) {
+ if c.key == edgeKey {
+ reads = append(reads, simRead{c.seq, body, etag, errors.Is(err, os.ErrNotExist)})
+ }
+ }
+ hook := func(p *Pipeline) error {
+ hooks = append(hooks, simHook{h.seq, p != nil})
+ events = append(events, fmt.Sprintf("%d hook pipelined=%v", h.seq, p != nil))
+ return noPipelineOnly(p)
+ }
+ var err error
+ noop := false
+ switch rng.Intn(3) {
+ case 0:
+ var res PromoteResult
+ res, err = RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v3", PromoteOptions{Authorize: hook})
+ noop = res.NoOp
+ case 1:
+ var res PromoteResult
+ res, err = RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v2", PromoteOptions{Authorize: hook})
+ noop = res.NoOp
+ case 2:
+ err = RootSigner(priv).RollbackAuthorized(ctx, h, "bab", "myapp", "edge", hook)
+ }
+ fail := func(msg string) {
+ t.Fatalf("seed %d: %s\nreplay with DEPLOY_SIM_SEED=%d\n%s", seed, msg, seed, strings.Join(events, "\n"))
+ }
+ if err != nil {
+ return // refusals and conflicts write nothing
+ }
+ if noop {
+ if len(hooks) == 0 || hooks[len(hooks)-1].pipelined {
+ fail("a no-op succeeded without a clean hook call")
+ }
+ return
+ }
+ if put == nil {
+ fail("succeeded without writing")
+ }
+ var r *simRead
+ for i := len(reads) - 1; i >= 0; i-- {
+ rd := reads[i]
+ if rd.seq >= put.seq {
+ continue
+ }
+ if (put.ifMatch == "" && rd.absent) || (put.ifMatch != "" && !rd.absent && rd.etag == put.ifMatch) {
+ r = &rd
+ break
+ }
+ }
+ if r == nil {
+ fail("no pointer read matches the write's condition")
+ }
+ var a *simHook
+ for i := len(hooks) - 1; i >= 0; i-- {
+ if hooks[i].seq < put.seq {
+ a = &hooks[i]
+ break
+ }
+ }
+ if a == nil {
+ fail("a write landed with no hook call")
+ }
+ if a.seq <= r.seq {
+ fail(fmt.Sprintf("G1: the hook at %d is not after the pointer read at %d", a.seq, r.seq))
+ }
+ if a.pipelined {
+ fail("G1: the hook saw a pipeline and the write landed")
+ }
+ if replacedAbsent != r.absent || !bytes.Equal(replaced, r.body) {
+ fail("G2: the write replaced bytes other than the ones it read")
+ }
+}