package host import ( "context" "fmt" "os" "strings" "testing" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14OperatorUnitPredecessorStop(t *testing.T) { for _, attack := range []string{"none", "invocation", "cgroup", "inode", "dropin", "fragment", "missing property", "child remains", "lost stop"} { t.Run(attack, func(t *testing.T) { b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "") binding, err := b.establish(t.Context(), in.Host) if err != nil { t.Fatal(err) } invocation, group, state := "original", "/system.slice/console.service", "active" id := rootstate.FileIdentity{Device: 1, Inode: 2} populated, stops := true, 0 k := claimPredecessorKernel{group: func(string) (rootstate.FileIdentity, bool, error) { return id, populated, nil }} k.ctl = func(_ context.Context, args ...string) (string, error) { if args[0] == "stop" { stops++ state = "inactive" if attack != "child remains" { populated = false } if attack == "lost stop" { return "", os.ErrInvalid } return "", nil } out := fmt.Sprintf("ActiveState=%s\nInvocationID=%s\nControlGroup=%s\nFragmentPath=/etc/systemd/system/console.service\nDropInPaths=", state, invocation, group) switch attack { case "dropin": out += "/etc/systemd/system/console.service.d/override.conf" case "fragment": out = strings.ReplaceAll(out, "/etc/systemd/system", "/run/systemd/system") case "missing property": out = strings.ReplaceAll(out, "\nDropInPaths=", "") } return out, nil } p, err := k.capture(t.Context(), binding) if attack == "dropin" || attack == "fragment" || attack == "missing property" { if err == nil || stops != 0 { t.Fatal("bad definition accepted", err) } return } if err != nil { t.Fatal(err) } switch attack { case "invocation": invocation = "successor" case "cgroup": group = "/system.slice/sibling.service" case "inode": id.Inode++ } ctx, cancel := context.WithCancel(t.Context()) if attack == "child remains" { original := k.ctl k.ctl = func(ctx context.Context, args ...string) (string, error) { out, e := original(ctx, args...) if args[0] == "stop" { cancel() } return out, e } } defer cancel() err = k.stop(ctx, p) if attack == "none" { if err != nil || stops != 1 { t.Fatal(err, stops) } if err = k.stop(t.Context(), p); err != nil || stops != 1 { t.Fatal("stop replay", err, stops) } } else if err == nil { t.Fatal("unsafe stop accepted") } if (attack == "invocation" || attack == "cgroup" || attack == "inode") && stops != 0 { t.Fatal("stopped successor") } if attack == "lost stop" { if err = k.stop(t.Context(), p); err != nil || stops != 1 { t.Fatal("lost stop replay", err, stops) } } }) } } func TestPlan14PredecessorDeviceAcrossBoot(t *testing.T) { p := claimUnitPredecessor{Unit: "console.service", Invocation: "before", Group: "system.slice/console.service", GroupIdentity: rootstate.FileIdentity{Device: 1, Inode: 2}, Boot: "boot-before"} state := "inactive" calls := 0 k := claimPredecessorKernel{ boot: func() (string, error) { return "boot-after", nil }, group: func(string) (rootstate.FileIdentity, bool, error) { return rootstate.FileIdentity{Device: 8, Inode: 9}, state == "active", nil }, ctl: func(_ context.Context, args ...string) (string, error) { if args[0] != "show" { calls++ return "", nil } return "ActiveState=" + state + "\nInvocationID=\nControlGroup=\nFragmentPath=/etc/systemd/system/console.service\nDropInPaths=\n", nil }, } if err := k.stop(t.Context(), p); err != nil { t.Fatal("old boot empty group refused", err) } state = "active" if err := k.stop(t.Context(), p); err == nil { t.Fatal("old boot authorized stopping new population") } if calls != 0 { t.Fatal("systemctl stop crossed a boot") } }