debugfs: add API to allow debugfs operations cancellation

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>
This commit is contained in:
Johannes Berg 2023-11-24 17:25:26 +01:00
parent f4acfcd4de
commit 8c88a47435
4 changed files with 137 additions and 1 deletions

View 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

View 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)

View 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;
};
};
};

View 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>