[PATCH 1/2] kernfs: do not hold of->mutex across sysfs ->store()
From: Krystian Kaniewski
Date: Wed Oct 21 2026 - 09:13:43 EST
Lockdep reports a circular dependency &pipe->mutex --> &of->mutex -->
&sbi->pipe_mutex. A splice between a pipe and a kernfs file calls
->write_iter() or ->read_iter() of the file with the pipe locked, and
kernfs takes of->mutex there. A write to /sys/power/resume runs
resume_store() under of->mutex, and its lookup_bdev() can trigger an
autofs mount request, whose autofs_write() takes sbi->pipe_mutex and then
the mutex of the daemon pipe.
of->mutex only has to keep the callbacks of one open file apart.
sysfs_kf_write() does not pass the open file to ->store(), and two opens
of the same attribute already run ->store() concurrently. Add a
write_unlocked flag to struct kernfs_ops, cache it in struct
kernfs_open_file next to atomic_write_len, and take of->mutex around
->write() only when the flag is clear. Set it for the regular sysfs
attributes. The preallocated buffer stays protected by
of->prealloc_mutex, and the other kernfs users keep of->mutex.
This does not remove every report through the autofs daemon pipe. A pipe
spliced straight into such an attribute stays locked while ->store()
looks up the path, and a ->store() that takes its own lock around a
lookup keeps that dependency.
Fixes: 1e8c813b083c ("PM: hibernate: don't use early_lookup_bdev in resume_store")
Assisted-by: Gemini:gemini-3.8-flash syzbot
Reported-by: syzbot+002fb7b2d0bc53bf7e75@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=002fb7b2d0bc53bf7e75
Link: https://syzkaller.appspot.com/ai_job?id=74fbf9ab-54e4-46c2-b9e9-7867573ff823
Signed-off-by: Krystian Kaniewski <krystianmkaniewski@xxxxxxxxx>
---
fs/kernfs/file.c | 52 +++++++++++++++++++++++++++---------------
fs/sysfs/file.c | 18 ++++++++++++++-
include/linux/kernfs.h | 11 +++++++++
3 files changed, 62 insertions(+), 19 deletions(-)
diff --git a/fs/kernfs/file.c b/fs/kernfs/file.c
index 8e0e90c933720..c954543c1a583 100644
--- a/fs/kernfs/file.c
+++ b/fs/kernfs/file.c
@@ -291,6 +291,26 @@ static ssize_t kernfs_fop_read_iter(struct kiocb *iocb, struct iov_iter *iter)
return kernfs_file_read_iter(iocb, iter);
}
+/* Call the ->write() operation of @of under an active reference. */
+static ssize_t kernfs_write_active(struct kernfs_open_file *of, char *buf,
+ size_t len, loff_t pos)
+{
+ const struct kernfs_ops *ops;
+ ssize_t ret;
+
+ if (!kernfs_get_active_of(of))
+ return -ENODEV;
+
+ ops = kernfs_ops(of->kn);
+ if (ops->write)
+ ret = ops->write(of, buf, len, pos);
+ else
+ ret = -EINVAL;
+
+ kernfs_put_active_of(of);
+ return ret;
+}
+
/*
* Copy data in from userland and pass it to the matching kernfs write
* operation.
@@ -305,7 +325,6 @@ static ssize_t kernfs_fop_write_iter(struct kiocb *iocb, struct iov_iter *iter)
{
struct kernfs_open_file *of = kernfs_of(iocb->ki_filp);
ssize_t len = iov_iter_count(iter);
- const struct kernfs_ops *ops;
char *buf;
if (of->atomic_write_len) {
@@ -330,25 +349,18 @@ static ssize_t kernfs_fop_write_iter(struct kiocb *iocb, struct iov_iter *iter)
buf[len] = '\0'; /* guarantee string termination */
/*
- * @of->mutex nests outside active ref and is used both to ensure that
- * the ops aren't called concurrently for the same open file.
+ * @of->mutex nests outside active ref and keeps the ops of one open
+ * file from running concurrently. ->write() of files with
+ * ->write_unlocked set does not need that and runs without it.
*/
- mutex_lock(&of->mutex);
- if (!kernfs_get_active_of(of)) {
+ if (of->write_unlocked) {
+ len = kernfs_write_active(of, buf, len, iocb->ki_pos);
+ } else {
+ mutex_lock(&of->mutex);
+ len = kernfs_write_active(of, buf, len, iocb->ki_pos);
mutex_unlock(&of->mutex);
- len = -ENODEV;
- goto out_free;
}
- ops = kernfs_ops(of->kn);
- if (ops->write)
- len = ops->write(of, buf, len, iocb->ki_pos);
- else
- len = -EINVAL;
-
- kernfs_put_active_of(of);
- mutex_unlock(&of->mutex);
-
if (len > 0)
iocb->ki_pos += len;
@@ -651,6 +663,8 @@ static int kernfs_fop_open(struct inode *inode, struct file *file)
* lockdep key, because the writable file /sys/power/resume may call vfs
* lookup helpers for arbitrary paths and readonly files can be read by
* overlayfs from vfs helpers when sysfs is a lower layer of overalyfs.
+ * (Note that regular sysfs attributes no longer run ->store() under
+ * @of->mutex, but other kernfs users still do.)
*
* All three cases look the same. They're supposed to
* look that way and give @of->mutex different static lockdep keys.
@@ -666,10 +680,12 @@ static int kernfs_fop_open(struct inode *inode, struct file *file)
of->file = file;
/*
- * Write path needs to atomic_write_len outside active reference.
- * Cache it in open_file. See kernfs_fop_write_iter() for details.
+ * Write path needs atomic_write_len and write_unlocked outside
+ * active reference. Cache them in open_file. See
+ * kernfs_fop_write_iter() for details.
*/
of->atomic_write_len = ops->atomic_write_len;
+ of->write_unlocked = ops->write_unlocked;
error = -EINVAL;
/*
diff --git a/fs/sysfs/file.c b/fs/sysfs/file.c
index cd5bb0f9fee6d..71a7faf434073 100644
--- a/fs/sysfs/file.c
+++ b/fs/sysfs/file.c
@@ -132,7 +132,19 @@ static ssize_t sysfs_kf_read(struct kernfs_open_file *of, char *buf,
return min_t(ssize_t, count, len);
}
-/* kernfs write callback for regular sysfs files */
+/*
+ * kernfs write callback for regular sysfs files.
+ *
+ * Regular sysfs files set .write_unlocked = true and are called without
+ * holding of->mutex. Sysfs ->store() maintains no per-open-file state, and
+ * concurrent opens already execute ->store() concurrently without
+ * serialization. Calling ->store() without of->mutex avoids circular
+ * locking dependencies when ->store() performs operations like path
+ * lookups or automounts while a generic VFS lock is held (such as a pipe
+ * lock during splice). Preallocated attributes still serialize buffer
+ * access via of->prealloc_mutex. Existing autofs and callback-owned lock
+ * dependencies remain outside this narrow change.
+ */
static ssize_t sysfs_kf_write(struct kernfs_open_file *of, char *buf,
size_t count, loff_t pos)
{
@@ -229,11 +241,13 @@ static const struct kernfs_ops sysfs_file_kfops_ro = {
static const struct kernfs_ops sysfs_file_kfops_wo = {
.write = sysfs_kf_write,
+ .write_unlocked = true,
};
static const struct kernfs_ops sysfs_file_kfops_rw = {
.seq_show = sysfs_kf_seq_show,
.write = sysfs_kf_write,
+ .write_unlocked = true,
};
static const struct kernfs_ops sysfs_prealloc_kfops_ro = {
@@ -244,12 +258,14 @@ static const struct kernfs_ops sysfs_prealloc_kfops_ro = {
static const struct kernfs_ops sysfs_prealloc_kfops_wo = {
.write = sysfs_kf_write,
.prealloc = true,
+ .write_unlocked = true,
};
static const struct kernfs_ops sysfs_prealloc_kfops_rw = {
.read = sysfs_kf_read,
.write = sysfs_kf_write,
.prealloc = true,
+ .write_unlocked = true,
};
static const struct kernfs_ops sysfs_bin_kfops_ro = {
diff --git a/include/linux/kernfs.h b/include/linux/kernfs.h
index 351a5101c8628..cbb09f9533e72 100644
--- a/include/linux/kernfs.h
+++ b/include/linux/kernfs.h
@@ -272,6 +272,7 @@ struct kernfs_open_file {
char *prealloc_buf;
size_t atomic_write_len;
+ bool write_unlocked;
bool mmapped:1;
bool released:1;
const struct vm_operations_struct *vm_ops;
@@ -320,6 +321,16 @@ struct kernfs_ops {
* ->prealloc. Provide ->read and ->write with ->prealloc.
*/
bool prealloc;
+ /*
+ * "write_unlocked" indicates that ->write() does not require
+ * per-open-file serialization with of->mutex and can be called
+ * concurrently for the same open file. Callbacks must handle
+ * concurrent execution. Buffer access for preallocated attributes
+ * is still protected by of->prealloc_mutex. Lock dependencies from
+ * callers (such as pipe locks during splice) or within callbacks
+ * remain outside this scope.
+ */
+ bool write_unlocked;
ssize_t (*write)(struct kernfs_open_file *of, char *buf, size_t bytes,
loff_t off);
--
2.53.0