//go:build linux package hostexec import ( "context" "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/version" "os" "path/filepath" "runtime" "sync/atomic" "testing" "github.com/carlosframework/platform/internal/hostfloor" ) func TestPendingRevalidatesRunningImageAfterCompletedUpdate(t *testing.T) { cfg, _, _, store, _ := fixture(t) cfg.UpdateGuardDir = t.TempDir() cfg.UpdateJournal = filepath.Join(t.TempDir(), "journal.json") var oldImage atomic.Bool oldImage.Store(true) var checked atomic.Int32 cfg.verifyRunning = func(context.Context) error { checked.Add(1) if oldImage.Load() { return hostfloor.ErrFloor } return nil } // Suspend precisely after pending persistence. Root finished its update by // the time the controller resumes, so marker-only checking would acknowledge. cfg.writeLocal = func(d *localDir, s State) error { if err := d.write(s); err != nil { return err } marker := filepath.Join(cfg.UpdateGuardDir, "update-begun.json") if err := hostfloor.Durable(marker, []byte(`{"id":"update"}`), 0644); err != nil { return err } if err := hostfloor.Durable(filepath.Join(cfg.UpdateGuardDir, "completed.json"), []byte("update"), 0644); err != nil { return err } return hostfloor.RemoveDurable(marker) } c, err := Open(t.Context(), cfg) if c != nil || !errors.Is(err, hostfloor.ErrFloor) { t.Fatal("old executing image acknowledged", err) } if checked.Load() == 0 { t.Fatal("running-image verifier skipped") } store.mu.Lock() calls := store.calls store.mu.Unlock() if calls != 0 { t.Fatal("acknowledged before identity check", calls) } } func TestUpdateIdentityRecheckedBeforeLeaseAcknowledgement(t *testing.T) { cfg, _, _, store, _ := fixture(t) cfg.UpdateGuardDir = t.TempDir() cfg.UpdateJournal = filepath.Join(t.TempDir(), "journal.json") var checked atomic.Int32 cfg.verifyRunning = func(context.Context) error { if checked.Add(1) > 1 { return hostfloor.ErrFloor } return nil } c, err := Open(t.Context(), cfg) if c != nil || !errors.Is(err, hostfloor.ErrFloor) { t.Fatal("identity change during cleanup accepted", err) } store.mu.Lock() calls := store.calls store.mu.Unlock() if calls != 0 { t.Fatal("lease written", calls) } } func TestUpdateMarkerBlocksBeforeIdentityCheck(t *testing.T) { cfg, _, _, _, _ := fixture(t) cfg.UpdateGuardDir = t.TempDir() cfg.UpdateJournal = filepath.Join(t.TempDir(), "journal.json") cfg.verifyRunning = func(context.Context) error { t.Fatal("verification ran despite update"); return nil } for _, path := range []string{filepath.Join(cfg.UpdateGuardDir, "update-begun.json"), cfg.UpdateJournal} { if err := os.WriteFile(path, []byte("present"), 0644); err != nil { t.Fatal(err) } if err := cfg.checkUpdateIdentity(t.Context()); err == nil { t.Fatal("update accepted") } if err := os.Remove(path); err != nil { t.Fatal(err) } } } func TestRunningIdentityUsesExecutingImageAndCurrentFloor(t *testing.T) { oldBuild := version.Build version.Build = "v1.2.3" t.Cleanup(func() { version.Build = oldBuild }) pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "bucket") cfg := Config{UpdateGuardDir: t.TempDir(), UpdateJournal: filepath.Join(t.TempDir(), "journal.json"), Store: release.NewDirStore(t.TempDir())} sg := release.RootSigner(priv) if err := os.Mkdir(filepath.Join(cfg.UpdateGuardDir, "identities"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(cfg.UpdateGuardDir, "carlos"), []byte("persistent checker"), 0755); err != nil { t.Fatal(err) } writeFloor := func(min string) { floor, _ := json.Marshal(hostfloor.Floor{Min: 1, MinRelease: min}) auth, err := release.SignRootEnvelope(sg, hostfloor.Authorization{Schema: 1, Purpose: "moves-protocol", DeploymentBucket: "bucket", Key: hostfloor.Key, SHA256: hostfloor.Digest(floor)}) if err != nil { t.Fatal(err) } policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "armed", Floor: floor, Authorization: auth}) if err := hostfloor.Durable(filepath.Join(cfg.UpdateGuardDir, "policy.json"), policy, 0644); err != nil { t.Fatal(err) } for key, wire := range map[string][]byte{hostfloor.Key: floor, hostfloor.AuthKey: auth} { _, etag, _ := cfg.Store.Get(t.Context(), key) if _, err := cfg.Store.Put(t.Context(), key, wire, etag); err != nil { t.Fatal(err) } } } writeIdentity := func(hash string) { platform := runtime.GOOS + "-" + runtime.GOARCH rel := release.SystemRelease{Version: version.Build, ReleaseTag: "v1.2.3", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version.Build, platform), SHA256: hash}}} store := release.NewDirStore(t.TempDir()) if _, err := release.PromoteSystem(t.Context(), store, sg, "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } wire, _, err := store.Get(t.Context(), release.SystemPointerKey("router", "stable")) if err != nil { t.Fatal(err) } if err := hostfloor.Durable(filepath.Join(cfg.UpdateGuardDir, "identities", "v1.2.3.json"), wire, 0644); err != nil { t.Fatal(err) } } writeFloor("v1.2.3") hash, err := hostfloor.ExecutingHash() if err != nil { t.Fatal(err) } writeIdentity(hash) if err := cfg.checkUpdateIdentity(t.Context()); err != nil { t.Fatal("valid running image refused", err) } writeIdentity(hostfloor.Digest([]byte("replacement at current binary path"))) if err := cfg.checkUpdateIdentity(t.Context()); err == nil { t.Fatal("replacement identity authorized stale executing inode") } writeIdentity(hash) writeFloor("v1.2.4") if err := cfg.checkUpdateIdentity(t.Context()); err == nil { t.Fatal("resumed image below current floor authorized") } }