]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
fs: add immutable rootfs
authorChristian Brauner <brauner@kernel.org>
Mon, 12 Jan 2026 15:47:10 +0000 (16:47 +0100)
committerChristian Brauner <brauner@kernel.org>
Mon, 12 Jan 2026 15:52:09 +0000 (16:52 +0100)
Currently pivot_root() doesn't work on the real rootfs because it
cannot be unmounted. Userspace has to do a recursive removal of the
initramfs contents manually before continuing the boot.

Really all we want from the real rootfs is to serve as the parent mount
for anything that is actually useful such as the tmpfs or ramfs for
initramfs unpacking or the rootfs itself. There's no need for the real
rootfs to actually be anything meaningful or useful. Add a immutable
rootfs called "nullfs" that can be selected via the "nullfs_rootfs"
kernel command line option.

The kernel will mount a tmpfs/ramfs on top of it, unpack the initramfs
and fire up userspace which mounts the rootfs and can then just do:

  chdir(rootfs);
  pivot_root(".", ".");
  umount2(".", MNT_DETACH);

and be done with it. (Ofc, userspace can also choose to retain the
initramfs contents by using something like pivot_root(".", "/initramfs")
without unmounting it.)

Technically this also means that the rootfs mount in unprivileged
namespaces doesn't need to become MNT_LOCKED anymore as it's guaranteed
that the immutable rootfs remains permanently empty so there cannot be
anything revealed by unmounting the covering mount.

In the future this will also allow us to create completely empty mount
namespaces without risking to leak anything.

systemd already handles this all correctly as it tries to pivot_root()
first and falls back to MS_MOVE only when that fails.

This goes back to various discussion in previous years and a LPC 2024
presentation about this very topic.

Link: https://patch.msgid.link/20260112-work-immutable-rootfs-v2-3-88dd1c34a204@kernel.org
Signed-off-by: Christian Brauner <brauner@kernel.org>
fs/Makefile
fs/mount.h
fs/namespace.c
fs/nullfs.c [new file with mode: 0644]
include/uapi/linux/magic.h
init/do_mounts.c
init/do_mounts.h

index a04274a3c8542048e9ce5b84c74be91d7b2fb5d6..becf133e4791cbea904e0333285e50fe84de9b08 100644 (file)
@@ -16,7 +16,7 @@ obj-y :=      open.o read_write.o file_table.o super.o \
                stack.o fs_struct.o statfs.o fs_pin.o nsfs.o \
                fs_dirent.o fs_context.o fs_parser.o fsopen.o init.o \
                kernel_read_file.o mnt_idmapping.o remap_range.o pidfs.o \
-               file_attr.o
+               file_attr.o nullfs.o
 
 obj-$(CONFIG_BUFFER_HEAD)      += buffer.o mpage.o
 obj-$(CONFIG_PROC_FS)          += proc_namespace.o
index 2d28ef2a3aed67ec7e3efbf78e22f0b34591396b..e0816c11a1989fdd2aa1c31056032e9e46e3e528 100644 (file)
@@ -5,6 +5,7 @@
 #include <linux/ns_common.h>
 #include <linux/fs_pin.h>
 
+extern struct file_system_type nullfs_fs_type;
 extern struct list_head notify_list;
 
 struct mnt_namespace {
index 9261f56ccc81927e32942ab7e5e2a26a4320ecaf..a44ebb2f1161d21fe4ece76c4c0d8f0426518a16 100644 (file)
@@ -75,6 +75,17 @@ static int __init initramfs_options_setup(char *str)
 
 __setup("initramfs_options=", initramfs_options_setup);
 
+bool nullfs_rootfs = false;
+
+static int __init nullfs_rootfs_setup(char *str)
+{
+       if (*str)
+               return 0;
+       nullfs_rootfs = true;
+       return 1;
+}
+__setup("nullfs_rootfs", nullfs_rootfs_setup);
+
 static u64 event;
 static DEFINE_XARRAY_FLAGS(mnt_id_xa, XA_FLAGS_ALLOC);
 static DEFINE_IDA(mnt_group_ida);
@@ -4582,8 +4593,9 @@ int path_pivot_root(struct path *new, struct path *old)
  * pointed to by put_old must yield the same directory as new_root. No other
  * file system may be mounted on put_old. After all, new_root is a mountpoint.
  *
- * Also, the current root cannot be on the 'rootfs' (initial ramfs) filesystem.
- * See Documentation/filesystems/ramfs-rootfs-initramfs.rst for alternatives
+ * Also, the current root cannot be on the 'rootfs' (initial ramfs) filesystem
+ * unless the kernel was booted with "nullfs_rootfs". See
+ * Documentation/filesystems/ramfs-rootfs-initramfs.rst for alternatives
  * in this situation.
  *
  * Notes:
@@ -5976,24 +5988,72 @@ struct mnt_namespace init_mnt_ns = {
 
 static void __init init_mount_tree(void)
 {
-       struct vfsmount *mnt;
-       struct mount *m;
+       struct vfsmount *mnt, *nullfs_mnt;
+       struct mount *mnt_root;
        struct path root;
 
+       /*
+        * When nullfs is used, we create two mounts:
+        *
+        * (1) nullfs with mount id 1
+        * (2) mutable rootfs with mount id 2
+        *
+        * with (2) mounted on top of (1).
+        */
+       if (nullfs_rootfs) {
+               nullfs_mnt = vfs_kern_mount(&nullfs_fs_type, 0, "nullfs", NULL);
+               if (IS_ERR(nullfs_mnt))
+                       panic("VFS: Failed to create nullfs");
+       }
+
        mnt = vfs_kern_mount(&rootfs_fs_type, 0, "rootfs", initramfs_options);
        if (IS_ERR(mnt))
                panic("Can't create rootfs");
 
-       m = real_mount(mnt);
-       init_mnt_ns.root = m;
-       init_mnt_ns.nr_mounts = 1;
-       mnt_add_to_ns(&init_mnt_ns, m);
+       if (nullfs_rootfs) {
+               VFS_WARN_ON_ONCE(real_mount(nullfs_mnt)->mnt_id != 1);
+               VFS_WARN_ON_ONCE(real_mount(mnt)->mnt_id != 2);
+
+               /* The namespace root is the nullfs mnt. */
+               mnt_root                = real_mount(nullfs_mnt);
+               init_mnt_ns.root        = mnt_root;
+
+               /* Mount mutable rootfs on top of nullfs. */
+               root.mnt                = nullfs_mnt;
+               root.dentry             = nullfs_mnt->mnt_root;
+
+               LOCK_MOUNT_EXACT(mp, &root);
+               if (unlikely(IS_ERR(mp.parent)))
+                       panic("VFS: Failed to mount rootfs on nullfs");
+               scoped_guard(mount_writer)
+                       attach_mnt(real_mount(mnt), mp.parent, mp.mp);
+
+               pr_info("VFS: Finished mounting rootfs on nullfs\n");
+       } else {
+               VFS_WARN_ON_ONCE(real_mount(mnt)->mnt_id != 1);
+
+               /* The namespace root is the mutable rootfs. */
+               mnt_root                = real_mount(mnt);
+               init_mnt_ns.root        = mnt_root;
+       }
+
+       /*
+        * We've dropped all locks here but that's fine. Not just are we
+        * the only task that's running, there's no other mount
+        * namespace in existence and the initial mount namespace is
+        * completely empty until we add the mounts we just created.
+        */
+       for (struct mount *p = mnt_root; p; p = next_mnt(p, mnt_root)) {
+               mnt_add_to_ns(&init_mnt_ns, p);
+               init_mnt_ns.nr_mounts++;
+       }
+
        init_task.nsproxy->mnt_ns = &init_mnt_ns;
        get_mnt_ns(&init_mnt_ns);
 
-       root.mnt = mnt;
-       root.dentry = mnt->mnt_root;
-
+       /* The root and pwd always point to the mutable rootfs. */
+       root.mnt        = mnt;
+       root.dentry     = mnt->mnt_root;
        set_fs_pwd(current->fs, &root);
        set_fs_root(current->fs, &root);
 
diff --git a/fs/nullfs.c b/fs/nullfs.c
new file mode 100644 (file)
index 0000000..fdbd3e5
--- /dev/null
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Christian Brauner <brauner@kernel.org> */
+#include <linux/fs/super_types.h>
+#include <linux/fs_context.h>
+#include <linux/magic.h>
+
+static const struct super_operations nullfs_super_operations = {
+       .statfs = simple_statfs,
+};
+
+static int nullfs_fs_fill_super(struct super_block *s, struct fs_context *fc)
+{
+       struct inode *inode;
+
+       s->s_maxbytes           = MAX_LFS_FILESIZE;
+       s->s_blocksize          = PAGE_SIZE;
+       s->s_blocksize_bits     = PAGE_SHIFT;
+       s->s_magic              = NULL_FS_MAGIC;
+       s->s_op                 = &nullfs_super_operations;
+       s->s_export_op          = NULL;
+       s->s_xattr              = NULL;
+       s->s_time_gran          = 1;
+       s->s_d_flags            = 0;
+
+       inode = new_inode(s);
+       if (!inode)
+               return -ENOMEM;
+
+       /* nullfs is permanently empty... */
+       make_empty_dir_inode(inode);
+       simple_inode_init_ts(inode);
+       inode->i_ino    = 1;
+       /* ... and immutable. */
+       inode->i_flags |= S_IMMUTABLE;
+
+       s->s_root = d_make_root(inode);
+       if (!s->s_root)
+               return -ENOMEM;
+
+       return 0;
+}
+
+/*
+ * For now this is a single global instance. If needed we can make it
+ * mountable by userspace at which point we will need to make it
+ * multi-instance.
+ */
+static int nullfs_fs_get_tree(struct fs_context *fc)
+{
+       return get_tree_single(fc, nullfs_fs_fill_super);
+}
+
+static const struct fs_context_operations nullfs_fs_context_ops = {
+       .get_tree       = nullfs_fs_get_tree,
+};
+
+static int nullfs_init_fs_context(struct fs_context *fc)
+{
+       fc->ops         = &nullfs_fs_context_ops;
+       fc->global      = true;
+       fc->sb_flags    = SB_NOUSER;
+       fc->s_iflags    = SB_I_NOEXEC | SB_I_NODEV;
+       return 0;
+}
+
+struct file_system_type nullfs_fs_type = {
+       .name                   = "nullfs",
+       .init_fs_context        = nullfs_init_fs_context,
+       .kill_sb                = kill_anon_super,
+};
index 638ca21b7a9095d51787b6561471c15646e68605..4f2da935a76ccae6e1c3310b2fae5379186548b8 100644 (file)
 #define SECRETMEM_MAGIC                0x5345434d      /* "SECM" */
 #define PID_FS_MAGIC           0x50494446      /* "PIDF" */
 #define GUEST_MEMFD_MAGIC      0x474d454d      /* "GMEM" */
+#define NULL_FS_MAGIC          0x4E554C4C      /* "NULL" */
 
 #endif /* __LINUX_MAGIC_H__ */
index defbbf1d55f768ec0c646d543928dba4878d8bfa..675397c8a7a4a1a28f7dd7d33371a175a6b8dd76 100644 (file)
@@ -492,6 +492,20 @@ void __init prepare_namespace(void)
        mount_root(saved_root_name);
 out:
        devtmpfs_mount();
+
+       if (nullfs_rootfs) {
+               if (init_pivot_root(".", ".")) {
+                       pr_err("VFS: Failed to pivot into new rootfs\n");
+                       return;
+               }
+               if (init_umount(".", MNT_DETACH)) {
+                       pr_err("VFS: Failed to unmount old rootfs\n");
+                       return;
+               }
+               pr_info("VFS: Pivoted into new rootfs\n");
+               return;
+       }
+
        init_mount(".", "/", NULL, MS_MOVE, NULL);
        init_chroot(".");
 }
index 6069ea3eb80d70106d6a8d8b3515d50176353206..fbfee810aa89e00112ae5a8281527b094860fd32 100644 (file)
@@ -15,6 +15,7 @@
 void  mount_root_generic(char *name, char *pretty_name, int flags);
 void  mount_root(char *root_device_name);
 extern int root_mountflags;
+extern bool nullfs_rootfs;
 
 static inline __init int create_dev(char *name, dev_t dev)
 {