[PATCH 2/2] autofs: do not wait for the daemon while splice holds a pipe
From: Krystian Kaniewski
Date: Wed Oct 21 2026 - 08:55:49 EST
splice holds the mutex of a pipe while it calls ->write_iter() of the
target file in iter_file_splice_write() or ->splice_read() of the source
file in splice_file_to_pipe(). When such a callback looks up a path in an
autofs mount, autofs sends a request through its daemon pipe and waits
for the daemon. A write to /sys/power/resume that comes from splice() does
this, so lockdep still reports &pipe->mutex --> &sbi->pipe_mutex -->
&pipe->mutex after of->mutex is gone from the sysfs store path. When the
spliced pipe is the daemon pipe itself, the task can wait for the mutex
that it already holds, which SIGKILL does not end, or wait for a daemon
that cannot read its pipe.
Mark the task while splice runs such a callback with a pipe really locked
(pipe->files set, as in pipe_lock()), and make autofs return -EAGAIN in
that context instead of sending a mount request or waiting for a pending
mount or expire. A new mount is refused before AUTOFS_INF_PENDING is set,
because another lookup that sees the flag would send the request instead.
The short wait for expire selection does not depend on the daemon and is
kept. RCU walks, lookups that need no request and all other callers keep
their behavior, and a child does not inherit the mark.
A lookup from such a callback that needs an automount now fails with
-EAGAIN instead of waiting for the daemon. This affects splice() from a
pipe into a file whose write handler looks up a path under an autofs
mount that is not mounted yet, for example /sys/power/resume. sendfile()
into a file goes through the private pipe of the task, which is never
locked, and keeps its behavior.
Fixes: 1e8c813b083c ("PM: hibernate: don't use early_lookup_bdev in resume_store")
Reported-by: syzbot+002fb7b2d0bc53bf7e75@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=002fb7b2d0bc53bf7e75
Signed-off-by: Krystian Kaniewski <krystianmkaniewski@xxxxxxxxx>
---
fs/autofs/expire.c | 6 ++++++
fs/autofs/root.c | 28 ++++++++++++++++++++++++++--
fs/splice.c | 13 ++++++++++++-
include/linux/sched.h | 2 ++
kernel/fork.c | 1 +
5 files changed, 47 insertions(+), 3 deletions(-)
diff --git a/fs/autofs/expire.c b/fs/autofs/expire.c
index 5c2d459e1e484..9f3bf1473acd4 100644
--- a/fs/autofs/expire.c
+++ b/fs/autofs/expire.c
@@ -514,6 +514,12 @@ int autofs_expire_wait(const struct path *path, int rcu_walk)
}
if (state & AUTOFS_INF_EXPIRING) {
spin_unlock(&sbi->fs_lock);
+ /*
+ * The expire waits for the daemon, which may need a pipe that
+ * the splice of this task holds. Refuse instead of waiting.
+ */
+ if (unlikely(current->in_splice_pipe_callback))
+ return -EAGAIN;
pr_debug("waiting for expire %p name=%pd\n", dentry, dentry);
diff --git a/fs/autofs/root.c b/fs/autofs/root.c
index b36439f4521eb..c4e04998a2345 100644
--- a/fs/autofs/root.c
+++ b/fs/autofs/root.c
@@ -252,6 +252,8 @@ static int autofs_mount_wait(const struct path *path, bool rcu_walk)
if (ino->flags & AUTOFS_INF_PENDING) {
if (rcu_walk)
return -ECHILD;
+ if (unlikely(current->in_splice_pipe_callback))
+ return -EAGAIN;
pr_debug("waiting for mount name=%pd\n", path->dentry);
status = autofs_wait(sbi, path, NFY_MOUNT);
pr_debug("mount wait done status=%d\n", status);
@@ -263,10 +265,18 @@ static int autofs_mount_wait(const struct path *path, bool rcu_walk)
return status;
}
+static bool autofs_expire_wait_blocked(int status)
+{
+ /* A marked caller is refused before a daemon can return a status. */
+ return status == -EAGAIN &&
+ unlikely(current->in_splice_pipe_callback);
+}
+
static int do_expire_wait(const struct path *path, bool rcu_walk)
{
struct dentry *dentry = path->dentry;
struct dentry *expiring;
+ int status;
expiring = autofs_lookup_expiring(dentry, rcu_walk);
if (IS_ERR(expiring))
@@ -280,7 +290,11 @@ static int do_expire_wait(const struct path *path, bool rcu_walk)
* be quite complete, but the directory has been removed
* so it must have been successful, just wait for it.
*/
- autofs_expire_wait(&this, 0);
+ status = autofs_expire_wait(&this, 0);
+ if (autofs_expire_wait_blocked(status)) {
+ dput(expiring);
+ return status;
+ }
autofs_del_expiring(expiring);
dput(expiring);
}
@@ -359,6 +373,8 @@ static struct vfsmount *autofs_d_automount(struct path *path)
* the mount.
*/
status = do_expire_wait(path, 0);
+ if (autofs_expire_wait_blocked(status))
+ return ERR_PTR(-EAGAIN);
if (status && status != -EAGAIN)
return NULL;
@@ -405,6 +421,11 @@ static struct vfsmount *autofs_d_automount(struct path *path)
goto done;
}
}
+ /* Do not publish PENDING without issuing its mount request. */
+ if (unlikely(current->in_splice_pipe_callback)) {
+ spin_unlock(&sbi->fs_lock);
+ return ERR_PTR(-EAGAIN);
+ }
ino->flags |= AUTOFS_INF_PENDING;
spin_unlock(&sbi->fs_lock);
status = autofs_mount_wait(path, 0);
@@ -442,8 +463,11 @@ static int autofs_d_manage(const struct path *path, bool rcu_walk)
}
/* Wait for pending expires */
- if (do_expire_wait(path, rcu_walk) == -ECHILD)
+ status = do_expire_wait(path, rcu_walk);
+ if (status == -ECHILD)
return -ECHILD;
+ if (autofs_expire_wait_blocked(status))
+ return -EAGAIN;
/*
* This dentry may be under construction so wait on mount
diff --git a/fs/splice.c b/fs/splice.c
index 9d8f63e2fd1ab..1756a8579776f 100644
--- a/fs/splice.c
+++ b/fs/splice.c
@@ -686,6 +686,7 @@ iter_file_splice_write(struct pipe_inode_info *pipe, struct file *out,
struct kiocb kiocb;
struct iov_iter from;
unsigned int head, tail;
+ bool was_in_splice_pipe_callback;
size_t left;
int n;
@@ -733,7 +734,11 @@ iter_file_splice_write(struct pipe_inode_info *pipe, struct file *out,
iov_iter_bvec(&from, ITER_SOURCE, array, n, sd.total_len - left);
init_sync_kiocb(&kiocb, out);
kiocb.ki_pos = sd.pos;
+ was_in_splice_pipe_callback = current->in_splice_pipe_callback;
+ if (pipe->files)
+ current->in_splice_pipe_callback = true;
ret = out->f_op->write_iter(&kiocb, &from);
+ current->in_splice_pipe_callback = was_in_splice_pipe_callback;
sd.pos = kiocb.ki_pos;
if (ret <= 0)
break;
@@ -1282,12 +1287,18 @@ ssize_t splice_file_to_pipe(struct file *in,
loff_t *offset,
size_t len, unsigned int flags)
{
+ bool was_in_splice_pipe_callback;
ssize_t ret;
pipe_lock(opipe);
ret = wait_for_space(opipe, flags);
- if (!ret)
+ if (!ret) {
+ was_in_splice_pipe_callback = current->in_splice_pipe_callback;
+ if (opipe->files)
+ current->in_splice_pipe_callback = true;
ret = do_splice_read(in, offset, opipe, len, flags);
+ current->in_splice_pipe_callback = was_in_splice_pipe_callback;
+ }
pipe_unlock(opipe);
if (ret > 0)
wakeup_pipe_readers(opipe);
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 705970d07614f..3ffa284976cc6 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1025,6 +1025,8 @@ struct task_struct {
/* Bit to tell TOMOYO we're in execve(): */
unsigned in_execve:1;
unsigned in_iowait:1;
+ /* A file callback is running while splice holds a pipe mutex. */
+ unsigned in_splice_pipe_callback:1;
#ifndef TIF_RESTORE_SIGMASK
unsigned restore_sigmask:1;
#endif
diff --git a/kernel/fork.c b/kernel/fork.c
index 5ef413368912e..d774e5092766c 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2133,6 +2133,7 @@ __latent_entropy struct task_struct *copy_process(
p = dup_task_struct(current, node);
if (!p)
goto fork_out;
+ p->in_splice_pipe_callback = 0;
/*
* Must run before the first fallible op, so error paths never
* free the parent's ret_stack.
--
2.53.0