lib/fs: Don't panic when watching a folder with symlinked root (#4846)

This commit is contained in:
Simon Frei 2018-03-28 23:01:25 +02:00 committed by Audrius Butkevicius
parent c51591b308
commit 26d87ec3bb
3 changed files with 86 additions and 16 deletions

View File

@ -25,7 +25,8 @@ var (
// The BasicFilesystem implements all aspects by delegating to package os. // The BasicFilesystem implements all aspects by delegating to package os.
// All paths are relative to the root and cannot (should not) escape the root directory. // All paths are relative to the root and cannot (should not) escape the root directory.
type BasicFilesystem struct { type BasicFilesystem struct {
root string root string
rootSymlinkEvaluated string
} }
func newBasicFilesystem(root string) *BasicFilesystem { func newBasicFilesystem(root string) *BasicFilesystem {
@ -52,21 +53,34 @@ func newBasicFilesystem(root string) *BasicFilesystem {
} }
} }
rootSymlinkEvaluated, err := filepath.EvalSymlinks(root)
if err != nil {
rootSymlinkEvaluated = root
}
return &BasicFilesystem{
root: adjustRoot(root),
rootSymlinkEvaluated: adjustRoot(rootSymlinkEvaluated),
}
}
func adjustRoot(root string) string {
// Attempt to enable long filename support on Windows. We may still not // Attempt to enable long filename support on Windows. We may still not
// have an absolute path here if the previous steps failed. // have an absolute path here if the previous steps failed.
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if filepath.IsAbs(root) && !strings.HasPrefix(root, `\\`) { if filepath.IsAbs(root) && !strings.HasPrefix(root, `\\`) {
root = `\\?\` + root root = `\\?\` + root
} }
// If we're not on Windows, we want the path to end with a slash to return root
// penetrate symlinks. On Windows, paths must not end with a slash. }
} else if root[len(root)-1] != filepath.Separator {
// If we're not on Windows, we want the path to end with a slash to
// penetrate symlinks. On Windows, paths must not end with a slash.
if root[len(root)-1] != filepath.Separator {
root = root + string(filepath.Separator) root = root + string(filepath.Separator)
} }
return &BasicFilesystem{ return root
root: root,
}
} }
// rooted expands the relative path to the full path that is then used with os // rooted expands the relative path to the full path that is then used with os
@ -109,7 +123,15 @@ func (f *BasicFilesystem) rooted(rel string) (string, error) {
} }
func (f *BasicFilesystem) unrooted(path string) string { func (f *BasicFilesystem) unrooted(path string) string {
return strings.TrimPrefix(strings.TrimPrefix(path, f.root), string(PathSeparator)) return rel(path, f.root)
}
func (f *BasicFilesystem) unrootedSymlinkEvaluated(path string) string {
return rel(path, f.rootSymlinkEvaluated)
}
func rel(path, prefix string) string {
return strings.TrimPrefix(strings.TrimPrefix(path, prefix), string(PathSeparator))
} }
func (f *BasicFilesystem) Chmod(name string, mode FileMode) error { func (f *BasicFilesystem) Chmod(name string, mode FileMode) error {

View File

@ -12,6 +12,7 @@ import (
"context" "context"
"errors" "errors"
"path/filepath" "path/filepath"
"strings"
"github.com/syncthing/notify" "github.com/syncthing/notify"
) )
@ -47,12 +48,12 @@ func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context
return nil, err return nil, err
} }
go f.watchLoop(name, absName, backendChan, outChan, ignore, ctx) go f.watchLoop(name, backendChan, outChan, ignore, ctx)
return outChan, nil return outChan, nil
} }
func (f *BasicFilesystem) watchLoop(name string, absName string, backendChan chan notify.EventInfo, outChan chan<- Event, ignore Matcher, ctx context.Context) { func (f *BasicFilesystem) watchLoop(name string, backendChan chan notify.EventInfo, outChan chan<- Event, ignore Matcher, ctx context.Context) {
for { for {
// Detect channel overflow // Detect channel overflow
if len(backendChan) == backendBuffer { if len(backendChan) == backendBuffer {
@ -105,12 +106,11 @@ func (f *BasicFilesystem) eventType(notifyType notify.Event) EventType {
// special case when the given path is the folder root without a trailing // special case when the given path is the folder root without a trailing
// pathseparator. // pathseparator.
func (f *BasicFilesystem) unrootedChecked(absPath string) string { func (f *BasicFilesystem) unrootedChecked(absPath string) string {
if absPath+string(PathSeparator) == f.root { if absPath+string(PathSeparator) == f.rootSymlinkEvaluated {
return "." return "."
} }
relPath := f.unrooted(absPath) if !strings.HasPrefix(absPath, f.rootSymlinkEvaluated) {
if relPath == absPath {
panic("bug: Notify backend is processing a change outside of the watched path: " + absPath) panic("bug: Notify backend is processing a change outside of the watched path: " + absPath)
} }
return relPath return f.unrootedSymlinkEvaluated(absPath)
} }

View File

@ -123,7 +123,7 @@ func TestWatchOutside(t *testing.T) {
} }
cancel() cancel()
}() }()
fs.watchLoop(".", testDirAbs, backendChan, outChan, fakeMatcher{}, ctx) fs.watchLoop(".", backendChan, outChan, fakeMatcher{}, ctx)
}() }()
backendChan <- fakeEventInfo(filepath.Join(filepath.Dir(testDirAbs), "outside")) backendChan <- fakeEventInfo(filepath.Join(filepath.Dir(testDirAbs), "outside"))
@ -139,7 +139,7 @@ func TestWatchSubpath(t *testing.T) {
fs := newBasicFilesystem(testDirAbs) fs := newBasicFilesystem(testDirAbs)
abs, _ := fs.rooted("sub") abs, _ := fs.rooted("sub")
go fs.watchLoop("sub", abs, backendChan, outChan, fakeMatcher{}, ctx) go fs.watchLoop("sub", backendChan, outChan, fakeMatcher{}, ctx)
backendChan <- fakeEventInfo(filepath.Join(abs, "file")) backendChan <- fakeEventInfo(filepath.Join(abs, "file"))
@ -208,6 +208,54 @@ func TestWatchErrorLinuxInterpretation(t *testing.T) {
} }
} }
func TestWatchSymlinkedRoot(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Involves symlinks")
}
name := "symlinkedRoot"
if err := testFs.MkdirAll(name, 0755); err != nil {
panic(fmt.Sprintf("Failed to create directory %s: %s", name, err))
}
defer testFs.RemoveAll(name)
root := filepath.Join(name, "root")
if err := testFs.MkdirAll(root, 0777); err != nil {
panic(err)
}
link := filepath.Join(name, "link")
if err := testFs.CreateSymlink(filepath.Join(testFs.URI(), root), link); err != nil {
panic(err)
}
linkedFs := NewFilesystem(FilesystemTypeBasic, filepath.Join(testFs.URI(), link))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if _, err := linkedFs.Watch(".", fakeMatcher{}, ctx, false); err != nil {
panic(err)
}
if err := linkedFs.MkdirAll("foo", 0777); err != nil {
panic(err)
}
// Give the panic some time to happen
sleepMs(100)
}
func TestUnrootedChecked(t *testing.T) {
var unrooted string
defer func() {
if recover() == nil {
t.Fatal("unrootedChecked did not panic on outside path, but returned", unrooted)
}
}()
fs := newBasicFilesystem(testDirAbs)
unrooted = fs.unrootedChecked("/random/other/path")
}
// path relative to folder root, also creates parent dirs if necessary // path relative to folder root, also creates parent dirs if necessary
func createTestFile(name string, file string) string { func createTestFile(name string, file string) string {
joined := filepath.Join(name, file) joined := filepath.Join(name, file)