Skip to content

Commit

Permalink
debugfs: add API to allow debugfs operations cancellation
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Johannes Berg committed Nov 27, 2023
1 parent f4acfcd commit 8c88a47
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 1 deletion.
82 changes: 82 additions & 0 deletions fs/debugfs/file.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/*
Expand Down Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion fs/debugfs/inode.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions fs/debugfs/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#ifndef _DEBUGFS_INTERNAL_H_
#define _DEBUGFS_INTERNAL_H_
#include <linux/lockdep.h>
#include <linux/list.h>

struct file_operations;

Expand All @@ -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;
};
};
};
Expand Down
19 changes: 19 additions & 0 deletions include/linux/debugfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 8c88a47

Please sign in to comment.