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