Skip to content

Commit

Permalink
Fix DeleteWatch/UnwatchMount does not work
Browse files Browse the repository at this point in the history
  • Loading branch information
opcoder0 committed Jan 2, 2023
1 parent 05ab819 commit 6e8fbeb
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 25 deletions.
71 changes: 46 additions & 25 deletions fanotify_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -221,14 +232,46 @@ 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")
}
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.
Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions fanotify_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -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")
Expand Down
54 changes: 54 additions & 0 deletions fanotify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 6e8fbeb

Please sign in to comment.