Commit 8c88a474 authored by Johannes Berg's avatar Johannes Berg
Browse files

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: default avatarGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Signed-off-by: default avatarJohannes Berg <johannes.berg@intel.com>
parent f4acfcd4
Loading
Loading
Loading
Loading
+82 −0
Original line number Diff line number Diff line
@@ -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
+31 −1
Original line number Diff line number Diff line
@@ -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)
+5 −0
Original line number Diff line number Diff line
@@ -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;
		};
	};
};
+19 −0
Original line number Diff line number Diff line
@@ -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>