package host import ( "context" "errors" "os" "strings" "testing" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14OperatorUnitMigration(t *testing.T) { for _, attack := range []string{"none", "no consent", "changed bytes", "replacement inode", "replacement invocation", "lost intent", "lost stop", "lost replace", "lost completion"} { t.Run(attack, func(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") old := []byte("[Service]\nExecStart=/opt/carlos/live/shop.example\n") shape.Program = strings.Repeat("a", 64) shape.PredecessorDigest = claimUnitDigest(old) if attack == "no consent" { shape.PredecessorDigest = "" } b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } binding, err := b.establish(t.Context(), in.Host) if err != nil { t.Fatal(err) } files := &plan14UnitFiles{body: old, id: plan14UnitIdentity(2)} state := &plan14UnitBindingFault{Store: &rollbackMemory{files: map[string][]byte{}}} stopped, replaced := false, false kernel := claimUnitPredecessor{Unit: shape.Unit, Invocation: "old", Group: "system.slice/console.service", GroupIdentity: rootstate.FileIdentity{Device: 3, Inode: 4}} m := claimUnitMaterializer{state: state, read: files.read, create: files.create, replace: files.replace, idle: func(string) error { if !stopped { return os.ErrPermission } return nil }} m.predecessor = func(context.Context, claimUnitBinding) (claimUnitPredecessor, error) { return kernel, nil } m.stopPredecessor = func(_ context.Context, got claimUnitPredecessor) error { r, e := m.load(in.Host) if e != nil || r.Migration == nil || r.Migration.Predecessor != got { t.Fatal("stop without durable exact intention", e) } if got != kernel { return os.ErrPermission } stopped = true if attack == "lost stop" { return os.ErrInvalid } return nil } m.replace = func(unit string, id rootstate.FileIdentity, body []byte) error { if !stopped { t.Fatal("replace before predecessor stop") } if err := files.replace(unit, id, body); err != nil { return err } replaced = true if attack == "lost completion" { state.fail = true } if attack == "lost replace" { return os.ErrInvalid } return nil } if attack == "changed bytes" { files.body = append(files.body, 'x') } if attack == "lost intent" { state.fail = true } if attack == "replacement inode" || attack == "replacement invocation" { // Persist the initial intention, then lose the reply before any effect. state.fail = true if m.install(t.Context(), binding) == nil { t.Fatal("lost intention accepted") } state.fail = false if attack == "replacement inode" { files.id.Inode++ } else { kernel.Invocation = "new" } } err = m.install(t.Context(), binding) if attack == "none" { if err != nil { t.Fatal(err) } } else if err == nil { t.Fatal("attack accepted") } if attack == "no consent" || attack == "changed bytes" || attack == "replacement inode" || attack == "replacement invocation" || attack == "lost intent" { if stopped || replaced { t.Fatal("unowned predecessor changed") } return } state.fail = false m.stopPredecessor = func(_ context.Context, got claimUnitPredecessor) error { if got != kernel { return os.ErrPermission } stopped = true return nil } m.replace = files.replace if err = m.install(t.Context(), binding); err != nil { t.Fatal("resume", err) } if files.retires != 1 { t.Fatal("repeated replacement", files.retires) } if err = m.verify(binding); err != nil { t.Fatal(err) } files.id.Inode++ if err = m.install(t.Context(), binding); !errors.Is(err, os.ErrPermission) { t.Fatal("rebound completed identity", err) } }) } }