Skip to content

Commit 6e8fbeb

Browse files
committed
Fix DeleteWatch/UnwatchMount does not work
1 parent 05ab819 commit 6e8fbeb

File tree

3 files changed

+125
-25
lines changed

3 files changed

+125
-25
lines changed

fanotify_api.go

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ type Listener struct {
7373
fd int
7474
// flags passed to fanotify_init
7575
flags uint
76+
// markMask current fanotify mark mask
77+
markMask uint64
7678
// mount fd is the file descriptor of the mountpoint
7779
mountpoint *os.File
7880
kernelMajorVersion int
@@ -203,14 +205,23 @@ func (l *Listener) Stop() {
203205
// for - [FileCreated], [FileAttribChanged], [FileMovedTo], [FileMovedFrom], [WatchedFileDeleted],
204206
// [WatchedFileOrDirectoryDeleted], [FileDeleted], [FileOrDirectoryDeleted]
205207
func (l *Listener) WatchMount(eventTypes EventType) error {
206-
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(eventTypes))
208+
mask := l.getMaskAfterAdd(eventTypes)
209+
l.clearWatch()
210+
l.markMask = uint64(mask)
211+
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(mask))
207212
}
208213

209214
// UnwatchMount removes the notification marks for the entire mount point.
210215
// This method returns an [ErrWatchPath] if the listener was not initialized to monitor
211216
// the entire mount point. To unmark specific files or directories use [DeleteWatch] method.
212217
func (l *Listener) UnwatchMount(eventTypes EventType) error {
213-
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_REMOVE|unix.FAN_MARK_MOUNT, uint64(eventTypes))
218+
if l.markMask == 0 {
219+
return l.clearWatch()
220+
}
221+
remaining := l.getMaskAfterRemove(eventTypes)
222+
l.clearWatch()
223+
l.markMask = uint64(remaining)
224+
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(remaining))
214225
}
215226

216227
// AddWatch adds or modifies the fanotify mark for the specified path.
@@ -221,14 +232,46 @@ func (l *Listener) UnwatchMount(eventTypes EventType) error {
221232
// - [FileCreated] cannot be or-ed / combined with [FileClosed]. The fanotify system does not generate any event for this combination.
222233
// - [FileOpened] with any of the event types containing OrDirectory causes an event flood for the directory and then stopping raising any events at all.
223234
// - [FileOrDirectoryOpened] with any of the other event types causes an event flood for the directory and then stopping raising any events at all.
235+
//
236+
// NOTE:
237+
// Any event type that contains "OrDirectory" applies the OrDirectory mask to any other applicable
238+
// marks. For example adding [FileDeleted] and [FileOrDirectoryOpened] will apply "OrDirectory" to
239+
// [FileDeleted] thus the resulting mask will be set to [FileOrDirectoryOpened] and [FileOrDirectoryDeleted]
224240
func (l *Listener) AddWatch(path string, eventTypes EventType) error {
225241
if l == nil {
226242
panic("nil listener")
227243
}
228244
if l.entireMount {
229245
return os.ErrInvalid
230246
}
231-
return l.fanotifyMark(path, unix.FAN_MARK_ADD, uint64(eventTypes|unix.FAN_EVENT_ON_CHILD))
247+
mask := l.getMaskAfterAdd(eventTypes)
248+
l.clearWatch()
249+
l.markMask = uint64(mask)
250+
return l.fanotifyMark(path, unix.FAN_MARK_ADD, uint64(mask|unix.FAN_EVENT_ON_CHILD))
251+
}
252+
253+
// DeleteWatch removes/unmarks the fanotify mark for the specified path.
254+
// Calling DeleteWatch on the listener initialized to monitor the entire mount point
255+
// results in [os.ErrInvalid]. Use [UnwatchMount] for deleting marks on the mount point.
256+
func (l *Listener) DeleteWatch(parentDir string, eventTypes EventType) error {
257+
if l.entireMount {
258+
return os.ErrInvalid
259+
}
260+
if l.markMask == 0 {
261+
return l.clearWatch()
262+
}
263+
remaining := l.getMaskAfterRemove(eventTypes)
264+
l.clearWatch()
265+
l.markMask = uint64(remaining)
266+
return l.fanotifyMark(parentDir, unix.FAN_MARK_ADD, uint64(remaining|unix.FAN_EVENT_ON_CHILD))
267+
}
268+
269+
// ClearWatch stops watching for all event types
270+
func (l *Listener) ClearWatch() error {
271+
if l == nil {
272+
panic("nil listener")
273+
}
274+
return l.clearWatch()
232275
}
233276

234277
// Allow sends an "allowed" response to the permission request event.
@@ -251,28 +294,6 @@ func (l *Listener) Deny(e Event) {
251294
unix.Write(l.fd, buf.Bytes())
252295
}
253296

254-
// DeleteWatch removes/unmarks the fanotify mark for the specified path.
255-
// Calling DeleteWatch on the listener initialized to monitor the entire mount point
256-
// results in [os.ErrInvalid]. Use [UnwatchMount] for deleting marks on the mount point.
257-
func (l *Listener) DeleteWatch(parentDir string, eventTypes EventType) error {
258-
if l.entireMount {
259-
return os.ErrInvalid
260-
}
261-
return l.fanotifyMark(parentDir, unix.FAN_MARK_REMOVE, uint64(eventTypes|unix.FAN_EVENT_ON_CHILD))
262-
}
263-
264-
// ClearWatch stops watching for all event types
265-
func (l *Listener) ClearWatch() error {
266-
if l == nil {
267-
panic("nil listener")
268-
}
269-
if err := unix.FanotifyMark(l.fd, unix.FAN_MARK_FLUSH, 0, -1, ""); err != nil {
270-
return err
271-
}
272-
l.watches = make(map[string]bool)
273-
return nil
274-
}
275-
276297
// Has returns true if event types (e) contains the passed in event type (et).
277298
func (e EventType) Has(et EventType) bool {
278299
return e&et == et

fanotify_event.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,22 @@ func fanotifyEventOK(meta *unix.FanotifyEventMetadata, n int) bool {
204204
int(meta.Event_len) <= n)
205205
}
206206

207+
func (l *Listener) getMaskAfterRemove(removeMask EventType) EventType {
208+
if l.markMask == 0 {
209+
return EventType(0)
210+
}
211+
mask := EventType(l.markMask) ^ removeMask
212+
return mask
213+
}
214+
215+
func (l *Listener) getMaskAfterAdd(addMask EventType) EventType {
216+
if l.markMask == 0 {
217+
return addMask
218+
}
219+
mask := EventType(l.markMask) | addMask
220+
return mask
221+
}
222+
207223
// permissionType is ignored when isNotificationListener is true.
208224
func newListener(mountpointPath string, entireMount bool, notificationOnly bool, permissionType PermissionType) (*Listener, error) {
209225

@@ -291,6 +307,15 @@ func newListener(mountpointPath string, entireMount bool, notificationOnly bool,
291307
return listener, nil
292308
}
293309

310+
func (l *Listener) clearWatch() error {
311+
if err := unix.FanotifyMark(l.fd, unix.FAN_MARK_FLUSH, 0, -1, ""); err != nil {
312+
return err
313+
}
314+
l.watches = make(map[string]bool)
315+
l.markMask = 0
316+
return nil
317+
}
318+
294319
func (l *Listener) fanotifyMark(path string, flags uint, mask uint64) error {
295320
if l == nil {
296321
panic("nil listener")

fanotify_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,60 @@ func TestMultipleEvents(t *testing.T) {
410410
}
411411
}
412412

413+
func TestAddPathBeforeWatchStart(t *testing.T) {
414+
l, err := NewListener("/", false, PermissionNone)
415+
assert.Nil(t, err)
416+
assert.NotNil(t, l)
417+
go l.Start()
418+
defer l.Stop()
419+
420+
watchDir := t.TempDir()
421+
422+
testFile := fmt.Sprintf("%s/test.txt", watchDir)
423+
pid, err := runAsCmd("touch", testFile) // create file
424+
assert.Nil(t, err)
425+
select {
426+
case <-time.After(100 * time.Millisecond):
427+
t.Logf("FileCreated Event not received as expected")
428+
case event := <-l.Events:
429+
t.Errorf("Timeout Error: Unexpected FileCreated event received (%s)", event)
430+
}
431+
touchPid := pid
432+
433+
eventTypes := FileModified.Or(FileDeleted)
434+
l.AddWatch(watchDir, eventTypes)
435+
// modify file
436+
os.WriteFile(testFile, []byte("test string"), 0666)
437+
pid = os.Getpid()
438+
select {
439+
case <-time.After(100 * time.Millisecond):
440+
t.Error("Timeout Error: FileModified event not received")
441+
case event := <-l.Events:
442+
assert.Equal(t, fmt.Sprintf("%s/%s", event.Path, event.FileName), testFile)
443+
assert.Equal(t, event.Pid, pid)
444+
assert.True(t, event.EventTypes.Has(FileModified))
445+
t.Logf("Received: (%s)", event)
446+
}
447+
448+
t.Logf("Pids: Self(%d), Touch(%d)", pid, touchPid)
449+
// NOTE: os.WriteFile sends two modify events; so draining them
450+
for len(l.Events) > 0 {
451+
e := <-l.Events
452+
t.Logf("Drain-Event: (%s)", e)
453+
}
454+
pid, err = runAsCmd("rm", "-f", testFile)
455+
assert.Nil(t, err)
456+
select {
457+
case <-time.After(100 * time.Millisecond):
458+
t.Error("Timeout Error: FileDeleted event not received")
459+
case event := <-l.Events:
460+
assert.Equal(t, fmt.Sprintf("%s/%s", event.Path, event.FileName), testFile)
461+
assert.Equal(t, event.Pid, pid)
462+
assert.True(t, event.EventTypes.Has(FileDeleted))
463+
t.Logf("Received: (%s)", event)
464+
}
465+
}
466+
413467
// FileCreated and FileClosed combination does not raise any events
414468
func TestWithCapSysAdmMarkCreateCloseBug(t *testing.T) {
415469
if *bug {

0 commit comments

Comments
 (0)