Re: [PATCH v2] fsnotify: Fix stale object mask after concurrent mark updates
Amir Goldstein <[email protected]> Sat, 1 Aug 2026 15:18:38 +0200
| Newsgroups | org.kernel.vger.linux-fsdevel,org.kernel.vger.stable |
|---|---|
| Message-ID | <CAOQ4uxh3Z-F6VKAZoF2cRnVL1r_XqxJxTFyW-P3q_yet98o+Ww@mail.gmail.com> |
On Sat, Aug 1, 2026 at 8:00 AM Youngjae Kwon <[email protected]> wrote: > > When a mark gets a new event bit, fanotify and inotify avoid recalculating > the object mask if the cached aggregate already contains that bit. This is > racy with a recalculation triggered by a concurrent update to another mark > on the same connector. > > The concurrent scan can read the mark before the new bit is added, while > the updater reads the old aggregate before that scan publishes its result. > The updater then skips recalculation and the scan publishes a mask without > the bit, leaving the object mask stale after both updates complete. > > This can be reproduced with two fanotify groups watching the same inode: > one thread removes FAN_MODIFY from one existing mark while another thread > adds FAN_MODIFY to the other mark. After both fanotify_mark() calls return, > writes can fail to produce FAN_MODIFY for the group whose mark now contains > the bit. This was reproduced on an unmodified v6.12.95 kernel. The > equivalent inotify interleaving loses IN_MODIFY events. > > Always recalculate the connector mask after successfully updating an > existing fanotify mark Please don't use this big hammer. See comments on v1. Thanks, Amir. > or inotify watch. In fanotify, FS_MODIFY handling > may clear ignore_mask concurrently without taking mark->lock, so > calculated mask snapshots are not reliable. In inotify, replacing a watch > temporarily sets mark->mask to zero, so a concurrent scan can observe zero > even when the old and final masks are equal. Mark updates are expected to > be infrequent, so avoid these racy optimizations. > > Link: https://lore.kernel.org/all/CACwKKmCZdiZDoFuYm6LZhQ=XvHPk0fNKH=X3LmoXMqakYqJaNw@mail.gmail.com/ > Fixes: 63c882a05416 ("inotify: reimplement inotify using fsnotify") > Fixes: 912ee3946c5e ("fanotify: do not call fanotify_update_object_mask in fanotify_add_mark") > Cc: [email protected] # needs adjustments for <= 7.0 > Suggested-by: Jan Kara <[email protected]> > Signed-off-by: Youngjae Kwon <[email protected]> > --- > Changes in v2: > - Always recalculate after a successful fanotify mark add/update or update of > an existing inotify watch. > - Drop the fanotify calculated-mask and cached-aggregate checks; neither can > close the race with concurrent ignore-mask clearing. > - Make fanotify_mark_update_flags() return void; its recalculation > bookkeeping and the now-redundant first-time guard are no longer needed. > - Drop the inotify old/new mask check because the replace path can expose a > temporary zero mask even when the old and final masks are equal. > > I will send backports for stable kernels through 7.0.y after this lands. > > fs/notify/fanotify/fanotify_user.c | 35 ++++++++---------------------- > fs/notify/inotify/inotify_user.c | 15 +------------ > 2 files changed, 10 insertions(+), 40 deletions(-) > > diff --git a/fs/notify/fanotify/fanotify_user.c b/fs/notify/fanotify/fanotify_user.c > index 9ee373ff5..a3648cec5 100644 > --- a/fs/notify/fanotify/fanotify_user.c > +++ b/fs/notify/fanotify/fanotify_user.c > @@ -1276,12 +1276,11 @@ static int fanotify_remove_mark(struct fsnotify_group *group, > return 0; > } > > -static bool fanotify_mark_update_flags(struct fsnotify_mark *fsn_mark, > +static void fanotify_mark_update_flags(struct fsnotify_mark *fsn_mark, > unsigned int fan_flags) > { > bool want_iref = !(fan_flags & FAN_MARK_EVICTABLE); > unsigned int ignore = fan_flags & FANOTIFY_MARK_IGNORE_BITS; > - bool recalc = false; > > /* > * When using FAN_MARK_IGNORE for the first time, mark starts using > @@ -1293,20 +1292,15 @@ static bool fanotify_mark_update_flags(struct fsnotify_mark *fsn_mark, > fsn_mark->flags |= FSNOTIFY_MARK_FLAG_HAS_IGNORE_FLAGS; > > /* > - * Setting FAN_MARK_IGNORED_SURV_MODIFY for the first time may lead to > - * the removal of the FS_MODIFY bit in calculated mask if it was set > - * because of an ignore mask that is now going to survive FS_MODIFY. > + * Setting FAN_MARK_IGNORED_SURV_MODIFY may remove FS_MODIFY from the > + * calculated mask if it was included only to clear the ignore mask. > */ > - if (ignore && (fan_flags & FAN_MARK_IGNORED_SURV_MODIFY) && > - !(fsn_mark->flags & FSNOTIFY_MARK_FLAG_IGNORED_SURV_MODIFY)) { > + if (ignore && (fan_flags & FAN_MARK_IGNORED_SURV_MODIFY)) > fsn_mark->flags |= FSNOTIFY_MARK_FLAG_IGNORED_SURV_MODIFY; > - if (!(fsn_mark->mask & FS_MODIFY)) > - recalc = true; > - } > > if (fsn_mark->connector->type != FSNOTIFY_OBJ_TYPE_INODE || > want_iref == !(fsn_mark->flags & FSNOTIFY_MARK_FLAG_NO_IREF)) > - return recalc; > + return; > > /* > * NO_IREF may be removed from a mark, but not added. > @@ -1314,28 +1308,19 @@ static bool fanotify_mark_update_flags(struct fsnotify_mark *fsn_mark, > */ > WARN_ON_ONCE(!want_iref); > fsn_mark->flags &= ~FSNOTIFY_MARK_FLAG_NO_IREF; > - > - return true; > } > > -static bool fanotify_mark_add_to_mask(struct fsnotify_mark *fsn_mark, > +static void fanotify_mark_add_to_mask(struct fsnotify_mark *fsn_mark, > __u32 mask, unsigned int fan_flags) > { > - bool recalc; > - > spin_lock(&fsn_mark->lock); > if (!(fan_flags & FANOTIFY_MARK_IGNORE_BITS)) > fsn_mark->mask |= mask; > else > fsn_mark->ignore_mask |= mask; > > - recalc = fsnotify_calc_mask(fsn_mark) & > - ~fsnotify_conn_mask(fsn_mark->connector); > - > - recalc |= fanotify_mark_update_flags(fsn_mark, fan_flags); > + fanotify_mark_update_flags(fsn_mark, fan_flags); > spin_unlock(&fsn_mark->lock); > - > - return recalc; > } > > struct fan_fsid { > @@ -1502,7 +1487,6 @@ static int fanotify_add_mark(struct fsnotify_group *group, > struct fan_fsid *fsid) > { > struct fsnotify_mark *fsn_mark; > - bool recalc; > int ret = 0; > > fsnotify_group_lock(group); > @@ -1534,9 +1518,8 @@ static int fanotify_add_mark(struct fsnotify_group *group, > goto out; > } > > - recalc = fanotify_mark_add_to_mask(fsn_mark, mask, fan_flags); > - if (recalc) > - fsnotify_recalc_mask(fsn_mark->connector); > + fanotify_mark_add_to_mask(fsn_mark, mask, fan_flags); > + fsnotify_recalc_mask(fsn_mark->connector); > > out: > fsnotify_group_unlock(group); > diff --git a/fs/notify/inotify/inotify_user.c b/fs/notify/inotify/inotify_user.c > index ed37491c1..5f19c24ec 100644 > --- a/fs/notify/inotify/inotify_user.c > +++ b/fs/notify/inotify/inotify_user.c > @@ -539,7 +539,6 @@ static int inotify_update_existing_watch(struct fsnotify_group *group, > { > struct fsnotify_mark *fsn_mark; > struct inotify_inode_mark *i_mark; > - __u32 old_mask, new_mask; > int replace = !(arg & IN_MASK_ADD); > int create = (arg & IN_MASK_CREATE); > int ret; > @@ -555,27 +554,15 @@ static int inotify_update_existing_watch(struct fsnotify_group *group, > i_mark = container_of(fsn_mark, struct inotify_inode_mark, fsn_mark); > > spin_lock(&fsn_mark->lock); > - old_mask = fsn_mark->mask; > if (replace) { > fsn_mark->mask = 0; > fsn_mark->flags &= ~INOTIFY_MARK_FLAGS; > } > fsn_mark->mask |= inotify_arg_to_mask(inode, arg); > fsn_mark->flags |= inotify_arg_to_flags(arg); > - new_mask = fsn_mark->mask; > spin_unlock(&fsn_mark->lock); > > - if (old_mask != new_mask) { > - /* more bits in old than in new? */ > - int dropped = (old_mask & ~new_mask); > - /* more bits in this fsn_mark than the inode's mask? */ > - int do_inode = (new_mask & ~READ_ONCE(inode->i_fsnotify_mask)); > - > - /* update the inode with this new fsn_mark */ > - if (dropped || do_inode) > - fsnotify_recalc_mask(fsn_mark->connector); > - > - } > + fsnotify_recalc_mask(fsn_mark->connector); > > /* return the wd */ > ret = i_mark->wd; > -- > 2.50.1