[RFC PATCH 2/3] fs: support tasks with a null root or cwd

John Ericson <[email protected]>
Newsgroups gmane.linux.nfs
Message-ID <20260629065934.1425479-3-John.Ericson__46680.4456476652$1782717524$gmane$org@Obsidian.Systems>
From: John Ericson <mail-/[email protected]>

A task's root directory (`fs->root`) and current working directory
(`fs->pwd`) are normally established by `chroot(2)`/`pivot_root(2)` and
`chdir(2)`/`fchdir(2)` (or inherited across `fork(2)`). Allow either to
instead be the null path, as documented in `struct fs_struct`. The two
are independent: a task may opt out of one, the other, or both.

A task with no root cannot use absolute pathnames, and its `..` is no
longer bounded by a process root: it climbs to the root of the mount the
walk is in (the security implications are discussed in `struct
fs_struct`). A task with no cwd cannot use `AT_FDCWD`-relative
pathnames. Either way it can still name files through the `*at(2)`
descriptors it holds.

Teach the readers of these fields to cope instead of dereferencing the
NULL dentry, each checking the field it uses:

- namei: `set_root()` now tolerates a NULL root (skipping the
  `nd->root.dentry->d_seq` read), so `nd_jump_root()` returns `-ENOENT`
  for absolute paths and symlinks, while `..` falls through to
  `follow_dotdot()` -- which already treats a NULL `nd->root` as "no
  boundary" and climbs. The `AT_FDCWD` legs of `path_init()` return
  `-ENOENT` with no cwd; real-dirfd lookups (`openat(2)`, `openat2(2)`)
  are unaffected.

- `getcwd(2)`, `/proc/PID/{root,cwd}`, `open_by_handle_at()` with
  `AT_FDCWD`, and the cachefiles `cull`/`inuse` commands return an error
  rather than dereferencing the NULL path.

The setters need no change: `chdir(2)`/`chroot(2)`/`pivot_root(2)`
resolve via `filename_lookup(AT_FDCWD, ...)`, which simply fails with no
root or cwd, and `fchdir(2)` installs a cwd from an fd without
consulting the old one. `d_path()` is unaffected: `__prepend_path()`
only compares against the root.

These opt-outs are not sticky; keeping a task rootless or cwd-less is an
orthogonal policy decision (e.g. seccomp filtering the setters above).

Link: https://lore.kernel.org/all/a49ce818-f38d-41b0-bbf7-80b8aad998b1-fsFgPW9xikU7lZ9V/[email protected]/
Signed-off-by: John Ericson <mail-/[email protected]>
Assisted-by: Claude:claude-opus-4-8
---
 fs/cachefiles/daemon.c    |  6 ++++--
 fs/d_path.c               |  6 +++++-
 fs/fhandle.c              |  3 +++
 fs/namei.c                | 22 ++++++++++++++++++++--
 fs/proc/base.c            |  8 ++++++--
 include/linux/fs_struct.h | 13 +++++++++++++
 6 files changed, 51 insertions(+), 7 deletions(-)

diff --git a/fs/cachefiles/daemon.c b/fs/cachefiles/daemon.c
index 4117b145ac94..344feeb89c61 100644
--- a/fs/cachefiles/daemon.c
+++ b/fs/cachefiles/daemon.c
@@ -652,7 +652,8 @@ static int cachefiles_daemon_cull(struct cachefiles_cache *cache, char *args)
 
 	get_fs_pwd(current->fs, &path);
 
-	if (!d_can_lookup(path.dentry))
+	/* A task may have no cwd. */
+	if (!path.mnt || !d_can_lookup(path.dentry))
 		goto notdir;
 
 	cachefiles_begin_secure(cache, &saved_cred);
@@ -723,7 +724,8 @@ static int cachefiles_daemon_inuse(struct cachefiles_cache *cache, char *args)
 
 	get_fs_pwd(current->fs, &path);
 
-	if (!d_can_lookup(path.dentry))
+	/* A task may have no cwd. */
+	if (!path.mnt || !d_can_lookup(path.dentry))
 		goto notdir;
 
 	cachefiles_begin_secure(cache, &saved_cred);
diff --git a/fs/d_path.c b/fs/d_path.c
index a48957c0971e..5f16d1efa37c 100644
--- a/fs/d_path.c
+++ b/fs/d_path.c
@@ -422,7 +422,11 @@ SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size)
 	rcu_read_lock();
 	get_fs_root_and_pwd_rcu(current->fs, &root, &pwd);
 
-	if (unlikely(d_unlinked(pwd.dentry))) {
+	/* A task may have no cwd. */
+	if (unlikely(!pwd.mnt)) {
+		rcu_read_unlock();
+		error = -ENOENT;
+	} else if (unlikely(d_unlinked(pwd.dentry))) {
 		rcu_read_unlock();
 		error = -ENOENT;
 	} else {
diff --git a/fs/fhandle.c b/fs/fhandle.c
index 1ca7eb3a6cb5..560f88f53633 100644
--- a/fs/fhandle.c
+++ b/fs/fhandle.c
@@ -180,6 +180,9 @@ static int get_path_anchor(int fd, struct path *root)
 
 	if (fd == AT_FDCWD) {
 		get_fs_pwd(current->fs, root);
+		/* A task may have no cwd. */
+		if (!root->mnt)
+			return -ENOENT;
 		return 0;
 	}
 
diff --git a/fs/namei.c b/fs/namei.c
index 5cc9f0f466b8..06b16815e866 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -1120,11 +1120,20 @@ static int set_root(struct nameidata *nd)
 		do {
 			seq = read_seqbegin(&fs->seq);
 			nd->root = fs->root;
-			nd->root_seq = __read_seqcount_begin(&nd->root.dentry->d_seq);
+			/*
+			 * A task may have no root. Leave nd->root as the NULL
+			 * path and skip the d_seq read: absolute lookups turn
+			 * the absence into -ENOENT in nd_jump_root(), while ".."
+			 * treats a NULL root as "no boundary" and climbs to its
+			 * mount root.
+			 */
+			if (likely(nd->root.mnt))
+				nd->root_seq = __read_seqcount_begin(&nd->root.dentry->d_seq);
 		} while (read_seqretry(&fs->seq, seq));
 	} else {
 		get_fs_root(fs, &nd->root);
-		nd->state |= ND_ROOT_GRABBED;
+		if (likely(nd->root.mnt))
+			nd->state |= ND_ROOT_GRABBED;
 	}
 	return 0;
 }
@@ -1143,6 +1152,9 @@ static int nd_jump_root(struct nameidata *nd)
 		if (unlikely(error))
 			return error;
 	}
+	/* Absolute paths need a root to jump to; a task may have none. */
+	if (unlikely(!nd->root.mnt))
+		return -ENOENT;
 	if (nd->flags & LOOKUP_RCU) {
 		struct dentry *d;
 		nd->path = nd->root;
@@ -2732,11 +2744,17 @@ static const char *path_init(struct nameidata *nd, unsigned flags)
 			do {
 				seq = read_seqbegin(&fs->seq);
 				nd->path = fs->pwd;
+				/* A task may have no cwd. */
+				if (unlikely(!nd->path.mnt))
+					return ERR_PTR(-ENOENT);
 				nd->inode = nd->path.dentry->d_inode;
 				nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
 			} while (read_seqretry(&fs->seq, seq));
 		} else {
 			get_fs_pwd(current->fs, &nd->path);
+			/* A task may have no cwd. */
+			if (unlikely(!nd->path.mnt))
+				return ERR_PTR(-ENOENT);
 			nd->inode = nd->path.dentry->d_inode;
 		}
 	} else {
diff --git a/fs/proc/base.c b/fs/proc/base.c
index 780f81259052..7f7cc86ce262 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -213,7 +213,9 @@ static int get_task_root(struct task_struct *task, struct path *root)
 	task_lock(task);
 	if (task->fs) {
 		get_fs_root(task->fs, root);
-		result = 0;
+		/* A task may have no root. */
+		if (root->mnt)
+			result = 0;
 	}
 	task_unlock(task);
 	return result;
@@ -227,7 +229,9 @@ static int proc_cwd_link(struct dentry *dentry, struct path *path,
 	task_lock(task);
 	if (task->fs) {
 		get_fs_pwd(task->fs, path);
-		result = 0;
+		/* A task may have no cwd. */
+		if (path->mnt)
+			result = 0;
 	}
 	task_unlock(task);
 	return result;
diff --git a/include/linux/fs_struct.h b/include/linux/fs_struct.h
index b5db5de9eb01..84423b4bd21a 100644
--- a/include/linux/fs_struct.h
+++ b/include/linux/fs_struct.h
@@ -13,18 +13,31 @@ struct fs_struct {
 	int umask;
 	int in_exec;
 
+	/*
+	 * Note that these paths are explicitly intended to be nullable.
+	 * Since they are inline structs and not pointers, we use `.mnt
+	 * == NULL` to indicate nullability of the path as a whole.
+	 */
+
 	/*
 	 * The root directory for the task(s) that points to this
 	 * `fs_struct`. The root directory also controls how `..`
 	 * resolve; path traversal is not allowed to resolve upwards
 	 * beyond the root directory. (It is for this latter reason that
 	 * `chroot` is a privileged operation.)
+	 *
+	 * If null (as described above), absolute paths will not
+	 * resolve. In addition `..` will be unbounded, until one
+	 * reaches the top of the mount tree.
 	 */
 	struct path root;
 
 	/*
 	 * The current working directory for the task(s) that points to
 	 * this `fs_struct`.
+	 *
+	 * If null (as described above), relative paths with `AT_FDCWD`
+	 * will not resolve.
 	 */
 	struct path pwd;
 } __randomize_layout;
-- 
2.51.2
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.