1// Copyright 2019 the Go-FUSE Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package fs
6
7import (
8	"context"
9	"fmt"
10	"io/ioutil"
11	"log"
12	"os"
13	"os/exec"
14	"path/filepath"
15	"reflect"
16	"runtime"
17	"sort"
18	"strings"
19	"sync"
20	"sync/atomic"
21	"syscall"
22	"testing"
23	"time"
24
25	"github.com/hanwen/go-fuse/v2/fuse"
26	"github.com/hanwen/go-fuse/v2/internal/testutil"
27	"github.com/hanwen/go-fuse/v2/posixtest"
28)
29
30type testCase struct {
31	*testing.T
32
33	dir     string
34	origDir string
35	mntDir  string
36
37	loopback InodeEmbedder
38	rawFS    fuse.RawFileSystem
39	server   *fuse.Server
40}
41
42// writeOrig writes a file into the backing directory of the loopback mount
43func (tc *testCase) writeOrig(path, content string, mode os.FileMode) {
44	if err := ioutil.WriteFile(filepath.Join(tc.origDir, path), []byte(content), mode); err != nil {
45		tc.Fatal(err)
46	}
47}
48
49func (tc *testCase) Clean() {
50	if err := tc.server.Unmount(); err != nil {
51		tc.Fatal(err)
52	}
53	if err := os.RemoveAll(tc.dir); err != nil {
54		tc.Fatal(err)
55	}
56}
57
58type testOptions struct {
59	entryCache    bool
60	attrCache     bool
61	suppressDebug bool
62	testDir       string
63	ro            bool
64}
65
66// newTestCase creates the directories `orig` and `mnt` inside a temporary
67// directory and mounts a loopback filesystem, backed by `orig`, on `mnt`.
68func newTestCase(t *testing.T, opts *testOptions) *testCase {
69	if opts == nil {
70		opts = &testOptions{}
71	}
72	if opts.testDir == "" {
73		opts.testDir = testutil.TempDir()
74	}
75	tc := &testCase{
76		dir: opts.testDir,
77		T:   t,
78	}
79	tc.origDir = tc.dir + "/orig"
80	tc.mntDir = tc.dir + "/mnt"
81	if err := os.Mkdir(tc.origDir, 0755); err != nil {
82		t.Fatal(err)
83	}
84	if err := os.Mkdir(tc.mntDir, 0755); err != nil {
85		t.Fatal(err)
86	}
87
88	var err error
89	tc.loopback, err = NewLoopbackRoot(tc.origDir)
90	if err != nil {
91		t.Fatalf("NewLoopback: %v", err)
92	}
93
94	oneSec := time.Second
95
96	attrDT := &oneSec
97	if !opts.attrCache {
98		attrDT = nil
99	}
100	entryDT := &oneSec
101	if !opts.entryCache {
102		entryDT = nil
103	}
104	tc.rawFS = NewNodeFS(tc.loopback, &Options{
105		EntryTimeout: entryDT,
106		AttrTimeout:  attrDT,
107	})
108
109	mOpts := &fuse.MountOptions{}
110	if !opts.suppressDebug {
111		mOpts.Debug = testutil.VerboseTest()
112	}
113	if opts.ro {
114		mOpts.Options = append(mOpts.Options, "ro")
115	}
116	tc.server, err = fuse.NewServer(tc.rawFS, tc.mntDir, mOpts)
117	if err != nil {
118		t.Fatal(err)
119	}
120
121	go tc.server.Serve()
122	if err := tc.server.WaitMount(); err != nil {
123		t.Fatal(err)
124	}
125	return tc
126}
127
128func TestBasic(t *testing.T) {
129	tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
130	defer tc.Clean()
131
132	tc.writeOrig("file", "hello", 0644)
133
134	fn := tc.mntDir + "/file"
135	fi, err := os.Lstat(fn)
136	if err != nil {
137		t.Fatalf("Lstat: %v", err)
138	}
139
140	if fi.Size() != 5 {
141		t.Errorf("got size %d want 5", fi.Size())
142	}
143
144	stat := fuse.ToStatT(fi)
145	if got, want := uint32(stat.Mode), uint32(fuse.S_IFREG|0644); got != want {
146		t.Errorf("got mode %o, want %o", got, want)
147	}
148
149	if err := os.Remove(fn); err != nil {
150		t.Errorf("Remove: %v", err)
151	}
152
153	if fi, err := os.Lstat(fn); err == nil {
154		t.Errorf("Lstat after remove: got file %v", fi)
155	}
156}
157
158func TestFileFdLeak(t *testing.T) {
159	tc := newTestCase(t, &testOptions{
160		suppressDebug: true,
161		attrCache:     true,
162		entryCache:    true,
163	})
164	defer func() {
165		if tc != nil {
166			tc.Clean()
167		}
168	}()
169
170	posixtest.FdLeak(t, tc.mntDir)
171
172	tc.Clean()
173	bridge := tc.rawFS.(*rawBridge)
174	tc = nil
175
176	if got := len(bridge.files); got > 3 {
177		t.Errorf("found %d used file handles, should be <= 3", got)
178	}
179}
180
181func TestNotifyEntry(t *testing.T) {
182	tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
183	defer tc.Clean()
184
185	orig := tc.origDir + "/file"
186	fn := tc.mntDir + "/file"
187	tc.writeOrig("file", "hello", 0644)
188
189	st := syscall.Stat_t{}
190	if err := syscall.Lstat(fn, &st); err != nil {
191		t.Fatalf("Lstat before: %v", err)
192	}
193
194	if err := os.Remove(orig); err != nil {
195		t.Fatalf("Remove: %v", err)
196	}
197
198	after := syscall.Stat_t{}
199	if err := syscall.Lstat(fn, &after); err != nil {
200		t.Fatalf("Lstat after: %v", err)
201	} else if !reflect.DeepEqual(st, after) {
202		t.Fatalf("got after %#v, want %#v", after, st)
203	}
204
205	if errno := tc.loopback.EmbeddedInode().NotifyEntry("file"); errno != 0 {
206		t.Errorf("notify failed: %v", errno)
207	}
208
209	if err := syscall.Lstat(fn, &after); err != syscall.ENOENT {
210		t.Fatalf("Lstat after: got %v, want ENOENT", err)
211	}
212}
213
214func TestReadDirStress(t *testing.T) {
215	tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
216	defer tc.Clean()
217
218	// Create 110 entries
219	for i := 0; i < 110; i++ {
220		name := fmt.Sprintf("file%036x", i)
221		if err := ioutil.WriteFile(filepath.Join(tc.mntDir, name), []byte("hello"), 0644); err != nil {
222			t.Fatalf("WriteFile %q: %v", name, err)
223		}
224	}
225
226	var wg sync.WaitGroup
227	stress := func(gr int) {
228		defer wg.Done()
229		for i := 1; i < 100; i++ {
230			f, err := os.Open(tc.mntDir)
231			if err != nil {
232				t.Error(err)
233				return
234			}
235			_, err = f.Readdirnames(-1)
236			if err != nil {
237				t.Errorf("goroutine %d iteration %d: %v", gr, i, err)
238				f.Close()
239				return
240			}
241			f.Close()
242		}
243	}
244
245	n := 3
246	for i := 1; i <= n; i++ {
247		wg.Add(1)
248		go stress(i)
249	}
250	wg.Wait()
251}
252
253// This test is racy. If an external process consumes space while this
254// runs, we may see spurious differences between the two statfs() calls.
255func TestStatFs(t *testing.T) {
256	tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
257	defer tc.Clean()
258
259	empty := syscall.Statfs_t{}
260	orig := empty
261	if err := syscall.Statfs(tc.origDir, &orig); err != nil {
262		t.Fatal("statfs orig", err)
263	}
264
265	mnt := syscall.Statfs_t{}
266	if err := syscall.Statfs(tc.mntDir, &mnt); err != nil {
267		t.Fatal("statfs mnt", err)
268	}
269
270	var mntFuse, origFuse fuse.StatfsOut
271	mntFuse.FromStatfsT(&mnt)
272	origFuse.FromStatfsT(&orig)
273
274	if !reflect.DeepEqual(mntFuse, origFuse) {
275		t.Errorf("Got %#v, want %#v", mntFuse, origFuse)
276	}
277}
278
279func TestGetAttrParallel(t *testing.T) {
280	// We grab a file-handle to provide to the API so rename+fstat
281	// can be handled correctly. Here, test that closing and
282	// (f)stat in parallel don't lead to fstat on closed files.
283	// We can only test that if we switch off caching
284	tc := newTestCase(t, &testOptions{suppressDebug: true})
285	defer tc.Clean()
286
287	N := 100
288
289	var fds []int
290	var fns []string
291	for i := 0; i < N; i++ {
292		fn := fmt.Sprintf("file%d", i)
293		tc.writeOrig(fn, "ello", 0644)
294		fn = filepath.Join(tc.mntDir, fn)
295		fns = append(fns, fn)
296		fd, err := syscall.Open(fn, syscall.O_RDONLY, 0)
297		if err != nil {
298			t.Fatalf("Open %d: %v", i, err)
299		}
300
301		fds = append(fds, fd)
302	}
303
304	var wg sync.WaitGroup
305	wg.Add(2 * N)
306	for i := 0; i < N; i++ {
307		go func(i int) {
308			if err := syscall.Close(fds[i]); err != nil {
309				t.Errorf("close %d: %v", i, err)
310			}
311			wg.Done()
312		}(i)
313		go func(i int) {
314			var st syscall.Stat_t
315			if err := syscall.Lstat(fns[i], &st); err != nil {
316				t.Errorf("lstat %d: %v", i, err)
317			}
318			wg.Done()
319		}(i)
320	}
321	wg.Wait()
322}
323
324func TestMknod(t *testing.T) {
325	tc := newTestCase(t, &testOptions{})
326	defer tc.Clean()
327
328	modes := map[string]uint32{
329		"regular": syscall.S_IFREG,
330		"socket":  syscall.S_IFSOCK,
331		"fifo":    syscall.S_IFIFO,
332	}
333
334	for nm, mode := range modes {
335		t.Run(nm, func(t *testing.T) {
336			p := filepath.Join(tc.mntDir, nm)
337			err := syscall.Mknod(p, mode|0755, (8<<8)|0)
338			if err != nil {
339				t.Fatalf("mknod(%s): %v", nm, err)
340			}
341
342			var st syscall.Stat_t
343			if err := syscall.Stat(p, &st); err != nil {
344				got := st.Mode &^ 07777
345				if want := mode; got != want {
346					t.Fatalf("stat(%s): got %o want %o", nm, got, want)
347				}
348			}
349
350			// We could test if the files can be
351			// read/written but: The kernel handles FIFOs
352			// without talking to FUSE at all. Presumably,
353			// this also holds for sockets.  Regular files
354			// are tested extensively elsewhere.
355		})
356	}
357}
358
359func TestPosix(t *testing.T) {
360	noisy := map[string]bool{
361		"ParallelFileOpen": true,
362		"ReadDir":          true,
363	}
364
365	for nm, fn := range posixtest.All {
366		t.Run(nm, func(t *testing.T) {
367			tc := newTestCase(t, &testOptions{
368				suppressDebug: noisy[nm],
369				attrCache:     true, entryCache: true})
370			defer tc.Clean()
371
372			fn(t, tc.mntDir)
373		})
374	}
375}
376
377func TestOpenDirectIO(t *testing.T) {
378	// Apparently, tmpfs does not allow O_DIRECT, so try to create
379	// a test temp directory in /var/tmp.
380	ext4Dir, err := ioutil.TempDir("/var/tmp", "go-fuse.TestOpenDirectIO")
381	if err != nil {
382		t.Fatalf("MkdirAll: %v", err)
383	}
384	defer os.RemoveAll(ext4Dir)
385
386	posixtest.DirectIO(t, ext4Dir)
387	if t.Failed() {
388		t.Skip("DirectIO failed on underlying FS")
389	}
390
391	opts := testOptions{
392		testDir:    ext4Dir,
393		attrCache:  true,
394		entryCache: true,
395	}
396
397	tc := newTestCase(t, &opts)
398	defer tc.Clean()
399	posixtest.DirectIO(t, tc.mntDir)
400}
401
402// TestFsstress is loosely modeled after xfstest's fsstress. It performs rapid
403// parallel removes / creates / readdirs. Coupled with inode reuse, this test
404// used to deadlock go-fuse quite quickly.
405//
406// Note: Run as
407//
408//     TMPDIR=/var/tmp go test -run TestFsstress
409//
410// to make sure the backing filesystem is ext4. /tmp is tmpfs on modern Linux
411// distributions, and tmpfs does not reuse inode numbers, hiding the problem.
412func TestFsstress(t *testing.T) {
413	tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
414	defer tc.Clean()
415
416	{
417		old := runtime.GOMAXPROCS(100)
418		defer runtime.GOMAXPROCS(old)
419	}
420
421	const concurrency = 10
422	var wg sync.WaitGroup
423	ctx, cancel := context.WithCancel(context.Background())
424
425	// operations taking 1 path argument
426	ops1 := map[string]func(string) error{
427		"mkdir":      func(p string) error { return syscall.Mkdir(p, 0700) },
428		"rmdir":      func(p string) error { return syscall.Rmdir(p) },
429		"mknod_reg":  func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFREG, 0) },
430		"remove":     os.Remove,
431		"unlink":     syscall.Unlink,
432		"mknod_sock": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFSOCK, 0) },
433		"mknod_fifo": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFIFO, 0) },
434		"mkfifo":     func(p string) error { return syscall.Mkfifo(p, 0700) },
435		"symlink":    func(p string) error { return syscall.Symlink("foo", p) },
436		"creat": func(p string) error {
437			fd, err := syscall.Open(p, syscall.O_CREAT|syscall.O_EXCL, 0700)
438			if err == nil {
439				syscall.Close(fd)
440			}
441			return err
442		},
443	}
444	// operations taking 2 path arguments
445	ops2 := map[string]func(string, string) error{
446		"rename": syscall.Rename,
447		"link":   syscall.Link,
448	}
449
450	type opStats struct {
451		ok   *int64
452		fail *int64
453		hung *int64
454	}
455	stats := make(map[string]opStats)
456
457	// pathN() returns something like /var/tmp/TestFsstress/TestFsstress.4
458	pathN := func(n int) string {
459		return fmt.Sprintf("%s/%s.%d", tc.mntDir, t.Name(), n)
460	}
461
462	opLoop := func(k string, n int) {
463		defer wg.Done()
464		op := ops1[k]
465		for {
466			p := pathN(1)
467			atomic.AddInt64(stats[k].hung, 1)
468			err := op(p)
469			atomic.AddInt64(stats[k].hung, -1)
470			if err != nil {
471				atomic.AddInt64(stats[k].fail, 1)
472			} else {
473				atomic.AddInt64(stats[k].ok, 1)
474			}
475			select {
476			case <-ctx.Done():
477				return
478			default:
479			}
480		}
481	}
482
483	op2Loop := func(k string, n int) {
484		defer wg.Done()
485		op := ops2[k]
486		n2 := (n + 1) % concurrency
487		for {
488			p1 := pathN(n)
489			p2 := pathN(n2)
490			atomic.AddInt64(stats[k].hung, 1)
491			err := op(p1, p2)
492			atomic.AddInt64(stats[k].hung, -1)
493			if err != nil {
494				atomic.AddInt64(stats[k].fail, 1)
495			} else {
496				atomic.AddInt64(stats[k].ok, 1)
497			}
498			select {
499			case <-ctx.Done():
500				return
501			default:
502			}
503		}
504	}
505
506	readdirLoop := func(k string) {
507		defer wg.Done()
508		for {
509			atomic.AddInt64(stats[k].hung, 1)
510			f, err := os.Open(tc.mntDir)
511			if err != nil {
512				panic(err)
513			}
514			_, err = f.Readdir(0)
515			if err != nil {
516				atomic.AddInt64(stats[k].fail, 1)
517			} else {
518				atomic.AddInt64(stats[k].ok, 1)
519			}
520			f.Close()
521			atomic.AddInt64(stats[k].hung, -1)
522			select {
523			case <-ctx.Done():
524				return
525			default:
526			}
527		}
528	}
529
530	// prepare stats map
531	var allOps []string
532	for k := range ops1 {
533		allOps = append(allOps, k)
534	}
535	for k := range ops2 {
536		allOps = append(allOps, k)
537	}
538	allOps = append(allOps, "readdir")
539	for _, k := range allOps {
540		var i1, i2, i3 int64
541		stats[k] = opStats{ok: &i1, fail: &i2, hung: &i3}
542	}
543
544	// spawn worker goroutines
545	for i := 0; i < concurrency; i++ {
546		for k := range ops1 {
547			wg.Add(1)
548			go opLoop(k, i)
549		}
550		for k := range ops2 {
551			wg.Add(1)
552			go op2Loop(k, i)
553		}
554	}
555	{
556		k := "readdir"
557		wg.Add(1)
558		go readdirLoop(k)
559	}
560
561	// spawn ls loop
562	//
563	// An external "ls" loop has a destructive effect that I am unable to
564	// reproduce through in-process operations.
565	if strings.ContainsAny(tc.mntDir, "'\\") {
566		// But let's not enable shell injection.
567		log.Panicf("shell injection attempt? mntDir=%q", tc.mntDir)
568	}
569	// --color=always enables xattr lookups for extra stress
570	cmd := exec.Command("bash", "-c", "while true ; do ls -l --color=always '"+tc.mntDir+"'; done")
571	err := cmd.Start()
572	if err != nil {
573		t.Fatal(err)
574	}
575	defer cmd.Process.Kill()
576
577	// Run the test for 1 second. If it deadlocks, it usually does within 20ms.
578	time.Sleep(1 * time.Second)
579
580	cancel()
581
582	// waitTimeout waits for the waitgroup for the specified max timeout.
583	// Returns true if waiting timed out.
584	waitTimeout := func(wg *sync.WaitGroup, timeout time.Duration) bool {
585		c := make(chan struct{})
586		go func() {
587			defer close(c)
588			wg.Wait()
589		}()
590		select {
591		case <-c:
592			return false // completed normally
593		case <-time.After(timeout):
594			return true // timed out
595		}
596	}
597
598	if waitTimeout(&wg, time.Second) {
599		t.Errorf("timeout waiting for goroutines to exit (deadlocked?)")
600	}
601
602	// Print operation statistics
603	var keys []string
604	for k := range stats {
605		keys = append(keys, k)
606	}
607	sort.Strings(keys)
608	t.Logf("Operation statistics:")
609	for _, k := range keys {
610		v := stats[k]
611		t.Logf("%10s: %5d ok, %6d fail, %2d hung", k, *v.ok, *v.fail, *v.hung)
612	}
613}
614
615func init() {
616	syscall.Umask(0)
617}
618