Re: [PATCH v1 03/12] fsnotify: don't hold a spin_lock across fsnotify_recalc_mask() calls.
NeilBrown <[email protected]>
| Newsgroups | org.kernel.vger.ceph-devel,org.kernel.vger.autofs,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel,org.kernel.vger.linux-nfs |
|---|---|
| Message-ID | <[email protected]> |
On Mon, 17 Aug 2026, Amir Goldstein wrote: > On Mon, Aug 17, 2026 at 1:03 PM Jan Kara <[email protected]> wrote: > > > > On Sat 15-08-26 10:28:41, NeilBrown wrote: > > > On Tue, 11 Aug 2026, NeilBrown wrote: > > > > On Tue, 11 Aug 2026, Miklos Szeredi wrote: > > > > > On Mon, 3 Aug 2026 at 03:39, NeilBrown <[email protected]> wrote: > > > > > > > > > > > @@ -116,11 +117,15 @@ static int dnotify_handle_event(struct fsnotify_mark *inode_mark, u32 mask, > > > > > > else { > > > > > > *prev = dn->dn_next; > > > > > > kmem_cache_free(dnotify_struct_cache, dn); > > > > > > - dnotify_recalc_inode_mask(inode_mark); > > > > > > + need_recalc = true; > > > > > > } > > > > > > } > > > > > > > > > > > > + if (need_recalc) > > > > > > + need_recalc = dnotify_recalc_inode_mask(inode_mark); > > > > > > spin_unlock(&inode_mark->lock); > > > > > > + if (need_recalc) > > > > > > + fsnotify_recalc_mask(inode_mark->connector); > > > > > > > > > > Is the fsnotify_group_lock() held in this case? I don't see it. > > > > > > > > It isn't held. Doesn't it need to be... > > > > It seems to protect marks, so maybe it does. > > > > > > > > srcu seems to be used to protect this section, so maybe we can rely on > > > > that. > > > > > > I dug into this some more, and we do rely on srcu, but don't need the > > > extra code below. > > > inode_mark->lock doesn't protect inode_mark->connector, so moving the > > > dereference out of the lock has no effect. > > > srcu_read_lock is taken before we get the ref to the mark, so the mark > > > and the connector cannot disappear underneath us. > > > A race could result in inode_mark->connector reading as NULL, but > > > fsnotify_recalc_mask() checks for NULL, so there is no risk for harm. > > > > > > Thanks for encouraging me to dig into this. > > > > Sorry for not replying earlier but I was on vacation. The lifetime rules > > around marks & connectors are subtle so we have to be really careful and > > dnotify with its single shot marks is peculiar which makes things even > > harder. fsnotify_recalc_mask() has a comment about locking in front of it: > > > > /* > > * Calculate mask of events for a list of marks. The caller must make sure > > * connector and connector->obj cannot disappear under us. Callers achieve > > * this by holding a mark->lock or mark->group->mark_mutex for a mark on this > > * list. > > */ > > > > and you very obviously start violating these rules with your changes. Now I > > admit I've forgotten all the details why I did it like this so let me > > reconstruct it :). > > > > Connector stays alive as long as there's any mark in its list. Both marks > > and connectors are protected by the srcu. Mark also has > > FSNOTIFY_MARK_FLAG_ATTACHED flag which is set iff the connector->obj is > > pointing to valid inode/mount/... FSNOTIFY_MARK_FLAG_ATTACHED changes only > > under mark_mutex so that's why mark_mutex is stabilizing the connector (and > > also connector->obj). This is what is used by most places calling > > fsnotify_recalc_mask(). But dnotify needs to mess with notification mark > > mask from event handling and there we cannot take mark_mutex due to lock > > ordering constraints. That's where the mark->lock rule comes into play > > because mark->lock also needs to be acquired to clear > > FSNOTIFY_MARK_FLAG_ATTACHED. That being said this dnotify use of > > fsnotify_recalc_mask() still looks somewhat racy because > > dnotify_handle_event() can get called after FSNOTIFY_MARK_FLAG_ATTACHED is > > cleared. > > > > Anyway if you move fsnotify_recalc_mask() call outside of mark->lock, you > > seem to make the race with dnotify clearing the mark from > > fcntl_dirnotify() easier to hit. Now in the notification path the inode > > itself is guaranteed to stay alive and the rest is protected by the SRCU so > > there's no direct UAF. But fsnotify_recalc_mask() simply isn't prepared for > > the connector changing under it due to object getting detached and so we > > could end up doing weird things like NULL ptr derefs or similar stuff. > > > > So this call to fsnotify_recalc_mask() from dnotify needs a more careful > > handling. Which is sad because I doubt anybody still uses dnotify... > > Maybe this is the way out. > > dnotify_recalc_inode_mask() can only remove bits from i_fsnotify_mask, > so it is an optimization. > > If we just remove fsnotify_recalc_mask() call from > dnotify_recalc_inode_mask(), then i_fsnotify_mask will be updated when > dnotify_mask gets removed eventually. I had thought something along these lines too. I don't think we need to drop all of fsnotify_recalc_mask(), only the fsnotify_conn_set_children_dentry_flags() part that walks the d_children list. And that only happens when we add to the mask, not when bits are cleared. So maybe the fsnotify_conn_set_children_dentry_flags() simply never happens in this context so it doesn't need fixing. I would prefer to make that clear from the code with a patch like the following. Note that I'm not (yet) convinced by the locking argument, but maybe we don't need to pursue it. Thanks, NeilBrown diff --git a/fs/notify/dnotify/dnotify.c b/fs/notify/dnotify/dnotify.c index 9fb73bafd41d..be66d4142563 100644 --- a/fs/notify/dnotify/dnotify.c +++ b/fs/notify/dnotify/dnotify.c @@ -75,7 +75,7 @@ static void dnotify_recalc_inode_mask(struct fsnotify_mark *fsn_mark) return; fsn_mark->mask = new_mask; - fsnotify_recalc_mask(fsn_mark->connector); + fsnotify_recalc_mask_inatomic(fsn_mark->connector); } /* diff --git a/fs/notify/mark.c b/fs/notify/mark.c index b2640d836a71..7ba79828b07d 100644 --- a/fs/notify/mark.c +++ b/fs/notify/mark.c @@ -322,7 +322,8 @@ static void fsnotify_conn_set_children_dentry_flags( * this by holding a mark->lock or mark->group->mark_mutex for a mark on this * list. */ -void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn) +static void do_fsnotify_recalc_mask(struct fsnotify_mark_connector *conn, + bool in_atomic) { bool update_children; @@ -339,10 +340,20 @@ void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn) * When parent stops watching, we clear false positive PARENT_WATCHED * flags lazily in __fsnotify_parent(). */ - if (update_children) + if (update_children && !WARN_ON(in_atomic)) fsnotify_conn_set_children_dentry_flags(conn); } +void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn) +{ + do_fsnotify_recalc_mask(conn, false); +} + +void fsnotify_recalc_mask_inatomic(struct fsnotify_mark_connector *conn) +{ + do_fsnotify_recalc_mask(conn, true); +} + /** * fsnotify_modify_mark_mask - set and/or clear flags in a mark's mask * @mark: mark to be modified diff --git a/include/linux/fsnotify_backend.h b/include/linux/fsnotify_backend.h index 618eed4d6d72..3e0fdd639d18 100644 --- a/include/linux/fsnotify_backend.h +++ b/include/linux/fsnotify_backend.h @@ -887,6 +887,7 @@ static inline __u32 fsnotify_calc_mask(struct fsnotify_mark *mark) extern __u32 fsnotify_conn_mask(struct fsnotify_mark_connector *conn); /* Calculate mask of events for a list of marks */ extern void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn); +extern void fsnotify_recalc_mask_inatomic(struct fsnotify_mark_connector *conn); extern void fsnotify_init_mark(struct fsnotify_mark *mark, struct fsnotify_group *group); /* Find mark belonging to given group in the list of marks */