Commit 6560fb0d authored by Jakob Unterwurzacher's avatar Jakob Unterwurzacher Committed by Han-Wen Nienhuys

Add TestParallelDiropsHang / emulate gvfs-udisks2-volume-monitor

There is a hang that appears when enabling CAP_PARALLEL_DIROPS on Linux
4.15.0: https://github.com/hanwen/go-fuse/issues/281

The hang was originally triggered by gvfs-udisks2-volume-monitor. This
test emulates what gvfs-udisks2-volume-monitor does.

On 4.15.0 kernels, the test will get stuck, and after 120 seconds you
get a kernel backtrace like this:

[ 1813.463679] INFO: task nodefs.test:2357 blocked for more than 120 seconds.
[ 1813.463685]       Not tainted 4.15.0-45-generic #48~16.04.1-Ubuntu
[ 1813.463687] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
[ 1813.463689] nodefs.test     D    0  2357   2311 0x00000004
[ 1813.463691] Call Trace:
[ 1813.463709]  __schedule+0x3d6/0x8b0
[ 1813.463712]  schedule+0x36/0x80
[ 1813.463714]  schedule_preempt_disabled+0xe/0x10
[ 1813.463716]  __mutex_lock.isra.2+0x2ae/0x4e0
[ 1813.463720]  ? ___slab_alloc+0x223/0x4e0
[ 1813.463722]  ? _cond_resched+0x1a/0x50
[ 1813.463724]  __mutex_lock_slowpath+0x13/0x20
[ 1813.463725]  ? __mutex_lock_slowpath+0x13/0x20
[ 1813.463727]  mutex_lock+0x2f/0x40
[ 1813.463729]  fuse_lock_inode+0x2a/0x30
[ 1813.463732]  fuse_lookup+0x31/0x140
[ 1813.463735]  ? d_alloc_parallel+0xc1/0x4c0
[ 1813.463738]  fuse_atomic_open+0x6d/0xf0
[ 1813.463740]  path_openat+0xc5d/0x13f0
[ 1813.463744]  do_filp_open+0x99/0x110
[ 1813.463747]  ? __check_object_size+0xfc/0x1a0
[ 1813.463749]  ? __alloc_fd+0x46/0x170
[ 1813.463752]  do_sys_open+0x12d/0x290
[ 1813.463754]  ? do_sys_open+0x12d/0x290
[ 1813.463756]  SyS_openat+0x14/0x20
[ 1813.463759]  do_syscall_64+0x73/0x130
[ 1813.463762]  entry_SYSCALL_64_after_hwframe+0x3d/0xa2
parent 25ee0996
...@@ -9,9 +9,13 @@ import ( ...@@ -9,9 +9,13 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"reflect" "reflect"
"sync"
"syscall" "syscall"
"testing" "testing"
"time"
"github.com/hanwen/go-fuse/fuse"
"github.com/hanwen/go-fuse/internal/testutil"
"github.com/kylelemons/godebug/pretty" "github.com/kylelemons/godebug/pretty"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
...@@ -198,3 +202,136 @@ func TestCopyFileRange(t *testing.T) { ...@@ -198,3 +202,136 @@ func TestCopyFileRange(t *testing.T) {
} }
} }
// Wait for a change in /proc/self/mounts. Efficient through the use of
// unix.Poll().
func waitProcMountsChange() error {
fd, err := syscall.Open("/proc/self/mounts", syscall.O_RDONLY, 0)
defer syscall.Close(fd)
if err != nil {
return err
}
pollFds := []unix.PollFd{
{
Fd: int32(fd),
Events: unix.POLLPRI,
},
}
_, err = unix.Poll(pollFds, 1000)
return err
}
// Wait until mountpoint "mnt" shows up /proc/self/mounts
func waitMount(mnt string) error {
for {
err := waitProcMountsChange()
if err != nil {
return err
}
content, err := ioutil.ReadFile("/proc/self/mounts")
if err != nil {
return err
}
if bytes.Contains(content, []byte(mnt)) {
return nil
}
}
}
// There is a hang that appears when enabling CAP_PARALLEL_DIROPS on Linux
// 4.15.0: https://github.com/hanwen/go-fuse/issues/281
// The hang was originally triggered by gvfs-udisks2-volume-monitor. This
// test emulates what gvfs-udisks2-volume-monitor does.
func TestParallelDiropsHang(t *testing.T) {
// We do NOT want to use newTestCase() here because we need to know the
// mnt path before the filesystem is mounted
dir := testutil.TempDir()
orig := dir + "/orig"
mnt := dir + "/mnt"
if err := os.Mkdir(orig, 0755); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(mnt, 0755); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
// Unblock the goroutines onces the mount shows up in /proc/self/mounts
wait := make(chan struct{})
go func() {
err := waitMount(mnt)
if err != nil {
t.Error(err)
}
// Unblock the goroutines regardless of an error. We don't want to hang
// the test.
close(wait)
}()
// gvfs-udisks2-volume-monitor hits the mount with three threads - we try to
// emulate exactly what it does acc. to an strace log.
var wg sync.WaitGroup
wg.Add(3)
// [pid 2117] lstat(".../mnt/autorun.inf", <unfinished ...>
go func() {
defer wg.Done()
<-wait
var st unix.Stat_t
unix.Lstat(mnt+"/autorun.inf", &st)
}()
// [pid 2116] open(".../mnt/.xdg-volume-info", O_RDONLY <unfinished ...>
go func() {
defer wg.Done()
<-wait
syscall.Open(mnt+"/.xdg-volume-info", syscall.O_RDONLY, 0)
}()
// 25 times this:
// [pid 1874] open(".../mnt", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC <unfinished ...>
// [pid 1874] fstat(11, {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
// [pid 1874] getdents(11, /* 2 entries */, 32768) = 48
// [pid 1874] close(11) = 0
go func() {
defer wg.Done()
<-wait
for i := 1; i <= 25; i++ {
f, err := os.Open(mnt)
if err != nil {
t.Error(err)
return
}
_, err = f.Stat()
if err != nil {
t.Error(err)
f.Close()
return
}
_, err = f.Readdirnames(-1)
if err != nil {
t.Errorf("iteration %d: fd %d: %v", i, f.Fd(), err)
return
}
f.Close()
}
}()
loopbackRoot, err := NewLoopbackRoot(orig)
if err != nil {
t.Fatalf("NewLoopbackRoot(%s): %v\n", orig, err)
}
sec := time.Second
opts := &Options{
AttrTimeout: &sec,
EntryTimeout: &sec,
}
opts.Debug = testutil.VerboseTest()
rawFS := NewNodeFS(loopbackRoot, opts)
server, err := fuse.NewServer(rawFS, mnt, &opts.MountOptions)
if err != nil {
t.Fatal(err)
}
go server.Serve()
wg.Wait()
server.Unmount()
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment