package host import ( "context" "os" "strings" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14GenerationRetirementRootBinding(t *testing.T) { for _, attack := range []string{"none", "open-writers", "unsettled", "controller", "inputs", "invocation", "executable", "installation", "kernel", "no-kernel", "no-journal"} { t.Run(attack, func(t *testing.T) { f, disk, _ := generationLaunchFixture(t) if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } r := readGenerationLaunch(t, disk) if attack != "unsettled" { if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { t.Fatal(err) } } if attack != "open-writers" { if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { t.Fatal(err) } } b := r.Binding switch attack { case "controller": b.ControllerPID++ case "inputs": b.Inputs = "changed" case "invocation": b.Invocation = "changed" case "executable": b.Executable.Inode++ case "installation": b.Installation = "changed" } if b != r.Binding { r.Binding = b } calls := 0 verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { calls++ if attack == "kernel" { return "", os.ErrPermission } return r.Binding.Digest, nil }} if attack == "no-kernel" { verifier.kernel = nil } if attack == "no-journal" { verifier.journal = nil } err := verifier.closed(t.Context(), r) if err == nil { _, err = verifier.verify(t.Context(), r) } if attack == "none" { if err != nil || calls != 1 { t.Fatalf("positive: %v, kernel %d", err, calls) } } else if err == nil { t.Fatal("unverified retirement accepted") } }) } } func TestPlan14GenerationRetirementAfterLostStarted(t *testing.T) { f, disk, _ := generationLaunchFixture(t) permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req) if err != nil { t.Fatal(err) } if err := f.journal.IntendUnit(t.Context(), *f.req.Reservation); err != nil { t.Fatal(err) } if err := f.journal.SettleUnit(t.Context(), f.target, f.req.Reservation.ID); err != nil { t.Fatal(err) } if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { t.Fatal(err) } verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return permit.reservation.Binding.Digest, nil }} if err := verifier.retire(t.Context(), &release.RootGenerations{Store: disk}, permit.reservation); err != nil { t.Fatal(err) } got, err := permit.ledger.Read(permit.reservation.Binding) if err != nil || got.Retirement == "" { t.Fatalf("pending not retired: %+v %v", got, err) } if err := f.journal.IntendUnit(t.Context(), *f.req.Reservation); err == nil { t.Fatal("closed launch replayed") } if _, err := permit.ledger.Reserve(permit.reservation.Binding); err == nil { t.Fatal("retired generation replayed") } } func TestPlan14GenerationRetirementKeepsActivePin(t *testing.T) { for _, mode := range []string{"running", "running-wrong-controller", "running-wrong-inputs", "running-wrong-process", "settled-open-writers", "closed-unsettled", "closed", "wrong-controller", "wrong-inputs", "kernel"} { t.Run(mode, func(t *testing.T) { f, disk, _ := generationLaunchFixture(t) if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } r := readGenerationLaunch(t, disk) if !strings.HasPrefix(mode, "running") && mode != "closed-unsettled" { if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { t.Fatal(err) } } if !strings.HasPrefix(mode, "running") && mode != "settled-open-writers" { if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { t.Fatal(err) } } if mode == "wrong-controller" || mode == "running-wrong-controller" { r.Binding.ControllerPID++ } if mode == "wrong-inputs" || mode == "running-wrong-inputs" { r.Binding.Inputs = "other" } if mode == "running-wrong-process" { r.Process.PID++ } calls := 0 v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { calls++ if mode == "kernel" { return "", os.ErrPermission } return r.Binding.Digest, nil }} err := v.retire(t.Context(), &release.RootGenerations{Store: disk}, r) got := readGenerationLaunch(t, disk) switch mode { case "running", "settled-open-writers", "closed-unsettled": if err != nil || calls != 0 || got != r { t.Fatalf("active pin changed or blocked the pass: %v calls=%d got=%+v", err, calls, got) } case "closed": if err != nil || calls != 1 || got.Retirement == "" { t.Fatalf("closed pin not retired: %v calls=%d got=%+v", err, calls, got) } default: if err == nil || got.Retirement != "" { t.Fatalf("uncertain pin released: %v %+v", err, got) } } }) } } func TestPlan14GenerationRetirementCensusGate(t *testing.T) { for _, flag := range []string{"", "1"} { for _, mode := range []string{"legacy", "census", "unreadable"} { t.Run(flag+"/"+mode, func(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", flag) f, disk, _ := generationLaunchFixture(t) if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } r := readGenerationLaunch(t, disk) if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { t.Fatal(err) } if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { t.Fatal(err) } opened, retired := 0, 0 source := productionRootGenerationPins().(generationPinSource) source.legacy = func() (bool, error) { if mode == "unreadable" { return false, os.ErrPermission } return mode == "legacy", nil } source.open = func() (rootstate.Store, error) { opened++; return disk, nil } source.retire = func(ctx context.Context, g *release.RootGenerations) error { retired++ verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil }} return verifier.retire(ctx, g, r) } _, err := source.StagedPins(t.Context(), f.root.generations.optDir) got := readGenerationLaunch(t, disk) switch mode { case "legacy": if err != nil || opened != 0 || retired != 0 || got != r { t.Fatalf("claims-off effects: %v %d %d %+v", err, opened, retired, got) } case "unreadable": if err == nil || opened != 0 || retired != 0 || got != r { t.Fatal("unknown census chose a path") } case "census": if err == nil || opened != 1 || retired != 1 || got.Retirement == "" { t.Fatalf("retirement substituted for completeness: %v %d %d %+v", err, opened, retired, got) } } }) } } } func TestPlan14GenerationRetirementContinuesPastActivePin(t *testing.T) { f, disk, _ := generationLaunchFixture(t) if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } running := readGenerationLaunch(t, disk) target := f.target target.ControllerID = "closed-controller" prior, err := f.journal.Read(t.Context(), f.target) if err != nil { t.Fatal(err) } prior.Target, prior.Reservations = target, nil prior.Files, prior.ProducerIDs, prior.NoState = nil, nil, true if err := f.journal.Prelaunch(t.Context(), prior); err != nil { t.Fatal(err) } b := running.Binding b.Target = generationOwner(target) b.Attempt, b.Invocation = "second:main", "second-invocation" g := &release.RootGenerations{Store: disk} second, err := g.Reserve(b) if err != nil { t.Fatal(err) } unit := *f.req.Reservation unit.Target, unit.ID, unit.InvocationID = target, b.Attempt, b.Invocation if err := f.journal.IntendUnit(t.Context(), unit); err != nil { t.Fatal(err) } if err := f.journal.SettleUnit(t.Context(), target, unit.ID); err != nil { t.Fatal(err) } if err := f.journal.CloseWriters(t.Context(), target); err != nil { t.Fatal(err) } calls := 0 v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(_ context.Context, r release.GenerationReservation) (string, error) { calls++ if r.Binding != second.Binding { t.Fatal("active reservation reached kernel retirement") } return r.Binding.Digest, nil }} if err := v.retireAll(t.Context(), g, []release.GenerationReservation{running, second}); err != nil { t.Fatal(err) } got, err := g.Read(running.Binding) if err != nil || got != running { t.Fatalf("active pin changed: %+v %v", got, err) } got, err = g.Read(second.Binding) if err != nil || calls != 1 || got.Retirement == "" { t.Fatalf("later closed pin not retired: %+v %v calls=%d", got, err, calls) } launch, err := f.journal.Read(t.Context(), f.target) if err != nil || launch.WritersClosed || launch.Reservations[0].State != hostbox.UnitStarted { t.Fatalf("active launch changed: %+v %v", launch, err) } } func TestPlan14GenerationRetirementKeepsPrelaunchPin(t *testing.T) { f, disk, _ := generationLaunchFixture(t) permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req) if err != nil { t.Fatal(err) } r := permit.reservation v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { t.Fatal("prelaunch reservation reached kernel retirement") return "", os.ErrPermission }} if err := v.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil { t.Fatal(err) } if got := readGenerationLaunch(t, disk); got != r { t.Fatalf("prelaunch pin changed: %+v", got) } if err := v.closed(t.Context(), r); err == nil { t.Fatal("prelaunch mistaken for closed authority") } }