]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
smb: client: introduce close_cached_dir_locked()
authorHenrique Carvalho <henrique.carvalho@suse.com>
Thu, 13 Nov 2025 18:09:13 +0000 (15:09 -0300)
committerSteve French <stfrench@microsoft.com>
Thu, 20 Nov 2025 09:03:30 +0000 (03:03 -0600)
Replace close_cached_dir() calls under cfid_list_lock with a new
close_cached_dir_locked() variant that uses kref_put() instead of
kref_put_lock() to avoid recursive locking when dropping references.

While the existing code works if the refcount >= 2 invariant holds,
this area has proven error-prone. Make deadlocks impossible and WARN
on invariant violations.

Cc: stable@vger.kernel.org
Reviewed-by: David Howells <dhowells@redhat.com>
Signed-off-by: Henrique Carvalho <henrique.carvalho@suse.com>
Signed-off-by: Steve French <stfrench@microsoft.com>
fs/smb/client/cached_dir.c

index 018055fd2cdb1010541efa025be0c60d78a07524..e3ea6fe7edb474ea18763311b9520cd1df755351 100644 (file)
@@ -16,6 +16,7 @@ static struct cached_fid *init_cached_dir(const char *path);
 static void free_cached_dir(struct cached_fid *cfid);
 static void smb2_close_cached_fid(struct kref *ref);
 static void cfids_laundromat_worker(struct work_struct *work);
+static void close_cached_dir_locked(struct cached_fid *cfid);
 
 struct cached_dir_dentry {
        struct list_head entry;
@@ -388,7 +389,7 @@ out:
                         * lease. Release one here, and the second below.
                         */
                        cfid->has_lease = false;
-                       close_cached_dir(cfid);
+                       close_cached_dir_locked(cfid);
                }
                spin_unlock(&cfids->cfid_list_lock);
 
@@ -480,18 +481,52 @@ void drop_cached_dir_by_name(const unsigned int xid, struct cifs_tcon *tcon,
        spin_lock(&cfid->cfids->cfid_list_lock);
        if (cfid->has_lease) {
                cfid->has_lease = false;
-               close_cached_dir(cfid);
+               close_cached_dir_locked(cfid);
        }
        spin_unlock(&cfid->cfids->cfid_list_lock);
        close_cached_dir(cfid);
 }
 
-
+/**
+ * close_cached_dir - drop a reference of a cached dir
+ *
+ * The release function will be called with cfid_list_lock held to remove the
+ * cached dirs from the list before any other thread can take another @cfid
+ * ref. Must not be called with cfid_list_lock held; use
+ * close_cached_dir_locked() called instead.
+ *
+ * @cfid: cached dir
+ */
 void close_cached_dir(struct cached_fid *cfid)
 {
+       lockdep_assert_not_held(&cfid->cfids->cfid_list_lock);
        kref_put_lock(&cfid->refcount, smb2_close_cached_fid, &cfid->cfids->cfid_list_lock);
 }
 
+/**
+ * close_cached_dir_locked - put a reference of a cached dir with
+ * cfid_list_lock held
+ *
+ * Calling close_cached_dir() with cfid_list_lock held has the potential effect
+ * of causing a deadlock if the invariant of refcount >= 2 is false.
+ *
+ * This function is used in paths that hold cfid_list_lock and expect at least
+ * two references. If that invariant is violated, WARNs and returns without
+ * dropping a reference; the final put must still go through
+ * close_cached_dir().
+ *
+ * @cfid: cached dir
+ */
+static void close_cached_dir_locked(struct cached_fid *cfid)
+{
+       lockdep_assert_held(&cfid->cfids->cfid_list_lock);
+
+       if (WARN_ON(kref_read(&cfid->refcount) < 2))
+               return;
+
+       kref_put(&cfid->refcount, smb2_close_cached_fid);
+}
+
 /*
  * Called from cifs_kill_sb when we unmount a share
  */