Files

1051 lines
25 KiB
Go

// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package posixtest file systems for generic posix conformance.
package posixtest
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"runtime"
"sync"
"syscall"
"testing"
"github.com/hanzoai/go-fuse/v2/fuse"
"github.com/hanzoai/go-fuse/v2/internal/fallocate"
"github.com/hanzoai/go-fuse/v2/internal/xattr"
"golang.org/x/sys/unix"
)
// All holds a map of all test functions
var All = map[string]func(*testing.T, string){
"AppendWrite": AppendWrite,
"DirSeek": DirSeek,
"DirectIO": DirectIO,
"Fallocate": Fallocate,
"FallocateKeepSize": FallocateKeepSize,
"FcntlFlockLocksFile": FcntlFlockLocksFile,
"FcntlFlockSetLk": FcntlFlockSetLk,
"FdLeak": FdLeak,
"FileBasic": FileBasic,
"FstatDeleted": FstatDeleted,
"Link": Link,
"LinkUnlinkRename": LinkUnlinkRename,
"LseekEnxioCheck": LseekEnxioCheck,
"LseekHoleSeeksToEOF": LseekHoleSeeksToEOF,
"MkdirRmdir": MkdirRmdir,
"NlinkZero": NlinkZero,
"OpenAt": OpenAt,
"OpenSymlinkRace": OpenSymlinkRace,
"ParallelFileOpen": ParallelFileOpen,
"ReadDir": ReadDir,
"ReadDirConsistency": ReadDirConsistency,
"RenameOpenDir": RenameOpenDir,
"RenameOverwriteDestExist": RenameOverwriteDestExist,
"RenameOverwriteDestNoExist": RenameOverwriteDestNoExist,
"SetattrSymlink": SetattrSymlink,
"SymlinkReadlink": SymlinkReadlink,
"TruncateFile": TruncateFile,
"TruncateNoFile": TruncateNoFile,
"XAttr": XAttr,
}
func SetattrSymlink(t *testing.T, mnt string) {
l := filepath.Join(mnt, "link")
if err := os.Symlink("doesnotexist", l); err != nil {
t.Fatalf("symlink: %v", err)
}
tvs := []unix.Timeval{
{Sec: 42, Usec: 1},
{Sec: 43, Usec: 2},
}
if err := unix.Lutimes(l, tvs); err != nil {
t.Fatalf("Lutimes: %v", err)
}
var st unix.Stat_t
if err := unix.Lstat(l, &st); err != nil {
t.Fatalf("Lstat: %v", err)
}
if st.Mtim.Sec != 43 {
// Can't check atime; it's hard to prevent implicit readlink calls.
t.Fatalf("got mtime %v, want 43", st.Mtim)
}
}
func DirectIO(t *testing.T, mnt string) {
fn := mnt + "/file.txt"
fd, err := syscall.Open(fn, syscall.O_TRUNC|syscall.O_CREAT|syscall.O_DIRECT|syscall.O_WRONLY, 0644)
if err == syscall.EINVAL {
t.Skip("FS does not support O_DIRECT")
}
if err != nil {
t.Fatalf("Open: %v", err)
}
defer func() {
if fd != 0 {
syscall.Close(fd)
}
}()
data := bytes.Repeat([]byte("bye"), 4096)
if n, err := syscall.Write(fd, data); err != nil || n != len(data) {
t.Fatalf("Write: %v (%d)", err, n)
}
err = syscall.Close(fd)
fd = 0
if err != nil {
t.Fatalf("Close: %v", err)
}
fd, err = syscall.Open(fn, syscall.O_DIRECT|syscall.O_RDONLY, 0644)
if err != nil {
t.Fatalf("Open 2: %v", err)
}
roundtrip := bytes.Repeat([]byte("xxx"), 4096)
if n, err := syscall.Read(fd, roundtrip); err != nil || n != len(data) {
t.Fatalf("ReadAt: %v (%d)", err, n)
}
if bytes.Compare(roundtrip, data) != 0 {
t.Errorf("roundtrip made changes: got %q.., want %q..", roundtrip[:10], data[:10])
}
}
// SymlinkReadlink tests basic symlink functionality
func SymlinkReadlink(t *testing.T, mnt string) {
err := os.Symlink("/foobar", mnt+"/link")
if err != nil {
t.Fatalf("Symlink: %v", err)
}
val, err := os.Readlink(mnt + "/link")
if err != nil {
t.Fatalf("Readlink: %v", err)
}
if val != "/foobar" {
t.Errorf("symlink mismatch: %v", val)
}
}
func FileBasic(t *testing.T, mnt string) {
content := []byte("hello world")
fn := mnt + "/file"
if err := os.WriteFile(fn, content, 0755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if got, err := os.ReadFile(fn); err != nil {
t.Fatalf("ReadFile: %v", err)
} else if bytes.Compare(got, content) != 0 {
t.Errorf("ReadFile: got %q, want %q", got, content)
}
f, err := os.Open(fn)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
t.Fatalf("Fstat: %v", err)
} else if int(fi.Size()) != len(content) {
t.Errorf("got size %d want 5", fi.Size())
}
stat := fuse.ToStatT(fi)
if got, want := uint32(stat.Mode), uint32(fuse.S_IFREG|0755); got != want {
t.Errorf("Fstat: got mode %o, want %o", got, want)
}
if err := f.Close(); err != nil {
t.Errorf("Close: %v", err)
}
}
func TruncateFile(t *testing.T, mnt string) {
content := []byte("hello world")
fn := mnt + "/file"
if err := os.WriteFile(fn, content, 0755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
f, err := os.OpenFile(fn, os.O_RDWR, 0644)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close()
const trunc = 5
if err := f.Truncate(5); err != nil {
t.Errorf("Truncate: %v", err)
}
if err := f.Close(); err != nil {
t.Errorf("Close: %v", err)
}
if got, err := os.ReadFile(fn); err != nil {
t.Fatalf("ReadFile: %v", err)
} else if want := content[:trunc]; bytes.Compare(got, want) != 0 {
t.Errorf("got %q, want %q", got, want)
}
}
func TruncateNoFile(t *testing.T, mnt string) {
fn := mnt + "/file"
if err := os.WriteFile(fn, []byte("hello"), 0644); err != nil {
t.Errorf("WriteFile: %v", err)
}
if err := syscall.Truncate(fn, 1); err != nil {
t.Fatalf("Truncate: %v", err)
}
var st syscall.Stat_t
if err := syscall.Lstat(fn, &st); err != nil {
t.Fatalf("Lstat: %v", err)
}
if st.Size != 1 {
t.Errorf("got size %d, want 1", st.Size)
}
}
func FdLeak(t *testing.T, mnt string) {
fn := mnt + "/file"
if err := os.WriteFile(fn, []byte("hello world"), 0755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
for i := 0; i < 100; i++ {
if _, err := os.ReadFile(fn); err != nil {
t.Fatalf("ReadFile: %v", err)
}
}
if runtime.GOOS == "linux" {
infos := listFds(0, "")
if len(infos) > 15 {
t.Errorf("found %d open file descriptors for 100x ReadFile: %v", len(infos), infos)
}
}
}
func MkdirRmdir(t *testing.T, mnt string) {
fn := mnt + "/dir"
if err := os.Mkdir(fn, 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
if fi, err := os.Lstat(fn); err != nil {
t.Fatalf("Lstat %v", err)
} else if !fi.IsDir() {
t.Fatalf("is not a directory")
}
if err := os.Remove(fn); err != nil {
t.Fatalf("Remove: %v", err)
}
}
func NlinkZero(t *testing.T, mnt string) {
src := mnt + "/src"
dst := mnt + "/dst"
if err := os.WriteFile(src, []byte("source"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := os.WriteFile(dst, []byte("dst"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
f, err := syscall.Open(dst, 0, 0)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer syscall.Close(f)
var st syscall.Stat_t
if err := syscall.Fstat(f, &st); err != nil {
t.Errorf("Fstat before: %v", err)
} else if st.Nlink != 1 {
t.Errorf("Nlink of file: got %d, want 1", st.Nlink)
}
if err := os.Rename(src, dst); err != nil {
t.Fatalf("Rename: %v", err)
}
if err := syscall.Fstat(f, &st); err != nil {
t.Errorf("Fstat after: %v", err)
} else if st.Nlink != 0 {
t.Errorf("Nlink of overwritten file: got %d, want 0", st.Nlink)
}
}
// FstatDeleted is similar to NlinkZero, but Fstat()s multiple deleted files
// in random order and checks that the results match an earlier Stat().
//
// Excercises the fd-finding logic in rawBridge.GetAttr.
func FstatDeleted(t *testing.T, mnt string) {
const iMax = 9
type file struct {
fd int
st unix.Stat_t
}
files := make(map[int]file)
for i := 0; i <= iMax; i++ {
// Create files with different sizes
path := fmt.Sprintf("%s/%d", mnt, i)
content := make([]byte, i)
err := os.WriteFile(path, content, 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
var st unix.Stat_t
err = unix.Stat(path, &st)
if err != nil {
t.Fatal(err)
}
// Open
fd, err := syscall.Open(path, syscall.O_RDONLY, 0)
if err != nil {
t.Fatal(err)
}
files[i] = file{fd, st}
defer syscall.Close(fd)
// Delete
err = syscall.Unlink(path)
if err != nil {
t.Fatal(err)
}
}
// Fstat in random order
for _, v := range files {
var st unix.Stat_t
err := unix.Fstat(v.fd, &st)
if err != nil {
t.Fatal(err)
}
// Ignore ctime, changes on unlink
v.st.Ctim = unix.Timespec{}
st.Ctim = unix.Timespec{}
// Nlink value should have dropped to zero
v.st.Nlink = 0
// Rest should stay the same
if v.st != st {
t.Errorf("stat mismatch: want=%v\n have=%v", v.st, st)
}
}
}
func ParallelFileOpen(t *testing.T, mnt string) {
fn := mnt + "/file"
if err := os.WriteFile(fn, []byte("content"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
N := 10
errs := make(chan error, N)
one := func(b byte) {
f, err := os.OpenFile(fn, os.O_RDWR, 0644)
if err != nil {
errs <- err
return
}
var buf [10]byte
f.Read(buf[:])
buf[0] = b
f.WriteAt(buf[0:1], 2)
f.Close()
errs <- nil
}
for i := 0; i < N; i++ {
go one(byte(i))
}
for i := 0; i < N; i++ {
if e := <-errs; e != nil {
t.Error(e)
}
}
}
func Link(t *testing.T, mnt string) {
link := mnt + "/link"
target := mnt + "/target"
if err := os.WriteFile(target, []byte("hello"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
st := syscall.Stat_t{}
if err := syscall.Lstat(target, &st); err != nil {
t.Fatalf("Lstat before: %v", err)
}
beforeIno := st.Ino
if err := os.Link(target, link); err != nil {
t.Errorf("Link: %v", err)
}
if err := syscall.Lstat(link, &st); err != nil {
t.Fatalf("Lstat after: %v", err)
}
if st.Ino != beforeIno {
t.Errorf("Lstat after: got %d, want %d", st.Ino, beforeIno)
}
if st.Nlink != 2 {
t.Errorf("Expect 2 links, got %d", st.Nlink)
}
}
func RenameOverwriteDestNoExist(t *testing.T, mnt string) {
RenameOverwrite(t, mnt, false)
}
func RenameOverwriteDestExist(t *testing.T, mnt string) {
RenameOverwrite(t, mnt, true)
}
func RenameOverwrite(t *testing.T, mnt string, destExists bool) {
if err := os.Mkdir(mnt+"/dir", 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
if err := os.WriteFile(mnt+"/file", []byte("hello"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if destExists {
if err := os.WriteFile(mnt+"/dir/renamed", []byte("xx"), 0644); err != nil {
t.Fatalf("WriteFile dest: %v", err)
}
}
st := syscall.Stat_t{}
if err := syscall.Lstat(mnt+"/file", &st); err != nil {
t.Fatalf("Lstat before: %v", err)
}
beforeIno := st.Ino
if err := os.Rename(mnt+"/file", mnt+"/dir/renamed"); err != nil {
t.Errorf("Rename: %v", err)
}
if fi, err := os.Lstat(mnt + "/file"); err == nil {
t.Fatalf("Lstat old: %v", fi)
}
if err := syscall.Lstat(mnt+"/dir/renamed", &st); err != nil {
t.Fatalf("Lstat after: %v", err)
}
if got := st.Ino; got != beforeIno {
t.Errorf("got ino %d, want %d", got, beforeIno)
}
}
func RenameOpenDir(t *testing.T, mnt string) {
if err := os.Mkdir(mnt+"/dir1", 0755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
// Different permissions so directories are easier to tell apart
if err := os.Mkdir(mnt+"/dir2", 0700); err != nil {
t.Fatalf("Mkdir: %v", err)
}
var st1 syscall.Stat_t
if err := syscall.Stat(mnt+"/dir2", &st1); err != nil {
t.Fatalf("Stat: %v", err)
}
fd, err := syscall.Open(mnt+"/dir2", syscall.O_RDONLY, 0)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer syscall.Close(fd)
if err := syscall.Rename(mnt+"/dir1", mnt+"/dir2"); err != nil {
t.Fatalf("Rename: %v", err)
}
var st2 syscall.Stat_t
if err := syscall.Fstat(fd, &st2); err != nil {
t.Skipf("Fstat failed: %v. Known limitation - see https://github.com/hanwen/go-fuse/issues/55", err)
}
if st2.Mode&syscall.S_IFMT != syscall.S_IFDIR {
t.Errorf("got mode %o, want %o", st2.Mode, syscall.S_IFDIR)
}
if st2.Ino != st1.Ino {
t.Errorf("got ino %d, want %d", st2.Ino, st1.Ino)
}
if st2.Mode&0777 != st1.Mode&0777 {
t.Skipf("got permissions %#o, want %#o. Known limitation - see https://github.com/hanwen/go-fuse/issues/55",
st2.Mode&0777, st1.Mode&0777)
}
}
func readAllDirEntries(fd int) ([]fuse.DirEntry, error) {
var buf [4096]byte
var result []fuse.DirEntry
for {
n, err := unix.ReadDirent(fd, buf[:])
if err != nil {
return nil, err
}
if n == 0 {
break
}
todo := buf[:n]
for len(todo) > 0 {
var de fuse.DirEntry
n := de.Parse(todo)
todo = todo[n:]
result = append(result, de)
}
}
return result, nil
}
// ReadDir creates 110 files one by one, checking that we get the expected
// entries after each file creation.
func ReadDir(t *testing.T, mnt string) {
want := map[string]bool{}
// 40 bytes of filename, so 110 entries overflows a
// 4096 page.
for i := 0; i < 110; i++ {
nm := fmt.Sprintf("file%036x", i)
want[nm] = true
if err := os.WriteFile(filepath.Join(mnt, nm), []byte("hello"), 0644); err != nil {
t.Fatalf("WriteFile %q: %v", nm, err)
}
// Verify that we get the expected entries
f, err := os.Open(mnt)
if err != nil {
t.Fatalf("Open: %v", err)
}
names, err := f.Readdirnames(-1)
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
f.Close()
got := map[string]bool{}
for _, e := range names {
got[e] = true
}
if len(got) != len(want) {
t.Errorf("mismatch got %d want %d", len(got), len(want))
}
for k := range got {
if !want[k] {
t.Errorf("got extra entry %q", k)
}
}
for k := range want {
if !got[k] {
t.Errorf("missing entry %q", k)
}
}
}
}
// Creates files, read them back using readdir
func ReadDirConsistency(t *testing.T, mnt string) {
var results [][]fuse.DirEntry
for i := 0; i < 2; i++ {
fd, err := syscall.Open(mnt, syscall.O_DIRECTORY, 0)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer syscall.Close(fd)
r, err := readAllDirEntries(fd)
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
results = append(results, r)
}
if !reflect.DeepEqual(results[0], results[1]) {
t.Errorf("got %v, want %v", results[0], results[1])
}
}
// LinkUnlinkRename implements rename with a link/unlink sequence
func LinkUnlinkRename(t *testing.T, mnt string) {
content := []byte("hello")
tmp := mnt + "/tmpfile"
if err := os.WriteFile(tmp, content, 0644); err != nil {
t.Fatalf("WriteFile %q: %v", tmp, err)
}
dest := mnt + "/file"
if err := syscall.Link(tmp, dest); err != nil {
t.Fatalf("Link %q %q: %v", tmp, dest, err)
}
if err := syscall.Unlink(tmp); err != nil {
t.Fatalf("Unlink %q: %v", tmp, err)
}
if back, err := os.ReadFile(dest); err != nil {
t.Fatalf("Read %q: %v", dest, err)
} else if bytes.Compare(back, content) != 0 {
t.Fatalf("Read got %q want %q", back, content)
}
}
// test open with O_APPEND
func AppendWrite(t *testing.T, mnt string) {
fd, err := syscall.Open(mnt+"/file", syscall.O_WRONLY|syscall.O_APPEND|syscall.O_CREAT, 0644)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer func() {
if fd != 0 {
syscall.Close(fd)
}
}()
if _, err := syscall.Write(fd, []byte("hello")); err != nil {
t.Fatalf("Write 1: %v", err)
}
if _, err := syscall.Write(fd, []byte("world")); err != nil {
t.Fatalf("Write 2: %v", err)
}
if err := syscall.Close(fd); err != nil {
t.Fatalf("Open: %v", err)
}
fd = 0
want := []byte("helloworld")
got, err := os.ReadFile(mnt + "/file")
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if bytes.Compare(got, want) != 0 {
t.Errorf("got %q want %q", got, want)
}
}
// OpenAt tests syscall.Openat().
//
// Hint:
// $ go test ./fs -run TestPosix/OpenAt -v
func OpenAt(t *testing.T, mnt string) {
dir1 := mnt + "/dir1"
err := os.Mkdir(dir1, 0777)
if err != nil {
t.Fatal(err)
}
dirfd, err := syscall.Open(dir1, syscall.O_RDONLY, 0)
if err != nil {
t.Fatal(err)
}
defer syscall.Close(dirfd)
dir2 := mnt + "/dir2"
err = os.Rename(dir1, dir2)
if err != nil {
t.Fatal(err)
}
fd, err := unix.Openat(dirfd, "file1", syscall.O_CREAT, 0700)
if err != nil {
t.Fatal(err)
}
defer syscall.Close(fd)
_, err = os.Stat(dir2 + "/file1")
if err != nil {
t.Error(err)
}
}
func Fallocate(t *testing.T, mnt string) {
rwFile, err := os.OpenFile(mnt+"/file", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
t.Fatalf("OpenFile failed: %v", err)
}
defer rwFile.Close()
err = fallocate.Fallocate(int(rwFile.Fd()), 0, 1024, 4096)
if err != nil {
t.Fatalf("Fallocate failed: %v", err)
}
fi, err := os.Lstat(mnt + "/file")
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
if fi.Size() < (1024 + 4096) {
t.Fatalf("fallocate should have changed file size. Got %d bytes",
fi.Size())
}
}
func FallocateKeepSize(t *testing.T, mnt string) {
f, err := os.Create(mnt + "/file")
if err != nil {
t.Fatal(err)
}
defer f.Close()
data := bytes.Repeat([]byte{42}, 100)
if _, err := f.Write(data); err != nil {
t.Fatal(err)
}
if err := syscall.Fallocate(int(f.Fd()), unix.FALLOC_FL_KEEP_SIZE, 50, 52); err != nil {
t.Fatal(err)
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
roundtrip, _ := io.ReadAll(f)
if !bytes.Equal(roundtrip, data) {
t.Fatalf("roundtrip not equal %q != %q", roundtrip, data)
}
}
func FcntlFlockSetLk(t *testing.T, mnt string) {
for i, cmd := range []int{syscall.F_SETLK, syscall.F_SETLKW} {
filename := mnt + fmt.Sprintf("/file%d", i)
f1, err := os.Create(filename)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f1.Close()
wlk := syscall.Flock_t{
Type: syscall.F_WRLCK,
Start: 0,
Len: 0,
}
if err := syscall.FcntlFlock(f1.Fd(), cmd, &wlk); err != nil {
t.Fatalf("FcntlFlock failed: %v", err)
}
f2, err := os.OpenFile(filename, os.O_RDWR, 0766)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f2.Close()
lk := syscall.Flock_t{}
if err := sysFcntlFlockGetOFDLock(f2.Fd(), &lk); err != nil {
t.Errorf("FcntlFlock failed: %v", err)
}
if lk.Type != syscall.F_WRLCK {
t.Errorf("got lk.Type=%v, want %v", lk.Type, syscall.F_WRLCK)
}
}
}
func FcntlFlockLocksFile(t *testing.T, mnt string) {
filename := mnt + "/test"
f1, err := os.Create(filename)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f1.Close()
wlk := syscall.Flock_t{
Type: syscall.F_WRLCK,
Start: 0,
Len: 0,
}
if err := syscall.FcntlFlock(f1.Fd(), syscall.F_SETLK, &wlk); err != nil {
t.Fatalf("FcntlFlock failed: %v", err)
}
f2, err := os.OpenFile(filename, os.O_RDWR, 0766)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f2.Close()
rlk := syscall.Flock_t{
Type: syscall.F_RDLCK,
Start: 0,
Len: 0,
}
if err := syscall.FcntlFlock(f2.Fd(), syscall.F_SETLK, &rlk); err != syscall.EAGAIN {
t.Errorf("FcntlFlock returned %v, expected EAGAIN", err)
}
}
func LseekHoleSeeksToEOF(t *testing.T, mnt string) {
fn := filepath.Join(mnt, "file.bin")
content := bytes.Repeat([]byte("abcxyz\n"), 1024)
if err := os.WriteFile(fn, content, 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
fd, err := syscall.Open(fn, syscall.O_RDONLY, 0644)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer syscall.Close(fd)
off, err := unix.Seek(fd, int64(len(content)/2), unix.SEEK_HOLE)
if err != nil {
t.Fatalf("Seek: %v", err)
} else if off != int64(len(content)) {
t.Errorf("got offset %d, want %d", off, len(content))
}
}
func LseekEnxioCheck(t *testing.T, mnt string) {
fn := filepath.Join(mnt, "file.bin")
content := bytes.Repeat([]byte("abcxyz\n"), 1024)
if err := os.WriteFile(fn, content, 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
fd, err := syscall.Open(fn, syscall.O_RDONLY, 0644)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer syscall.Close(fd)
testCases := []struct {
name string
offset int64
whence int
}{
{
name: "Lseek SEEK_DATA where offset is at EOF returns ENXIO",
offset: int64(len(content)),
whence: unix.SEEK_DATA,
},
{
name: "Lseek SEEK_DATA where offset greater than EOF returns ENXIO",
offset: int64(len(content)) + 1,
whence: unix.SEEK_DATA,
},
{
name: "Lseek SEEK_HOLE where offset is greater than EOF returns ENXIO",
offset: int64(len(content)) + 1,
whence: unix.SEEK_HOLE,
},
}
for _, tc := range testCases {
_, err := unix.Seek(fd, tc.offset, tc.whence)
if err != nil {
if !errors.Is(err, syscall.ENXIO) {
t.Errorf("Failed test case: %s; got %v, want %v", tc.name, err, syscall.ENXIO)
}
}
}
}
// XAttr; they aren't posix but cross-platform enough to put into posixtest/
func XAttr(t *testing.T, mntDir string) {
buf := make([]byte, 1024)
attrNameSpace := "user"
attrName := "xattrtest"
attr := fmt.Sprintf("%s.%s", attrNameSpace, attrName)
fn := mntDir + "/file"
if err := os.WriteFile(fn, []byte{}, 0666); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if _, err := unix.Getxattr(fn, attr, buf); err == unix.ENOTSUP {
t.Skipf("filesystem for %s does not support xattrs. Rerun this test with a $TMPDIR override", fn)
}
if _, err := unix.Getxattr(fn, attr, buf); err != xattr.ENOATTR {
t.Fatalf("got %v want ENOATTR", err)
}
value := []byte("value")
if err := unix.Setxattr(fn, attr, value, 0); err != nil {
t.Fatalf("Setxattr: %v", err)
}
sz, err := unix.Listxattr(fn, nil)
if err != nil {
t.Fatalf("Listxattr: %v", err)
}
buf = make([]byte, sz)
if _, err := unix.Listxattr(fn, buf); err != nil {
t.Fatalf("Listxattr: %v", err)
} else {
attributes := xattr.ParseAttrNames(buf[:sz])
found := false
for _, a := range attributes {
if string(a) == attr || attrNameSpace+string(a) == attr {
found = true
break
}
}
if !found {
t.Fatalf("Listxattr: %q (not found: %q", attributes, attr)
}
}
sz, err = unix.Getxattr(fn, attr, buf)
if err != nil {
t.Fatalf("Getxattr: %v", err)
}
if bytes.Compare(buf[:sz], value) != 0 {
t.Fatalf("Getxattr got %q want %q", buf[:sz], value)
}
if err := unix.Removexattr(fn, attr); err != nil {
t.Fatalf("Removexattr: %v", err)
}
if _, err := unix.Getxattr(fn, attr, buf); err != xattr.ENOATTR {
t.Fatalf("got %v want ENOATTR", err)
}
}
// Test if Open() is vulnerable to symlink-race attacks using two goroutines:
//
// goroutine "shuffler":
// In a loop:
// * Replace empty file "OpenSymlinkRace" with a symlink pointing to /etc/passwd
// * Replace back with empty file
//
// goroutine "opener":
// In a loop:
// * Open "OpenSymlinkRace" and call Fstat on it. Now there's three cases:
// 1. Size=0: we opened the empty file created by shuffler. Normal and uninteresting.
// 2. Size>0 but Dev number different: we (this test) opened /etc/passwd ourselves
// because we resolved the symlink. Normal.
// 3. Size>0 and Dev number matches the FUSE mount: go-fuse opened /etc/passwd.
// The attack has worked.
func OpenSymlinkRace(t *testing.T, mnt string) {
path := mnt + "/OpenSymlinkRace"
var wg sync.WaitGroup
wg.Add(2)
const iterations = 1000
fd, err := syscall.Creat(path, 0600)
if err != nil {
t.Error(err)
return
}
// Find and save the device number of the FUSE mount
var st syscall.Stat_t
err = syscall.Fstat(fd, &st)
fuseMountDev := st.Dev
if err != nil {
t.Fatal(err)
}
syscall.Close(fd)
// Shuffler
go func() {
defer wg.Done()
tmp := path + ".tmp"
for i := 0; i < iterations; i++ {
// Stop when another thread has failed
if t.Failed() {
return
}
// Make "path" a regular file
fd, err := syscall.Creat(tmp, 0600)
if err != nil {
t.Errorf("shuffler: Creat: %v", err)
return
}
syscall.Close(fd)
err = syscall.Rename(tmp, path)
if err != nil {
t.Error(err)
return
}
// Make "path" a symlink
err = syscall.Symlink("/etc/passwd", tmp)
if err != nil {
t.Errorf("shuffler: Symlink: %v", err)
return
}
err = syscall.Rename(tmp, path)
if err != nil {
t.Errorf("shuffler: Rename: %v", err)
return
}
}
}()
// Keep some statistics
type statsT struct {
EINVAL int
ENOENT int
ELOOP int
empty int
resolvedSymlink int
}
var stats statsT
// Opener
go func() {
defer wg.Done()
var st syscall.Stat_t
for i := 0; i < iterations; i++ {
// Stop when another thread has failed
if t.Failed() {
return
}
fd, err := syscall.Open(path, syscall.O_RDONLY, 0)
if err != nil {
if err == syscall.EINVAL {
// Happens when the kernel tries to read the symlink but
// it's already a file again in the backing directory
stats.EINVAL++
continue
}
if err == syscall.ELOOP {
// Looks like there's some symlink-safety
stats.ELOOP++
continue
}
if err == syscall.ENOENT {
// Not sure why we get these, but we do
stats.ENOENT++
continue
}
t.Errorf("opener: Open: %v", err)
return
}
err = syscall.Fstat(fd, &st)
syscall.Close(fd)
if err != nil {
t.Errorf("opener: Fstat: %v", err)
return
}
if st.Size == 0 {
stats.empty++
continue
}
if st.Dev != fuseMountDev {
stats.resolvedSymlink++
} else {
// opened /etc/passwd
t.Errorf("opener: successful symlink attack in iteration %d. We tricked go-fuse into opening /etc/passwd.", i)
return
}
}
}()
wg.Wait()
t.Logf("opener stats: %#v", stats)
}