debugfs: add API to allow debugfs operations cancellation
authorJohannes Berg <johannes.berg@intel.com>
Fri, 24 Nov 2023 16:25:26 +0000 (17:25 +0100)
committerJohannes Berg <johannes.berg@intel.com>
Mon, 27 Nov 2023 10:24:55 +0000 (11:24 +0100)
In some cases there might be longer-running hardware accesses
in debugfs files, or attempts to acquire locks, and we want
to still be able to quickly remove the files.

Introduce a cancellations API to use inside the debugfs handler
functions to be able to cancel such operations on a per-file
basis.

Acked-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
fs/debugfs/file.c
fs/debugfs/inode.c
fs/debugfs/internal.h
include/linux/debugfs.h

index 3eff92450fd58c3cf277b21994770ca05c2bbab4..5568cdea3490a444a86884cc0a0006eb3d217429 100644 (file)
@@ -114,6 +114,8 @@ int debugfs_file_get(struct dentry *dentry)
                lockdep_init_map(&fsd->lockdep_map, fsd->lock_name ?: "debugfs",
                                 &fsd->key, 0);
 #endif
+               INIT_LIST_HEAD(&fsd->cancellations);
+               mutex_init(&fsd->cancellations_mtx);
        }
 
        /*
@@ -156,6 +158,86 @@ void debugfs_file_put(struct dentry *dentry)
 }
 EXPORT_SYMBOL_GPL(debugfs_file_put);
 
+/**
+ * debugfs_enter_cancellation - enter a debugfs cancellation
+ * @file: the file being accessed
+ * @cancellation: the cancellation object, the cancel callback
+ *     inside of it must be initialized
+ *
+ * When a debugfs file is removed it needs to wait for all active
+ * operations to complete. However, the operation itself may need
+ * to wait for hardware or completion of some asynchronous process
+ * or similar. As such, it may need to be cancelled to avoid long
+ * waits or even deadlocks.
+ *
+ * This function can be used inside a debugfs handler that may
+ * need to be cancelled. As soon as this function is called, the
+ * cancellation's 'cancel' callback may be called, at which point
+ * the caller should proceed to call debugfs_leave_cancellation()
+ * and leave the debugfs handler function as soon as possible.
+ * Note that the 'cancel' callback is only ever called in the
+ * context of some kind of debugfs_remove().
+ *
+ * This function must be paired with debugfs_leave_cancellation().
+ */
+void debugfs_enter_cancellation(struct file *file,
+                               struct debugfs_cancellation *cancellation)
+{
+       struct debugfs_fsdata *fsd;
+       struct dentry *dentry = F_DENTRY(file);
+
+       INIT_LIST_HEAD(&cancellation->list);
+
+       if (WARN_ON(!d_is_reg(dentry)))
+               return;
+
+       if (WARN_ON(!cancellation->cancel))
+               return;
+
+       fsd = READ_ONCE(dentry->d_fsdata);
+       if (WARN_ON(!fsd ||
+                   ((unsigned long)fsd & DEBUGFS_FSDATA_IS_REAL_FOPS_BIT)))
+               return;
+
+       mutex_lock(&fsd->cancellations_mtx);
+       list_add(&cancellation->list, &fsd->cancellations);
+       mutex_unlock(&fsd->cancellations_mtx);
+
+       /* if we're already removing wake it up to cancel */
+       if (d_unlinked(dentry))
+               complete(&fsd->active_users_drained);
+}
+EXPORT_SYMBOL_GPL(debugfs_enter_cancellation);
+
+/**
+ * debugfs_leave_cancellation - leave cancellation section
+ * @file: the file being accessed
+ * @cancellation: the cancellation previously registered with
+ *     debugfs_enter_cancellation()
+ *
+ * See the documentation of debugfs_enter_cancellation().
+ */
+void debugfs_leave_cancellation(struct file *file,
+                               struct debugfs_cancellation *cancellation)
+{
+       struct debugfs_fsdata *fsd;
+       struct dentry *dentry = F_DENTRY(file);
+
+       if (WARN_ON(!d_is_reg(dentry)))
+               return;
+
+       fsd = READ_ONCE(dentry->d_fsdata);
+       if (WARN_ON(!fsd ||
+                   ((unsigned long)fsd & DEBUGFS_FSDATA_IS_REAL_FOPS_BIT)))
+               return;
+
+       mutex_lock(&fsd->cancellations_mtx);
+       if (!list_empty(&cancellation->list))
+               list_del(&cancellation->list);
+       mutex_unlock(&fsd->cancellations_mtx);
+}
+EXPORT_SYMBOL_GPL(debugfs_leave_cancellation);
+
 /*
  * Only permit access to world-readable files when the kernel is locked down.
  * We also need to exclude any file that has ways to write or alter it as root
index 80f4f000dcc138a405b940041f3219bfe7a49005..d53c2860b03c5ab92be5a7af4aad9f373c664636 100644 (file)
@@ -247,6 +247,8 @@ static void debugfs_release_dentry(struct dentry *dentry)
                lockdep_unregister_key(&fsd->key);
                kfree(fsd->lock_name);
 #endif
+               WARN_ON(!list_empty(&fsd->cancellations));
+               mutex_destroy(&fsd->cancellations_mtx);
        }
 
        kfree(fsd);
@@ -756,8 +758,36 @@ static void __debugfs_file_removed(struct dentry *dentry)
        lock_map_acquire(&fsd->lockdep_map);
        lock_map_release(&fsd->lockdep_map);
 
-       if (!refcount_dec_and_test(&fsd->active_users))
+       /* if we hit zero, just wait for all to finish */
+       if (!refcount_dec_and_test(&fsd->active_users)) {
                wait_for_completion(&fsd->active_users_drained);
+               return;
+       }
+
+       /* if we didn't hit zero, try to cancel any we can */
+       while (refcount_read(&fsd->active_users)) {
+               struct debugfs_cancellation *c;
+
+               /*
+                * Lock the cancellations. Note that the cancellations
+                * structs are meant to be on the stack, so we need to
+                * ensure we either use them here or don't touch them,
+                * and debugfs_leave_cancellation() will wait for this
+                * to be finished processing before exiting one. It may
+                * of course win and remove the cancellation, but then
+                * chances are we never even got into this bit, we only
+                * do if the refcount isn't zero already.
+                */
+               mutex_lock(&fsd->cancellations_mtx);
+               while ((c = list_first_entry_or_null(&fsd->cancellations,
+                                                    typeof(*c), list))) {
+                       list_del_init(&c->list);
+                       c->cancel(dentry, c->cancel_data);
+               }
+               mutex_unlock(&fsd->cancellations_mtx);
+
+               wait_for_completion(&fsd->active_users_drained);
+       }
 }
 
 static void remove_one(struct dentry *victim)
index c7d61cfc97d264ba674dc3cb32c2e29ae4af1ba0..0c4c68cf161f8742cf25c072291a26095e35f74e 100644 (file)
@@ -8,6 +8,7 @@
 #ifndef _DEBUGFS_INTERNAL_H_
 #define _DEBUGFS_INTERNAL_H_
 #include <linux/lockdep.h>
+#include <linux/list.h>
 
 struct file_operations;
 
@@ -29,6 +30,10 @@ struct debugfs_fsdata {
                        struct lock_class_key key;
                        char *lock_name;
 #endif
+
+                       /* protect cancellations */
+                       struct mutex cancellations_mtx;
+                       struct list_head cancellations;
                };
        };
 };
index ea2d919fd9c7990061ba4f469f92bbe5880a7b45..c9c65b132c0fd7fcf12c95c0ed50281bb0e66efa 100644 (file)
@@ -171,6 +171,25 @@ ssize_t debugfs_write_file_bool(struct file *file, const char __user *user_buf,
 ssize_t debugfs_read_file_str(struct file *file, char __user *user_buf,
                              size_t count, loff_t *ppos);
 
+/**
+ * struct debugfs_cancellation - cancellation data
+ * @list: internal, for keeping track
+ * @cancel: callback to call
+ * @cancel_data: extra data for the callback to call
+ */
+struct debugfs_cancellation {
+       struct list_head list;
+       void (*cancel)(struct dentry *, void *);
+       void *cancel_data;
+};
+
+void __acquires(cancellation)
+debugfs_enter_cancellation(struct file *file,
+                          struct debugfs_cancellation *cancellation);
+void __releases(cancellation)
+debugfs_leave_cancellation(struct file *file,
+                          struct debugfs_cancellation *cancellation);
+
 #else
 
 #include <linux/err.h>