[RFC PATCH 2/3] fs: support tasks with a null root or cwd
John Ericson <[email protected]> Mon, 29 Jun 2026 02:58:21 -0400
| Newsgroups | dev.linux.lists.netfs,org.kernel.vger.linux-api,org.kernel.vger.linux-arch,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel,org.kernel.vger.linux-nfs |
|---|---|
| Message-ID | <[email protected]> |
From: John Ericson <[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/[email protected]/ Signed-off-by: John Ericson <[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