Re: [PATCH] fsnotify: Fix stale object mask after concurrent mark updates
Amir Goldstein <[email protected]> Sat, 1 Aug 2026 15:17:17 +0200
| Newsgroups | org.kernel.vger.linux-fsdevel |
|---|---|
| Message-ID | <CAOQ4uxjE8TSqz2NcQHMEgun7K7tUUGVWKL4+8Crxz3CHVGPxwQ@mail.gmail.com> |
On Fri, Jul 31, 2026 at 6:41 PM 권영재 / 학생 / 전기·정보공학부 <[email protected]> wrote: > > 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. I think you should use raw mark->mask comparison in the normal add case. That should be enough to prevent races from NOT sending a due event. You can do unconditional recalc on update of ignore_mask, which is a more rare use case. > 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 That seems like an unnecessary overkill. It feels wrong to me to kill the optimization of the common use case for esoteric corner use cases. > and every update of an existing inotify watch. The Not that I think that update of inotify watch mask is worth optimizing, but seems that the temporary set to zero can easily be fixed: if (replace) { fsn_mark->mask = inotify_arg_to_mask(inode, arg); fsn_mark->flags = (fsn_mark->flags & ~INOTIFY_MARK_FLAGS) | inotify_arg_to_flags(arg); } else { fsn_mark->mask |= inotify_arg_to_mask(inode, arg); fsn_mark->flags |= inotify_arg_to_flags(arg); } Perhaps there is still a data race here and not worth the trouble of discussing it at all.. Thanks, Amir. > 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 > > > >