mirror of
https://github.com/hanzoai/go-fuse.git
synced 2026-10-30 14:46:18 +00:00
1051 lines
25 KiB
Go
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)
|
|
}
|