//go:build linux && rootrunner package host import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootparticipation" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" "sync/atomic" "syscall" "testing" "time" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14RootFloorCrashOrderingRootRunner(t *testing.T) { for _, after := range []bool{false, true} { for fail := 1; fail <= 5; fail++ { dir := generationRootRunnerDir(t) id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} f, err := openRootMovesFloors(dir, id, true) if err != nil { t.Fatal(err) } a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} b := moves.Floor{Min: 2, MinRelease: "v2.0.0"} if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "root observation", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { t.Fatal(err) } n := 0 broken := f broken.private = &floorFaultStore{Store: f.private, n: &n, fail: fail, after: after} broken.public = &floorFaultStore{Store: f.public, n: &n, fail: fail, after: after} if err := broken.refresh(t.Context(), &b, nil, nil); err == nil { t.Fatal("injected write succeeded") } f.close() f, err = openRootMovesFloors(dir, id, false) if err != nil { t.Fatal(err) } if p, e := readRootFloorProjection(dir); e == nil && p.Floor != nil && *p.Floor == b { t.Fatal("failed write acknowledged B") } if err := f.refresh(t.Context(), &b, nil, nil); err != nil { t.Fatal("retry", err) } if p, e := readRootFloorProjection(dir); e != nil || p.Floor == nil || *p.Floor != b { t.Fatal("durable retry", p, e) } f.close() // Root-owned leaf under a service-owned ancestor must not open. if err := os.Chown(rootFloorDir(dir), 65534, 65534); err != nil { t.Fatal(err) } if _, err := rootstate.Open(filepath.Join(rootFloorDir(dir), "private"), false, false); err == nil { t.Fatal("hostile parent accepted") } if err := os.Chown(rootFloorDir(dir), 0, 0); err != nil { t.Fatal(err) } } } } func TestPlan14FloorRiseWithoutPlatformUpdateRootRunner(t *testing.T) { for _, prev := range []string{"none", "v2.0.0", "v1.0.0"} { t.Run(prev, func(t *testing.T) { opt := generationRootRunnerDir(t) dd := filepath.Join(opt, "data") if err := os.Mkdir(dd, 0755); err != nil { t.Fatal(err) } t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy") pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) rootFloorRunnerBinary(t, opt, systemBinaryPath(opt), "v2.0.0", key) if prev != "none" { rootFloorRunnerBinary(t, opt, systemPrevPath(opt), prev, key) } id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} f, err := openRootMovesFloors(opt, id, true) if err != nil { t.Fatal(err) } defer f.close() a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { t.Fatal(err) } store := release.NewDirStore(t.TempDir()) raw, _ := json.Marshal(b) if _, err := store.Put(t.Context(), moves.FloorKey, raw, ""); err != nil { t.Fatal(err) } if _, err := syncLocalFloor(t.Context(), store, dd); err != nil { t.Fatal(err) } if ad := withMovesFields(edgesFile{Box: "box"}, dd, opt); ad.MovesFloorMinRelease == b.MinRelease { t.Fatal("agent mirror acknowledged B") } gets := 0 j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), idle: func(context.Context) error { return nil }, binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }, legacy: func() ([]*moves.Floor, error) { return rootFloorLegacy(dd) }, remote: func(ctx context.Context) (*moves.Floor, error) { gets++; return moves.ReadFloor(ctx, store) }} if err := j.refresh(t.Context()); err != nil { t.Fatal(err) } ad := withMovesFields(edgesFile{Box: "box"}, dd, opt) if gets != 1 || ad.MovesFloorState != "accepted" || ad.MovesFloorMinRelease != b.MinRelease || ad.PrevRelease != prev { t.Fatal("root projection", gets, ad) } if (ad.PrevAllowedFor == b.MinRelease) != (prev == "v2.0.0") { t.Fatal("prev verdict", ad) } mirror := filepath.Join(dd, ".moves") if err := os.Rename(mirror, mirror+"-moved"); err != nil { t.Fatal(err) } if err := os.Mkdir(mirror, 0700); err != nil { t.Fatal(err) } renamed := withMovesFields(edgesFile{Box: "box"}, dd, opt) if renamed.MovesFloorMinRelease != ad.MovesFloorMinRelease || renamed.PrevRelease != ad.PrevRelease { t.Fatal("mirror rename changed acceptance") } if err := os.RemoveAll(filepath.Join(dd, ".moves")); err != nil { t.Fatal(err) } again := withMovesFields(edgesFile{Box: "box"}, dd, opt) if again.MovesFloorMinRelease != ad.MovesFloorMinRelease || again.PrevRelease != ad.PrevRelease { t.Fatal("mirror deletion changed acceptance") } if err := os.WriteFile(systemBinaryPath(opt), []byte("forged"), 0755); err != nil { t.Fatal(err) } if j.refresh(t.Context()) == nil { t.Fatal("unsigned binary accepted") } if withMovesFields(edgesFile{Box: "box"}, dd, opt).MovesFloorState == "accepted" { t.Fatal("unsigned current acknowledged") } }) } } func rootFloorRunnerBinary(t *testing.T, opt, path, version string, key ed25519.PrivateKey) { t.Helper() raw := []byte("#!/bin/sh\n# " + version + "\nexit 0\n") if err := os.WriteFile(path, raw, 0755); err != nil { t.Fatal(err) } identities, err := rootstate.Open(filepath.Join(opt, "system", "guard", "identities"), true, true) if err != nil { t.Fatal(err) } defer identities.Close() sum := sha256.Sum256(raw) platform := runtime.GOOS + "-" + runtime.GOARCH p := release.SystemPointer{Schema: 1, Component: "router", Channel: "stable", Spread: "0s", PromotedAt: time.Now(), Current: release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version, platform), SHA256: hex.EncodeToString(sum[:])}}}} wire, err := release.SignRootEnvelope(release.RootSigner(key), p) if err != nil { t.Fatal(err) } if _, err := release.VerifySystemIdentity(key.Public().(ed25519.PublicKey), wire); err != nil { t.Fatal(err) } if err := identities.Write(version+".json", wire); err != nil { t.Fatal(err) } } func TestPlan14RootFloorAgentProjectionPayload(t *testing.T) { opt := os.Getenv("PLAN14_P3_AGENT_OPT") if opt == "" || os.Geteuid() == 0 { t.Fatal("service-UID payload only") } ad := withMovesFields(edgesFile{Box: "box"}, filepath.Join(opt, "data"), opt) if ad.MovesFloorState != "accepted" || ad.MovesFloorMinRelease != "v2.0.0" { t.Fatal("service projection refused", ad) } // The same UID must be unable to change either projection or private state. for _, path := range []string{filepath.Join(rootFloorDir(opt), rootFloorProjectionFile), filepath.Join(rootFloorDir(opt), "private", rootFloorAccepted)} { if err := os.WriteFile(path, []byte("forged"), 0644); err == nil { t.Fatal("service changed root floor") } } } func TestPlan14RootFloorAdoptRefreshRootRunner(t *testing.T) { // This fixture owns its whole namespace; refuse any existing root census. if err := os.Mkdir("/var/lib/carlos-claims", 0700); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.RemoveAll("/var/lib/carlos-claims"); err != nil { t.Error(err) } }) census, err := rootstate.Open(rootparticipation.Namespace, true, false) if err != nil { t.Fatal(err) } raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) if err := census.Write("census.json", raw); err != nil { t.Fatal(err) } census.Close() opt := generationRootRunnerDir(t) dd := filepath.Join(opt, "data") if err := os.Mkdir(dd, 0755); err != nil { t.Fatal(err) } storeDir := filepath.Join(opt, "deployment") if err := os.Mkdir(storeDir, 0755); err != nil { t.Fatal(err) } t.Setenv("CARLOS_OPT_DIR", opt) t.Setenv("CARLOS_DATA_DIR", dd) t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy") t.Setenv("CARLOS_DEPLOYMENT_DIR", storeDir) pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) rootFloorRunnerBinary(t, opt, systemBinaryPath(opt), "v2.0.0", key) id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} f, err := openRootMovesFloors(opt, id, true) if err != nil { t.Fatal(err) } defer f.close() a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { t.Fatal(err) } store := release.NewDirStore(storeDir) raw, _ = json.Marshal(b) if _, err := store.Put(t.Context(), moves.FloorKey, raw, ""); err != nil { t.Fatal(err) } if _, err := syncLocalFloor(t.Context(), store, dd); err != nil { t.Fatal(err) } // The real provider reads only this loopback S3 fixture; it makes no AWS call. var gets atomic.Int32 endpoint := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" || r.URL.Path != "/deploy/"+moves.FloorKey { http.NotFound(w, r) return } gets.Add(1) w.Header().Set("ETag", "fixture") w.Write(raw) })) defer endpoint.Close() t.Setenv("AWS_REGION", "eu-west-1") t.Setenv("AWS_DEFAULT_REGION", "eu-west-1") t.Setenv("AWS_ACCESS_KEY_ID", "fixture") t.Setenv("AWS_SECRET_ACCESS_KEY", "fixture") t.Setenv("AWS_SESSION_TOKEN", "") t.Setenv("AWS_EC2_METADATA_DISABLED", "true") t.Setenv("AWS_ENDPOINT_URL_S3", endpoint.URL) t.Setenv("CARLOS_S3_ENDPOINT", endpoint.URL) t.Setenv("AWS_PROFILE", "") t.Setenv("AWS_DEFAULT_PROFILE", "") if err := withClaimRootRelease(t.Context(), true, func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(opt, "release"), true, false) }, func() error { return rootJudgePrev(nil) }); err != nil { t.Fatal(err) } if gets.Load() == 0 { t.Fatal("no authoritative GET before no-prev return") } ad := withMovesFields(edgesFile{Box: "box"}, dd, opt) if ad.MovesFloorState != "accepted" || ad.MovesFloorMinRelease != b.MinRelease || ad.PrevRelease != "none" { t.Fatal("actual adopt refresh", ad) } rawBinary, err := os.ReadFile(os.Args[0]) if err != nil { t.Fatal(err) } agentBinary := filepath.Join(opt, "agent.test") if err := os.WriteFile(agentBinary, rawBinary, 0755); err != nil { t.Fatal(err) } cmd := exec.CommandContext(t.Context(), agentBinary, "-test.run=^TestPlan14RootFloorAgentProjectionPayload$", "-test.v") cmd.Dir = opt cmd.Env = append(os.Environ(), "PLAN14_P3_AGENT_OPT="+opt) cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 65534, Gid: 65534}} if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("service UID: %v\n%s", err, out) } // Direct apply/recovery adapters cannot use erased legacy files to go back. if err := os.RemoveAll(filepath.Join(dd, ".moves")); err != nil { t.Fatal(err) } app := systemApplier{optDir: opt, dataDir: dd, rootMoves: rootMovesFloorIdentity} if app.floorAllowsForward("v1.0.0") == nil { t.Fatal("legacy deletion allowed downgrade") } if err := app.floorAllowsForward("v2.0.0"); err != nil { t.Fatal("same floor refused", err) } } func TestPlan14RootFloorSignedTargetRootRunner(t *testing.T) { opt := generationRootRunnerDir(t) pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} f, err := openRootMovesFloors(opt, id, true) if err != nil { t.Fatal(err) } defer f.close() b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &b, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { t.Fatal(err) } store := release.NewDirStore(t.TempDir()) platform := runtime.GOOS + "-" + runtime.GOARCH bytes := []byte("new signed target without earlier root history") sum := sha256.Sum256(bytes) p, err := release.PromoteSystem(t.Context(), store, release.RootSigner(key), "router", "stable", release.SystemRelease{Version: "v3.0.0", ReleaseTag: "v3.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey("v3.0.0", platform), SHA256: hex.EncodeToString(sum[:])}}}, release.SystemPromoteOptions{}) if err != nil { t.Fatal(err) } staged := filepath.Join(opt, "staging", "carlos") if err := os.Mkdir(filepath.Dir(staged), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(staged, bytes, 0755); err != nil { t.Fatal(err) } calls := 0 a := systemApplier{optDir: opt, channel: "stable", rootMoves: func() (rootFloorIdentity, bool, error) { return id, true, nil }, openStore: func(context.Context) (release.Store, ed25519.PublicKey, error) { return store, pub, nil }, ops: sysOps{run: func(context.Context, time.Duration, string, ...string) (string, error) { calls++; return "ran", nil }}} if _, err := a.verifyPointer(t.Context()); err != nil { t.Fatal(err) } if _, err := a.guardRun(t.Context(), time.Second, staged, "version"); err != nil || calls != 1 { t.Fatal("signed target with unknown history refused", err, calls) } for _, storeName := range []struct { store rootstate.Store name string }{{f.private, rootFloorAccepted}, {f.private, rootFloorDeny}, {f.public, rootFloorSelector}, {f.public, rootFloorProjectionFile}} { if err := storeName.store.Remove(storeName.name); err != nil { t.Fatal(err) } } if _, err := a.guardRun(t.Context(), time.Second, staged, "version"); err != nil || calls != 2 { t.Fatal("unknown floor blocked signed version", err, calls) } changed := *p changed.Current.Version = "other" if rememberRootFloorSystemIdentity(t.Context(), store, pub, opt, &changed) == nil { t.Fatal("pointer substitution accepted") } if err := os.WriteFile(staged, []byte("substitution"), 0755); err != nil { t.Fatal(err) } if _, err := a.guardRun(t.Context(), time.Second, staged, "version"); err == nil || calls != 2 { t.Fatal("unsigned target executed", err, calls) } }