From 6e8fbebfd4a2cff7084f81aa3e0f23d65001247d Mon Sep 17 00:00:00 2001 From: opcoder0 <110003254+opcoder0@users.noreply.github.com> Date: Mon, 2 Jan 2023 17:39:30 +1100 Subject: [PATCH] Fix DeleteWatch/UnwatchMount does not work --- fanotify_api.go | 71 ++++++++++++++++++++++++++++++----------------- fanotify_event.go | 25 +++++++++++++++++ fanotify_test.go | 54 +++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 25 deletions(-) diff --git a/fanotify_api.go b/fanotify_api.go index 5226676..0a27504 100644 --- a/fanotify_api.go +++ b/fanotify_api.go @@ -73,6 +73,8 @@ type Listener struct { fd int // flags passed to fanotify_init flags uint + // markMask current fanotify mark mask + markMask uint64 // mount fd is the file descriptor of the mountpoint mountpoint *os.File kernelMajorVersion int @@ -203,14 +205,23 @@ func (l *Listener) Stop() { // for - [FileCreated], [FileAttribChanged], [FileMovedTo], [FileMovedFrom], [WatchedFileDeleted], // [WatchedFileOrDirectoryDeleted], [FileDeleted], [FileOrDirectoryDeleted] func (l *Listener) WatchMount(eventTypes EventType) error { - return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(eventTypes)) + mask := l.getMaskAfterAdd(eventTypes) + l.clearWatch() + l.markMask = uint64(mask) + return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(mask)) } // UnwatchMount removes the notification marks for the entire mount point. // This method returns an [ErrWatchPath] if the listener was not initialized to monitor // the entire mount point. To unmark specific files or directories use [DeleteWatch] method. func (l *Listener) UnwatchMount(eventTypes EventType) error { - return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_REMOVE|unix.FAN_MARK_MOUNT, uint64(eventTypes)) + if l.markMask == 0 { + return l.clearWatch() + } + remaining := l.getMaskAfterRemove(eventTypes) + l.clearWatch() + l.markMask = uint64(remaining) + return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(remaining)) } // AddWatch adds or modifies the fanotify mark for the specified path. @@ -221,6 +232,11 @@ func (l *Listener) UnwatchMount(eventTypes EventType) error { // - [FileCreated] cannot be or-ed / combined with [FileClosed]. The fanotify system does not generate any event for this combination. // - [FileOpened] with any of the event types containing OrDirectory causes an event flood for the directory and then stopping raising any events at all. // - [FileOrDirectoryOpened] with any of the other event types causes an event flood for the directory and then stopping raising any events at all. +// +// NOTE: +// Any event type that contains "OrDirectory" applies the OrDirectory mask to any other applicable +// marks. For example adding [FileDeleted] and [FileOrDirectoryOpened] will apply "OrDirectory" to +// [FileDeleted] thus the resulting mask will be set to [FileOrDirectoryOpened] and [FileOrDirectoryDeleted] func (l *Listener) AddWatch(path string, eventTypes EventType) error { if l == nil { panic("nil listener") @@ -228,7 +244,34 @@ func (l *Listener) AddWatch(path string, eventTypes EventType) error { if l.entireMount { return os.ErrInvalid } - return l.fanotifyMark(path, unix.FAN_MARK_ADD, uint64(eventTypes|unix.FAN_EVENT_ON_CHILD)) + mask := l.getMaskAfterAdd(eventTypes) + l.clearWatch() + l.markMask = uint64(mask) + return l.fanotifyMark(path, unix.FAN_MARK_ADD, uint64(mask|unix.FAN_EVENT_ON_CHILD)) +} + +// DeleteWatch removes/unmarks the fanotify mark for the specified path. +// Calling DeleteWatch on the listener initialized to monitor the entire mount point +// results in [os.ErrInvalid]. Use [UnwatchMount] for deleting marks on the mount point. +func (l *Listener) DeleteWatch(parentDir string, eventTypes EventType) error { + if l.entireMount { + return os.ErrInvalid + } + if l.markMask == 0 { + return l.clearWatch() + } + remaining := l.getMaskAfterRemove(eventTypes) + l.clearWatch() + l.markMask = uint64(remaining) + return l.fanotifyMark(parentDir, unix.FAN_MARK_ADD, uint64(remaining|unix.FAN_EVENT_ON_CHILD)) +} + +// ClearWatch stops watching for all event types +func (l *Listener) ClearWatch() error { + if l == nil { + panic("nil listener") + } + return l.clearWatch() } // Allow sends an "allowed" response to the permission request event. @@ -251,28 +294,6 @@ func (l *Listener) Deny(e Event) { unix.Write(l.fd, buf.Bytes()) } -// DeleteWatch removes/unmarks the fanotify mark for the specified path. -// Calling DeleteWatch on the listener initialized to monitor the entire mount point -// results in [os.ErrInvalid]. Use [UnwatchMount] for deleting marks on the mount point. -func (l *Listener) DeleteWatch(parentDir string, eventTypes EventType) error { - if l.entireMount { - return os.ErrInvalid - } - return l.fanotifyMark(parentDir, unix.FAN_MARK_REMOVE, uint64(eventTypes|unix.FAN_EVENT_ON_CHILD)) -} - -// ClearWatch stops watching for all event types -func (l *Listener) ClearWatch() error { - if l == nil { - panic("nil listener") - } - if err := unix.FanotifyMark(l.fd, unix.FAN_MARK_FLUSH, 0, -1, ""); err != nil { - return err - } - l.watches = make(map[string]bool) - return nil -} - // Has returns true if event types (e) contains the passed in event type (et). func (e EventType) Has(et EventType) bool { return e&et == et diff --git a/fanotify_event.go b/fanotify_event.go index c484a34..7db488d 100644 --- a/fanotify_event.go +++ b/fanotify_event.go @@ -204,6 +204,22 @@ func fanotifyEventOK(meta *unix.FanotifyEventMetadata, n int) bool { int(meta.Event_len) <= n) } +func (l *Listener) getMaskAfterRemove(removeMask EventType) EventType { + if l.markMask == 0 { + return EventType(0) + } + mask := EventType(l.markMask) ^ removeMask + return mask +} + +func (l *Listener) getMaskAfterAdd(addMask EventType) EventType { + if l.markMask == 0 { + return addMask + } + mask := EventType(l.markMask) | addMask + return mask +} + // permissionType is ignored when isNotificationListener is true. func newListener(mountpointPath string, entireMount bool, notificationOnly bool, permissionType PermissionType) (*Listener, error) { @@ -291,6 +307,15 @@ func newListener(mountpointPath string, entireMount bool, notificationOnly bool, return listener, nil } +func (l *Listener) clearWatch() error { + if err := unix.FanotifyMark(l.fd, unix.FAN_MARK_FLUSH, 0, -1, ""); err != nil { + return err + } + l.watches = make(map[string]bool) + l.markMask = 0 + return nil +} + func (l *Listener) fanotifyMark(path string, flags uint, mask uint64) error { if l == nil { panic("nil listener") diff --git a/fanotify_test.go b/fanotify_test.go index 589fce4..c9636b7 100644 --- a/fanotify_test.go +++ b/fanotify_test.go @@ -410,6 +410,60 @@ func TestMultipleEvents(t *testing.T) { } } +func TestAddPathBeforeWatchStart(t *testing.T) { + l, err := NewListener("/", false, PermissionNone) + assert.Nil(t, err) + assert.NotNil(t, l) + go l.Start() + defer l.Stop() + + watchDir := t.TempDir() + + testFile := fmt.Sprintf("%s/test.txt", watchDir) + pid, err := runAsCmd("touch", testFile) // create file + assert.Nil(t, err) + select { + case <-time.After(100 * time.Millisecond): + t.Logf("FileCreated Event not received as expected") + case event := <-l.Events: + t.Errorf("Timeout Error: Unexpected FileCreated event received (%s)", event) + } + touchPid := pid + + eventTypes := FileModified.Or(FileDeleted) + l.AddWatch(watchDir, eventTypes) + // modify file + os.WriteFile(testFile, []byte("test string"), 0666) + pid = os.Getpid() + select { + case <-time.After(100 * time.Millisecond): + t.Error("Timeout Error: FileModified event not received") + case event := <-l.Events: + assert.Equal(t, fmt.Sprintf("%s/%s", event.Path, event.FileName), testFile) + assert.Equal(t, event.Pid, pid) + assert.True(t, event.EventTypes.Has(FileModified)) + t.Logf("Received: (%s)", event) + } + + t.Logf("Pids: Self(%d), Touch(%d)", pid, touchPid) + // NOTE: os.WriteFile sends two modify events; so draining them + for len(l.Events) > 0 { + e := <-l.Events + t.Logf("Drain-Event: (%s)", e) + } + pid, err = runAsCmd("rm", "-f", testFile) + assert.Nil(t, err) + select { + case <-time.After(100 * time.Millisecond): + t.Error("Timeout Error: FileDeleted event not received") + case event := <-l.Events: + assert.Equal(t, fmt.Sprintf("%s/%s", event.Path, event.FileName), testFile) + assert.Equal(t, event.Pid, pid) + assert.True(t, event.EventTypes.Has(FileDeleted)) + t.Logf("Received: (%s)", event) + } +} + // FileCreated and FileClosed combination does not raise any events func TestWithCapSysAdmMarkCreateCloseBug(t *testing.T) { if *bug {