carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "net"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/rootstate"
14 "golang.org/x/sys/unix"
15)
16
17func TestPlan14FlatSocketMigration(t *testing.T) {
18 for _, attack := range []string{"none", "replacement", "boot", "lost-rename", "archive-replacement", "erased"} {
19 t.Run(attack, func(t *testing.T) {
20 dir := t.TempDir()
21 source, archive := filepath.Join(dir, "old.sock"), filepath.Join(dir, "retained.sock")
22 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: source, Net: "unix"})
23 if err != nil {
24 t.Fatal(err)
25 }
26 listener.SetUnlinkOnClose(false)
27 defer listener.Close()
28 id, err := captureClaimUnitSocket(source, "boot-one")
29 if err != nil {
30 t.Fatal(err)
31 }
32 listener.Close()
33 boot := "boot-one"
34 switch attack {
35 case "erased":
36 if err := os.Remove(source); err != nil {
37 t.Fatal(err)
38 }
39 case "boot":
40 boot = "boot-two"
41 case "replacement":
42 if err := os.Rename(source, source+".old"); err != nil {
43 t.Fatal(err)
44 }
45 if err := os.WriteFile(source, []byte("replacement"), 0600); err != nil {
46 t.Fatal(err)
47 }
48 case "lost-rename", "archive-replacement":
49 if err := os.Rename(source, archive); err != nil {
50 t.Fatal(err)
51 }
52 if attack == "archive-replacement" {
53 if err := os.Rename(archive, archive+".old"); err != nil {
54 t.Fatal(err)
55 }
56 if err := os.WriteFile(archive, []byte("replacement"), 0600); err != nil {
57 t.Fatal(err)
58 }
59 }
60 }
61 renames := 0
62 err = parkClaimUnitSocketWith(source, archive, boot, id, func(sfd int, src string, dfd int, dst string, flags uint) error {
63 renames++
64 return unix.Renameat2(sfd, src, dfd, dst, flags)
65 })
66 if attack == "none" || attack == "lost-rename" || attack == "erased" {
67 if err != nil {
68 t.Fatal(err)
69 }
70 if err = parkClaimUnitSocket(source, archive, boot, id); err != nil {
71 t.Fatal("replay", err)
72 }
73 if _, err := os.Lstat(source); !os.IsNotExist(err) {
74 t.Fatal("old socket still occupies routing name", err)
75 }
76 } else {
77 if err == nil {
78 t.Fatal("unowned socket accepted")
79 }
80 if renames != 0 {
81 t.Fatal("unowned socket renamed", renames)
82 }
83 }
84 })
85 }
86}
87
88func TestPlan14OperatorSocketMigrationDurableRetry(t *testing.T) {
89 for _, loss := range []string{"stop", "park", "replace"} {
90 t.Run(loss, func(t *testing.T) {
91 b, in, shape := plan14UnitBindingFixture(t, "woodstar.service", "", "/run/carlos/woodstar.sock")
92 old := []byte("[Service]\nExecStart=/old/program\n")
93 shape.Program, shape.PredecessorDigest = strings.Repeat("a", 64), claimUnitDigest(old)
94 b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil }
95 binding, err := b.establish(t.Context(), in.Host)
96 if err != nil {
97 t.Fatal(err)
98 }
99 dir := t.TempDir()
100 source, archive := filepath.Join(dir, "source.sock"), filepath.Join(dir, "archive.sock")
101 ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: source, Net: "unix"})
102 if err != nil {
103 t.Fatal(err)
104 }
105 ln.SetUnlinkOnClose(false)
106 defer ln.Close()
107 want, err := captureClaimUnitSocket(source, "boot")
108 if err != nil {
109 t.Fatal(err)
110 }
111 files := &plan14UnitFiles{body: old, id: plan14UnitIdentity(2)}
112 state := &rollbackMemory{files: map[string][]byte{}}
113 p := claimUnitPredecessor{Unit: shape.Unit, Invocation: "old", Group: "system.slice/woodstar.service", Boot: "boot", GroupIdentity: rootstate.FileIdentity{Device: 3, Inode: 4}}
114 stopped, parked := false, false
115 m := claimUnitMaterializer{state: state, read: files.read, create: files.create, idle: func(string) error {
116 if !stopped {
117 return os.ErrPermission
118 }
119 return nil
120 }}
121 m.predecessor = func(context.Context, claimUnitBinding) (claimUnitPredecessor, error) { return p, nil }
122 m.socketCapture = func(context.Context, claimUnitShape, claimUnitPredecessor) (claimUnitSocketIdentity, error) {
123 return want, nil
124 }
125 m.stopPredecessor = func(context.Context, claimUnitPredecessor) error {
126 r, err := m.load(in.Host)
127 if err != nil || r.Migration == nil || r.Migration.Socket != want {
128 t.Fatal("stop without durable socket identity", err)
129 }
130 stopped = true
131 if loss == "stop" {
132 return os.ErrClosed
133 }
134 return nil
135 }
136 m.socketPark = func(_ claimUnitBinding, id claimUnitSocketIdentity) error {
137 if !stopped || id != want {
138 t.Fatal("socket park before exact predecessor stop")
139 }
140 if err := parkClaimUnitSocket(source, archive, "boot", id); err != nil {
141 return err
142 }
143 parked = true
144 if loss == "park" {
145 return os.ErrClosed
146 }
147 return nil
148 }
149 m.replace = func(unit string, id rootstate.FileIdentity, raw []byte) error {
150 if !parked {
151 t.Fatal("unit replaced before socket migration")
152 }
153 if err := files.replace(unit, id, raw); err != nil {
154 return err
155 }
156 if loss == "replace" {
157 return os.ErrClosed
158 }
159 return nil
160 }
161 if err := m.install(t.Context(), binding); err == nil {
162 t.Fatal("lost reply accepted")
163 }
164 loss = ""
165 if err := m.install(t.Context(), binding); err != nil {
166 t.Fatal("durable retry", err)
167 }
168 if err := m.verify(binding); err != nil || files.retires != 1 {
169 t.Fatal("migration did not finish exactly once", err, files.retires)
170 }
171 })
172 }
173}
174
175func TestPlan14FlatSocketMigrationRenameRace(t *testing.T) {
176 for _, occupied := range []bool{false, true} {
177 t.Run(map[bool]string{false: "restore", true: "occupied"}[occupied], func(t *testing.T) {
178 dir := t.TempDir()
179 source, archive := filepath.Join(dir, "source.sock"), filepath.Join(dir, "archive.sock")
180 ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: source, Net: "unix"})
181 if err != nil {
182 t.Fatal(err)
183 }
184 ln.SetUnlinkOnClose(false)
185 defer ln.Close()
186 id, err := captureClaimUnitSocket(source, "boot")
187 if err != nil {
188 t.Fatal(err)
189 }
190 err = parkClaimUnitSocketWith(source, archive, "boot", id, func(sfd int, src string, dfd int, dst string, flags uint) error {
191 if err := os.Rename(source, source+".old"); err != nil {
192 t.Fatal(err)
193 }
194 if err := os.WriteFile(source, []byte("raced"), 0600); err != nil {
195 t.Fatal(err)
196 }
197 err := unix.Renameat2(sfd, src, dfd, dst, flags)
198 if occupied && err == nil {
199 err = os.WriteFile(source, []byte("newest"), 0600)
200 }
201 return err
202 })
203 if err == nil {
204 t.Fatal("raced replacement accepted")
205 }
206 path, want := source, "raced"
207 if occupied {
208 path, want = archive, "raced"
209 got, e := os.ReadFile(source)
210 if e != nil || string(got) != "newest" {
211 t.Fatal("new source overwritten", e)
212 }
213 }
214 got, err := os.ReadFile(path)
215 if err != nil || string(got) != want {
216 t.Fatal("raced file lost", string(got), err)
217 }
218 })
219 }
220}
221