From 21bcea3ef2025796a29ba88f2747d864ed535758 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 1 Jun 2026 15:56:34 +0200 Subject: fs: add switch_fs_struct() Don't open-code the guts of replacing current's fs struct. Link: https://patch.msgid.link/20260601-work-kthread-nullfs-v4-1-77ee053060e0@kernel.org Signed-off-by: Christian Brauner (Amutable) --- kernel/fork.c | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) (limited to 'kernel') diff --git a/kernel/fork.c b/kernel/fork.c index 13e38e89a1f3..27f775113be6 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -3215,7 +3215,7 @@ static int unshare_fd(unsigned long unshare_flags, struct files_struct **new_fdp */ int ksys_unshare(unsigned long unshare_flags) { - struct fs_struct *fs, *new_fs = NULL; + struct fs_struct *new_fs = NULL; struct files_struct *new_fd = NULL; struct cred *new_cred = NULL; struct nsproxy *new_nsproxy = NULL; @@ -3293,23 +3293,13 @@ int ksys_unshare(unsigned long unshare_flags) new_nsproxy = NULL; } - task_lock(current); + if (new_fs) + new_fs = switch_fs_struct(new_fs); - if (new_fs) { - fs = current->fs; - read_seqlock_excl(&fs->seq); - current->fs = new_fs; - if (--fs->users) - new_fs = NULL; - else - new_fs = fs; - read_sequnlock_excl(&fs->seq); - } - - if (new_fd) + if (new_fd) { + guard(task_lock)(current); swap(current->files, new_fd); - - task_unlock(current); + } if (new_cred) { /* Install the new user namespace */ -- cgit v1.2.3 From 1d4ee94a51bdb98610bf7283e6297b133f8c1025 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 1 Jun 2026 15:56:37 +0200 Subject: fs: add real_fs to track task's actual fs_struct Add a real_fs field to task_struct that always mirrors the fs field. This lays the groundwork for distinguishing between a task's permanent fs_struct and one that is temporarily overridden via scoped_with_init_fs(). When a kthread temporarily overrides current->fs for path lookup, we need to know the original fs_struct for operations like exit_fs() and unshare_fs_struct() that must operate on the real, permanent fs. For now real_fs is always equal to fs. It is maintained alongside fs in all the relevant paths: exit_fs(), unshare_fs_struct(), switch_fs_struct(), and copy_fs(). Link: https://patch.msgid.link/20260601-work-kthread-nullfs-v4-4-77ee053060e0@kernel.org Signed-off-by: Christian Brauner (Amutable) --- kernel/fork.c | 8 +++++++- kernel/kcmp.c | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'kernel') diff --git a/kernel/fork.c b/kernel/fork.c index 27f775113be6..69b522fc0179 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -1616,6 +1616,8 @@ static int copy_exec_state(u64 clone_flags, struct task_struct *tsk) static int copy_fs(u64 clone_flags, struct task_struct *tsk) { struct fs_struct *fs = current->fs; + + VFS_WARN_ON_ONCE(current->fs != current->real_fs); if (clone_flags & CLONE_FS) { /* tsk->fs is already what we want */ read_seqlock_excl(&fs->seq); @@ -1628,7 +1630,7 @@ static int copy_fs(u64 clone_flags, struct task_struct *tsk) read_sequnlock_excl(&fs->seq); return 0; } - tsk->fs = copy_fs_struct(fs); + tsk->real_fs = tsk->fs = copy_fs_struct(fs); if (!tsk->fs) return -ENOMEM; return 0; @@ -3246,6 +3248,10 @@ int ksys_unshare(unsigned long unshare_flags) if (unshare_flags & CLONE_NEWNS) unshare_flags |= CLONE_FS; + /* No unsharing with overriden fs state */ + VFS_WARN_ON_ONCE(unshare_flags & (CLONE_NEWNS | CLONE_FS) && + current->fs != current->real_fs); + err = check_unshare_flags(unshare_flags); if (err) goto bad_unshare_out; diff --git a/kernel/kcmp.c b/kernel/kcmp.c index 7c1a65bd5f8d..76476aeee067 100644 --- a/kernel/kcmp.c +++ b/kernel/kcmp.c @@ -186,7 +186,7 @@ SYSCALL_DEFINE5(kcmp, pid_t, pid1, pid_t, pid2, int, type, ret = kcmp_ptr(task1->files, task2->files, KCMP_FILES); break; case KCMP_FS: - ret = kcmp_ptr(task1->fs, task2->fs, KCMP_FS); + ret = kcmp_ptr(task1->real_fs, task2->real_fs, KCMP_FS); break; case KCMP_SIGHAND: ret = kcmp_ptr(task1->sighand, task2->sighand, KCMP_SIGHAND); -- cgit v1.2.3 From ed4b1672529018b2013c62235c4bf1ab0ae0e3d4 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 1 Jun 2026 15:56:53 +0200 Subject: fs: add umh argument to struct kernel_clone_args Add a umh field to struct kernel_clone_args. When set, copy_fs() copies from pid 1's fs_struct instead of the kthread's fs_struct. This ensures usermodehelper threads always get init's filesystem state regardless of their parent's (kthreadd's) fs. Usermodehelper threads are not allowed to create mount namespaces (CLONE_NEWNS), share filesystem state (CLONE_FS), or be started from a non-initial mount namespace. No usermodehelper currently does this so we don't need to worry about this restriction. Set .umh = 1 in user_mode_thread(). At this stage pid 1's fs points to rootfs which is the same as kthreadd's fs, so this is functionally equivalent. Link: https://patch.msgid.link/20260601-work-kthread-nullfs-v4-20-77ee053060e0@kernel.org Signed-off-by: Christian Brauner (Amutable) --- kernel/fork.c | 25 +++++++++++++++++++++---- kernel/umh.c | 6 ++---- 2 files changed, 23 insertions(+), 8 deletions(-) (limited to 'kernel') diff --git a/kernel/fork.c b/kernel/fork.c index 69b522fc0179..b85b649c710d 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -1613,11 +1613,27 @@ static int copy_exec_state(u64 clone_flags, struct task_struct *tsk) return task_exec_state_copy(tsk); } -static int copy_fs(u64 clone_flags, struct task_struct *tsk) +static int copy_fs(u64 clone_flags, struct task_struct *tsk, bool umh) { - struct fs_struct *fs = current->fs; + struct fs_struct *fs; + + /* + * Usermodehelper may copy userspace_init_fs filesystem state but + * they don't get to create mount namespaces, share the + * filesystem state, or be started from a non-initial mount + * namespace. + */ + if (umh) { + if (clone_flags & (CLONE_NEWNS | CLONE_FS)) + return -EINVAL; + if (current->nsproxy->mnt_ns != &init_mnt_ns) + return -EINVAL; + fs = userspace_init_fs; + } else { + fs = current->fs; + VFS_WARN_ON_ONCE(current->fs != current->real_fs); + } - VFS_WARN_ON_ONCE(current->fs != current->real_fs); if (clone_flags & CLONE_FS) { /* tsk->fs is already what we want */ read_seqlock_excl(&fs->seq); @@ -2278,7 +2294,7 @@ __latent_entropy struct task_struct *copy_process( retval = copy_files(clone_flags, p, args->no_files); if (retval) goto bad_fork_cleanup_semundo; - retval = copy_fs(clone_flags, p); + retval = copy_fs(clone_flags, p, args->umh); if (retval) goto bad_fork_cleanup_files; retval = copy_sighand(clone_flags, p); @@ -2820,6 +2836,7 @@ pid_t user_mode_thread(int (*fn)(void *), void *arg, unsigned long flags) .exit_signal = (flags & CSIGNAL), .fn = fn, .fn_arg = arg, + .umh = 1, }; return kernel_clone(&args); diff --git a/kernel/umh.c b/kernel/umh.c index 48117c569e1a..6e2c7bb315c6 100644 --- a/kernel/umh.c +++ b/kernel/umh.c @@ -71,10 +71,8 @@ static int call_usermodehelper_exec_async(void *data) spin_unlock_irq(¤t->sighand->siglock); /* - * Initial kernel threads share ther FS with init, in order to - * get the init root directory. But we've now created a new - * thread that is going to execve a user process and has its own - * 'struct fs_struct'. Reset umask to the default. + * Usermodehelper threads get a copy of userspace init's + * fs_struct. Reset umask to the default. */ current->fs->umask = 0022; -- cgit v1.2.3