| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/rootstate" |
| 11 | ) |
| 12 | |
| 13 | type plan14UnitFiles struct { |
| 14 | body []byte |
| 15 | id rootstate.FileIdentity |
| 16 | creates, retires int |
| 17 | } |
| 18 | |
| 19 | func (f *plan14UnitFiles) read(string) ([]byte, rootstate.FileIdentity, error) { |
| 20 | if f.body == nil { |
| 21 | return nil, rootstate.FileIdentity{}, os.ErrNotExist |
| 22 | } |
| 23 | return append([]byte(nil), f.body...), f.id, nil |
| 24 | } |
| 25 | func (f *plan14UnitFiles) create(_ string, b []byte) error { |
| 26 | if f.body != nil { |
| 27 | return os.ErrExist |
| 28 | } |
| 29 | f.body = append([]byte(nil), b...) |
| 30 | f.id = plan14UnitIdentity(2) |
| 31 | f.creates++ |
| 32 | return nil |
| 33 | } |
| 34 | func (f *plan14UnitFiles) replace(_ string, id rootstate.FileIdentity, b []byte) error { |
| 35 | if id != f.id { |
| 36 | return os.ErrPermission |
| 37 | } |
| 38 | f.body = append([]byte(nil), b...) |
| 39 | f.id.Inode++ |
| 40 | f.retires++ |
| 41 | return nil |
| 42 | } |
| 43 | func TestPlan14UnitMaterialization(t *testing.T) { |
| 44 | for _, attack := range []string{"none", "foreign file", "busy install", "replacement", "changed bytes", "lost intent", "lost materialized", "busy", "retired"} { |
| 45 | t.Run(attack, func(t *testing.T) { |
| 46 | b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") |
| 47 | shape.Program = strings.Repeat("a", 64) |
| 48 | b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } |
| 49 | binding, err := b.establish(t.Context(), in.Host) |
| 50 | if err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | files := &plan14UnitFiles{} |
| 54 | state := &plan14UnitBindingFault{Store: &rollbackMemory{files: map[string][]byte{}}} |
| 55 | m := claimUnitMaterializer{idle: func(string) error { return nil }, state: state, read: files.read, create: files.create, replace: files.replace} |
| 56 | if attack == "foreign file" { |
| 57 | files.body = []byte("foreign") |
| 58 | files.id = plan14UnitIdentity(9) |
| 59 | } |
| 60 | if attack == "busy install" { |
| 61 | m.idle = func(string) error { return os.ErrPermission } |
| 62 | } |
| 63 | if attack == "lost intent" { |
| 64 | state.fail = true |
| 65 | } |
| 66 | err = m.install(t.Context(), binding) |
| 67 | if attack == "foreign file" || attack == "lost intent" || attack == "busy install" { |
| 68 | if err == nil || files.creates != 0 { |
| 69 | t.Fatal("effect before durable intent", err, files.creates) |
| 70 | } |
| 71 | return |
| 72 | } |
| 73 | if err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | if files.creates != 1 { |
| 77 | t.Fatal(files.creates) |
| 78 | } |
| 79 | if err = m.install(t.Context(), binding); err != nil || files.creates != 1 { |
| 80 | t.Fatal("replay", err) |
| 81 | } |
| 82 | switch attack { |
| 83 | case "replacement": |
| 84 | files.id.Inode++ |
| 85 | case "changed bytes": |
| 86 | files.body = append(files.body, 'x') |
| 87 | case "lost materialized": |
| 88 | record, err := m.load(in.Host) |
| 89 | if err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | record.Identity = rootstate.FileIdentity{} |
| 93 | if err = m.save(record); err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | if err = m.install(t.Context(), binding); err != nil || files.creates != 1 { |
| 97 | t.Fatal("lost materialization recovery", err) |
| 98 | } |
| 99 | } |
| 100 | gone := func() error { |
| 101 | if attack == "busy" { |
| 102 | return os.ErrPermission |
| 103 | } |
| 104 | return nil |
| 105 | } |
| 106 | err = m.retire(t.Context(), binding, gone) |
| 107 | if attack == "replacement" || attack == "changed bytes" || attack == "busy" { |
| 108 | if err == nil || files.retires != 0 { |
| 109 | t.Fatal("retired foreign or running unit", err) |
| 110 | } |
| 111 | return |
| 112 | } |
| 113 | if err != nil || files.retires != 1 { |
| 114 | t.Fatal("retire", err, files.retires) |
| 115 | } |
| 116 | if !strings.Contains(string(files.body), "RefuseManualStart=yes") { |
| 117 | t.Fatal("retirement exposed old unit") |
| 118 | } |
| 119 | if err = m.retire(t.Context(), binding, gone); err != nil || files.retires != 1 { |
| 120 | t.Fatal("retire replay", err) |
| 121 | } |
| 122 | if err = m.install(t.Context(), binding); !errors.Is(err, os.ErrPermission) { |
| 123 | t.Fatal("retired unit reinstalled", err) |
| 124 | } |
| 125 | }) |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | func plan14UnitIdentity(inode uint64) rootstate.FileIdentity { |
| 130 | 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} |
| 131 | } |
| 132 | |