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) --- include/linux/fs_struct.h | 2 ++ 1 file changed, 2 insertions(+) (limited to 'include') diff --git a/include/linux/fs_struct.h b/include/linux/fs_struct.h index 0070764b790a..ade459383f92 100644 --- a/include/linux/fs_struct.h +++ b/include/linux/fs_struct.h @@ -40,6 +40,8 @@ static inline void get_fs_pwd(struct fs_struct *fs, struct path *pwd) read_sequnlock_excl(&fs->seq); } +struct fs_struct *switch_fs_struct(struct fs_struct *new_fs); + extern bool current_chrooted(void); static inline int current_umask(void) -- cgit v1.2.3 From 67c54d1d730a8b7a43f2560dcea9b7dc95fba1cd Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 1 Jun 2026 15:56:36 +0200 Subject: fs: add scoped_with_init_fs() Similar to scoped_with_kernel_creds() allow a temporary override of current->fs to serve the few places where lookup is performed from kthread context or needs init's filesytem state. Link: https://patch.msgid.link/20260601-work-kthread-nullfs-v4-3-77ee053060e0@kernel.org Signed-off-by: Christian Brauner (Amutable) --- include/linux/fs_struct.h | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'include') diff --git a/include/linux/fs_struct.h b/include/linux/fs_struct.h index ade459383f92..e11d0e57168f 100644 --- a/include/linux/fs_struct.h +++ b/include/linux/fs_struct.h @@ -6,6 +6,7 @@ #include #include #include +#include struct fs_struct { int users; @@ -49,4 +50,34 @@ static inline int current_umask(void) return current->fs->umask; } +/* + * Temporarily use userspace_init_fs for path resolution in kthreads. + * Callers should use scoped_with_init_fs() which automatically + * restores the original fs_struct at scope exit. + */ +static inline struct fs_struct *__override_init_fs(void) +{ + struct fs_struct *fs; + + fs = current->fs; + WRITE_ONCE(current->fs, fs); + return fs; +} + +static inline void __revert_init_fs(struct fs_struct *revert_fs) +{ + VFS_WARN_ON_ONCE(current->fs != revert_fs); + WRITE_ONCE(current->fs, revert_fs); +} + +DEFINE_CLASS(__override_init_fs, + struct fs_struct *, + __revert_init_fs(_T), + __override_init_fs(), void) + +#define scoped_with_init_fs() \ + scoped_class(__override_init_fs, __UNIQUE_ID(label)) + +void __init init_userspace_fs(void); + #endif /* _LINUX_FS_STRUCT_H */ -- 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) --- include/linux/sched.h | 1 + 1 file changed, 1 insertion(+) (limited to 'include') diff --git a/include/linux/sched.h b/include/linux/sched.h index 373bcc0598d1..1e4136c2b2a3 100644 --- a/include/linux/sched.h +++ b/include/linux/sched.h @@ -1191,6 +1191,7 @@ struct task_struct { unsigned long last_switch_time; #endif /* Filesystem information: */ + struct fs_struct *real_fs; struct fs_struct *fs; /* Open file information: */ -- cgit v1.2.3 From 9a8e296958884b807a02759975170b6559901242 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 1 Jun 2026 15:56:38 +0200 Subject: fs: make userspace_init_fs a dynamically-initialized pointer Change userspace_init_fs from a declared-but-unused extern struct to a dynamically initialized pointer. Add init_userspace_fs() which is called early in kernel_init() (PID 1) to record PID 1's fs_struct as the canonical userspace filesystem state. Wire up __override_init_fs() and __revert_init_fs() to actually swap current->fs to/from userspace_init_fs. Previously these were no-ops that stored current->fs back to itself. Fix nullfs_userspace_init() to compare against userspace_init_fs instead of &init_fs. When PID 1 unshares its filesystem state, revert userspace_init_fs to init_fs's root (nullfs) so that stale filesystem state is not silently inherited by kworkers and usermodehelpers. At this stage PID 1's fs still points to rootfs (set by init_mount_tree), so userspace_init_fs points to rootfs and scoped_with_init_fs() is functionally equivalent to its previous no-op behavior. Link: https://patch.msgid.link/20260601-work-kthread-nullfs-v4-5-77ee053060e0@kernel.org Signed-off-by: Christian Brauner (Amutable) --- include/linux/fs_struct.h | 15 ++++++++------- include/linux/init_task.h | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'include') diff --git a/include/linux/fs_struct.h b/include/linux/fs_struct.h index e11d0e57168f..97eef8d3863d 100644 --- a/include/linux/fs_struct.h +++ b/include/linux/fs_struct.h @@ -17,6 +17,7 @@ struct fs_struct { } __randomize_layout; extern struct kmem_cache *fs_cachep; +extern struct fs_struct *userspace_init_fs; extern void exit_fs(struct task_struct *); extern void set_fs_root(struct fs_struct *, const struct path *); @@ -57,17 +58,17 @@ static inline int current_umask(void) */ static inline struct fs_struct *__override_init_fs(void) { - struct fs_struct *fs; + struct fs_struct *old_fs; - fs = current->fs; - WRITE_ONCE(current->fs, fs); - return fs; + old_fs = current->fs; + WRITE_ONCE(current->fs, userspace_init_fs); + return old_fs; } -static inline void __revert_init_fs(struct fs_struct *revert_fs) +static inline void __revert_init_fs(struct fs_struct *old_fs) { - VFS_WARN_ON_ONCE(current->fs != revert_fs); - WRITE_ONCE(current->fs, revert_fs); + VFS_WARN_ON_ONCE(current->fs != userspace_init_fs); + WRITE_ONCE(current->fs, old_fs); } DEFINE_CLASS(__override_init_fs, diff --git a/include/linux/init_task.h b/include/linux/init_task.h index a6cb241ea00c..61536be773f5 100644 --- a/include/linux/init_task.h +++ b/include/linux/init_task.h @@ -24,6 +24,7 @@ extern struct files_struct init_files; extern struct fs_struct init_fs; +extern struct fs_struct *userspace_init_fs; extern struct nsproxy init_nsproxy; #ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE -- 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) --- include/linux/sched/task.h | 1 + 1 file changed, 1 insertion(+) (limited to 'include') diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h index 41ed884cffc9..e0c1ca8c6a18 100644 --- a/include/linux/sched/task.h +++ b/include/linux/sched/task.h @@ -31,6 +31,7 @@ struct kernel_clone_args { u32 io_thread:1; u32 user_worker:1; u32 no_files:1; + u32 umh:1; unsigned long stack; unsigned long stack_size; unsigned long tls; -- cgit v1.2.3