Re: [PATCH] fsnotify: Fix stale object mask after concurrent mark updates

­권영재 / 학생 / 전기·정보공학부 <[email protected]> Sat, 1 Aug 2026 01:41:13 +0900
Newsgroups org.kernel.vger.linux-fsdevel
Message-ID <CACwKKmDrTKHRZczEk4z-gO+yZNXgvEgFDzKFfJSUMR4oK5VA6A@mail.gmail.com>
Hi Honza,

The last line can affect the result, but it only narrows the race.

send_to_group() can clear mark->ignore_mask without mark->lock. Suppose a
mark has FS_MODIFY in its normal mask and FS_ACCESS in a non-surviving
ignore mask. After old_mask is read, FS_MODIFY clears the ignore mask and
a concurrent recalculation scans the mark without FS_ACCESS. The add then
puts FS_ACCESS in the normal mask, making new_mask equal to old_mask. If
the scan publishes before the aggregate check, the last line catches it.
If it publishes afterward, the check sees the old aggregate, skips, and
the scan then publishes a mask without FS_ACCESS.

A raw mark->mask comparison catches this case but misses FAN_MARK_IGNORE
updates. The inotify old/new check has the same structural problem: its
replace path temporarily sets mark->mask to zero, which a concurrent scan
can observe. If the final mask equals the old mask, the updater skips and
the scan can publish an aggregate without that mark's bits.

I will therefore make recalculation unconditional after every successful
fanotify add/update and every update of an existing inotify watch. The
fanotify remove path can remain unchanged because it only reduces the
calculated mask; inotify removal has no analogous conditional skip.

I will send a v2 implementing this.

Thanks,
Youngjae Kwon

2026년 7월 31일 (금) 오후 9:27, Jan Kara <[email protected]>님이 작성:
>
> On Fri 31-07-26 00:22:41, Youngjae Kwon 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 request recalculation when an existing mark's calculated mask
> > actually changes. For fanotify, compare fsnotify_calc_mask() before and
> > after normal-mask, ignore-mask, and flag updates. Keep the cached aggregate
> > check for unchanged updates, and keep the flag helper's inode-reference
> > handling.
> >
> > 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]>
>
> Looks mostly good, just one question below:
>
> >       spin_lock(&fsn_mark->lock);
> > +     old_mask = fsnotify_calc_mask(fsn_mark);
> >       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);
> > +     recalc = fanotify_mark_update_flags(fsn_mark, fan_flags);
> > +     new_mask = fsnotify_calc_mask(fsn_mark);
> > +     recalc |= old_mask != new_mask;
> > +     recalc |= new_mask & ~fsnotify_conn_mask(fsn_mark->connector);
>
> How could this last line ever change anything? If old_mask == new_mask,
> then it would look like a bug that some bit from the new_mask (and thus
> old_mask) isn't already included in the aggregate connector mask?
>
> I can fix this up on commit but I want to understand why you've kept the
> line here just in case I'm missing something...
>
>                                                                 Honza
>
> >       spin_unlock(&fsn_mark->lock);
> >
> >       return recalc;
> > diff --git a/fs/notify/inotify/inotify_user.c b/fs/notify/inotify/inotify_user.c
> > index ed37491c1..c44f2daa1 100644
> > --- a/fs/notify/inotify/inotify_user.c
> > +++ b/fs/notify/inotify/inotify_user.c
> > @@ -565,17 +565,8 @@ static int inotify_update_existing_watch(struct fsnotify_group *group,
> >       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);
> > -
> > -     }
> > +     if (old_mask != new_mask)
> > +             fsnotify_recalc_mask(fsn_mark->connector);
> >
> >       /* return the wd */
> >       ret = i_mark->wd;
> > --
> > 2.50.1
> --
> Jan Kara <[email protected]>
> SUSE Labs, CR
>
>