package host import ( "context" "errors" "os" "strings" "testing" "github.com/carlosframework/platform/internal/rootstate" ) type plan14UnitFiles struct { body []byte id rootstate.FileIdentity creates, retires int } func (f *plan14UnitFiles) read(string) ([]byte, rootstate.FileIdentity, error) { if f.body == nil { return nil, rootstate.FileIdentity{}, os.ErrNotExist } return append([]byte(nil), f.body...), f.id, nil } func (f *plan14UnitFiles) create(_ string, b []byte) error { if f.body != nil { return os.ErrExist } f.body = append([]byte(nil), b...) f.id = plan14UnitIdentity(2) f.creates++ return nil } func (f *plan14UnitFiles) replace(_ string, id rootstate.FileIdentity, b []byte) error { if id != f.id { return os.ErrPermission } f.body = append([]byte(nil), b...) f.id.Inode++ f.retires++ return nil } func TestPlan14UnitMaterialization(t *testing.T) { for _, attack := range []string{"none", "foreign file", "busy install", "replacement", "changed bytes", "lost intent", "lost materialized", "busy", "retired"} { t.Run(attack, func(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") shape.Program = strings.Repeat("a", 64) 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{} state := &plan14UnitBindingFault{Store: &rollbackMemory{files: map[string][]byte{}}} m := claimUnitMaterializer{idle: func(string) error { return nil }, state: state, read: files.read, create: files.create, replace: files.replace} if attack == "foreign file" { files.body = []byte("foreign") files.id = plan14UnitIdentity(9) } if attack == "busy install" { m.idle = func(string) error { return os.ErrPermission } } if attack == "lost intent" { state.fail = true } err = m.install(t.Context(), binding) if attack == "foreign file" || attack == "lost intent" || attack == "busy install" { if err == nil || files.creates != 0 { t.Fatal("effect before durable intent", err, files.creates) } return } if err != nil { t.Fatal(err) } if files.creates != 1 { t.Fatal(files.creates) } if err = m.install(t.Context(), binding); err != nil || files.creates != 1 { t.Fatal("replay", err) } switch attack { case "replacement": files.id.Inode++ case "changed bytes": files.body = append(files.body, 'x') case "lost materialized": record, err := m.load(in.Host) if err != nil { t.Fatal(err) } record.Identity = rootstate.FileIdentity{} if err = m.save(record); err != nil { t.Fatal(err) } if err = m.install(t.Context(), binding); err != nil || files.creates != 1 { t.Fatal("lost materialization recovery", err) } } gone := func() error { if attack == "busy" { return os.ErrPermission } return nil } err = m.retire(t.Context(), binding, gone) if attack == "replacement" || attack == "changed bytes" || attack == "busy" { if err == nil || files.retires != 0 { t.Fatal("retired foreign or running unit", err) } return } if err != nil || files.retires != 1 { t.Fatal("retire", err, files.retires) } if !strings.Contains(string(files.body), "RefuseManualStart=yes") { t.Fatal("retirement exposed old unit") } if err = m.retire(t.Context(), binding, gone); err != nil || files.retires != 1 { t.Fatal("retire replay", err) } if err = m.install(t.Context(), binding); !errors.Is(err, os.ErrPermission) { t.Fatal("retired unit reinstalled", err) } }) } } func plan14UnitIdentity(inode uint64) rootstate.FileIdentity { return rootstate.FileIdentity{Device: 1, Inode: inode, Boot: "fixture-boot", Filesystem: "fixture-filesystem", Handle: "fixture-handle", HandleType: 1, Mount: 1, MountRoot: "/", MountPoint: "/", MountNamespace: "fixture-namespace", Mode: 0100644} }