[PATCH] pidfd: revalidate task paths before handing them out

From: Christian Brauner

Date: Thu Nov 05 2026 - 16:25:55 EST


PIDFD_GET_EXE, PIDFD_GET_CWD and PIDFD_GET_ROOT sample the task's path
and open it right away. The path isn't found by a lookup in its parent,
it's jumped to, exactly like /proc/<pid>/{exe,cwd,root} jump to it. But
there complete_walk() runs ->d_weak_revalidate() on the jumped dentry
before the O_PATH open happens and the ioctls don't.

So on nfs, 9p and overlayfs the ioctls hand out a path that
/proc/<pid>/cwd would have refused with ESTALE. And they hand out the
failfs root. The failfs root is only unpinnable because its
->d_weak_revalidate() refuses every jump to it. After
fchdir(FD_FAILFS_ROOT) a task can now simply do

ioctl(pidfd_open(getpid(), 0), PIDFD_GET_CWD, 0)

and gets an O_PATH file descriptor to it which no path lookup allows,
not even with O_PATH.

So add task_path_open() for opening paths handed out by a task. It
gives the filesystem the same chance to refuse the path that
complete_walk() gives it and then does the O_PATH open. It's called
after the target's exec_update_lock has been dropped so a filesystem
that needs to go on the wire doesn't block the target's exec. There's
no lookup so there are no lookup flags to pass.

Fixes: d53dc95eb961 ("pidfd: Add task path ioctls")
Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
---
fs/internal.h | 1 +
fs/open.c | 30 ++++++++++++++++++++++++++++++
fs/pidfs.c | 3 +--
3 files changed, 32 insertions(+), 2 deletions(-)

diff --git a/fs/internal.h b/fs/internal.h
index 3f2fbac40f8e..e43ebcfd9846 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -205,6 +205,7 @@ int do_fchownat(int dfd, const char __user *filename, uid_t user, gid_t group,
int chown_common(const struct path *path, uid_t user, gid_t group);
extern int vfs_open(const struct path *, struct file *);
int vfs_open_consume(struct path *, struct file *);
+struct file *task_path_open(const struct path *path);

/*
* inode.c
diff --git a/fs/open.c b/fs/open.c
index e43f02ff64ac..55441ff0525c 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -1156,6 +1156,36 @@ struct file *dentry_open(const struct path *path, int flags,
}
EXPORT_SYMBOL(dentry_open);

+/**
+ * task_path_open - open a path handed out by a task
+ * @path: the task's path
+ *
+ * Open a task's exe, cwd or root for the caller. The path wasn't found by
+ * a lookup, it's handed out directly, the same way a /proc/<pid>/{exe,cwd,root}
+ * jump lands on it. So give the filesystem the chance to refuse it that
+ * complete_walk() gives it for a jumped dentry.
+ *
+ * Only O_PATH opens are supported. Anything else would need the permission
+ * checks a lookup performs before the open.
+ *
+ * Return: An O_PATH file on success, an error pointer on failure.
+ */
+struct file *task_path_open(const struct path *path)
+{
+ struct dentry *dentry = path->dentry;
+
+ if (unlikely(dentry->d_flags & DCACHE_OP_WEAK_REVALIDATE)) {
+ int status = dentry->d_op->d_weak_revalidate(dentry, 0);
+
+ if (status < 0)
+ return ERR_PTR(status);
+ if (!status)
+ return ERR_PTR(-ESTALE);
+ }
+
+ return dentry_open(path, O_PATH, current_cred());
+}
+
struct file *dentry_open_nonotify(const struct path *path, int flags,
const struct cred *cred)
{
diff --git a/fs/pidfs.c b/fs/pidfs.c
index f2a113006552..a2a0691cf131 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -775,8 +775,7 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
if (error)
return error;

- return FD_ADD(O_CLOEXEC,
- dentry_open(&path, O_PATH, current_cred()));
+ return FD_ADD(O_CLOEXEC, task_path_open(&path));
}

ns_common = pidfd_get_namespace(pidfd_pid(file), cmd, arg);
--
2.53.0