-
-
Notifications
You must be signed in to change notification settings - Fork 953
Description
Describe the bug
The backend_kqueue.go's addWatch function is responsible for returning the real path to the file which was added. If the given file is a symlink, it will resolve the symlink's target destination. This functionality was modified in this pull-request to use the os.Readlink method instead of filepath.EvalSymlinks method to resolve the target file's symlink.
os.Readlink returns a relative path while filepath.EvalSymlinks returns the fully resolved absolute path. This poses an issue when addWatch needs to resolve a relative symlink. If not run in the target directory, and if the returned link
is a relative path, the watch will receive an error during the lstat operation on link
. It will then silently fail without notifying the user as the error condition for the lstat operation is configured not to return the error.
As a result, the watcher.Add operation will fail to add a watch to the relative symlink and fail silently as the error check for the lstat operation doesn't return the error. This bug emerged in fsnotify v1.7.0 and is still present in the master branch. I am unable to reproduce this bug on linux.
Code to Reproduce
I added logging to the fsnotify vendor code's addWatch method's follow symlink logic. Specifically, log what the symlink resolves to with os.Readlink and with filepath.EvalSymlinks. Additionally, we log to stdout if fi, err = os.Lstat(name)
returns an error.
# fsnotify v1.8.0 code
// Follow symlinks.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := os.Readlink(name)
log.Printf("OS readlink result: %s", link)
link2, err := filepath.EvalSymlinks(name)
log.Printf("Eval symlink result: %s", link2)
if err != nil {
log.Printf("Unresolvable symlink: %v", err)
// Return nil because Linux can add unresolvable symlinks to the
// watch list without problems, so maintain consistency with
// that. There will be no file events for broken symlinks.
// TODO: more specific check; returns os.PathError; ENOENT?
//return "", nil
}
_, alreadyWatching = w.watches.byPath(link)
if alreadyWatching {
// Add to watches so we don't get spurious Create events later
// on when we diff the directories.
w.watches.addLink(name, 0)
return link, nil
}
info.linkName = name
name = link
fi, err = os.Lstat(name)
if err != nil {
log.Printf("Failed to lstat file resolved from os.Readlink, returning silently: %v", err)
return "", nil
}
}
Created TestFSNotifyAddWatchWithRelativeSymlink which only calls watcher.add on a relative symlink:
package httputil
import (
"github.com/fsnotify/fsnotify"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
func TestFSNotifyAddWatchWithRelativeSymlink(t *testing.T) {
// Try to add a watcher on a relative symlink
watcher, err := fsnotify.NewWatcher()
if err != nil {
t.Fatalf("Failed to create new Watcher: %v", err)
}
assert.NoError(t, err)
defer watcher.Close()
// Create initial file and it's symlink.
fileSymlink := createFileAndSymlink(t, "")
// Add the symlink to the file to the watcher.
if err := watcher.Add(fileSymlink); err != nil {
t.Fatalf("Error adding file %s to file watcher: %v", fileSymlink, err)
}
}
func createFileAndSymlink(t *testing.T, content string) string {
t.Helper()
tempDir := t.TempDir()
// Step 1: Create a timestamped directory and the `podName` file
timestampDir := filepath.Join(tempDir, "..data_2024")
if err := os.MkdirAll(timestampDir, 0o755); err != nil {
t.Fatalf("could not create timestamped directory: %v", err)
}
fileWithContentsPath := filepath.Join(timestampDir, "file_with_contents")
if err := os.WriteFile(fileWithContentsPath, []byte(content), 0o666); err != nil {
t.Fatalf("could not write file_with_contents file: %v", err)
}
// Step 2: Create the `..data` symlink pointing to the timestamped directory
symlink := filepath.Join(tempDir, "..data")
if err := os.Symlink(filepath.Base(timestampDir), symlink); err != nil {
t.Fatalf("could not create ..data symlink: %v", err)
}
t.Logf("Created symlink: %s -> %s", symlink, filepath.Base(timestampDir))
return symlink
}
See the output to see how the addWatch operation fails on the link resolved from os.Readlink:
=== RUN TestFSNotifyAddWatchWithRelativeSymlink
fnsotify_test.go:21: Created symlink: /var/folders/nw/gf79fm_j291dfhp64th3hndh0000gq/T/TestFSNotifyAddWatchWithRelativeSymlink1791335754/001/..data -> ..data_2024
2024/12/18 10:49:37 OS readlink result: ..data_2024
2024/12/18 10:49:37 Eval symlink result: /private/var/folders/nw/gf79fm_j291dfhp64th3hndh0000gq/T/TestFSNotifyAddWatchWithRelativeSymlink1791335754/001/..data_2024
2024/12/18 10:49:37 Failed to lstat file resolved from os.Readlink, returning silently: lstat ..data_2024: no such file or directory
--- PASS: TestFSNotifyAddWatchWithRelativeSymlink (0.00s)
As seen above, the addWatch method will not be able to lstat the ..data/podName symlink because it is not an absolute path, it then returns. Though this test still passes as the error is not returned by the addWatch method for an lstat operation error.
File operations to reproduce
No file operations are needed, running the above test with logging will demonstrate that a watch is not added to the target destination of the symlink.
To confirm that the the watch is not added to the file_with_contents, performing a write on the file_with_contents file and checking for events would show this.
Which operating system and version are you using?
macOS: Sonoma 14.6.1
Which fsnotify version are you using?
v1.7.0
Did you try the latest main branch?
Yes