| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/hostbox" |
| 15 | "github.com/carlosframework/platform/internal/hostclaim" |
| 16 | execlease "github.com/carlosframework/platform/internal/hostexec" |
| 17 | "github.com/carlosframework/platform/internal/hostguard" |
| 18 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 19 | "golang.org/x/sys/unix" |
| 20 | ) |
| 21 | |
| 22 | type launchParticipationDisk struct{ *rollbackDisk } |
| 23 | |
| 24 | func (d *launchParticipationDisk) Lock(ctx context.Context, name string) (func(), error) { |
| 25 | f, err := os.OpenFile(filepath.Join(d.dir, name), os.O_CREATE|os.O_RDWR, 0600) |
| 26 | if err != nil { |
| 27 | return nil, err |
| 28 | } |
| 29 | for { |
| 30 | if err = ctx.Err(); err != nil { |
| 31 | f.Close() |
| 32 | return nil, err |
| 33 | } |
| 34 | err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) |
| 35 | if err == nil { |
| 36 | return func() { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN); _ = f.Close() }, nil |
| 37 | } |
| 38 | if !errors.Is(err, unix.EAGAIN) && !errors.Is(err, unix.EWOULDBLOCK) { |
| 39 | f.Close() |
| 40 | return nil, err |
| 41 | } |
| 42 | select { |
| 43 | case <-ctx.Done(): |
| 44 | f.Close() |
| 45 | return nil, ctx.Err() |
| 46 | case <-time.After(time.Millisecond): |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | func TestPlan14ExecutionFinalLaunchUsesRootParticipation(t *testing.T) { |
| 52 | for _, atFinal := range []bool{false, true} { |
| 53 | for _, attack := range []string{"none", "lost", "deadline", "boot", "process", "missing", "participation-only", "census", "update"} { |
| 54 | t.Run(attack+"/"+map[bool]string{true: "final", false: "initial"}[atFinal], func(t *testing.T) { |
| 55 | f := newLaunchFixture(t, rev2Operation()) |
| 56 | disk := f.root.executionState.(*launchParticipationDisk) |
| 57 | mutate := func() { |
| 58 | raw, err := disk.Read(f.target.Host + ".json") |
| 59 | if err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | var r execlease.ExecutionRecord |
| 63 | if err = json.Unmarshal(raw, &r); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | switch attack { |
| 67 | case "lost": |
| 68 | r.Status = "lost" |
| 69 | case "deadline": |
| 70 | r.SafeUntil = 100 |
| 71 | case "boot": |
| 72 | r.Peer.KernelBootID = "other-boot" |
| 73 | case "process": |
| 74 | r.Peer.Start = "other-start" |
| 75 | case "participation-only": |
| 76 | r.Authority.ParticipationOnly = true |
| 77 | case "missing": |
| 78 | if err = disk.Remove(f.target.Host + ".json"); err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | return |
| 82 | case "census": |
| 83 | if err = disk.Remove("census.json"); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | return |
| 87 | case "update": |
| 88 | if err = disk.Write("update.json", []byte(`{"Schema":1,"ID":"update","Revision":1}`)); err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | return |
| 92 | } |
| 93 | raw, _ = json.Marshal(r) |
| 94 | if err = disk.Write(f.target.Host+".json", raw); err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | } |
| 98 | // A real held service lock and a forged future deadline cannot grant root work. |
| 99 | mirror := filepath.Join(f.root.Guard.DataDir, ".hostexec") |
| 100 | if err := os.MkdirAll(mirror, 0700); err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | raw, _ := json.Marshal(execlease.State{V: 1, Binding: execlease.Binding{Host: f.target.Host, Incarnation: f.target.Incarnation, Op: f.target.Op, Box: f.target.Box, Enrollment: f.target.Enrollment, BootID: f.peer.BootID, ControllerID: "agent"}, Process: execlease.ControllerProcess{KernelBootID: f.peer.BootID, PID: f.peer.PID, Start: f.peer.ProcessStart}, State: "active", SafeUntil: 1 << 62, LeaseETag: "forged", Expires: time.Now().Add(365 * 24 * time.Hour)}) |
| 104 | if err := os.WriteFile(filepath.Join(mirror, f.target.Host+".json"), raw, 0600); err != nil { |
| 105 | t.Fatal(err) |
| 106 | } |
| 107 | for _, suffix := range []string{".lock", ".live"} { |
| 108 | lock, err := os.OpenFile(filepath.Join(f.root.ExecLockDir, f.target.Host+suffix), os.O_CREATE|os.O_RDWR, 0660) |
| 109 | if err != nil { |
| 110 | t.Fatal(err) |
| 111 | } |
| 112 | if err = lock.Chmod(0660); err != nil { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | if err = unix.Flock(int(lock.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | t.Cleanup(func() { lock.Close() }) |
| 119 | } |
| 120 | if atFinal { |
| 121 | f.units.beforeCheck = mutate |
| 122 | } else { |
| 123 | mutate() |
| 124 | } |
| 125 | _, err := f.root.Serve(t.Context(), f.peer, f.req) |
| 126 | starts, _ := f.units.counts() |
| 127 | if attack == "none" { |
| 128 | if err != nil || starts != 1 { |
| 129 | t.Fatalf("positive start=%d err=%v", starts, err) |
| 130 | } |
| 131 | } else if err == nil || starts != 0 { |
| 132 | t.Fatalf("root refusal bypassed: start=%d err=%v", starts, err) |
| 133 | } |
| 134 | }) |
| 135 | } |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func configureLaunchParticipation(t *testing.T, f *launchFixture) { |
| 140 | t.Helper() |
| 141 | disk := &launchParticipationDisk{&rollbackDisk{dir: t.TempDir()}} |
| 142 | f.root.executionState = disk |
| 143 | raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) |
| 144 | if err := disk.Write("census.json", raw); err != nil { |
| 145 | t.Fatal(err) |
| 146 | } |
| 147 | if f.claims.obs.Claim.Op.Kind == hostclaim.KindServing { |
| 148 | return |
| 149 | } |
| 150 | writeRev2Local(t, f, "active", 1000) |
| 151 | unlock, err := disk.HoldLive(f.target.Host + ".live") |
| 152 | if err != nil { |
| 153 | t.Fatal(err) |
| 154 | } |
| 155 | t.Cleanup(unlock) |
| 156 | } |
| 157 | |
| 158 | func TestPlan14ExecutionRootLocalIgnoresAgentAbsence(t *testing.T) { |
| 159 | for _, serving := range []bool{false, true} { |
| 160 | for _, lost := range []bool{false, true} { |
| 161 | t.Run(map[bool]string{true: "serving", false: "operation"}[serving]+"/"+map[bool]string{true: "lost", false: "valid"}[lost], func(t *testing.T) { |
| 162 | op := rev2Operation() |
| 163 | if serving { |
| 164 | op = servingOp() |
| 165 | } |
| 166 | f := newLaunchFixture(t, op) |
| 167 | disk := f.root.executionState.(*launchParticipationDisk) |
| 168 | if lost { |
| 169 | q := execlease.ExecutionRequest{Schema: 1, ID: "old", Operation: "executionBegin", Host: f.target.Host, Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Session: f.target.Session, Op: "operation"} |
| 170 | raw, _ := json.Marshal(execlease.ExecutionRecord{Schema: 1, Request: q, Token: "lost", Status: "lost", Authority: execlease.ExecutionAuthority{Migration: 1, Box: f.actor.Box, Enrollment: f.actor.Enrollment}, Replay: map[string]execlease.ExecutionReplay{}}) |
| 171 | if err := disk.Write(f.target.Host+".json", raw); err != nil { |
| 172 | t.Fatal(err) |
| 173 | } |
| 174 | } |
| 175 | p := &productionClaimLocal{Resources: ClaimRuntimeResources{Mode: "instance-control", DataDir: f.root.Guard.DataDir}, assembly: &ClaimAssembly{}, lifetime: t.Context(), executionRead: f.root.readExecution} |
| 176 | obs, _ := f.claims.Read(t.Context(), f.target.Host) |
| 177 | selection := hostbox.Selection{Box: f.actor.Box, Region: f.actor.Region, Captured: hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp}} |
| 178 | for _, err := range []error{p.prepareWith(t.Context(), *f.actor, obs), p.Check(t.Context(), *f.actor, obs.Claim, selection)} { |
| 179 | if lost && err == nil { |
| 180 | t.Fatal("missing agent mirror cleared root loss") |
| 181 | } |
| 182 | if !lost && err != nil { |
| 183 | t.Fatal("valid root participation refused", err) |
| 184 | } |
| 185 | } |
| 186 | }) |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | func TestPlan14ExecutionServingRequiresRootSettlement(t *testing.T) { |
| 192 | for _, status := range []string{"settled", "lost", "unknown"} { |
| 193 | for _, attack := range []string{"erase", "rename", "replace locks"} { |
| 194 | t.Run(status+"/"+attack, func(t *testing.T) { |
| 195 | f := newLaunchFixture(t, servingOp()) |
| 196 | disk := f.root.executionState.(*launchParticipationDisk) |
| 197 | q := execlease.ExecutionRequest{Schema: 1, ID: "old", Operation: "executionBegin", Host: f.target.Host, Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Session: f.target.Session, Op: "prior-operation"} |
| 198 | raw, _ := json.Marshal(execlease.ExecutionRecord{Schema: 1, Request: q, Token: "old", Status: status, Authority: execlease.ExecutionAuthority{Migration: 1, Box: f.actor.Box, Enrollment: f.actor.Enrollment}, Replay: map[string]execlease.ExecutionReplay{}}) |
| 199 | if err := disk.Write(f.target.Host+".json", raw); err != nil { |
| 200 | t.Fatal(err) |
| 201 | } |
| 202 | mirror := filepath.Join(f.root.Guard.DataDir, ".hostexec") |
| 203 | if err := os.Mkdir(mirror, 0700); err != nil { |
| 204 | t.Fatal(err) |
| 205 | } |
| 206 | name := filepath.Join(mirror, f.target.Host+".json") |
| 207 | if err := os.WriteFile(name, []byte(`{"state":"lost"}`), 0600); err != nil { |
| 208 | t.Fatal(err) |
| 209 | } |
| 210 | if attack == "rename" { |
| 211 | if err := os.Rename(mirror, mirror+".old"); err != nil { |
| 212 | t.Fatal(err) |
| 213 | } |
| 214 | if err := os.Mkdir(mirror, 0700); err != nil { |
| 215 | t.Fatal(err) |
| 216 | } |
| 217 | } else { |
| 218 | if err := os.Remove(name); err != nil { |
| 219 | t.Fatal(err) |
| 220 | } |
| 221 | } |
| 222 | if attack == "replace locks" { |
| 223 | for _, suffix := range []string{".lock", ".live"} { |
| 224 | name := filepath.Join(f.root.ExecLockDir, f.target.Host+suffix) |
| 225 | file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0660) |
| 226 | if err != nil { |
| 227 | t.Fatal(err) |
| 228 | } |
| 229 | if err = unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { |
| 230 | t.Fatal(err) |
| 231 | } |
| 232 | t.Cleanup(func() { file.Close() }) |
| 233 | if err = os.Rename(name, name+".old"); err != nil { |
| 234 | t.Fatal(err) |
| 235 | } |
| 236 | if err = os.WriteFile(name, nil, 0660); err != nil { |
| 237 | t.Fatal(err) |
| 238 | } |
| 239 | } |
| 240 | } |
| 241 | _, err := f.root.Serve(t.Context(), f.peer, f.req) |
| 242 | starts, _ := f.units.counts() |
| 243 | if status == "settled" { |
| 244 | if err != nil || starts != 1 { |
| 245 | t.Fatalf("settled serving refused: starts=%d err=%v", starts, err) |
| 246 | } |
| 247 | } else if err == nil || starts != 0 { |
| 248 | t.Fatalf("agent absence granted serving: starts=%d err=%v", starts, err) |
| 249 | } |
| 250 | }) |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | func (d *rollbackDisk) HoldLive(name string) (func(), error) { |
| 256 | f, err := os.OpenFile(filepath.Join(d.dir, name), os.O_CREATE|os.O_RDWR, 0600) |
| 257 | if err != nil { |
| 258 | return nil, err |
| 259 | } |
| 260 | lock := unix.Flock_t{Type: unix.F_WRLCK} |
| 261 | if err := unix.FcntlFlock(f.Fd(), unix.F_OFD_SETLK, &lock); err != nil { |
| 262 | f.Close() |
| 263 | return nil, err |
| 264 | } |
| 265 | return func() { f.Close() }, nil |
| 266 | } |
| 267 | func (d *rollbackDisk) LiveHeld(name string) (bool, error) { |
| 268 | f, err := os.OpenFile(filepath.Join(d.dir, name), os.O_RDWR, 0) |
| 269 | if err != nil { |
| 270 | return false, err |
| 271 | } |
| 272 | defer f.Close() |
| 273 | lock := unix.Flock_t{Type: unix.F_RDLCK} |
| 274 | err = unix.FcntlFlock(f.Fd(), unix.F_OFD_GETLK, &lock) |
| 275 | return lock.Type == unix.F_WRLCK && lock.Pid == -1, err |
| 276 | } |
| 277 | |