//go:build linux package host import ( "context" "errors" "fmt" "os" "path/filepath" "syscall" "testing" "github.com/carlosframework/platform/internal/moves" ) // Every creating open of .moves fsyncs the data dir, not only the one that // made the directory (fix round 2, finding 4): a second writer must not // report a floor durable on the strength of a first writer's unsynced mkdir. func TestEveryFloorWriteSyncsTheDataDir(t *testing.T) { n := 0 orig := syncParent syncParent = func(fd int) error { n++; return orig(fd) } defer func() { syncParent = orig }() dd := t.TempDir() armLocalFloor(t, dd, "v0.30.0") first := n armLocalFloor(t, dd, "v0.31.0") if first == 0 || n <= first { t.Fatalf("data dir fsyncs: %d after the first write, %d after the second", first, n) } syncParent = func(int) error { return errors.New("EIO") } if err := writeLocalFloor(dd, moves.Floor{Min: 1, MinRelease: "v0.32.0"}); err == nil { t.Fatal("a write whose data dir fsync failed reported success") } if f, _ := readLocalFloor(dd); f == nil || f.MinRelease != "v0.31.0" { t.Fatalf("floor after a failed fsync = %+v", f) } } // Only a lock that cannot be created counts as "cannot exist" (the F15 // fallback). Contention never does: a writer is active (fix round 3). func TestFloorLockCannotExistIsOnlyForAnUncreatableLock(t *testing.T) { for _, tc := range []struct { err error want bool }{ {fmt.Errorf("mkdir /data/.moves: %w", syscall.EROFS), true}, {fmt.Errorf("mkdir /data/.moves: %w", syscall.ENOSPC), true}, {fmt.Errorf("open floor.lock: %w", syscall.EACCES), true}, {fmt.Errorf("mkdir: %w", syscall.EPERM), true}, {fmt.Errorf("mkdir: %w", syscall.EDQUOT), true}, {fmt.Errorf("lock /data/.moves/floor.lock: %w", errFloorLockBusy), false}, {fmt.Errorf("open /data/.moves: %w", syscall.ELOOP), false}, {fmt.Errorf("fsync /data: %w", syscall.EIO), false}, {errors.Join(syscall.EROFS, errFloorLockBusy), false}, } { if got := floorLockCannotExist(tc.err); got != tc.want { t.Errorf("floorLockCannotExist(%v) = %v, want %v", tc.err, got, tc.want) } } } // TestActivationGuardLockOrder pins the lock order the merge of claim Plan 8 // relies on: at an activation (the install's rename, inside // activationGuard, with a floor armed so the holds are frozen too), Run's // apply flock and .moves/floor.lock are both held. activationGuard takes // floor.lock only while the apply flock is already held; it never takes the // apply flock (withFloorLock's comment has the whole order). func TestActivationGuardLockOrder(t *testing.T) { f, _ := floorFixture(t, "v0.31.0", "v0.32.0") armLocalFloor(t, f.a.dataDir, "v0.30.0") busy := func(path string, flags int) error { fd, err := syscall.Open(path, flags|syscall.O_CLOEXEC, 0) if err != nil { return fmt.Errorf("open %s: %w", path, err) } defer syscall.Close(fd) if err := syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB); !errors.Is(err, syscall.EWOULDBLOCK) { return fmt.Errorf("%s: flock = %v, want it held (EWOULDBLOCK)", path, err) } return nil } swapped := false f.a.beforeSwap = func() { swapped = true if err := busy(f.a.lockPath(), syscall.O_RDWR); err != nil { t.Errorf("apply flock: %v", err) } if err := busy(filepath.Join(f.a.dataDir, movesDirName, floorLockFile), syscall.O_RDONLY); err != nil { t.Errorf("floor lock: %v", err) } } if err := f.a.Run(context.Background(), false); err != nil { t.Fatal(err) } if !swapped { t.Fatal("the install never reached its guarded rename") } if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied { t.Fatalf("result = %+v", r) } } func TestMovesDirRefusesLinkedRootAndLock(t *testing.T) { root := t.TempDir() alias := filepath.Join(t.TempDir(), "data") if err := os.Symlink(root, alias); err != nil { t.Fatal(err) } if d, err := openMovesDir(alias, true); err == nil { d.Close() t.Fatal("opened symlinked data root") } d, err := openMovesDir(root, true) if err != nil { t.Fatal(err) } defer d.Close() victim := filepath.Join(root, "victim") if err := os.WriteFile(victim, []byte("unchanged"), 0o600); err != nil { t.Fatal(err) } if err := os.Link(victim, filepath.Join(root, movesDirName, floorLockFile)); err != nil { t.Fatal(err) } if err := d.Lock(); err == nil { t.Fatal("locked a multiply linked file") } body, err := os.ReadFile(victim) if err != nil || string(body) != "unchanged" { t.Fatalf("victim = %q, %v", body, err) } } func TestMovesDirWritesStayWithLockedDirectory(t *testing.T) { root := t.TempDir() d, err := openMovesDir(root, true) if err != nil { t.Fatal(err) } defer d.Close() if err := d.Lock(); err != nil { t.Fatal(err) } moved := filepath.Join(root, "old-moves") if err := os.Rename(filepath.Join(root, movesDirName), moved); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(root, movesDirName), 0o755); err != nil { t.Fatal(err) } if err := d.WriteDurable("floor.json", []byte("written"), 0o644); err != nil { t.Fatal(err) } body, err := os.ReadFile(filepath.Join(moved, "floor.json")) if err != nil || string(body) != "written" { t.Fatalf("pinned directory = %q, %v", body, err) } if _, err := os.Stat(filepath.Join(root, movesDirName, "floor.json")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("write reached unlocked replacement: %v", err) } }