package host import ( "context" "errors" "fmt" "os" "path/filepath" "sync/atomic" "testing" "time" "crypto/ed25519" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type plan14HeldStore struct { *rollbackDisk token chan struct{} attempts atomic.Int32 } func (s *plan14HeldStore) Lock(ctx context.Context, _ string) (func(), error) { s.attempts.Add(1) select { case s.token <- struct{}{}: return func() { <-s.token }, nil case <-ctx.Done(): return nil, ctx.Err() } } func (s *plan14HeldStore) Close() error { return nil } type plan14HeldReader struct{ f *hrFixture } func (r plan14HeldReader) Read(ctx context.Context, host string) (hostclaim.Observed, error) { o, err := r.f.claims(ctx, host) o.ETag = "claim-etag" return o, err } func plan14HeldRoot(t *testing.T) (*hrFixture, hostguard.Guard, *plan14HeldStore, *plan14HeldStore) { t.Helper() f := newHRFixture(t) hosts := &plan14HeldStore{rollbackDisk: &rollbackDisk{dir: t.TempDir()}, token: make(chan struct{}, 1)} releases := &plan14HeldStore{rollbackDisk: &rollbackDisk{dir: t.TempDir()}, token: make(chan struct{}, 1)} g := hostguard.Guard{DataDir: f.data, LockDir: t.TempDir(), Claims: plan14HeldReader{f}, Root: hostguard.NewRootAuthority(hosts)} h := f.claim.Holder in := hostguard.RootInstallation{Host: f.claim.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: "uk-box", Enrollment: "en", Migration: 1} if err := g.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(g.DataDir, "hostguard"), 0700); err != nil { t.Fatal(err) } agent := g agent.Root = nil if err := agent.Activate(t.Context(), f.claim.Host, "uk-box", nil); err != nil { t.Fatal(err) } f.root.OpenRelease = func() (rootstate.Store, error) { return releases, nil } source := hostbox.WorkSource{Guard: g, Box: "uk-box", Region: "uk", Now: time.Now, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }, Policy: MovesClaimPolicy(f.records)} f.root.Works = func(ctx context.Context, _ hostbox.Actor, host, action string) (hostwrite.Work, error) { return source.Select(ctx, host, action) } f.root.Selected = hostbox.Selected return f, g, hosts, releases } func TestPlan14HeldReplayChecksRootFence(t *testing.T) { for _, attack := range []string{"delete", "rename", "replace lock", "forge"} { t.Run(attack, func(t *testing.T) { f, g, _, _ := plan14HeldRoot(t) q := f.request(heldReleasePublish) if _, err := f.root.Serve(t.Context(), hrPeer, q); err != nil { t.Fatal(err) } if out, err := f.root.Serve(t.Context(), hrPeer, q); err != nil || !out.Replayed { t.Fatal(out, err) } target := hostguard.FenceTarget{Host: q.Host, Incarnation: q.Incarnation, Stamp: q.Stamp, Session: q.Session, Box: q.Box, Enrollment: "en", FenceID: "later-root-fence", BootID: "boot", ControllerID: "edge", ClaimETag: "claim-etag", DirectoryRevision: "1", Op: q.MoveID, Phase: q.Phase} if _, err := g.BeginFence(t.Context(), target); err != nil { t.Fatal(err) } path := filepath.Join(g.DataDir, "hostguard", q.Host+".json") var err error switch attack { case "delete": err = os.Remove(path) case "rename": err = os.Rename(filepath.Dir(path), filepath.Dir(path)+".old") case "replace lock": err = os.Rename(g.LockDir, g.LockDir+".old") if err == nil { err = os.Mkdir(g.LockDir, 0700) } case "forge": err = os.WriteFile(path, []byte(`{}`), 0600) } if err != nil { t.Fatal(err) } if _, err := f.root.Serve(t.Context(), hrPeer, q); !errors.Is(err, hostguard.ErrFenced) { t.Fatalf("fenced replay: %v", err) } pending, err := g.PendingFences(t.Context()) if err != nil || len(pending) != 1 || pending[0] != target { t.Fatal(pending, err) } }) } } func TestPlan14AdoptPublishNoLockInversion(t *testing.T) { for _, first := range []string{"adopt", "publish", "cancel"} { t.Run(first, func(t *testing.T) { f, g, hosts, releases := plan14HeldRoot(t) ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second) defer cancel() entered := make(chan struct{}) unblock := make(chan struct{}) var active, effects atomic.Int32 effect := func(wait bool) error { if active.Add(1) != 1 { active.Add(-1) return errors.New("overlapping root effects") } defer active.Add(-1) if wait { close(entered) select { case <-unblock: case <-ctx.Done(): return ctx.Err() } } effects.Add(1) return nil } f.root.materialize = func(ctx context.Context, _ release.Store, _ ed25519.PublicKey, _ *release.DigestRatchet, _ string, _ registry.Route, _ string, _ *release.ProofGate, w hostwrite.Work, lock func(func() error) error) (*release.Manifest, error) { err := w.With(ctx, hostwrite.Identity{Release: "v7"}, func() error { return lock(func() error { return effect(first == "publish") }) }) return nil, err } adopt := func() error { return withRootReleaseStore(ctx, f.root.OpenRelease, func() error { captured, err := g.Capture(f.claim.Host) if err != nil { return err } return g.With(ctx, f.claim.Host, captured, func(*hostclaim.Claim) error { return nil }, func() error { return effect(first != "publish") }) }) } publish := func(c context.Context) error { _, err := f.root.Serve(c, hrPeer, f.request(heldReleasePublish)) return err } firstDone := make(chan error, 1) if first == "publish" { go func() { firstDone <- publish(ctx) }() } else { go func() { firstDone <- adopt() }() } select { case <-entered: case err := <-firstDone: t.Fatal("first failed", err) case <-ctx.Done(): t.Fatal(ctx.Err()) } hostAttempts := hosts.attempts.Load() releaseAttempts := releases.attempts.Load() secondDone := make(chan error, 1) waiting, stop := context.WithCancel(ctx) defer stop() if first == "publish" { go func() { secondDone <- adopt() }() } else { go func() { secondDone <- publish(waiting) }() } for releases.attempts.Load() == releaseAttempts { select { case err := <-secondDone: t.Fatal("waiter escaped release lock", err) case <-ctx.Done(): t.Fatal("release was not requested", ctx.Err()) case <-time.After(time.Millisecond): } } if hosts.attempts.Load() != hostAttempts { t.Fatal("waiter entered host lock before release") } if first == "cancel" { stop() select { case err := <-secondDone: if !errors.Is(err, context.Canceled) { t.Fatal(err) } case <-ctx.Done(): t.Fatal("canceled publisher stuck") } if effects.Load() != 0 { t.Fatal("canceled publisher performed effects") } if _, err := os.Stat(f.root.receiptPath(f.claim.Host)); !errors.Is(err, os.ErrNotExist) { t.Fatal("canceled publisher wrote receipt", err) } } close(unblock) if err := <-firstDone; err != nil { t.Fatal(err) } if first != "cancel" { if err := <-secondDone; err != nil { t.Fatal(err) } if effects.Load() != 2 { t.Fatal(effects.Load()) } } }) } } func TestPlan14HeldRevalidatesCanonicalBindingUnderGuard(t *testing.T) { for _, replay := range []bool{false, true} { for _, changed := range []string{"record incarnation", "record key"} { t.Run(fmt.Sprintf("%t/%s", replay, changed), func(t *testing.T) { f, _, _, _ := plan14HeldRoot(t) q := f.request(heldReleasePublish) if replay { if _, err := f.root.Serve(t.Context(), hrPeer, q); err != nil { t.Fatal(err) } } original := f.root.Works f.root.Works = func(ctx context.Context, actor hostbox.Actor, host, action string) (hostwrite.Work, error) { w, err := original(ctx, actor, host, action) f.set(func(f *hrFixture) { if changed == "record incarnation" { f.rec.CreatedAt = f.rec.CreatedAt.Add(time.Second) } else { f.claim.Holder.Key = "apps/bab/other/instances/" + q.Host + ".json" } }) return w, err } effects := 0 f.root.materialize = func(ctx context.Context, _ release.Store, _ ed25519.PublicKey, _ *release.DigestRatchet, _ string, _ registry.Route, _ string, _ *release.ProofGate, w hostwrite.Work, _ func(func() error) error) (*release.Manifest, error) { return nil, w.With(ctx, hostwrite.Identity{Release: q.Version}, func() error { effects++; return nil }) } if out, err := f.root.Serve(t.Context(), hrPeer, q); !errors.Is(err, hostbox.ErrChanged) || out.Replayed || effects != 0 { t.Fatalf("changed binding admitted: %+v %v effects=%d", out, err, effects) } }) } } }