diff --git a/internal/fusefrontend/fs_dir.go b/internal/fusefrontend/fs_dir.go index ae52412..2102008 100644 --- a/internal/fusefrontend/fs_dir.go +++ b/internal/fusefrontend/fs_dir.go @@ -271,20 +271,14 @@ func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, f cDirAbsPath := filepath.Join(fs.args.Cipherdir, cDirName) var cipherEntries []fuse.DirEntry var status fuse.Status - if syscallcompat.HaveGetdents { - // Getdents avoids calling Lstat on each file. - cipherEntries, err = syscallcompat.Getdents(cDirAbsPath) - if err != nil { - return nil, fuse.ToStatus(err) - } - } else { - haveGetdentsWarnOnce.Do(func() { - tlog.Warn.Printf("OpenDir: Getdents not available, falling back to OpenDir") - }) - cipherEntries, status = fs.FileSystem.OpenDir(cDirName, context) - if !status.Ok() { - return nil, status - } + fd, err := syscall.Open(cDirAbsPath, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fuse.ToStatus(err) + } + defer syscall.Close(fd) + cipherEntries, err = syscallcompat.Getdents(fd) + if err != nil { + return nil, fuse.ToStatus(err) } // Get DirIV (stays nil if PlaintextNames is used) var cachedIV []byte diff --git a/internal/syscallcompat/getdents_linux.go b/internal/syscallcompat/getdents_linux.go index 55bffff..49f9233 100644 --- a/internal/syscallcompat/getdents_linux.go +++ b/internal/syscallcompat/getdents_linux.go @@ -12,26 +12,17 @@ import ( "syscall" "unsafe" + "golang.org/x/sys/unix" + "github.com/hanwen/go-fuse/fuse" "github.com/rfjakob/gocryptfs/internal/tlog" ) -// HaveGetdents is true if we have a working implementation of Getdents -const HaveGetdents = true - const sizeofDirent = int(unsafe.Sizeof(syscall.Dirent{})) -// Getdents wraps syscall.Getdents and converts the result to []fuse.DirEntry. -// The function takes a path instead of an fd because we need to be able to -// call Lstat on files. Fstatat is not yet available in Go as of v1.9: -// https://github.com/golang/go/issues/14216 -func Getdents(dir string) ([]fuse.DirEntry, error) { - fd, err := syscall.Open(dir, syscall.O_RDONLY, 0) - if err != nil { - return nil, err - } - defer syscall.Close(fd) +// getdents wraps syscall.Getdents and converts the result to []fuse.DirEntry. +func getdents(fd int) ([]fuse.DirEntry, error) { // Collect syscall result in smartBuf. // "bytes.Buffer" is smart about expanding the capacity and avoids the // exponential runtime of simple append(). @@ -84,7 +75,7 @@ func Getdents(dir string) ([]fuse.DirEntry, error) { // os.File.Readdir() drops "." and "..". Let's be compatible. continue } - mode, err := convertDType(s.Type, dir+"/"+name) + mode, err := convertDType(fd, name, s.Type) if err != nil { // The file may have been deleted in the meantime. Just skip it // and go on. @@ -125,17 +116,17 @@ func getdentsName(s syscall.Dirent) (string, error) { var dtUnknownWarnOnce sync.Once // convertDType converts a Dirent.Type to at Stat_t.Mode value. -func convertDType(dtype uint8, file string) (uint32, error) { +func convertDType(dirfd int, name string, dtype uint8) (uint32, error) { if dtype != syscall.DT_UNKNOWN { // Shift up by four octal digits = 12 bits return uint32(dtype) << 12, nil } - // DT_UNKNOWN: we have to call Lstat() + // DT_UNKNOWN: we have to call stat() dtUnknownWarnOnce.Do(func() { - tlog.Warn.Printf("Getdents: convertDType: received DT_UNKNOWN, falling back to Lstat") + tlog.Warn.Printf("Getdents: convertDType: received DT_UNKNOWN, falling back to stat") }) - var st syscall.Stat_t - err := syscall.Lstat(file, &st) + var st unix.Stat_t + err := Fstatat(dirfd, name, &st, unix.AT_SYMLINK_NOFOLLOW) if err != nil { return 0, err } diff --git a/internal/syscallcompat/getdents_other.go b/internal/syscallcompat/getdents_other.go index 4ef5b8f..6d08a9b 100644 --- a/internal/syscallcompat/getdents_other.go +++ b/internal/syscallcompat/getdents_other.go @@ -1,17 +1,49 @@ -// +build !linux - package syscallcompat import ( - "log" + "os" + "syscall" + + "golang.org/x/sys/unix" "github.com/hanwen/go-fuse/fuse" ) -// HaveGetdents is true if we have a working implementation of Getdents -const HaveGetdents = false - -func Getdents(dir string) ([]fuse.DirEntry, error) { - log.Panic("only implemented on Linux") - return nil, nil +// emulateGetdents reads all directory entries from the open directory "fd" +// and returns them in a fuse.DirEntry slice. +func emulateGetdents(fd int) (out []fuse.DirEntry, err error) { + // os.File closes the fd in its finalizer. Duplicate the fd to not affect + // the original fd. + newFd, err := syscall.Dup(fd) + if err != nil { + return nil, err + } + f := os.NewFile(uintptr(newFd), "") + defer f.Close() + // Get all file names in the directory + names, err := f.Readdirnames(0) + if err != nil { + return nil, err + } + // Stat all of them and convert to fuse.DirEntry + out = make([]fuse.DirEntry, 0, len(names)) + for _, name := range names { + var st unix.Stat_t + err = Fstatat(fd, name, &st, unix.AT_SYMLINK_NOFOLLOW) + if err == syscall.ENOENT { + // File disappeared between readdir and stat. Pretend we did not + // see it. + continue + } + if err != nil { + return nil, err + } + newEntry := fuse.DirEntry{ + Name: name, + Mode: uint32(st.Mode) & syscall.S_IFMT, + Ino: st.Ino, + } + out = append(out, newEntry) + } + return out, nil } diff --git a/internal/syscallcompat/getdents_test.go b/internal/syscallcompat/getdents_test.go index 8f2bd09..131ffee 100644 --- a/internal/syscallcompat/getdents_test.go +++ b/internal/syscallcompat/getdents_test.go @@ -12,9 +12,19 @@ import ( "github.com/hanwen/go-fuse/fuse" ) +var getdentsUnderTest = getdents + func TestGetdents(t *testing.T) { + t.Logf("testing native getdents") + testGetdents(t) + t.Logf("testing emulateGetdents") + getdentsUnderTest = emulateGetdents + testGetdents(t) +} + +func testGetdents(t *testing.T) { // Fill a directory with filenames of length 1 ... 255 - testDir, err := ioutil.TempDir("", "TestGetdents") + testDir, err := ioutil.TempDir(tmpDir, "TestGetdents") if err != nil { t.Fatal(err) } @@ -35,17 +45,21 @@ func TestGetdents(t *testing.T) { if err != nil { t.Fatal(err) } + defer fd.Close() readdirEntries, err := fd.Readdir(0) if err != nil { t.Fatal(err) } - fd.Close() readdirMap := make(map[string]*syscall.Stat_t) for _, v := range readdirEntries { readdirMap[v.Name()] = fuse.ToStatT(v) } - // Read using our Getdents() - getdentsEntries, err := Getdents(dir) + // Read using our Getdents() implementation + _, err = fd.Seek(0, 0) // Rewind directory + if err != nil { + t.Fatal(err) + } + getdentsEntries, err := getdentsUnderTest(int(fd.Fd())) if err != nil { t.Fatal(err) } diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index fb8d8b3..b5b949e 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -7,6 +7,8 @@ import ( "golang.org/x/sys/unix" + "github.com/hanwen/go-fuse/fuse" + "github.com/rfjakob/gocryptfs/internal/tlog" ) @@ -115,3 +117,8 @@ func Fstatat(dirfd int, path string, stat *unix.Stat_t, flags int) (err error) { } return unix.Fstatat(dirfd, path, stat, flags) } + +// Getdents syscall. +func Getdents(fd int) ([]fuse.DirEntry, error) { + return getdents(fd) +}