//go:build linux package securefile import ( "bytes" "errors" "os" "path/filepath" "syscall" "testing" ) func TestOpenDirAndReadFileAcceptProtectedRegularFile(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "record.json") want := []byte(`{"record":"synthetic"}`) writeFile(t, path, want, 0o640) dir, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir() error = %v", err) } t.Cleanup(func() { _ = dir.Close() }) got, err := dir.ReadFile("record.json", 4096) if err != nil { t.Fatalf("ReadFile() error = %v", err) } if !bytes.Equal(got, want) { t.Fatalf("ReadFile() = %q, want %q", got, want) } } func TestProtectedPathsRejectSymlinks(t *testing.T) { t.Run("root", func(t *testing.T) { parent := t.TempDir() target := t.TempDir() root := filepath.Join(parent, "registry") if err := os.Symlink(target, root); err != nil { t.Fatalf("Symlink() error = %v", err) } if _, err := OpenDir(root); err == nil { t.Fatal("OpenDir() error = nil, want symlink refusal") } }) t.Run("directory", func(t *testing.T) { root := t.TempDir() target := t.TempDir() if err := os.Symlink(target, filepath.Join(root, "active")); err != nil { t.Fatalf("Symlink() error = %v", err) } dir, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir() error = %v", err) } t.Cleanup(func() { _ = dir.Close() }) if _, err := dir.OpenDir("active"); err == nil { t.Fatal("OpenDir(active) error = nil, want symlink refusal") } }) t.Run("record", func(t *testing.T) { root := t.TempDir() dir, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir() error = %v", err) } t.Cleanup(func() { _ = dir.Close() }) target := filepath.Join(root, "target.json") writeFile(t, target, []byte(`{"safe":false}`), 0o640) if err := os.Symlink(target, filepath.Join(root, "record.json")); err != nil { t.Fatalf("Symlink() error = %v", err) } if _, err := dir.ReadFile("record.json", 4096); err == nil { t.Fatal("ReadFile() error = nil, want symlink refusal") } }) t.Run("secret", func(t *testing.T) { root := t.TempDir() target := filepath.Join(root, "target.secret") writeFile(t, target, []byte("synthetic-secret"), 0o600) secret := filepath.Join(root, "secret") if err := os.Symlink(target, secret); err != nil { t.Fatalf("Symlink() error = %v", err) } if _, err := ReadSecret(secret, 128); err == nil { t.Fatal("ReadSecret() error = nil, want symlink refusal") } }) } func TestProtectedPathsRejectUnsafeModes(t *testing.T) { t.Run("setgid protected directories", func(t *testing.T) { root := t.TempDir() if err := os.Chmod(root, 0o750|os.ModeSetgid); err != nil { t.Fatalf("Chmod(root) error = %v", err) } dir, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir(2750 root) error = %v", err) } t.Cleanup(func() { _ = dir.Close() }) for _, mode := range []os.FileMode{ 0o750 | os.ModeSetuid, 0o750 | os.ModeSetgid | os.ModeSetuid, 0o750 | os.ModeSticky, 0o770 | os.ModeSetgid, 0o752 | os.ModeSetgid, } { path := t.TempDir() if err := os.Chmod(path, mode); err != nil { t.Fatalf("Chmod(%v) error = %v", mode, err) } if _, err := OpenDir(path); err == nil { t.Fatalf("OpenDir(%v) error = nil, want unsafe-mode refusal", mode) } } }) t.Run("root", func(t *testing.T) { root := t.TempDir() if err := os.Chmod(root, 0o770); err != nil { t.Fatalf("Chmod() error = %v", err) } if _, err := OpenDir(root); err == nil { t.Fatal("OpenDir() error = nil, want unsafe mode refusal") } }) t.Run("child directory", func(t *testing.T) { root := t.TempDir() child := filepath.Join(root, "active") if err := os.Mkdir(child, 0o770); err != nil { t.Fatalf("Mkdir() error = %v", err) } if err := os.Chmod(child, 0o770); err != nil { t.Fatalf("Chmod() error = %v", err) } dir, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir() error = %v", err) } t.Cleanup(func() { _ = dir.Close() }) if _, err := dir.OpenDir("active"); err == nil { t.Fatal("OpenDir(active) error = nil, want unsafe mode refusal") } }) t.Run("record", func(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "record.json") writeFile(t, path, []byte(`{"unsafe":true}`), 0o660) dir, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir() error = %v", err) } t.Cleanup(func() { _ = dir.Close() }) if _, err := dir.ReadFile("record.json", 4096); err == nil { t.Fatal("ReadFile() error = nil, want unsafe mode refusal") } }) t.Run("secret", func(t *testing.T) { root := t.TempDir() secret := filepath.Join(root, "secret") writeFile(t, secret, []byte("synthetic-secret"), 0o640) if _, err := ReadSecret(secret, 128); err == nil { t.Fatal("ReadSecret() error = nil, want non-0600 refusal") } }) } func TestReadFileRejectsOversizeContent(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "record.json") writeFile(t, path, bytes.Repeat([]byte{'a'}, 4097), 0o640) dir, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir() error = %v", err) } t.Cleanup(func() { _ = dir.Close() }) if _, err := dir.ReadFile("record.json", 4096); err == nil { t.Fatal("ReadFile() error = nil, want bounded-read refusal") } } func TestReadSecretAndCreateSecretUse0600AndExclusiveCreate(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "input.secret") want := []byte("synthetic-secret") writeFile(t, input, want, 0o600) got, err := ReadSecret(input, 128) if err != nil { t.Fatalf("ReadSecret() error = %v", err) } if !bytes.Equal(got, want) { t.Fatalf("ReadSecret() = %q, want %q", got, want) } output := filepath.Join(root, "output.secret") file, err := CreateSecret(output) if err != nil { t.Fatalf("CreateSecret() error = %v", err) } if _, err := file.Write(want); err != nil { _ = file.Close() t.Fatalf("Write() error = %v", err) } if err := file.Sync(); err != nil { _ = file.Close() t.Fatalf("Sync() error = %v", err) } if err := file.Close(); err != nil { t.Fatalf("Close() error = %v", err) } info, err := os.Stat(output) if err != nil { t.Fatalf("Stat() error = %v", err) } if got, want := info.Mode().Perm(), os.FileMode(0o600); got != want { t.Fatalf("output mode = %04o, want %04o", got, want) } if _, err := CreateSecret(output); err == nil { t.Fatal("CreateSecret(existing) error = nil, want exclusive-create refusal") } else if !errors.Is(err, os.ErrExist) { t.Fatalf("CreateSecret(existing) error = %v, want os.ErrExist", err) } } func TestLockSharedAllowsReadersAndBlocksExclusiveWriter(t *testing.T) { root := t.TempDir() first, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir(first) error = %v", err) } defer first.Close() second, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir(second) error = %v", err) } defer second.Close() third, err := OpenDir(root) if err != nil { t.Fatalf("OpenDir(third) error = %v", err) } defer third.Close() if first.fd == second.fd || second.fd == third.fd || first.fd == third.fd { t.Fatal("OpenDir() reused a descriptor; want separate lock file descriptions") } type sharedLocker interface { LockShared(string) (*Lock, error) } firstLocker, ok := any(first).(sharedLocker) if !ok { t.Fatal("Dir lacks shared advisory locking") } secondLocker, ok := any(second).(sharedLocker) if !ok { t.Fatal("Dir lacks shared advisory locking") } if _, err := firstLocker.LockShared("registry.lock"); err == nil { t.Fatal("LockShared(missing) error = nil, want preprovisioned-lock refusal") } if _, err := os.Lstat(filepath.Join(root, "registry.lock")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("LockShared(missing) created lock: %v", err) } bootstrap, err := third.Lock("registry.lock") if err != nil { t.Fatalf("Lock(bootstrap) error = %v", err) } if err := bootstrap.Close(); err != nil { t.Fatalf("Close(bootstrap) error = %v", err) } if info, err := os.Stat(filepath.Join(root, "registry.lock")); err != nil || info.Mode().Perm() != 0o640 { t.Fatalf("bootstrapped lock mode = (%v, %v), want 0640", info, err) } firstShared, err := firstLocker.LockShared("registry.lock") if err != nil { t.Fatalf("LockShared(first) error = %v", err) } defer firstShared.Close() secondShared, err := secondLocker.LockShared("registry.lock") if err != nil { t.Fatalf("LockShared(second) error = %v", err) } defer secondShared.Close() probe, err := os.OpenFile(filepath.Join(root, "registry.lock"), os.O_RDWR, 0) if err != nil { t.Fatalf("OpenFile(lock probe) error = %v", err) } if err := syscall.Flock(int(probe.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err == nil { _ = syscall.Flock(int(probe.Fd()), syscall.LOCK_UN) _ = probe.Close() t.Fatal("exclusive lock acquired while separate shared locks were held") } else if !errors.Is(err, syscall.EWOULDBLOCK) && !errors.Is(err, syscall.EAGAIN) { _ = probe.Close() t.Fatalf("nonblocking exclusive lock error = %v, want EWOULDBLOCK", err) } if err := probe.Close(); err != nil { t.Fatalf("Close(lock probe) error = %v", err) } if err := secondShared.Close(); err != nil { t.Fatalf("Close(second shared) error = %v", err) } secondShared = nil if err := firstShared.Close(); err != nil { t.Fatalf("Close(first shared) error = %v", err) } firstShared = nil exclusive, err := third.Lock("registry.lock") if err != nil { t.Fatalf("Lock(exclusive after shared release) error = %v", err) } if err := exclusive.Close(); err != nil { t.Fatalf("Close(exclusive) error = %v", err) } } func writeFile(t *testing.T, path string, data []byte, mode os.FileMode) { t.Helper() if err := os.WriteFile(path, data, mode); err != nil { t.Fatalf("WriteFile(%q) error = %v", path, err) } if err := os.Chmod(path, mode); err != nil { t.Fatalf("Chmod(%q) error = %v", path, err) } }