fs: add immutable rootfs
Subsystems (3)
Christian Brauner
maintainerv2 cover fs: add immutable rootfs
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.
Now in vfs-7.0.nullfs.
---
Changes in v2:
- Rename to "nullfs".
- Update documentation.
- Link to v1: https://patch.msgid.link/[email protected]
---
Christian Brauner (4):
fs: ensure that internal tmpfs mount gets mount id zero
fs: add init_pivot_root()
fs: add immutable rootfs
docs: mention nullfs
.../filesystems/ramfs-rootfs-initramfs.rst | 32 +++- fs/Makefile | 2 +- fs/init.c | 17 ++ fs/internal.h | 1 + fs/mount.h | 1 + fs/namespace.c | 181 ++++++++++++++------- fs/nullfs.c | 70 ++++++++ include/linux/init_syscalls.h | 1 + include/uapi/linux/magic.h | 1 + init/do_mounts.c | 14 ++ init/do_mounts.h | 1 + 11 files changed, 254 insertions(+), 67 deletions(-)
---
base-commit: 8f0b4cce4481fb22653697cced8d0d04027cb1e8
change-id: 20260102-work-immutable-rootfs-b5f23e0f5a27
Christian Brauner
maintainerPATCH v2 1/4 fs: ensure that internal tmpfs mount gets mount id zero
and the rootfs get mount id one as it always has. Before we actually mount the rootfs we create an internal tmpfs mount which has mount id zero but is never exposed anywhere. Continue that "tradition".
--- fs/namespace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
fs/namespace.c +1 -1
@@ -221,7 +221,7 @@ static int mnt_alloc_id(struct mount *mnt)int res;xa_lock (&mnt_id_xa); - res =__xa_alloc (&mnt_id_xa, &mnt->mnt_id, mnt,XA_LIMIT (1 ,INT_MAX ),GFP_KERNEL ); + res =__xa_alloc (&mnt_id_xa, &mnt->mnt_id, mnt, xa_limit_31b,GFP_KERNEL );if (!res) mnt->mnt_id_unique = ++mnt_id_ctr;xa_unlock (&mnt_id_xa);
Signature · 1 line
2.47.3
Re: [PATCH v2 1/4] fs: ensure that internal tmpfs mount gets mount id zero
Quoted 16 lines · Christian Brauner
and the rootfs get mount id one as it always has. Before we actually mount the rootfs we create an internal tmpfs mount which has mount id zero but is never exposed anywhere. Continue that "tradition".
Fixes 7f9bfafc5f49 ("fs: use xarray for old mount id")Cc <[email protected]>Signed-off-by Christian Brauner <[email protected]>--- fs/namespace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)fs/namespace.c +0
-0
fs/namespace.c static int mnt_alloc_id(struct mount *mnt) · line 227
Quoted 8 lines
fs/namespace.c +1
-1@@ -221,7 +221,7 @@ static int mnt_alloc_id(struct mount *mnt)int res;xa_lock (&mnt_id_xa); - res =__xa_alloc (&mnt_id_xa, &mnt->mnt_id, mnt,XA_LIMIT (1 ,INT_MAX ),GFP_KERNEL ); + res =__xa_alloc (&mnt_id_xa, &mnt->mnt_id, mnt, xa_limit_31b,GFP_KERNEL );if (!res) mnt->mnt_id_unique = ++mnt_id_ctr;xa_unlock (&mnt_id_xa);
Christian Brauner
maintainerPATCH v2 2/4 fs: add init_pivot_root()
In reply to Christian BraunerWe will soon be able to pivot_root() with the introduction of the immutable rootfs. Add a wrapper for kernel internal usage.
--- fs/init.c | 17 +++++++ fs/internal.h | 1 + fs/namespace.c | 101 ++++++++++++++++++++++-------------------- include/linux/init_syscalls.h | 1 + 4 files changed, 73 insertions(+), 47 deletions(-)
fs/init.c +17 -0
@@ -13,6 +13,23 @@#include <linux/security.h> #include "internal.h" +int __init init_pivot_root (const char *new_root,const char *put_old) +{ + struct path new_path __free(path_put) = {}; + struct path old_path __free(path_put) = {}; +int ret; + + ret =kern_path (new_root,LOOKUP_FOLLOW |LOOKUP_DIRECTORY , &new_path); +if (ret) +return ret; + + ret =kern_path (put_old,LOOKUP_FOLLOW |LOOKUP_DIRECTORY , &old_path); +if (ret) +return ret; + +return path_pivot_root (&new_path, &old_path); +} +int __init init_mount (const char *dev_name,const char *dir_name,const char *type_page,unsigned long flags,void *data_page) {
fs/internal.h +1 -0
@@ -90,6 +90,7 @@ extern bool may_mount(void);int path_mount (const char *dev_name,const struct path *path,const char *type_page,unsigned long flags,void *data_page);int path_umount (const struct path *path,int flags); +int path_pivot_root (struct path *new,struct path *old);int show_path (struct seq_file *m,struct dentry *root);
fs/namespace.c +54 -47
150 lines not on this page · raw message
include/linux/init_syscalls.h +1 -0
@@ -17,3 +17,4 @@ int __init init_mkdir(const char *pathname, umode_t mode);int __init init_rmdir (const char *pathname);int __init init_utimes (char *filename,struct timespec64 *ts);int __init init_dup (struct file *file); +int __init init_pivot_root (const char *new_root,const char *put_old);
Signature · 1 line
2.47.3
Christian Brauner
maintainerPATCH v2 3/4 fs: add immutable rootfs
In reply to Christian BraunerCurrently 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.
--- fs/Makefile | 2 +- fs/mount.h | 1 + fs/namespace.c | 82 +++++++++++++++++++++++++++++++++++++++------- fs/nullfs.c | 70 +++++++++++++++++++++++++++++++++++++++ include/uapi/linux/magic.h | 1 + init/do_mounts.c | 14 ++++++++ init/do_mounts.h | 1 + 7 files changed, 159 insertions(+), 12 deletions(-)
fs/Makefile +1 -1
@@ -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
fs/mount.h +1 -0
@@ -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 {
fs/namespace.c +71 -11
109 lines not on this page · raw message
fs/nullfs.c new file +70 -0
70 lines not on this page · raw message
include/uapi/linux/magic.h +1 -0
@@ -104,5 +104,6 @@#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__ */
init/do_mounts.c +14 -0
@@ -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 ("." ); }
init/do_mounts.h +1 -0
@@ -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) {
Signature · 1 line
2.47.3
Christian Brauner
maintainerPATCH v2 4/4 docs: mention nullfs
In reply to Christian BraunerAdd a section about nullfs and how it enables pivot_root() to work.
--- .../filesystems/ramfs-rootfs-initramfs.rst | 32 ++++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-)
Documentation/filesystems/ramfs-rootfs-initramfs.rst +23 -9
@@ -76,10 +76,15 @@ What is rootfs? --------------- Rootfs is a special instance of ramfs (or tmpfs, if that's enabled), which is -always present in 2.6 systems. You can't unmount rootfs for approximately the -same reason you can't kill the init process; rather than having special code -to check for and handle an empty list, it's smaller and simpler for the kernel -to just make sure certain lists can't become empty. +always present in 2.6 systems. Traditionally, you can't unmount rootfs for +approximately the same reason you can't kill the init process; rather than +having special code to check for and handle an empty list, it's smaller and +simpler for the kernel to just make sure certain lists can't become empty. + +However, if the kernel is booted with "nullfs_rootfs", an immutable empty +filesystem called nullfs is used as the true root, with the mutable rootfs +(tmpfs/ramfs) mounted on top of it. This allows pivot_root() and unmounting +of the initramfs to work normally. Most systems just mount another filesystem over rootfs and ignore it. The amount of space an empty instance of ramfs takes up is tiny.
@@ -121,17 +126,26 @@ All this differs from the old initrd in several ways: program. See the switch_root utility, below.) - When switching another root device, initrd would pivot_root and then - umount the ramdisk. But initramfs is rootfs: you can neither pivot_root - rootfs, nor unmount it. Instead delete everything out of rootfs to - free up the space (find -xdev / -exec rm '{}' ';'), overmount rootfs - with the new root (cd /newmount; mount --move . /; chroot .), attach - stdin/stdout/stderr to the new /dev/console, and exec the new init. + umount the ramdisk. Traditionally, initramfs is rootfs: you can neither + pivot_root rootfs, nor unmount it. Instead delete everything out of + rootfs to free up the space (find -xdev / -exec rm '{}' ';'), overmount + rootfs with the new root (cd /newmount; mount --move . /; chroot .), + attach stdin/stdout/stderr to the new /dev/console, and exec the new init. Since this is a remarkably persnickety process (and involves deleting commands before you can run them), the klibc package introduced a helper program (utils/run_init.c) to do all this for you. Most other packages (such as busybox) have named this command "switch_root". + However, if the kernel is booted with "nullfs_rootfs", pivot_root() works + normally from the initramfs. Userspace can simply do:: + + chdir(new_root); + pivot_root(".", "."); + umount2(".", MNT_DETACH); + + This is the preferred method when nullfs_rootfs is enabled. + Populating initramfs: ---------------------
Signature · 1 line
2.47.3
Quoted 64 lines · Christian Brauner
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_LOCKEDanymore 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 toMS_MOVEonly when that fails.This goes back to various discussion in previous years and a LPC 2024 presentation about this very topic.
Now in vfs-7.0.nullfs.
Signed-off-by Christian Brauner <[email protected]>---
Changes in v2:
- Rename to "nullfs".
- Update documentation.
- Link to v1: https://patch.msgid.link/[email protected]---
Christian Brauner (4):
fs: ensure that internal tmpfs mount gets mount id zero
fs: addinit_pivot_root()
fs: add immutable rootfs
docs: mention nullfs.../filesystems/ramfs-rootfs-initramfs.rst | 32 +++- fs/Makefile | 2 +- fs/init.c | 17 ++ fs/internal.h | 1 + fs/mount.h | 1 + fs/namespace.c | 181 ++++++++++++++------- fs/nullfs.c | 70 ++++++++ include/linux/init_syscalls.h | 1 + include/uapi/linux/magic.h | 1 + init/do_mounts.c | 14 ++ init/do_mounts.h | 1 + 11 files changed, 254 insertions(+), 67 deletions(-)
Quoted 3 lines
---
base-commit:8f0b4cce4481fb22653697cced8d0d04027cb1e8
change-id: 20260102-work-immutable-rootfs-b5f23e0f5a27
I like the set overall. My only real gripe is the new command-line option. Won't that make distro adoption hard?
IIUC, with this new method, you're just adding a new nullfs at the bottom of the stack and then mounting the traditional ramfs as the mutable rootfs on top of it.
Would there be any harm in just always doing this (and dropping the command-line option)?
You would end up "leaking" both the nullfs and ramfs in the case of traditional userspace that was unaware of the extra mount, but that seems like it should be something we could live with.
Signature · 1 line
Jeff Layton <[email protected]>
Christian Brauner
maintainerQuoted 76 lines · Jeff Layton
Quoted 64 lines · Christian Brauner
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_LOCKEDanymore 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 toMS_MOVEonly when that fails.This goes back to various discussion in previous years and a LPC 2024 presentation about this very topic.
Now in vfs-7.0.nullfs.
Signed-off-by Christian Brauner <[email protected]>---
Changes in v2:
- Rename to "nullfs".
- Update documentation.
- Link to v1: https://patch.msgid.link/[email protected]---
Christian Brauner (4):
fs: ensure that internal tmpfs mount gets mount id zero
fs: addinit_pivot_root()
fs: add immutable rootfs
docs: mention nullfs.../filesystems/ramfs-rootfs-initramfs.rst | 32 +++- fs/Makefile | 2 +- fs/init.c | 17 ++ fs/internal.h | 1 + fs/mount.h | 1 + fs/namespace.c | 181 ++++++++++++++------- fs/nullfs.c | 70 ++++++++ include/linux/init_syscalls.h | 1 + include/uapi/linux/magic.h | 1 + init/do_mounts.c | 14 ++ init/do_mounts.h | 1 + 11 files changed, 254 insertions(+), 67 deletions(-)Quoted 3 lines
---
base-commit:8f0b4cce4481fb22653697cced8d0d04027cb1e8
change-id: 20260102-work-immutable-rootfs-b5f23e0f5a27I like the set overall. My only real gripe is the new command-line option. Won't that make distro adoption hard?
IIUC, with this new method, you're just adding a new nullfs at the bottom of the stack and then mounting the traditional ramfs as the mutable rootfs on top of it.
Quoted 6 lines
Would there be any harm in just always doing this (and dropping the command-line option)?
You would end up "leaking" both the nullfs and ramfs in the case of traditional userspace that was unaware of the extra mount, but that seems like it should be something we could live with.
I would be open to trying to make it unconditional. IOW, a patch on top of the current set that removes the "nullfs_rootfs" command line so that we can simply revert if it turns out that is an issue.
Christian Brauner
maintainer[RFC PATCH 5/4] fs: use nullfs unconditionally as the real rootfs
Remove the "nullfs_rootfs" boot parameter and try to simply always use nullfs. The mutable rootfs will be mounted on top of it. Systems that don't use pivot_root() to pivot away from the real rootfs will have an additional mount stick around but that shouldn't be a problem at all. If it is we'll rever this commit.
This also simplifies the boot process and removes the need for the traditional switch_root workarounds.
--- .../filesystems/ramfs-rootfs-initramfs.rst | 24 ++----- fs/namespace.c | 64 ++++++------------- init/do_mounts.c | 20 ++---- init/do_mounts.h | 1 - 4 files changed, 32 insertions(+), 77 deletions(-)
Documentation/filesystems/ramfs-rootfs-initramfs.rst +4 -20
@@ -76,13 +76,8 @@ What is rootfs? --------------- Rootfs is a special instance of ramfs (or tmpfs, if that's enabled), which is -always present in 2.6 systems. Traditionally, you can't unmount rootfs for -approximately the same reason you can't kill the init process; rather than -having special code to check for and handle an empty list, it's smaller and -simpler for the kernel to just make sure certain lists can't become empty. - -However, if the kernel is booted with "nullfs_rootfs", an immutable empty -filesystem called nullfs is used as the true root, with the mutable rootfs +always present in Linux systems. The kernel uses an immutable empty filesystem +called nullfs as the true root of the VFS hierarchy, with the mutable rootfs (tmpfs/ramfs) mounted on top of it. This allows pivot_root() and unmounting of the initramfs to work normally.
@@ -126,25 +121,14 @@ All this differs from the old initrd in several ways: program. See the switch_root utility, below.) - When switching another root device, initrd would pivot_root and then - umount the ramdisk. Traditionally, initramfs is rootfs: you can neither - pivot_root rootfs, nor unmount it. Instead delete everything out of - rootfs to free up the space (find -xdev / -exec rm '{}' ';'), overmount - rootfs with the new root (cd /newmount; mount --move . /; chroot .), - attach stdin/stdout/stderr to the new /dev/console, and exec the new init. - - Since this is a remarkably persnickety process (and involves deleting - commands before you can run them), the klibc package introduced a helper - program (utils/run_init.c) to do all this for you. Most other packages - (such as busybox) have named this command "switch_root". - - However, if the kernel is booted with "nullfs_rootfs", pivot_root() works + umount the ramdisk. With nullfs as the true root, pivot_root() works normally from the initramfs. Userspace can simply do:: chdir(new_root); pivot_root(".", "."); umount2(".", MNT_DETACH); - This is the preferred method when nullfs_rootfs is enabled. + This is the preferred method for switching root filesystems. Populating initramfs: ---------------------
fs/namespace.c +21 -43
97 lines not on this page · raw message
init/do_mounts.c +7 -13
@@ -493,21 +493,15 @@ void __init prepare_namespace(void) 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" ); +if (init_pivot_root ("." ,"." )) { +pr_err ("VFS: Failed to pivot into new rootfs\n" );return ; } - -init_mount ("." ,"/" ,NULL ,MS_MOVE ,NULL ); -init_chroot ("." ); +if (init_umount ("." ,MNT_DETACH )) { +pr_err ("VFS: Failed to unmount old rootfs\n" ); +return ; + } +pr_info ("VFS: Pivoted into new rootfs\n" ); }static bool is_tmpfs;
init/do_mounts.h +0 -1
@@ -15,7 +15,6 @@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) {
Signature · 1 line
2.47.3
Re: [RFC PATCH 5/4] fs: use nullfs unconditionally as the real rootfs
Quoted 208 lines · Christian Brauner
Remove the "
nullfs_rootfs" boot parameter and try to simply always use nullfs. The mutable rootfs will be mounted on top of it. Systems that don't usepivot_root()to pivot away from the real rootfs will have an additional mount stick around but that shouldn't be a problem at all. If it is we'll rever this commit.This also simplifies the boot process and removes the need for the traditional
switch_rootworkarounds.Suggested-by Jeff Layton <[email protected]>Signed-off-by Christian Brauner <[email protected]>--- .../filesystems/ramfs-rootfs-initramfs.rst | 24 ++----- fs/namespace.c | 64 ++++++------------- init/do_mounts.c | 20 ++---- init/do_mounts.h | 1 - 4 files changed, 32 insertions(+), 77 deletions(-)Documentation/filesystems/ramfs-rootfs-initramfs.rst +4
-20@@ -76,13 +76,8 @@ What is rootfs? --------------- Rootfs is a special instance of ramfs (or tmpfs, if that's enabled), which is -always present in 2.6 systems. Traditionally, you can't unmount rootfs for -approximately the same reason you can't kill the init process; rather than -having special code to check for and handle an empty list, it's smaller and -simpler for the kernel to just make sure certain lists can't become empty. - -However, if the kernel is booted with "nullfs_rootfs", an immutable empty -filesystem called nullfs is used as the true root, with the mutable rootfs +always present in Linux systems. The kernel uses an immutable empty filesystem +called nullfs as the true root of the VFS hierarchy, with the mutable rootfs (tmpfs/ramfs) mounted on top of it. This allows pivot_root() and unmounting of the initramfs to work normally.@@ -126,25 +121,14 @@ All this differs from the old initrd in several ways: program. See the switch_root utility, below.) - When switching another root device, initrd would pivot_root and then - umount the ramdisk. Traditionally, initramfs is rootfs: you can neither - pivot_root rootfs, nor unmount it. Instead delete everything out of - rootfs to free up the space (find -xdev / -exec rm '{}' ';'), overmount - rootfs with the new root (cd /newmount; mount --move . /; chroot .), - attach stdin/stdout/stderr to the new /dev/console, and exec the new init. - - Since this is a remarkably persnickety process (and involves deleting - commands before you can run them), the klibc package introduced a helper - program (utils/run_init.c) to do all this for you. Most other packages - (such as busybox) have named this command "switch_root". - - However, if the kernel is booted with "nullfs_rootfs", pivot_root() works + umount the ramdisk. With nullfs as the true root, pivot_root() works normally from the initramfs. Userspace can simply do:: chdir(new_root); pivot_root(".", "."); umount2(".", MNT_DETACH); - This is the preferred method when nullfs_rootfs is enabled. + This is the preferred method for switching root filesystems. Populating initramfs: ---------------------fs/namespace.c +21
-43@@ -75,17 +75,6 @@ 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);@@ -4593,10 +4582,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 -* unless the kernel was booted with "nullfs_rootfs". See -* Documentation/filesystems/ramfs-rootfs-initramfs.rst for alternatives -* in this situation. +* The immutable nullfs filesystem is mounted as the true root of the VFS +* hierarchy. The mutable rootfs (tmpfs/ramfs) is layered on top of this, +* allowing pivot_root() to work normally from initramfs. * * Notes: * - we don't move root/cwd if they are not at the root (reason: if something @@ -5993,49 +5981,39 @@ static void __init init_mount_tree(void)struct path root;/* -* When nullfs is used, we create two mounts: +* 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" ); - } + 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" ); -if (nullfs_rootfs) { -VFS_WARN_ON_ONCE (real_mount (nullfs_mnt)->mnt_id !=1 ); -VFS_WARN_ON_ONCE (real_mount (mnt)->mnt_id !=2 ); +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; +/* 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; +/* 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); +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; - } +pr_info ("VFS: Finished mounting rootfs on nullfs\n" );/* * We've dropped all locks here but that's fine. Not just are we init/do_mounts.c +7
-13@@ -493,21 +493,15 @@ void __init prepare_namespace(void) 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" ); +if (init_pivot_root ("." ,"." )) { +pr_err ("VFS: Failed to pivot into new rootfs\n" );return ; } - -init_mount ("." ,"/" ,NULL ,MS_MOVE ,NULL ); -init_chroot ("." ); +if (init_umount ("." ,MNT_DETACH )) { +pr_err ("VFS: Failed to unmount old rootfs\n" ); +return ; + } +pr_info ("VFS: Pivoted into new rootfs\n" ); }static bool is_tmpfs;init/do_mounts.h +0
-0
init/do_mounts.h line 20
Quoted 7 lines
init/do_mounts.h +0
-1@@ -15,7 +15,6 @@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) {
I like the idea of getting rid of the command-line option. Anything we can do to make this more automatic seems like a good idea.
If you're worried about it, you could flip the command-line option
around: do the new method by default, but allow a command-line option that makes it set up the rootfs the old way.
Signature · 1 line
Jeff Layton <[email protected]>
Quoted 64 lines · Christian Brauner
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_LOCKEDanymore 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 toMS_MOVEonly when that fails.This goes back to various discussion in previous years and a LPC 2024 presentation about this very topic.
Now in vfs-7.0.nullfs.
Signed-off-by Christian Brauner <[email protected]>---
Changes in v2:
- Rename to "nullfs".
- Update documentation.
- Link to v1: https://patch.msgid.link/[email protected]---
Christian Brauner (4):
fs: ensure that internal tmpfs mount gets mount id zero
fs: addinit_pivot_root()
fs: add immutable rootfs
docs: mention nullfs.../filesystems/ramfs-rootfs-initramfs.rst | 32 +++- fs/Makefile | 2 +- fs/init.c | 17 ++ fs/internal.h | 1 + fs/mount.h | 1 + fs/namespace.c | 181 ++++++++++++++------- fs/nullfs.c | 70 ++++++++ include/linux/init_syscalls.h | 1 + include/uapi/linux/magic.h | 1 + init/do_mounts.c | 14 ++ init/do_mounts.h | 1 + 11 files changed, 254 insertions(+), 67 deletions(-)
Quoted 3 lines
---
base-commit:8f0b4cce4481fb22653697cced8d0d04027cb1e8
change-id: 20260102-work-immutable-rootfs-b5f23e0f5a27
Christian Brauner <[email protected]>:
Quoted 5 lines
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
Note: this *is* possible to get access to nullfs.
In the end of this email you will find code, which proves this. I tested it on current vfs.all. The program will print "done", and this will prove my statement.
I think this is not a bug. I just want to make sure you are aware of this.
=====
#define GNU_SOURCE
#define _GNU_SOURCE
#include <fcntl.h>
#include <sys/mount.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sched.h>
#include <sys/wait.h>
#define OPEN_TREE_NAMESPACE (1 << 1) /* Clone the target tree into a new mount namespace */
/* Get information about namespace. */
#define NS_MNT_GET_INFO _IOR(0xb7, 10, struct mnt_ns_info)
struct mnt_ns_info {
__u32 size;
__u32 nr_mounts;
__u64 mnt_ns_id;
};int
main (void)
{
mkdir ("/foo", 0777);
if (mount ("tmpfsfoo", "/foo", "tmpfs", 0, NULL) == -1)
{
fprintf (stderr, "mount\n");
return 1;
}
int ns = open_tree (-EBADFD, "/foo", OPEN_TREE_NAMESPACE);
if (ns == -1)
{
fprintf (stderr, "open_tree failed\n");
return 1;
}
if (fork () == 0)
{
if (setns (ns, CLONE_NEWNS) == -1)
{
abort ();
}
if (umount2 ("/", MNT_DETACH) == -1) // This umount2 will succeed
{
abort ();
}
_exit (0);
}
{
int status;
if (wait (&status) == -1)
{
abort ();
}
if (status != 0)
{
abort ();
}
}
if (fork () == 0)
{
if (setns (ns, CLONE_NEWNS) == -1)
{
abort ();
}
if (umount2 ("/", MNT_DETACH) == 0) // This umount2 will fail, because we got to nullfs
{
abort ();
}
_exit (0);
}
{
int status;
if (wait (&status) == -1)
{
abort ();
}
if (status != 0)
{
abort ();
}
}
printf ("done\n");
return 0;
}Signature · 1 line
Askar Safin
Christian, important! Your patchset breaks userspace! (And I tested this.)
Christian Brauner <[email protected]>:
Quoted 3 lines
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.
listmount is buggy (see details below), but currently on a typical Linux distro the bug is hidden. Your new nullfs patchset exposes the bug, and now typical Linux configuration becomes buggy!
Look at this loop:
https://elixir.bootlin.com/linux/v6.19-rc5/source/fs/namespace.c#L5518 .
This loop is executed, when we call listmount on non-our namespace with LSMT_ROOT.
As you can see, this loop finds mount, which is exactly one layer above initial root mount. But why? What if our initial root mount was overmounted multiple times?
What (in my opinion) is actually needed here is to find topmost overmount of initial root mount. We know how to do this, we do this here:
https://elixir.bootlin.com/linux/v6.19-rc5/source/fs/namespace.c#L6096 .
Fortunately, current listmount code works in a typical configuration. Usually we indeed overmount initramfs exactly one time. So, listmount usually does the right thing.
But this nullfs patchset breaks listmount. Now on a typical distro (we assume that initramfs implementations (i. e. dracut, etc) are not yet changed to take advantage of nullfs) initial root mount (i. e. nullfs) will be overmounted two times: initramfs and actual disk root fs.
So listmount with LSMT_ROOT in somebody's else namespace will return initramfs instead of actual topmost root fs.
I think this listmount bug should be fixed before nullfs is applied to mainline.
I tested listmount behavior I'm talking about. On both vfs.all (i. e. with nullfs patches applied) and on some older vfs.git commit (without nullfs).
Signature · 1 line
Askar Safin
Christian Brauner
maintainerQuoted 1 line · Askar Safin
Christian, important! Your patchset breaks userspace! (And I tested this.)
If a bug is found in a piece of code we _calmly_ point it out and fix it.
Quoted 2 lines
I tested listmount behavior I'm talking about. On both vfs.all (i. e. with nullfs patches applied) and on some older vfs.git commit (without nullfs).
Looking at a foreign mount namespace over which the caller is privileged intentionally lists all mounts on top of the namespace root. In contrast to mountinfo which always looks at another mount namespace from the perspective of the process that is located within that mount namespace listmount() on a foreing mount namespace looks at the namespace itself and aims to list all mounts in that namespace. Since it is a new api there can be no regressions.
Quoted 5 lines · Christian Brauner
Quoted 1 line · Askar Safin
Christian, important! Your patchset breaks userspace! (And I tested this.)
If a bug is found in a piece of code we _calmly_ point it out and fix it.
Okay, I will be calmer next time.
Quoted 10 lines
Quoted 2 lines
I tested listmount behavior I'm talking about. On both vfs.all (i. e. with nullfs patches applied) and on some older vfs.git commit (without nullfs).
Looking at a foreign mount namespace over which the caller is privileged intentionally lists all mounts on top of the namespace root. In contrast to mountinfo which always looks at another mount namespace from the perspective of the process that is located within that mount namespace
listmount()on a foreing mount namespace looks at the namespace itself and aims to list all mounts in that namespace. Since it is a new api there can be no regressions.
Okay. Thank you for your explanation.
But then instead of a loop (
https://elixir.bootlin.com/linux/v6.19-rc5/source/fs/namespace.c#L5518
)
I suggest simply using ns->root->overmount.
Signature · 1 line
Askar Safin
Christian Brauner <[email protected]>:
Quoted 3 lines
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.
I want to point out at yet another benefit of this nullfs patchset:
it prevents data loss on external devices.
Here is why:
https://lore.kernel.org/all/[email protected]/
(search for phrase "data lose arise").
Signature · 1 line
Askar Safin
Christian Brauner <[email protected]>:
Quoted 2 lines
Add a immutable
rootfs called "nullfs"
Why not fix pivot_root instead?
I. e. why not make sure pivot_root will work on true root of VFS?
Signature · 1 line
Askar Safin