From 8cad407e0b011e05e34f3aca8093dfdb4a2630dc Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Sat, 20 Feb 2021 00:30:12 -0800 Subject: [PATCH] fix: Bring support for symlink on regular files on NAS (#11383) fixes #11203 --- cmd/os-readdir_other.go | 39 +++++++++++++++++++-- cmd/os-readdir_test.go | 3 +- cmd/os-readdir_unix.go | 48 +++++++++++++++++++++----- cmd/os-readdir_windows.go | 71 +++++++++++++++++++++++++++++++++------ docs/gateway/nas.md | 17 ++++++++++ 5 files changed, 155 insertions(+), 23 deletions(-) diff --git a/cmd/os-readdir_other.go b/cmd/os-readdir_other.go index 79f761127..39a457d45 100644 --- a/cmd/os-readdir_other.go +++ b/cmd/os-readdir_other.go @@ -56,6 +56,24 @@ func readDirFn(dirPath string, filter func(name string, typ os.FileMode) error) return osErrToFileErr(err) } for _, fi := range fis { + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + fi, err = os.Stat(pathJoin(dirPath, fi.Name())) + if err != nil { + // It got deleted in the meantime, not found + // or returns too many symlinks ignore this + // file/directory. + if osIsNotExist(err) || isSysErrPathNotFound(err) || + isSysErrTooManySymlinks(err) { + continue + } + return err + } + + // Ignore symlinked directories. + if fi.IsDir() { + continue + } + } if err = filter(fi.Name(), fi.Mode()); err == errDoneForNow { // filtering requested to return by caller. return nil @@ -97,11 +115,26 @@ func readDirN(dirPath string, count int) (entries []string, err error) { } } for _, fi := range fis { - // Not need to follow symlink. if fi.Mode()&os.ModeSymlink == os.ModeSymlink { - continue + fi, err = os.Stat(pathJoin(dirPath, fi.Name())) + if err != nil { + // It got deleted in the meantime, not found + // or returns too many symlinks ignore this + // file/directory. + if osIsNotExist(err) || isSysErrPathNotFound(err) || + isSysErrTooManySymlinks(err) { + continue + } + return err + } + + // Ignore symlinked directories. + if fi.IsDir() { + continue + } } - if fi.Mode().IsDir() { + + if fi.IsDir() { // Append SlashSeparator instead of "\" so that sorting is achieved as expected. entries = append(entries, fi.Name()+SlashSeparator) } else if fi.Mode().IsRegular() { diff --git a/cmd/os-readdir_test.go b/cmd/os-readdir_test.go index 64ef1ea3b..5b16d601e 100644 --- a/cmd/os-readdir_test.go +++ b/cmd/os-readdir_test.go @@ -149,7 +149,8 @@ func setupTestReadDirSymlink(t *testing.T) (testResults []result) { } // Add to entries. entries = append(entries, name1) - // Symlinks are ignored. + // Symlinks are preserved for regular files + entries = append(entries, name2) } if err := os.MkdirAll(filepath.Join(dir, "mydir"), 0777); err != nil { t.Fatalf("Unable to create \"mydir\", %s", err) diff --git a/cmd/os-readdir_unix.go b/cmd/os-readdir_unix.go index 76ecf193c..a328ce064 100644 --- a/cmd/os-readdir_unix.go +++ b/cmd/os-readdir_unix.go @@ -123,8 +123,29 @@ func readDirFn(dirPath string, fn func(name string, typ os.FileMode) error) erro if len(name) == 0 || bytes.Equal(name, []byte{'.'}) || bytes.Equal(name, []byte{'.', '.'}) { continue } - if typ&os.ModeSymlink == os.ModeSymlink { - continue + + // Fallback for filesystems (like old XFS) that don't + // support Dirent.Type and have DT_UNKNOWN (0) there + // instead. + if typ == unexpectedFileMode || typ&os.ModeSymlink == os.ModeSymlink { + fi, err := os.Stat(pathJoin(dirPath, string(name))) + if err != nil { + // It got deleted in the meantime, not found + // or returns too many symlinks ignore this + // file/directory. + if osIsNotExist(err) || isSysErrPathNotFound(err) || + isSysErrTooManySymlinks(err) { + continue + } + return err + } + + // Ignore symlinked directories. + if typ&os.ModeSymlink == os.ModeSymlink && fi.IsDir() { + continue + } + + typ = fi.Mode() & os.ModeType } if err = fn(string(name), typ); err == errDoneForNow { // fn() requested to return by caller. @@ -176,11 +197,12 @@ func readDirN(dirPath string, count int) (entries []string, err error) { if len(name) == 0 || bytes.Equal(name, []byte{'.'}) || bytes.Equal(name, []byte{'.', '.'}) { continue } + // Fallback for filesystems (like old XFS) that don't // support Dirent.Type and have DT_UNKNOWN (0) there // instead. - if typ == unexpectedFileMode { - fi, err := os.Lstat(pathJoin(dirPath, string(name))) + if typ == unexpectedFileMode || typ&os.ModeSymlink == os.ModeSymlink { + fi, err := os.Stat(pathJoin(dirPath, string(name))) if err != nil { // It got deleted in the meantime, not found // or returns too many symlinks ignore this @@ -191,22 +213,30 @@ func readDirN(dirPath string, count int) (entries []string, err error) { } return nil, err } + + // Ignore symlinked directories. + if typ&os.ModeSymlink == os.ModeSymlink && fi.IsDir() { + continue + } + typ = fi.Mode() & os.ModeType } - if typ&os.ModeSymlink == os.ModeSymlink { - continue - } + + var nameStr string if typ.IsRegular() { - entries = append(entries, string(name)) + nameStr = string(name) } else if typ.IsDir() { // Use temp buffer to append a slash to avoid string concat. tmp = tmp[:len(name)+1] copy(tmp, name) tmp[len(tmp)-1] = '/' // SlashSeparator - entries = append(entries, string(tmp)) + nameStr = string(tmp) } + count-- + entries = append(entries, nameStr) } + return } diff --git a/cmd/os-readdir_windows.go b/cmd/os-readdir_windows.go index cc79d1d13..99119d532 100644 --- a/cmd/os-readdir_windows.go +++ b/cmd/os-readdir_windows.go @@ -42,6 +42,15 @@ func readDirFn(dirPath string, filter func(name string, typ os.FileMode) error) } defer f.Close() + // Check if file or dir. This is the quickest way. + // Do not remove this check, on windows syscall.FindNextFile + // would throw an exception if Fd() points to a file + // instead of a directory, we need to quickly fail + // in such situations - this workadound is expected. + if _, err = f.Seek(0, io.SeekStart); err == nil { + return errFileNotFound + } + data := &syscall.Win32finddata{} for { e := syscall.FindNextFile(syscall.Handle(f.Fd()), data) @@ -63,13 +72,33 @@ func readDirFn(dirPath string, filter func(name string, typ os.FileMode) error) if name == "" || name == "." || name == ".." { // Useless names continue } - if data.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 { - continue - } + var typ os.FileMode = 0 // regular file - if data.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + switch { + case data.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0: + // Reparse point is a symlink + fi, err := os.Stat(pathJoin(dirPath, string(name))) + if err != nil { + // It got deleted in the meantime, not found + // or returns too many symlinks ignore this + // file/directory. + if osIsNotExist(err) || isSysErrPathNotFound(err) || + isSysErrTooManySymlinks(err) { + continue + } + return err + } + + if fi.IsDir() { + // Ignore symlinked directories. + continue + } + + typ = fi.Mode() + case data.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0: typ = os.ModeDir } + if e = filter(name, typ); e == errDoneForNow { // filtering requested to return by caller. return nil @@ -88,10 +117,14 @@ func readDirN(dirPath string, count int) (entries []string, err error) { defer f.Close() // Check if file or dir. This is the quickest way. - _, err = f.Seek(0, io.SeekStart) - if err == nil { + // Do not remove this check, on windows syscall.FindNextFile + // would throw an exception if Fd() points to a file + // instead of a directory, we need to quickly fail + // in such situations - this workadound is expected. + if _, err = f.Seek(0, io.SeekStart); err == nil { return nil, errFileNotFound } + data := &syscall.Win32finddata{} handle := syscall.Handle(f.Fd()) @@ -113,15 +146,33 @@ func readDirN(dirPath string, count int) (entries []string, err error) { if name == "" || name == "." || name == ".." { // Useless names continue } + switch { case data.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0: - continue + // Reparse point is a symlink + fi, err := os.Stat(pathJoin(dirPath, string(name))) + if err != nil { + // It got deleted in the meantime, not found + // or returns too many symlinks ignore this + // file/directory. + if osIsNotExist(err) || isSysErrPathNotFound(err) || + isSysErrTooManySymlinks(err) { + continue + } + return nil, err + } + + if fi.IsDir() { + // directory symlinks are ignored. + continue + } case data.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0: - entries = append(entries, name+SlashSeparator) - default: - entries = append(entries, name) + name = name + SlashSeparator } + count-- + entries = append(entries, name) + } return entries, nil diff --git a/docs/gateway/nas.md b/docs/gateway/nas.md index df763e097..42ad78601 100644 --- a/docs/gateway/nas.md +++ b/docs/gateway/nas.md @@ -79,6 +79,23 @@ export MINIO_NOTIFY_WEBHOOK_QUEUE_DIR_1=/tmp/webhk > NOTE: Please check the docs for the corresponding ENV setting. Alternatively, We can obtain other ENVs in the form `mc admin config set alias/ --env` +## Symlink support + +NAS gateway implementation allows symlinks on regular files, + +### Behavior + +- For reads symlink resolves to file symlink points to. +- For deletes + - Delete of symlink deletes the symlink but not the real file to which the symlink points. + - Delete of actual file automatically makes symlink'ed file invisible, dangling symlinks won't be visible. + +#### Caveats +- Disallows follow of directory symlinks to avoid security issues, and leaving them as is on namespace makes them very inconsistent. +- Dangling symlinks are ignored automatically. + +*Directory symlinks is not and will not be supported as there are no safe ways to handle them.* + ## Explore Further - [`mc` command-line interface](https://docs.min.io/docs/minio-client-quickstart-guide) - [`aws` command-line interface](https://docs.min.io/docs/aws-cli-with-minio)