| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "syscall" |
| 12 | "testing" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/moves" |
| 15 | ) |
| 16 | |
| 17 | // Every creating open of .moves fsyncs the data dir, not only the one that |
| 18 | // made the directory (fix round 2, finding 4): a second writer must not |
| 19 | // report a floor durable on the strength of a first writer's unsynced mkdir. |
| 20 | func TestEveryFloorWriteSyncsTheDataDir(t *testing.T) { |
| 21 | n := 0 |
| 22 | orig := syncParent |
| 23 | syncParent = func(fd int) error { n++; return orig(fd) } |
| 24 | defer func() { syncParent = orig }() |
| 25 | dd := t.TempDir() |
| 26 | armLocalFloor(t, dd, "v0.30.0") |
| 27 | first := n |
| 28 | armLocalFloor(t, dd, "v0.31.0") |
| 29 | if first == 0 || n <= first { |
| 30 | t.Fatalf("data dir fsyncs: %d after the first write, %d after the second", first, n) |
| 31 | } |
| 32 | syncParent = func(int) error { return errors.New("EIO") } |
| 33 | if err := writeLocalFloor(dd, moves.Floor{Min: 1, MinRelease: "v0.32.0"}); err == nil { |
| 34 | t.Fatal("a write whose data dir fsync failed reported success") |
| 35 | } |
| 36 | if f, _ := readLocalFloor(dd); f == nil || f.MinRelease != "v0.31.0" { |
| 37 | t.Fatalf("floor after a failed fsync = %+v", f) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // Only a lock that cannot be created counts as "cannot exist" (the F15 |
| 42 | // fallback). Contention never does: a writer is active (fix round 3). |
| 43 | func TestFloorLockCannotExistIsOnlyForAnUncreatableLock(t *testing.T) { |
| 44 | for _, tc := range []struct { |
| 45 | err error |
| 46 | want bool |
| 47 | }{ |
| 48 | {fmt.Errorf("mkdir /data/.moves: %w", syscall.EROFS), true}, |
| 49 | {fmt.Errorf("mkdir /data/.moves: %w", syscall.ENOSPC), true}, |
| 50 | {fmt.Errorf("open floor.lock: %w", syscall.EACCES), true}, |
| 51 | {fmt.Errorf("mkdir: %w", syscall.EPERM), true}, |
| 52 | {fmt.Errorf("mkdir: %w", syscall.EDQUOT), true}, |
| 53 | {fmt.Errorf("lock /data/.moves/floor.lock: %w", errFloorLockBusy), false}, |
| 54 | {fmt.Errorf("open /data/.moves: %w", syscall.ELOOP), false}, |
| 55 | {fmt.Errorf("fsync /data: %w", syscall.EIO), false}, |
| 56 | {errors.Join(syscall.EROFS, errFloorLockBusy), false}, |
| 57 | } { |
| 58 | if got := floorLockCannotExist(tc.err); got != tc.want { |
| 59 | t.Errorf("floorLockCannotExist(%v) = %v, want %v", tc.err, got, tc.want) |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | // TestActivationGuardLockOrder pins the lock order the merge of claim Plan 8 |
| 65 | // relies on: at an activation (the install's rename, inside |
| 66 | // activationGuard, with a floor armed so the holds are frozen too), Run's |
| 67 | // apply flock and .moves/floor.lock are both held. activationGuard takes |
| 68 | // floor.lock only while the apply flock is already held; it never takes the |
| 69 | // apply flock (withFloorLock's comment has the whole order). |
| 70 | func TestActivationGuardLockOrder(t *testing.T) { |
| 71 | f, _ := floorFixture(t, "v0.31.0", "v0.32.0") |
| 72 | armLocalFloor(t, f.a.dataDir, "v0.30.0") |
| 73 | busy := func(path string, flags int) error { |
| 74 | fd, err := syscall.Open(path, flags|syscall.O_CLOEXEC, 0) |
| 75 | if err != nil { |
| 76 | return fmt.Errorf("open %s: %w", path, err) |
| 77 | } |
| 78 | defer syscall.Close(fd) |
| 79 | if err := syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB); !errors.Is(err, syscall.EWOULDBLOCK) { |
| 80 | return fmt.Errorf("%s: flock = %v, want it held (EWOULDBLOCK)", path, err) |
| 81 | } |
| 82 | return nil |
| 83 | } |
| 84 | swapped := false |
| 85 | f.a.beforeSwap = func() { |
| 86 | swapped = true |
| 87 | if err := busy(f.a.lockPath(), syscall.O_RDWR); err != nil { |
| 88 | t.Errorf("apply flock: %v", err) |
| 89 | } |
| 90 | if err := busy(filepath.Join(f.a.dataDir, movesDirName, floorLockFile), syscall.O_RDONLY); err != nil { |
| 91 | t.Errorf("floor lock: %v", err) |
| 92 | } |
| 93 | } |
| 94 | if err := f.a.Run(context.Background(), false); err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | if !swapped { |
| 98 | t.Fatal("the install never reached its guarded rename") |
| 99 | } |
| 100 | if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied { |
| 101 | t.Fatalf("result = %+v", r) |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | func TestMovesDirRefusesLinkedRootAndLock(t *testing.T) { |
| 106 | root := t.TempDir() |
| 107 | alias := filepath.Join(t.TempDir(), "data") |
| 108 | if err := os.Symlink(root, alias); err != nil { |
| 109 | t.Fatal(err) |
| 110 | } |
| 111 | if d, err := openMovesDir(alias, true); err == nil { |
| 112 | d.Close() |
| 113 | t.Fatal("opened symlinked data root") |
| 114 | } |
| 115 | d, err := openMovesDir(root, true) |
| 116 | if err != nil { |
| 117 | t.Fatal(err) |
| 118 | } |
| 119 | defer d.Close() |
| 120 | victim := filepath.Join(root, "victim") |
| 121 | if err := os.WriteFile(victim, []byte("unchanged"), 0o600); err != nil { |
| 122 | t.Fatal(err) |
| 123 | } |
| 124 | if err := os.Link(victim, filepath.Join(root, movesDirName, floorLockFile)); err != nil { |
| 125 | t.Fatal(err) |
| 126 | } |
| 127 | if err := d.Lock(); err == nil { |
| 128 | t.Fatal("locked a multiply linked file") |
| 129 | } |
| 130 | body, err := os.ReadFile(victim) |
| 131 | if err != nil || string(body) != "unchanged" { |
| 132 | t.Fatalf("victim = %q, %v", body, err) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | func TestMovesDirWritesStayWithLockedDirectory(t *testing.T) { |
| 137 | root := t.TempDir() |
| 138 | d, err := openMovesDir(root, true) |
| 139 | if err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | defer d.Close() |
| 143 | if err := d.Lock(); err != nil { |
| 144 | t.Fatal(err) |
| 145 | } |
| 146 | moved := filepath.Join(root, "old-moves") |
| 147 | if err := os.Rename(filepath.Join(root, movesDirName), moved); err != nil { |
| 148 | t.Fatal(err) |
| 149 | } |
| 150 | if err := os.Mkdir(filepath.Join(root, movesDirName), 0o755); err != nil { |
| 151 | t.Fatal(err) |
| 152 | } |
| 153 | if err := d.WriteDurable("floor.json", []byte("written"), 0o644); err != nil { |
| 154 | t.Fatal(err) |
| 155 | } |
| 156 | body, err := os.ReadFile(filepath.Join(moved, "floor.json")) |
| 157 | if err != nil || string(body) != "written" { |
| 158 | t.Fatalf("pinned directory = %q, %v", body, err) |
| 159 | } |
| 160 | if _, err := os.Stat(filepath.Join(root, movesDirName, "floor.json")); !errors.Is(err, os.ErrNotExist) { |
| 161 | t.Fatalf("write reached unlocked replacement: %v", err) |
| 162 | } |
| 163 | } |
| 164 | |