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

Jan Kara <[email protected]> Wed, 5 Aug 2026 11:54:14 +0200
Newsgroups gmane.linux.file-systems,gmane.linux.kernel.stable
Message-ID <gc2bqp2c3a6emhshpewjalti2j26a5zsrmd6s4gopzvmofklqw@xjnfn4tbaktb>
On Sun 02-08-26 10:58:00, Youngjae Kwon wrote:
> When a mark gets a new event bit, fanotify and inotify may 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.
> 
> For normal fanotify additions, recalculate whenever the raw mark mask
> changes. The normal mask is not cleared asynchronously, so an unchanged
> addition cannot introduce missing interest. Always recalculate ignore-mask
> updates because FS_MODIFY handling may clear the ignore mask without taking
> mark->lock, making snapshot comparisons unreliable.
> 
> Always recalculate after updating an existing inotify watch. Its replace
> path temporarily sets mark->mask to zero, so a concurrent scan can observe
> zero even when the old and final masks are equal. Assigning the replacement
> mask directly would avoid the transient zero, but existing-watch updates
> are infrequent, so unconditional recalculation is simpler.
> 
> 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]>
> Suggested-by: Amir Goldstein <[email protected]>
> Signed-off-by: Youngjae Kwon <[email protected]>

Thanks the patch looks good to me, I've added it to my tree. Amir, are you
happy with the changes like this?

								Honza

> ---
> Changes in v3:
> - Preserve the normal fanotify duplicate-add fast path by recalculating
>   only when the raw normal mask actually changes.
> - Recalculate fanotify ignore-mask updates unconditionally because the
>   ignore mask may be cleared concurrently without mark->lock.
> - Keep v2's unconditional recalculation for existing inotify watch updates.
> 
> I will send backports for stable kernels through 7.0.y after this lands.
> 
>  fs/notify/fanotify/fanotify_user.c | 12 +++++++-----
>  fs/notify/inotify/inotify_user.c   | 15 +--------------
>  2 files changed, 8 insertions(+), 19 deletions(-)
> 
> diff --git a/fs/notify/fanotify/fanotify_user.c b/fs/notify/fanotify/fanotify_user.c
> index 9ee373ff5..9c27db2b7 100644
> --- a/fs/notify/fanotify/fanotify_user.c
> +++ b/fs/notify/fanotify/fanotify_user.c
> @@ -1321,16 +1321,18 @@ static bool fanotify_mark_update_flags(struct fsnotify_mark *fsn_mark,
>  static bool fanotify_mark_add_to_mask(struct fsnotify_mark *fsn_mark,
>  				      __u32 mask, unsigned int fan_flags)
>  {
> +	__u32 old_mask;
>  	bool recalc;
>  
>  	spin_lock(&fsn_mark->lock);
> -	if (!(fan_flags & FANOTIFY_MARK_IGNORE_BITS))
> +	if (!(fan_flags & FANOTIFY_MARK_IGNORE_BITS)) {
> +		old_mask = fsn_mark->mask;
>  		fsn_mark->mask |= mask;
> -	else
> +		recalc = old_mask != fsn_mark->mask;
> +	} else {
>  		fsn_mark->ignore_mask |= mask;
> -
> -	recalc = fsnotify_calc_mask(fsn_mark) &
> -		~fsnotify_conn_mask(fsn_mark->connector);
> +		recalc = true;
> +	}
>  
>  	recalc |= fanotify_mark_update_flags(fsn_mark, fan_flags);
>  	spin_unlock(&fsn_mark->lock);
> 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
-- 
Jan Kara <[email protected]>
SUSE Labs, CR