* [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=)
@ 2026-07-18 19:15 Eric Curtin
2026-07-18 19:15 ` [RFC PATCH 1/2] init: support mounting the root filesystem from an image file Eric Curtin
` (2 more replies)
0 siblings, 3 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-18 19:15 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
Image-based Linux systems (bootc-style OS updaters, ChromeOS/Android-like
A/B schemes, embedded appliances) keep one or more immutable root
filesystem images as sealed files on a writable filesystem and pick one
at boot. Booting such a system today always requires an initramfs, even
when that initramfs has nothing else to do; its only jobs are to parse
the kernel command line, mount the state filesystem, verify the image,
loop-mount it and switch_root into it.
This series teaches the kernel to do all of that directly:
root=PARTUUID=... rootimage=/deploy/a/root.erofs rootimagefstype=erofs \
rootimageverity=sha256:a9548f4c... rootimagesrcdir=/var/state
Patch 1 adds rootimage= (plus rootimagefstype=/rootimageflags=/
rootimagesrcdir=): root= then merely names the "carrier" filesystem.
The image file on it is mounted read-only through the filesystem's
file-backed mount support (available in erofs since v6.12), so no loop
device is involved, and it becomes the root that prepare_namespace()
pivots into. The carrier mount is detached by default, or moved to
rootimagesrcdir= inside the new root, which image-based systems
practically always want since the carrier holds their writable state.
Patch 2 adds rootimageverity=, which requires the image to carry a
specific fsverity file digest, reusing the fsverity_get_digest()
interface that IMA and overlayfs already use for digest pinning. With
a signed or measured command line (e.g. a unified kernel image), the
chain of trust extends to every byte of the root filesystem with no
userspace boot stage: the pinned digest authenticates the image's
Merkle tree root, and fsverity keeps verifying reads against it at
runtime, so later tampering with the carrier is caught as well.
This is the file-backed analogue of dm-mod.create= (CONFIG_DM_INIT),
which moved the equivalent block-device setup out of the initramfs for
verity-partition layouts back in v5.1. For file-based deployment
layouts nothing similar exists, so distributions ship a dracut stack
whose only purpose is the five steps above, and which remains the
largest and most failure-prone moving part of an otherwise fully
image-defined boot.
Tested on arm64 (qemu -M virt with KVM), with no initrd= at any point:
- control: plain ext4 root boots as before
- rootimage=: an erofs image file on ext4 is mounted as /, the carrier
ends up on /var/state, PID 1 runs from the image ~0.18s after kernel
entry
- rootimageverity= with the correct digest: boots, digest logged
- rootimageverity= with a wrong digest: panics with expected-vs-got
- build: defconfig and allnoconfig (!CONFIG_FS_VERITY, !CONFIG_BLOCK),
both with W=1, no warnings
Open questions for review:
- Should the carrier always be detached, dropping rootimagesrcdir=?
The image mount pins the carrier superblock either way, so userspace
can re-mount it, but a conflicting ro/rw state then needs a remount
dance.
- Should this be behind a Kconfig option like CONFIG_DM_INIT is? All
added code is __init and freed after boot.
- Naming: rootimage= vs. extending root= syntax (e.g. root=image:...).
This series was developed with AI assistance (see the Assisted-by tags
and Documentation/process/coding-assistants.rst).
Eric Curtin (2):
init: support mounting the root filesystem from an image file
init: support pinning the root image's fsverity digest
.../admin-guide/kernel-parameters.txt | 42 ++++
init/do_mounts.c | 221 ++++++++++++++++--
2 files changed, 250 insertions(+), 13 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 1/2] init: support mounting the root filesystem from an image file
2026-07-18 19:15 [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Eric Curtin
@ 2026-07-18 19:15 ` Eric Curtin
2026-07-18 19:15 ` [RFC PATCH 2/2] init: support pinning the root image's fsverity digest Eric Curtin
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
2 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-18 19:15 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
Image-based Linux systems (bootc-style OS updaters, ChromeOS/Android-like
A/B schemes, embedded appliances) commonly keep one or more immutable
root filesystem images as plain files on a writable "carrier" filesystem
and pick one at boot. Today that always requires an initramfs, even when
the initramfs has nothing else to do: the kernel can only mount a block
device (or NFS/CIFS/ubi/mtd) as root, so userspace must mount the
carrier, loop-mount the image and switch_root into it.
Add a rootimage= parameter naming an image file on the filesystem
specified by root=. When set, root= only designates the carrier: after
mounting it as usual, mount the image file read-only on /image and make
that the root that prepare_namespace() pivots into. The image is
mounted through the filesystem's file-backed mount support (available in
erofs since v6.12), so no loop device is involved. rootimagefstype= and
rootimageflags= mirror rootfstype=/rootflags= for the image mount, while
"ro"/"rw"/rootflags= keep applying to the carrier.
The carrier mount is detached again by default; the image mount keeps
its superblock pinned, so userspace can still mount it later. Since
systems following this model virtually always need the carrier mounted
(it holds their writable state), rootimagesrcdir= optionally names a
directory inside the image where the carrier mount is moved instead.
This is the file-backed analogue of what dm-mod.create= (CONFIG_DM_INIT)
already does for device-mapper targets: moving a fixed, declarative bit
of early-boot setup from initramfs userspace into the kernel so that
small and verified systems can boot with no initramfs at all.
Assisted-by: opencode:claude-fable-5
Signed-off-by: Eric Curtin <ericcurtin17@gmail.com>
---
.../admin-guide/kernel-parameters.txt | 29 ++++
init/do_mounts.c | 158 ++++++++++++++++--
2 files changed, 174 insertions(+), 13 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index b5493a7f8..5dbd56098 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -6699,6 +6699,35 @@ Kernel parameters
rootfstype= [KNL] Set root filesystem type
+ rootimage= [KNL] Mount the actual root filesystem from this
+ filesystem image file (absolute path) located on the
+ filesystem specified by root=, instead of using that
+ filesystem as the root directly. This allows
+ image-based systems that keep their (typically
+ read-only, e.g. erofs) root filesystem images as
+ plain files on a carrier filesystem to boot without
+ an initramfs. The image is mounted read-only via
+ the filesystem's file-backed mount support (no loop
+ device is set up); "ro", "rw" and rootflags= keep
+ applying to the carrier filesystem. Once the image
+ is mounted, the carrier mount is detached (but kept
+ busy by the image mount itself) unless
+ rootimagesrcdir= is also given.
+
+ rootimageflags= [KNL] Set root image filesystem mount option string,
+ used together with rootimage=.
+
+ rootimagefstype= [KNL] Set root image filesystem type, used together
+ with rootimage=. Default is to try all filesystems
+ known to the kernel that can mount a block device or
+ an image file.
+
+ rootimagesrcdir= [KNL] Directory (absolute path) inside the root
+ image where the mount of the carrier filesystem
+ holding the image (i.e. the filesystem specified by
+ root=) is moved to, instead of detaching it. Used
+ together with rootimage=.
+
rootwait [KNL] Wait (indefinitely) for root device to show up.
Useful for devices that are detected asynchronously
(e.g. USB and MMC devices).
diff --git a/init/do_mounts.c b/init/do_mounts.c
index 95e0b3a0f..1b96ef30b 100644
--- a/init/do_mounts.c
+++ b/init/do_mounts.c
@@ -122,13 +122,51 @@ __setup("rootflags=", root_data_setup);
__setup("rootfstype=", fs_names_setup);
__setup("rootdelay=", root_delay_setup);
+/*
+ * rootimage= mounts the actual root filesystem from an image file located
+ * on the filesystem specified by root= instead of using that filesystem
+ * as the root directly.
+ */
+static char * __initdata root_image;
+static int __init root_image_setup(char *str)
+{
+ root_image = str;
+ return 1;
+}
+
+static char * __initdata root_image_fs_names;
+static int __init root_image_fs_names_setup(char *str)
+{
+ root_image_fs_names = str;
+ return 1;
+}
+
+static char * __initdata root_image_mount_data;
+static int __init root_image_data_setup(char *str)
+{
+ root_image_mount_data = str;
+ return 1;
+}
+
+static char * __initdata root_image_srcdir;
+static int __init root_image_srcdir_setup(char *str)
+{
+ root_image_srcdir = str;
+ return 1;
+}
+
+__setup("rootimage=", root_image_setup);
+__setup("rootimagefstype=", root_image_fs_names_setup);
+__setup("rootimageflags=", root_image_data_setup);
+__setup("rootimagesrcdir=", root_image_srcdir_setup);
+
/* This can return zero length strings. Caller should check */
-static int __init split_fs_names(char *page, size_t size)
+static int __init split_fs_names(char *page, size_t size, char *names)
{
int count = 1;
char *p = page;
- strscpy(p, root_fs_names, size);
+ strscpy(p, names, size);
while (*p++) {
if (p[-1] == ',') {
p[-1] = '\0';
@@ -139,8 +177,9 @@ static int __init split_fs_names(char *page, size_t size)
return count;
}
-static int __init do_mount_root(const char *name, const char *fs,
- const int flags, const void *data)
+static int __init do_mount_root(const char *name, const char *dir,
+ const char *fs, const int flags,
+ const void *data)
{
struct super_block *s;
char *data_page = NULL;
@@ -154,11 +193,11 @@ static int __init do_mount_root(const char *name, const char *fs,
strscpy_pad(data_page, data, PAGE_SIZE);
}
- ret = init_mount(name, "/root", fs, flags, data_page);
+ ret = init_mount(name, dir, fs, flags, data_page);
if (ret)
goto out;
- init_chdir("/root");
+ init_chdir(dir);
s = current->fs->pwd.dentry->d_sb;
ROOT_DEV = s->s_dev;
printk(KERN_INFO
@@ -185,7 +224,7 @@ void __init mount_root_generic(char *name, char *pretty_name, int flags)
scnprintf(b, BDEVNAME_SIZE, "unknown-block(%u,%u)",
MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
if (root_fs_names)
- num_fs = split_fs_names(fs_names, PAGE_SIZE);
+ num_fs = split_fs_names(fs_names, PAGE_SIZE, root_fs_names);
else
num_fs = list_bdev_fs_names(fs_names, PAGE_SIZE);
retry:
@@ -194,7 +233,7 @@ void __init mount_root_generic(char *name, char *pretty_name, int flags)
if (!*p)
continue;
- err = do_mount_root(name, p, flags, root_mount_data);
+ err = do_mount_root(name, "/root", p, flags, root_mount_data);
switch (err) {
case 0:
goto out;
@@ -266,7 +305,8 @@ static void __init mount_nfs_root(void)
*/
timeout = NFSROOT_TIMEOUT_MIN;
for (try = 1; ; try++) {
- if (!do_mount_root(root_dev, "nfs", root_mountflags, root_data))
+ if (!do_mount_root(root_dev, "/root", "nfs", root_mountflags,
+ root_data))
return;
if (try > NFSROOT_RETRY_MAX)
break;
@@ -303,7 +343,7 @@ static void __init mount_cifs_root(void)
timeout = CIFSROOT_TIMEOUT_MIN;
for (try = 1; ; try++) {
- if (!do_mount_root(root_dev, "cifs", root_mountflags,
+ if (!do_mount_root(root_dev, "/root", "cifs", root_mountflags,
root_data))
return;
if (try > CIFSROOT_RETRY_MAX)
@@ -345,7 +385,7 @@ static int __init mount_nodev_root(char *root_device_name)
fs_names = kmalloc(PAGE_SIZE, GFP_KERNEL);
if (!fs_names)
return -EINVAL;
- num_fs = split_fs_names(fs_names, PAGE_SIZE);
+ num_fs = split_fs_names(fs_names, PAGE_SIZE, root_fs_names);
for (i = 0, fstype = fs_names; i < num_fs;
i++, fstype += strlen(fstype) + 1) {
@@ -353,8 +393,8 @@ static int __init mount_nodev_root(char *root_device_name)
continue;
if (!fs_is_nodev(fstype))
continue;
- err = do_mount_root(root_device_name, fstype, root_mountflags,
- root_mount_data);
+ err = do_mount_root(root_device_name, "/root", fstype,
+ root_mountflags, root_mount_data);
if (!err)
break;
}
@@ -402,6 +442,96 @@ void __init mount_root(char *root_device_name)
}
}
+/*
+ * Mount the actual root filesystem from the image file rootimage= on the
+ * filesystem that was just mounted from root= (the "carrier"), so that
+ * image-based systems can boot without an initramfs.
+ *
+ * Called with the carrier mounted at /root and the cwd there. The image
+ * is always mounted read-only; "ro"/"rw"/rootflags= keep applying to the
+ * carrier. On success the cwd is the image's root, ready for the pivot
+ * in prepare_namespace(). The carrier mount is moved to rootimagesrcdir=
+ * inside the image if set, and detached otherwise; either way the image
+ * mount keeps the carrier superblock pinned.
+ */
+static void __init mount_root_image(void)
+{
+ unsigned long flags = MS_RDONLY | MS_SILENT;
+ char *path, *fs_names, *p;
+ struct file *file;
+ int num_fs, i, err;
+
+ if (root_image[0] != '/')
+ panic("VFS: rootimage= must be an absolute path");
+
+ path = kmalloc(PATH_MAX, GFP_KERNEL);
+ fs_names = kmalloc(PAGE_SIZE, GFP_KERNEL);
+ if (!path || !fs_names)
+ panic("VFS: unable to mount root image: not enough memory");
+
+ if (snprintf(path, PATH_MAX, "/root%s", root_image) >= PATH_MAX)
+ panic("VFS: rootimage= path too long");
+
+ /*
+ * Nothing that could modify the carrier runs yet, so the image
+ * cannot change between this open and the mount below.
+ */
+ file = filp_open(path, O_RDONLY | O_LARGEFILE, 0);
+ if (IS_ERR(file))
+ panic("VFS: unable to open root image %s: error %ld",
+ root_image, PTR_ERR(file));
+
+ err = init_mkdir("/image", 0700);
+ if (err < 0 && err != -EEXIST)
+ panic("VFS: unable to create /image: error %d", err);
+
+ if (root_image_fs_names)
+ num_fs = split_fs_names(fs_names, PAGE_SIZE,
+ root_image_fs_names);
+ else
+ num_fs = list_bdev_fs_names(fs_names, PAGE_SIZE);
+
+ for (i = 0, p = fs_names; i < num_fs; i++, p += strlen(p) + 1) {
+ if (!*p)
+ continue;
+ err = do_mount_root(path, "/image", p, flags,
+ root_image_mount_data);
+ switch (err) {
+ case 0:
+ goto mounted;
+ case -EACCES:
+ case -EINVAL:
+ case -ENOTBLK:
+ continue;
+ }
+ panic("VFS: unable to mount root image %s: error %d",
+ root_image, err);
+ }
+ panic("VFS: no filesystem could mount root image %s", root_image);
+
+mounted:
+ fput(file);
+
+ if (root_image_srcdir) {
+ if (root_image_srcdir[0] != '/')
+ panic("VFS: rootimagesrcdir= must be an absolute path");
+ if (snprintf(path, PATH_MAX, ".%s", root_image_srcdir) >=
+ PATH_MAX)
+ panic("VFS: rootimagesrcdir= path too long");
+ err = init_mount("/root", path, NULL, MS_MOVE, NULL);
+ if (err)
+ pr_err("VFS: failed to move the root image's carrier filesystem to %s: error %d\n",
+ root_image_srcdir, err);
+ } else {
+ err = -EINVAL;
+ }
+ if (err)
+ init_umount("/root", MNT_DETACH);
+
+ kfree(path);
+ kfree(fs_names);
+}
+
/* wait for any asynchronous scanning to complete */
static void __init wait_for_root(char *root_device_name)
{
@@ -481,6 +611,8 @@ void __init prepare_namespace(void)
if (root_wait)
wait_for_root(saved_root_name);
mount_root(saved_root_name);
+ if (root_image)
+ mount_root_image();
devtmpfs_mount();
if (init_pivot_root(".", ".")) {
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [RFC PATCH 2/2] init: support pinning the root image's fsverity digest
2026-07-18 19:15 [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Eric Curtin
2026-07-18 19:15 ` [RFC PATCH 1/2] init: support mounting the root filesystem from an image file Eric Curtin
@ 2026-07-18 19:15 ` Eric Curtin
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
2 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-18 19:15 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
When the root filesystem is mounted from an image file with rootimage=,
the carrier filesystem holding the image is typically writable and
therefore untrusted. Systems that seal their root images with fsverity
currently need an initramfs for the sole purpose of checking that the
image carries the expected fsverity digest before mounting it.
Add rootimageverity=<hash algorithm>:<hex digest>, which requires the
rootimage= file to have fsverity enabled with exactly this file digest
and fails the boot otherwise, using the same fsverity_get_digest()
interface that IMA and overlayfs already use for digest pinning.
Combined with a trusted kernel command line (e.g. a signed unified
kernel image, or a TPM-measured bootloader configuration), this extends
the chain of trust to every byte of the root filesystem without any
userspace boot stage: the digest pins the image's Merkle tree, and
fsverity keeps verifying all data read from the image against it at
runtime, so post-boot tampering with the carrier filesystem is detected
as well. It is the file-backed counterpart of setting up a dm-verity
target for a partition-backed root via dm-mod.create=.
Verification is done on the file the kernel is about to mount: it is
opened before mounting (which also loads the fsverity information) and
kept open across the mount, and no userspace exists yet that could race
a replacement in between.
Assisted-by: opencode:claude-fable-5
Signed-off-by: Eric Curtin <ericcurtin17@gmail.com>
---
.../admin-guide/kernel-parameters.txt | 13 ++++
init/do_mounts.c | 63 +++++++++++++++++++
2 files changed, 76 insertions(+)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 5dbd56098..105ebb171 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -6728,6 +6728,19 @@ Kernel parameters
root=) is moved to, instead of detaching it. Used
together with rootimage=.
+ rootimageverity= [KNL] Require the root image specified by
+ rootimage= to have fsverity enabled with this file
+ digest, given as <hash algorithm>:<hex digest>,
+ e.g. sha256:dd1b3fa9... The boot is aborted if the
+ image carries no or a different fsverity digest.
+ Because fsverity keeps verifying data read from the
+ image against its Merkle tree at runtime, a trusted
+ (e.g. signed or TPM-measured) kernel command line
+ extends the chain of trust to the complete root
+ filesystem contents without an initramfs. Requires
+ CONFIG_FS_VERITY and a carrier filesystem with
+ fsverity support.
+
rootwait [KNL] Wait (indefinitely) for root device to show up.
Useful for devices that are detected asynchronously
(e.g. USB and MMC devices).
diff --git a/init/do_mounts.c b/init/do_mounts.c
index 1b96ef30b..4eb792b27 100644
--- a/init/do_mounts.c
+++ b/init/do_mounts.c
@@ -24,6 +24,9 @@
#include <linux/nfs_fs_sb.h>
#include <linux/nfs_mount.h>
#include <linux/raid/detect.h>
+#include <linux/fsverity.h>
+#include <linux/hex.h>
+#include <crypto/hash_info.h>
#include <uapi/linux/mount.h>
#include "do_mounts.h"
@@ -155,10 +158,18 @@ static int __init root_image_srcdir_setup(char *str)
return 1;
}
+static char * __initdata root_image_verity;
+static int __init root_image_verity_setup(char *str)
+{
+ root_image_verity = str;
+ return 1;
+}
+
__setup("rootimage=", root_image_setup);
__setup("rootimagefstype=", root_image_fs_names_setup);
__setup("rootimageflags=", root_image_data_setup);
__setup("rootimagesrcdir=", root_image_srcdir_setup);
+__setup("rootimageverity=", root_image_verity_setup);
/* This can return zero length strings. Caller should check */
static int __init split_fs_names(char *page, size_t size, char *names)
@@ -442,6 +453,55 @@ void __init mount_root(char *root_device_name)
}
}
+#ifdef CONFIG_FS_VERITY
+/*
+ * Require the root image to carry the fsverity file digest given by
+ * rootimageverity=<hash algorithm>:<hex digest>. @file must have been
+ * opened so that its fsverity information is loaded. Any deviation
+ * fails the boot: with a trusted command line this pins the complete
+ * image contents, which fsverity keeps verifying against the image's
+ * Merkle tree as they are read.
+ */
+static void __init verify_root_image(struct file *file)
+{
+ u8 want[FS_VERITY_MAX_DIGEST_SIZE], got[FS_VERITY_MAX_DIGEST_SIZE];
+ enum hash_algo want_algo, got_algo;
+ int want_size, got_size, i;
+ char *hex;
+
+ hex = strchr(root_image_verity, ':');
+ if (!hex)
+ panic("VFS: rootimageverity= expects <algorithm>:<hex digest>");
+ *hex++ = '\0';
+ i = match_string(hash_algo_name, HASH_ALGO__LAST, root_image_verity);
+ if (i < 0)
+ panic("VFS: rootimageverity=: unknown hash algorithm \"%s\"",
+ root_image_verity);
+ want_algo = i;
+ want_size = hash_digest_size[want_algo];
+ if (strlen(hex) != 2 * want_size || hex2bin(want, hex, want_size))
+ panic("VFS: rootimageverity=: expected %d-byte hex digest",
+ want_size);
+
+ got_size = fsverity_get_digest(file_inode(file), got, NULL, &got_algo);
+ if (!got_size)
+ panic("VFS: root image does not have fsverity enabled");
+ if (got_algo != want_algo || got_size != want_size ||
+ memcmp(want, got, want_size))
+ panic("VFS: root image fsverity digest mismatch: expected %s:%*phN, got %s:%*phN",
+ hash_algo_name[want_algo], want_size, want,
+ hash_algo_name[got_algo], got_size, got);
+
+ pr_info("VFS: verified root image fsverity digest %s:%*phN\n",
+ hash_algo_name[want_algo], want_size, want);
+}
+#else /* !CONFIG_FS_VERITY */
+static void __init verify_root_image(struct file *file)
+{
+ panic("VFS: rootimageverity= requires CONFIG_FS_VERITY");
+}
+#endif /* !CONFIG_FS_VERITY */
+
/*
* Mount the actual root filesystem from the image file rootimage= on the
* filesystem that was just mounted from root= (the "carrier"), so that
@@ -481,6 +541,9 @@ static void __init mount_root_image(void)
panic("VFS: unable to open root image %s: error %ld",
root_image, PTR_ERR(file));
+ if (root_image_verity)
+ verify_root_image(file);
+
err = init_mkdir("/image", 0700);
if (err < 0 && err != -EEXIST)
panic("VFS: unable to create /image: error %d", err);
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=)
2026-07-18 19:15 [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Eric Curtin
2026-07-18 19:15 ` [RFC PATCH 1/2] init: support mounting the root filesystem from an image file Eric Curtin
2026-07-18 19:15 ` [RFC PATCH 2/2] init: support pinning the root image's fsverity digest Eric Curtin
@ 2026-07-27 8:55 ` Christian Brauner
2026-07-27 10:31 ` Eric Curtin
` (5 more replies)
2 siblings, 6 replies; 10+ messages in thread
From: Christian Brauner @ 2026-07-27 8:55 UTC (permalink / raw)
To: Eric Curtin
Cc: Alexander Viro, Christian Brauner, Jan Kara, Jonathan Corbet,
Shuah Khan, Eric Biggers, Theodore Y . Ts'o, Gao Xiang,
Chao Yu, fsverity, linux-erofs, linux-fsdevel, linux-doc,
linux-kernel
On 2026-07-18 20:15 +0100, Eric Curtin wrote:
> Image-based Linux systems (bootc-style OS updaters, ChromeOS/Android-like
> A/B schemes, embedded appliances) keep one or more immutable root
> filesystem images as sealed files on a writable filesystem and pick one
> at boot. Booting such a system today always requires an initramfs, even
> when that initramfs has nothing else to do; its only jobs are to parse
> the kernel command line, mount the state filesystem, verify the image,
> loop-mount it and switch_root into it.
>
> This series teaches the kernel to do all of that directly:
>
> root=PARTUUID=... rootimage=/deploy/a/root.erofs rootimagefstype=erofs \
> rootimageverity=sha256:a9548f4c... rootimagesrcdir=/var/state
>
> Patch 1 adds rootimage= (plus rootimagefstype=/rootimageflags=/
> rootimagesrcdir=): root= then merely names the "carrier" filesystem.
> The image file on it is mounted read-only through the filesystem's
> file-backed mount support (available in erofs since v6.12), so no loop
> device is involved, and it becomes the root that prepare_namespace()
> pivots into. The carrier mount is detached by default, or moved to
> rootimagesrcdir= inside the new root, which image-based systems
> practically always want since the carrier holds their writable state.
>
> Patch 2 adds rootimageverity=, which requires the image to carry a
> specific fsverity file digest, reusing the fsverity_get_digest()
> interface that IMA and overlayfs already use for digest pinning. With
> a signed or measured command line (e.g. a unified kernel image), the
> chain of trust extends to every byte of the root filesystem with no
> userspace boot stage: the pinned digest authenticates the image's
> Merkle tree root, and fsverity keeps verifying reads against it at
> runtime, so later tampering with the carrier is caught as well.
>
> This is the file-backed analogue of dm-mod.create= (CONFIG_DM_INIT),
> which moved the equivalent block-device setup out of the initramfs for
> verity-partition layouts back in v5.1. For file-based deployment
> layouts nothing similar exists, so distributions ship a dracut stack
> whose only purpose is the five steps above, and which remains the
> largest and most failure-prone moving part of an otherwise fully
> image-defined boot.
So I have several structual and design issues with this.
In the first pach the filp_open() and subsequent fput() are unused.
And that claim "the image cannot change between this open and the mount
below" is resting on very weak assumptions.
The second patch then checks the digest on the struct file from its own
filp_open(). But the mount goes through init_mount() with the path... so
erofs does a third lookup via filp_open(fc->source)...
Nothing guarantees that there's a relationship between the verified
inode and the path the superblock is created from. The only guarantee is
that no userspace yet exists but which can't really be enforced.
To support this properly we need new infrastructure such that you can
actually hand an already opened file as the source. I know that erofs is
intended to merge support for this at least but this should be generic
infra available to any fs.
Note also that the default detach mode creates a zombie superblock and
the image superblocks source string (/root/deploy/...) dangles after the
pivot. /proc/self/mounts names a path that doesn't exist in the final
namespace. And unlike loop there's no /sys/block/loopX/loop/backing_file
to recover which file backs the root.
The other claim is that the digest sufficiently guarantees the integrity
of the root filesystem. That's not correct per se...
An erofs multi-device filesystem blows this apart and
rootimageflags=device= would also add more. So that digest only cover
the primary image.
The bigger flaw is that the untrusted "carrier" (LLMs do pick the worst
terms for things...) is fully parsed before anything is verified.
In other words, mounting the carrier means running the ext4 (or
whatever) parsers and infra over attacker-writable data, and the
fsverity descriptor are themselves read from that untrusted fs. So
basically you're hosed before you ever got to the fsverity part.
This is also now the third fsverity-digest-pinning mechanism (IMA
appraisal, the IPE LSM and now rootimageverity=). With the least
expressive policy language of the three. No signature or keyring option
either and so the boot entry must be regenerated per image update.
You're embedding a whole deployment topology into the core code here as
well... There are _five_ interacting __setup parameters. Every real
deployment immediately wants a sixth knob: A/B fallback inside the
mechanism. Because in your current model every failure is a panic().
But that also means image selection must live in the bootloader anyway:
composefs/overlay stacking, LUKS-encrypted state partitions as command
lines can't do crypto secrets, fsck of the carrier, directory-based
deployments yadayadayada.
This is just turning do_mounts.c into policy it shouldn't be involved
in. We just refused pushing the bootloader spec into the initramfs for
the same reasons - and that one I'm much more sympathetic to since I was
involved in creating it. I'd love to see parts of that in the kernel but
I refuse it because I believe fundamentally the kernel is in the
business of keeping complexity in userspace not the other way around.
Also the users the cover letter touches on such as distro kernels ship
erofs and its decompressors, the block drivers, and the fs stack as
modules. Afaict, rootimage= needs all of it builtin:
- bootc/ostree: composefs — multi-mount + overlay + external objects.
inexpressible here and partially unverifiable
- ChromeOS/Android-style A/B: dm-verity partitions already served by
dm-mod.create= and a fallback story.
- embedded squashfs appliances: no file-backed mount support in squashfs
What remains are single-erofs-filesystems with fully-builtin kernels.
That's kinda what CONFIG_INITRAMFS_SOURCE embeds a five-line static
/init into the same signed/measured kernel image with identical trust
properties and full policy freedom.
UKI users likewise already sign and measure their initrd as part of the
PE. The series only really makes sense if you get rid of that.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=)
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
@ 2026-07-27 10:31 ` Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 0/4] " Eric Curtin
` (4 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-27 10:31 UTC (permalink / raw)
To: Christian Brauner
Cc: Eric Curtin, Alexander Viro, Jan Kara, Jonathan Corbet,
Shuah Khan, Eric Biggers, Theodore Y . Ts'o, Gao Xiang,
Chao Yu, fsverity, linux-erofs, linux-fsdevel, linux-doc,
linux-kernel
On 2026-07-27 10:55 +0200, Christian Brauner wrote:
> So I have several structual and design issues with this.
[...]
> You're embedding a whole deployment topology into the core code here as
> well... There are _five_ interacting __setup parameters. Every real
> deployment immediately wants a sixth knob: A/B fallback inside the
> mechanism. Because in your current model every failure is a panic().
>
> But that also means image selection must live in the bootloader anyway:
> composefs/overlay stacking, LUKS-encrypted state partitions as command
> lines can't do crypto secrets, fsck of the carrier, directory-based
> deployments yadayadayada.
>
> This is just turning do_mounts.c into policy it shouldn't be involved
> in.
Thanks for the detailed review. Let me state the goal plainly, since I
think it explains the choices even though several of your specific
findings below are real bugs I should just fix rather than defend.
The goal is A/B image updates from a *single* partition, without
paying for a second userspace spin-up at boot.
Today there are basically two ways to do A/B on Linux:
1. N partitions/dm-verity volumes, each a full root image, selected
by the bootloader (dm-mod.create= + a GPT attribute flip). This
is the ChromeOS/Android model. It costs Nx the storage/partition
budget up front and the slot count is fixed at provisioning time.
2. A single filesystem holding N image files (what bootc/ostree/
composefs already do), where something picks which file becomes
root. Far more space- and slot-count-flexible, but "pick a file
and expose it as root" is currently dracut/initramfs's job, so
you pay for a whole extra userspace stage whose only work is the
five steps in the cover letter.
rootimage= targets (2): keep single-partition flexibility, drop the
"spin up a second userspace just to switch_root" cost. On fast-boot
targets that first userspace (loading dracut/busybox, forking,
possibly loading modules) isn't free and is pure overhead for five
mechanical steps.
To be clear, I'm not trying to relitigate "no bootloader spec in the
kernel" - I agree with that call, and A/B slot selection/fallback
bookkeeping is meant to stay in the bootloader, exactly as it does
with dm-mod.create= today. rootimage= is only meant to do the same
mechanical "resolve the one path the bootloader already chose, verify
it, make it root" step dm-mod.create() does for partitions - just for
the file-backed case, where there's no dm table to describe it. So
the "sixth knob" is intentionally out of scope; where you're right is
that I haven't specified what happens when this step fails (currently
panic()), and that needs a real answer, not "bootloader's problem".
On the rest, these read like real bugs, not disagreements, and I'd
rather fix them than defend the current code:
- filp_open()/fput() unused, and no guaranteed relationship between
the fsverity-checked file and the path/inode init_mount() and
erofs's own filp_open(fc->source) resolve independently - agreed,
this needs a real "mount from an already-open fd" path.
- multi-device erofs / rootimageflags=device= meaning the digest
only covers the primary image - agreed, real gap.
- the carrier fs being fully parsed, including reading the fsverity
descriptor itself, before anything is verified - agreed, that
ordering defeats the point.
- zombie superblock / dangling source path / no loop-style
backing-file equivalent after detach - agreed, needs fixing.
I'll rework this into a v2 that only tries to solve the "take an
already-resolved, already-open file and make it root, verified"
problem, drop the multi-knob framing, and either fix the fd-vs-path
lookup properly or wait for the generic "mount from fd" infra you
mentioned.
Thanks for going through this in such detail.
Eric
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v2 0/4] init: boot image-based systems without an initramfs (rootimage=)
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
2026-07-27 10:31 ` Eric Curtin
@ 2026-07-27 10:48 ` Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 1/4] fs: allow in-kernel mounters to hand filesystems an already-open source file Eric Curtin
` (3 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-27 10:48 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
Changes since v1 (https://lore.kernel.org/all/20260718191551.1703670-1-ericcurtin17@gmail.com/),
following Christian Brauner's review
(https://lore.kernel.org/all/20260727-gepaukt-eislauf-waran-7c03f0e47609@brauner/):
The goal is unchanged: A/B image updates from a single partition,
without paying for a second userspace spin-up at boot. What changed is
how rootimage=/rootimageverity= get from "here is a path" to "here is
the verified, mounted root", which is what most of the v1 review was
about.
- Patches 1-2 are new: they add generic, reusable VFS infrastructure
(struct fs_context::source_file, path_mount_file()/
init_mount_file(), vfs_parse_fs_param_file()) that lets an in-kernel
caller hand a filesystem an already-open struct file as its mount
source instead of a path, and convert erofs's existing file-backed
mount support to use it when given. This is the "new infrastructure
available to any fs" asked for, so that init/do_mounts.c never has
to independently re-resolve a path that something else already
opened and validated.
- Patch 3 (rootimage=) now opens the image file exactly once and
mounts that same struct file via the above, instead of opening it,
doing nothing with the open, and separately handing erofs a path
that it re-resolved on its own with no guaranteed relationship to
what was checked - which is what v1 did, and which is where most of
the "second lookup" / "weak assumptions" findings came from. There
is no fd-vs-path race left because there is no second lookup left.
rootimagesrcdir= is now mandatory (rootimagesrcdir=none opts out
explicitly) instead of silently detaching the carrier by default,
so there's no more zombie superblock/dangling mountpoint by
default; you have to ask for that outcome by name.
- Patch 4 (rootimageverity=) is mostly unchanged in what it checks,
but now runs on the exact file patch 3 mounts, so the digest check
and the mount are guaranteed to agree on what "the image" is. It
also now refuses rootimageverity= together with rootimageflags=
containing device= (multi-device erofs images), since the digest
only ever covers the primary image file and silently ignoring the
rest would be a false sense of integrity.
Not changed / not attempting to fix in this version, per the v1
discussion:
- The carrier filesystem itself is still fully parsed (superblock,
directory entries, extents) before rootimageverity= gets to check
anything, since reaching the image file at all requires that. This
is structural to a file-backed image sitting on a general-purpose
writable filesystem, the same way a dm-verity root still needs a
trusted block layer under it; rootimage=/rootimageverity= are meant
to sit on top of an already-appropriately-trusted carrier (e.g.
itself dm-verity/LUKS-backed, or a well-audited always-read-only
fs), not conjure trust in an arbitrary writable one out of thin air.
- A/B slot selection and fallback/rollback bookkeeping are still the
bootloader's job, exactly as with dm-mod.create= today; this series
only does the "resolve the one path the bootloader already chose,
verify it, make it root" mechanical step for the file-backed case,
not image-based deployment policy in general.
- There is still no sysfs equivalent of
/sys/block/loopX/loop/backing_file to discover which file backs the
root after boot (the mandatory rootimagesrcdir= at least means the
carrier itself stays reachable). This would need its own generic
piece of infrastructure and is left for a follow-up.
Eric Curtin (4):
fs: allow in-kernel mounters to hand filesystems an already-open
source file
erofs: use fs_context source_file for file-backed mounts when given
init: support mounting the root filesystem from an image file
init: support pinning the root image's fsverity digest
.../admin-guide/kernel-parameters.txt | 56 ++++
Documentation/filesystems/mount_api.rst | 18 ++
fs/erofs/super.c | 19 ++
fs/fs_context.c | 39 ++-
fs/init.c | 18 ++
fs/internal.h | 2 +
fs/namespace.c | 96 ++++++-
include/linux/fs_context.h | 4 +
include/linux/init_syscalls.h | 2 +
init/do_mounts.c | 269 +++++++++++++++++-
10 files changed, 514 insertions(+), 9 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH v2 1/4] fs: allow in-kernel mounters to hand filesystems an already-open source file
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
2026-07-27 10:31 ` Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 0/4] " Eric Curtin
@ 2026-07-27 10:48 ` Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 2/4] erofs: use fs_context source_file for file-backed mounts when given Eric Curtin
` (2 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-27 10:48 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
do_new_mount() and its path_mount() wrapper only ever let a filesystem's
->get_tree() resolve its own source, either from a path string that the
kernel translates to fc->source via vfs_parse_fs_param_source(), or from
whatever the filesystem's own ->parse_param() does with it. There is no
way for a caller that already has an open, already-validated struct file
in hand to mount from *that exact file*: the best it can do is hand the
filesystem a path and hope nothing about the target changed by the time
the filesystem re-resolves it, which is not a guarantee the VFS can make.
Add struct fs_context::source_file alongside the existing ::source, set
via a new vfs_parse_fs_param_file() instead of the string parameter
path, and a new path_mount_file() / init_mount_file() pair mirroring
path_mount() / init_mount() that use it. This is deliberately not wired
up to the fsconfig(FSCONFIG_SET_FD) uAPI: it is for in-kernel callers
only, so no existing filesystem's ->parse_param() needs to change to
avoid misinterpreting it, and none is affected by this patch.
Filesystems that support being mounted from a plain file and want to
accept exactly the file a caller hands them, instead of independently
re-resolving fc->source (unset in this case), check fc->source_file
from their ->get_tree(). erofs is converted to do so in the next patch;
the immediate motivation is letting init/do_mounts.c mount an fsverity-
checked root image from the same struct file it just checked, instead
of a path that erofs would then look up again on its own.
Signed-off-by: Eric Curtin <ericcurtin17@gmail.com>
---
Documentation/filesystems/mount_api.rst | 18 +++++
fs/fs_context.c | 39 +++++++++-
fs/init.c | 18 +++++
fs/internal.h | 2 +
fs/namespace.c | 96 ++++++++++++++++++++++++-
include/linux/fs_context.h | 4 ++
include/linux/init_syscalls.h | 2 +
7 files changed, 175 insertions(+), 4 deletions(-)
diff --git a/Documentation/filesystems/mount_api.rst b/Documentation/filesystems/mount_api.rst
index e8b94357b4df..7b799f61c39d 100644
--- a/Documentation/filesystems/mount_api.rst
+++ b/Documentation/filesystems/mount_api.rst
@@ -138,6 +138,24 @@ The fs_context fields are as follows:
This specifies the source. It may be a block device (e.g. /dev/sda1) or
something more exotic, such as the "host:/path" that NFS desires.
+ * ::
+
+ struct file *source_file
+
+ An alternative to ``source`` for in-kernel mounters only (there is no
+ userspace-visible way to set this): an already-open file to use as the
+ source, set by vfs_parse_fs_param_file() instead of parsing a path
+ string. At most one of ``source`` and ``source_file`` is ever set.
+
+ Filesystems that support being mounted from a plain file (as opposed to
+ a block device) and want to let such an in-kernel caller hand them an
+ exact, already-validated file - rather than a path they would then have
+ to re-resolve themselves, with no way to guarantee the two refer to the
+ same thing - should check this field in their ``get_tree`` and use it
+ directly instead of falling back to opening ``source``. See
+ fs/erofs/super.c:erofs_fc_get_tree() for reference, and
+ init/do_mounts.c:mount_root_image() for the in-kernel caller.
+
* ::
char *subtype
diff --git a/fs/fs_context.c b/fs/fs_context.c
index 23ad66cd94e1..ab853147b87b 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -85,7 +85,7 @@ int vfs_parse_fs_param_source(struct fs_context *fc, struct fs_parameter *param)
if (param->type != fs_value_is_string)
return invalf(fc, "Non-string source");
- if (fc->source)
+ if (fc->source || fc->source_file)
return invalf(fc, "Multiple sources");
fc->source = param->string;
@@ -94,6 +94,36 @@ int vfs_parse_fs_param_source(struct fs_context *fc, struct fs_parameter *param)
}
EXPORT_SYMBOL(vfs_parse_fs_param_source);
+/**
+ * vfs_parse_fs_param_file - Set an already-open file as the mount source
+ * @fc: The filesystem context to modify
+ * @key: Parameter name; only "source" is currently supported
+ * @file: The file to use as the mount source
+ *
+ * In-kernel callers (see path_mount_file()) can use this to hand a
+ * filesystem an already-opened struct file as its source, instead of a
+ * path string for the filesystem to resolve on its own. A reference is
+ * taken; it is released when @fc is freed.
+ *
+ * This deliberately has no fsconfig(FSCONFIG_SET_FD) counterpart: it is
+ * not reachable from userspace, so no filesystem needs to opt in to
+ * receive it and existing ->parse_param() implementations are unaffected.
+ * Filesystems that want to use the file directly (rather than silently
+ * ignoring it and falling back to re-resolving fc->source, which is not
+ * set in this case) check fc->source_file from their ->get_tree().
+ */
+int vfs_parse_fs_param_file(struct fs_context *fc, const char *key,
+ struct file *file)
+{
+ if (strcmp(key, "source"))
+ return -EINVAL;
+ if (fc->source || fc->source_file)
+ return invalf(fc, "Multiple sources");
+ fc->source_file = get_file(file);
+ return 0;
+}
+EXPORT_SYMBOL(vfs_parse_fs_param_file);
+
/**
* vfs_parse_fs_param - Add a single parameter to a superblock config
* @fc: The filesystem context to modify
@@ -376,6 +406,7 @@ struct fs_context *vfs_dup_fs_context(struct fs_context *src_fc)
fc->fs_private = NULL;
fc->s_fs_info = NULL;
fc->source = NULL;
+ fc->source_file = NULL;
fc->security = NULL;
get_filesystem(fc->fs_type);
get_net(fc->net_ns);
@@ -504,6 +535,8 @@ void put_fs_context(struct fs_context *fc)
put_fc_log(fc);
put_filesystem(fc->fs_type);
kfree(fc->source);
+ if (fc->source_file)
+ fput(fc->source_file);
kfree(fc);
}
EXPORT_SYMBOL(put_fs_context);
@@ -543,6 +576,10 @@ void vfs_clean_context(struct fs_context *fc)
security_free_mnt_opts(&fc->security);
kfree(fc->source);
fc->source = NULL;
+ if (fc->source_file) {
+ fput(fc->source_file);
+ fc->source_file = NULL;
+ }
fc->exclusive = false;
fc->purpose = FS_CONTEXT_FOR_RECONFIGURE;
diff --git a/fs/init.c b/fs/init.c
index 33e312d74f58..d896d937e3e2 100644
--- a/fs/init.c
+++ b/fs/init.c
@@ -44,6 +44,24 @@ int __init init_mount(const char *dev_name, const char *dir_name,
return ret;
}
+/*
+ * Like init_mount(), but the source is an already-open file rather than a
+ * path for the filesystem to resolve on its own; see path_mount_file().
+ */
+int __init init_mount_file(struct file *file, const char *dir_name,
+ const char *type_page, unsigned long flags, void *data_page)
+{
+ struct path path;
+ int ret;
+
+ ret = kern_path(dir_name, LOOKUP_FOLLOW, &path);
+ if (ret)
+ return ret;
+ ret = path_mount_file(file, &path, type_page, flags, data_page);
+ path_put(&path);
+ return ret;
+}
+
int __init init_umount(const char *name, int flags)
{
int lookup_flags = LOOKUP_MOUNTPOINT;
diff --git a/fs/internal.h b/fs/internal.h
index d77578d66d42..04fb75383024 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -91,6 +91,8 @@ 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_mount_file(struct file *file, 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);
diff --git a/fs/namespace.c b/fs/namespace.c
index fe919abd2f01..31bf263d1efc 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3783,10 +3783,14 @@ static int do_new_mount_fc(struct fs_context *fc, const struct path *mountpoint,
/*
* create a new mount for userspace and request it to be added into the
* namespace's tree
+ *
+ * @name is the source given as a path string, as usual; @file is an
+ * alternative, already-open source (see path_mount_file()). At most one
+ * of the two is ever set by callers.
*/
-static int do_new_mount(const struct path *path, const char *fstype,
- int sb_flags, int mnt_flags,
- const char *name, void *data)
+static int do_new_mount_2(const struct path *path, const char *fstype,
+ int sb_flags, int mnt_flags,
+ const char *name, struct file *file, void *data)
{
struct file_system_type *type;
struct fs_context *fc;
@@ -3826,6 +3830,8 @@ static int do_new_mount(const struct path *path, const char *fstype,
err = vfs_parse_fs_string(fc, "subtype", subtype);
if (!err && name)
err = vfs_parse_fs_string(fc, "source", name);
+ if (!err && file)
+ err = vfs_parse_fs_param_file(fc, "source", file);
if (!err)
err = parse_monolithic_mount_data(fc, data);
if (!err && !mount_capable(fc))
@@ -3837,6 +3843,14 @@ static int do_new_mount(const struct path *path, const char *fstype,
return err;
}
+static int do_new_mount(const struct path *path, const char *fstype,
+ int sb_flags, int mnt_flags,
+ const char *name, void *data)
+{
+ return do_new_mount_2(path, fstype, sb_flags, mnt_flags, name, NULL,
+ data);
+}
+
static void lock_mount_exact(const struct path *path,
struct pinned_mountpoint *mp, bool copy_mount,
unsigned int copy_flags)
@@ -4155,6 +4169,82 @@ int path_mount(const char *dev_name, const struct path *path,
data_page);
}
+/**
+ * path_mount_file - Mount a new filesystem sourced from an already-open file
+ * @file: The already-open source file
+ * @path: The mountpoint
+ * @type_page: Filesystem type
+ * @flags: MS_* flags
+ * @data_page: Filesystem-specific mount data
+ *
+ * Like path_mount(), except the source is an already-open struct file
+ * rather than a path for the filesystem to look up on its own. This is
+ * only usable for a plain new mount: bind/move/remount/propagation-change
+ * requests, which path_mount() distinguishes by inspecting @dev_name, make
+ * no sense for an anonymous open file and are rejected.
+ *
+ * @file is not consumed; the fs_context takes its own reference (see
+ * vfs_parse_fs_param_file()).
+ *
+ * This has no io_uring/syscall-visible counterpart: it exists so that
+ * in-kernel mounters (see init_mount_file()) can hand a filesystem the
+ * exact file they resolved and validated, instead of a path that the
+ * filesystem then re-resolves independently, which is the only way to
+ * guarantee the two refer to the same thing.
+ */
+int path_mount_file(struct file *file, const struct path *path,
+ const char *type_page, unsigned long flags,
+ void *data_page)
+{
+ unsigned int mnt_flags = 0, sb_flags;
+
+ if ((flags & MS_MGC_MSK) == MS_MGC_VAL)
+ flags &= ~MS_MGC_MSK;
+
+ if (data_page)
+ ((char *)data_page)[PAGE_SIZE - 1] = 0;
+
+ if (flags & (MS_NOUSER | MS_REMOUNT | MS_BIND | MS_MOVE |
+ MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
+ return -EINVAL;
+
+ if (!may_mount())
+ return -EPERM;
+ if (flags & SB_MANDLOCK)
+ warn_mandlock();
+
+ if (!(flags & MS_NOATIME))
+ mnt_flags |= MNT_RELATIME;
+ if (flags & MS_NOSUID)
+ mnt_flags |= MNT_NOSUID;
+ if (flags & MS_NODEV)
+ mnt_flags |= MNT_NODEV;
+ if (flags & MS_NOEXEC)
+ mnt_flags |= MNT_NOEXEC;
+ if (flags & MS_NOATIME)
+ mnt_flags |= MNT_NOATIME;
+ if (flags & MS_NODIRATIME)
+ mnt_flags |= MNT_NODIRATIME;
+ if (flags & MS_STRICTATIME)
+ mnt_flags &= ~(MNT_RELATIME | MNT_NOATIME);
+ if (flags & MS_RDONLY)
+ mnt_flags |= MNT_READONLY;
+ if (flags & MS_NOSYMFOLLOW)
+ mnt_flags |= MNT_NOSYMFOLLOW;
+
+ sb_flags = flags & (SB_RDONLY |
+ SB_SYNCHRONOUS |
+ SB_MANDLOCK |
+ SB_DIRSYNC |
+ SB_SILENT |
+ SB_POSIXACL |
+ SB_LAZYTIME |
+ SB_I_VERSION);
+
+ return do_new_mount_2(path, type_page, sb_flags, mnt_flags, NULL,
+ file, data_page);
+}
+
int do_mount(const char *dev_name, const char __user *dir_name,
const char *type_page, unsigned long flags, void *data_page)
{
diff --git a/include/linux/fs_context.h b/include/linux/fs_context.h
index 0d6c8a6d7be2..b35d7583f0bd 100644
--- a/include/linux/fs_context.h
+++ b/include/linux/fs_context.h
@@ -16,6 +16,7 @@
struct cred;
struct dentry;
+struct file;
struct file_operations;
struct file_system_type;
struct mnt_namespace;
@@ -99,6 +100,7 @@ struct fs_context {
const struct cred *cred; /* The mounter's credentials */
struct p_log log; /* Logging buffer */
const char *source; /* The source name (eg. dev path) */
+ struct file *source_file; /* Already-open source file, in-kernel only */
void *security; /* LSM options */
void *s_fs_info; /* Proposed s_fs_info */
unsigned int sb_flags; /* Proposed superblock flags (SB_*) */
@@ -148,6 +150,8 @@ extern int vfs_get_tree(struct fs_context *fc);
extern void put_fs_context(struct fs_context *fc);
extern int vfs_parse_fs_param_source(struct fs_context *fc,
struct fs_parameter *param);
+int vfs_parse_fs_param_file(struct fs_context *fc, const char *key,
+ struct file *file);
extern void fc_drop_locked(struct fs_context *fc);
extern int get_tree_nodev(struct fs_context *fc,
diff --git a/include/linux/init_syscalls.h b/include/linux/init_syscalls.h
index 28776ee28d8e..ac937cdb9d46 100644
--- a/include/linux/init_syscalls.h
+++ b/include/linux/init_syscalls.h
@@ -2,6 +2,8 @@
int __init init_mount(const char *dev_name, const char *dir_name,
const char *type_page, unsigned long flags, void *data_page);
+int __init init_mount_file(struct file *file, const char *dir_name,
+ const char *type_page, unsigned long flags, void *data_page);
int __init init_umount(const char *name, int flags);
int __init init_chdir(const char *filename);
int __init init_chroot(const char *filename);
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [RFC PATCH v2 2/4] erofs: use fs_context source_file for file-backed mounts when given
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
` (2 preceding siblings ...)
2026-07-27 10:48 ` [RFC PATCH v2 1/4] fs: allow in-kernel mounters to hand filesystems an already-open source file Eric Curtin
@ 2026-07-27 10:48 ` Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 3/4] init: support mounting the root filesystem from an image file Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 4/4] init: support pinning the root image's fsverity digest Eric Curtin
5 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-27 10:48 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
erofs_fc_get_tree() falls back to filp_open(fc->source, ...) itself
when get_tree_bdev_flags() reports the source isn't a block device.
That is the only way to reach erofs's file-backed mount support today,
which means an in-kernel caller that already opened and validated (e.g.
fsverity-checked) that exact file has no way to make erofs mount it: it
can only hand over the path again and trust erofs's independent lookup
to land on the same file, which it has no way to guarantee.
Check fc->source_file first and use it directly when set, taking a
reference and skipping the independent filp_open(fc->source) entirely
(fc->source is unset in this case). This has no effect on the existing
path-based fsconfig()/mount(2) flow, since userspace has no way to set
fc->source_file (see the previous patch); it only enables in-kernel
callers such as the following init/do_mounts.c change.
Signed-off-by: Eric Curtin <ericcurtin17@gmail.com>
---
fs/erofs/super.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/fs/erofs/super.c b/fs/erofs/super.c
index 802add6652fd..b6cd4fde3ca2 100644
--- a/fs/erofs/super.c
+++ b/fs/erofs/super.c
@@ -799,6 +799,25 @@ static int erofs_fc_get_tree(struct fs_context *fc)
if (IS_ENABLED(CONFIG_EROFS_FS_ONDEMAND) && sbi->fsid)
return get_tree_nodev(fc, erofs_fc_fill_super);
+ /*
+ * An in-kernel caller (see path_mount_file()) has handed us the
+ * exact, already-open file to mount, rather than a path for us to
+ * resolve ourselves: use it directly instead of independently
+ * re-opening fc->source (which isn't set in this case), so that
+ * whatever the caller validated about this file - e.g. an fsverity
+ * digest - is guaranteed to still apply to what actually gets
+ * mounted.
+ */
+ if (fc->source_file) {
+ if (!IS_ENABLED(CONFIG_EROFS_FS_BACKED_BY_FILE))
+ return invalf(fc, "File-backed mounts not supported");
+ if (!S_ISREG(file_inode(fc->source_file)->i_mode) ||
+ !fc->source_file->f_mapping->a_ops->read_folio)
+ return invalf(fc, "Source file is not a plain, read-foliable file");
+ sbi->dif0.file = get_file(fc->source_file);
+ return get_tree_nodev(fc, erofs_fc_fill_super);
+ }
+
ret = get_tree_bdev_flags(fc, erofs_fc_fill_super,
IS_ENABLED(CONFIG_EROFS_FS_BACKED_BY_FILE) ?
GET_TREE_BDEV_QUIET_LOOKUP : 0);
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [RFC PATCH v2 3/4] init: support mounting the root filesystem from an image file
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
` (3 preceding siblings ...)
2026-07-27 10:48 ` [RFC PATCH v2 2/4] erofs: use fs_context source_file for file-backed mounts when given Eric Curtin
@ 2026-07-27 10:48 ` Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 4/4] init: support pinning the root image's fsverity digest Eric Curtin
5 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-27 10:48 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
Image-based Linux systems (bootc-style OS updaters, ChromeOS/Android-like
A/B schemes, embedded appliances) commonly keep one or more immutable
root filesystem images as plain files on a writable "carrier"
filesystem and pick one at boot. Today that always requires an
initramfs, even when the initramfs has nothing else to do: the kernel
can only mount a block device (or NFS/CIFS/ubi/mtd) as root, so
userspace must mount the carrier, loop-mount the image and switch_root
into it.
Add a rootimage= parameter naming an image file on the filesystem
specified by root=. When set, root= only designates the carrier: after
mounting it as usual, mount the image file read-only on /image and make
that the root that prepare_namespace() pivots into. The image is
mounted through the filesystem's file-backed mount support (available
in erofs since v6.12) using path_mount_file()/init_mount_file() from
the preceding patches: the image is opened exactly once, and that same
struct file is what actually gets mounted, rather than a path for the
filesystem driver to resolve a second time on its own.
rootimagefstype= and rootimageflags= mirror rootfstype=/rootflags= for
the image mount, while "ro"/"rw"/rootflags= keep applying to the
carrier.
rootimagesrcdir= names a directory inside the image where the carrier
mount is moved once the image is up, since systems following this model
virtually always need continued access to the carrier for their
writable state. Unlike an earlier version of this patch, this is
mandatory rather than defaulting to silently detaching the carrier:
that default made it too easy to end up with a carrier mount that's
still alive (the image mount pins its superblock either way) but
unreachable from the new namespace, with no equivalent of
/sys/block/loopX/loop/backing_file to even tell what it was. Systems
that positively don't need the carrier again can still get the old
behaviour with rootimagesrcdir=none, but have to say so.
This is the file-backed analogue of what dm-mod.create= (CONFIG_DM_INIT)
already does for device-mapper targets: moving a fixed, declarative bit
of early-boot setup from initramfs userspace into the kernel so that
small and verified systems can boot with no initramfs at all.
Assisted-by: opencode:claude-fable-5
Signed-off-by: Eric Curtin <ericcurtin17@gmail.com>
---
.../admin-guide/kernel-parameters.txt | 34 ++++
init/do_mounts.c | 188 +++++++++++++++++-
2 files changed, 217 insertions(+), 5 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 4d0f545fb3ec..a1cd5973e497 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -6732,6 +6732,40 @@ Kernel parameters
rootfstype= [KNL] Set root filesystem type
+ rootimage= [KNL] Mount the actual root filesystem from this
+ filesystem image file (absolute path) located on the
+ filesystem specified by root=, instead of using that
+ filesystem as the root directly. This allows
+ image-based systems that keep their (typically
+ read-only, e.g. erofs) root filesystem images as
+ plain files on a carrier filesystem to boot without
+ an initramfs. The image is mounted read-only via
+ the filesystem's file-backed mount support (no loop
+ device is set up); "ro", "rw" and rootflags= keep
+ applying to the carrier filesystem. Requires
+ rootimagesrcdir=.
+
+ rootimageflags= [KNL] Set root image filesystem mount option string,
+ used together with rootimage=.
+
+ rootimagefstype= [KNL] Set root image filesystem type, used together
+ with rootimage=. Default is to try all filesystems
+ known to the kernel that can mount a block device or
+ an image file.
+
+ rootimagesrcdir= [KNL] Directory (absolute path) inside the root
+ image where the mount of the carrier filesystem
+ holding the image (i.e. the filesystem specified by
+ root=) is moved to. Mandatory when rootimage= is
+ used, since image-based systems almost always need
+ continued access to the carrier for their writable
+ state, and there would otherwise be no way to reach
+ it again once switch_root has happened. Pass
+ rootimagesrcdir=none instead to explicitly detach the
+ carrier mount, accepting that it becomes unreachable
+ (though still pinned alive by the image mount) after
+ boot.
+
rootwait [KNL] Wait (indefinitely) for root device to show up.
Useful for devices that are detected asynchronously
(e.g. USB and MMC devices).
diff --git a/init/do_mounts.c b/init/do_mounts.c
index 55ed3ac0b70f..316421d8b65b 100644
--- a/init/do_mounts.c
+++ b/init/do_mounts.c
@@ -122,13 +122,52 @@ __setup("rootflags=", root_data_setup);
__setup("rootfstype=", fs_names_setup);
__setup("rootdelay=", root_delay_setup);
+/*
+ * rootimage= mounts the actual root filesystem from an image file located
+ * on the filesystem specified by root= (the "carrier") instead of using
+ * that filesystem as the root directly, so that image-based systems can
+ * boot without an initramfs. See mount_root_image().
+ */
+static char * __initdata root_image;
+static int __init root_image_setup(char *str)
+{
+ root_image = str;
+ return 1;
+}
+
+static char * __initdata root_image_fs_names;
+static int __init root_image_fs_names_setup(char *str)
+{
+ root_image_fs_names = str;
+ return 1;
+}
+
+static char * __initdata root_image_mount_data;
+static int __init root_image_data_setup(char *str)
+{
+ root_image_mount_data = str;
+ return 1;
+}
+
+static char * __initdata root_image_srcdir;
+static int __init root_image_srcdir_setup(char *str)
+{
+ root_image_srcdir = str;
+ return 1;
+}
+
+__setup("rootimage=", root_image_setup);
+__setup("rootimagefstype=", root_image_fs_names_setup);
+__setup("rootimageflags=", root_image_data_setup);
+__setup("rootimagesrcdir=", root_image_srcdir_setup);
+
/* This can return zero length strings. Caller should check */
-static int __init split_fs_names(char *page, size_t size)
+static int __init split_fs_names(char *page, size_t size, const char *names)
{
int count = 1;
char *p = page;
- strscpy(p, root_fs_names, size);
+ strscpy(p, names, size);
while (*p++) {
if (p[-1] == ',') {
p[-1] = '\0';
@@ -186,7 +225,7 @@ void __init mount_root_generic(char *name, char *pretty_name, int flags)
scnprintf(b, BDEVNAME_SIZE, "unknown-block(%u,%u)",
MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
if (root_fs_names)
- num_fs = split_fs_names(fs_names, PAGE_SIZE);
+ num_fs = split_fs_names(fs_names, PAGE_SIZE, root_fs_names);
else
num_fs = list_bdev_fs_names(fs_names, PAGE_SIZE);
retry:
@@ -244,7 +283,144 @@ void __init mount_root_generic(char *name, char *pretty_name, int flags)
out:
put_page(page);
}
-
+
+/*
+ * Like do_mount_root(), but the source is the already-open @file rather
+ * than a device/path name for the filesystem to resolve on its own; see
+ * mount_root_image(). Unlike do_mount_root(), ROOT_DEV is left alone:
+ * it identifies the carrier (the boot device), which remains meaningful,
+ * while a file-backed root image typically has no device of its own.
+ */
+static int __init do_mount_root_file(struct file *file, const char *dir,
+ const char *fs, const int flags,
+ const void *data)
+{
+ struct super_block *s;
+ struct page *p = NULL;
+ char *data_page = NULL;
+ int ret;
+
+ if (data) {
+ p = alloc_page(GFP_KERNEL);
+ if (!p)
+ return -ENOMEM;
+ data_page = page_address(p);
+ strscpy_pad(data_page, data, PAGE_SIZE);
+ }
+
+ ret = init_mount_file(file, dir, fs, flags, data_page);
+ if (ret)
+ goto out;
+
+ init_chdir(dir);
+ s = current->fs->pwd.dentry->d_sb;
+ printk(KERN_INFO "VFS: Mounted root image (%s filesystem)%s.\n",
+ s->s_type->name, sb_rdonly(s) ? " readonly" : "");
+
+out:
+ if (p)
+ put_page(p);
+ return ret;
+}
+
+/*
+ * Mount the actual root filesystem from the image file rootimage= on the
+ * filesystem that was just mounted from root= (the "carrier"), so that
+ * image-based systems can boot without an initramfs.
+ *
+ * Called with the carrier mounted at /root and the cwd there. The image
+ * file is opened exactly once, and that same struct file is what actually
+ * gets mounted via do_mount_root_file(), rather than a path that the
+ * filesystem driver would then have to resolve again on its own with no
+ * guarantee of landing on the same thing.
+ *
+ * On success the cwd is the image's root, ready for the pivot in
+ * prepare_namespace(). rootimagesrcdir= is mandatory: the carrier is
+ * moved there inside the image, since image-based systems almost always
+ * need continued access to it for their writable state, and silently
+ * leaving it detached-but-pinned by default made it too easy to end up
+ * with a carrier mount that's alive but unreachable from the new
+ * namespace. rootimagesrcdir=none opts into that explicitly instead.
+ */
+static void __init mount_root_image(void)
+{
+ unsigned long flags = MS_RDONLY | MS_SILENT;
+ char *path, *fs_names, *p;
+ struct file *file;
+ int num_fs, i, err;
+
+ if (root_image[0] != '/')
+ panic("VFS: rootimage= must be an absolute path");
+ if (!root_image_srcdir)
+ panic("VFS: rootimage= requires rootimagesrcdir= (use rootimagesrcdir=none to detach the carrier instead, making it unreachable after boot)");
+ if (strcmp(root_image_srcdir, "none") && root_image_srcdir[0] != '/')
+ panic("VFS: rootimagesrcdir= must be an absolute path or \"none\"");
+
+ path = kmalloc(PATH_MAX, GFP_KERNEL);
+ fs_names = kmalloc(PAGE_SIZE, GFP_KERNEL);
+ if (!path || !fs_names)
+ panic("VFS: unable to mount root image: not enough memory");
+
+ if (snprintf(path, PATH_MAX, "/root%s", root_image) >= PATH_MAX)
+ panic("VFS: rootimage= path too long");
+
+ file = filp_open(path, O_RDONLY | O_LARGEFILE, 0);
+ if (IS_ERR(file))
+ panic("VFS: unable to open root image %s: error %ld",
+ root_image, PTR_ERR(file));
+
+ err = init_mkdir("/image", 0700);
+ if (err < 0 && err != -EEXIST)
+ panic("VFS: unable to create /image: error %d", err);
+
+ if (root_image_fs_names)
+ num_fs = split_fs_names(fs_names, PAGE_SIZE,
+ root_image_fs_names);
+ else
+ num_fs = list_bdev_fs_names(fs_names, PAGE_SIZE);
+
+ for (i = 0, p = fs_names; i < num_fs; i++, p += strlen(p) + 1) {
+ if (!*p)
+ continue;
+ err = do_mount_root_file(file, "/image", p, flags,
+ root_image_mount_data);
+ switch (err) {
+ case 0:
+ goto mounted;
+ case -EACCES:
+ case -EINVAL:
+ case -ENOTBLK:
+ continue;
+ }
+ panic("VFS: unable to mount root image %s: error %d",
+ root_image, err);
+ }
+ panic("VFS: no filesystem could mount root image %s", root_image);
+
+mounted:
+ /*
+ * do_mount_root_file() took its own reference to the file (see
+ * vfs_parse_fs_param_file()); this reference was only needed to
+ * open it in the first place.
+ */
+ fput(file);
+
+ if (!strcmp(root_image_srcdir, "none")) {
+ init_umount("/root", MNT_DETACH);
+ } else {
+ if (snprintf(path, PATH_MAX, "/image%s", root_image_srcdir) >=
+ PATH_MAX)
+ panic("VFS: rootimagesrcdir= path too long");
+ err = init_mount("/root", path, NULL, MS_MOVE, NULL);
+ if (err)
+ panic("VFS: failed to move the root image's carrier filesystem to %s: error %d",
+ root_image_srcdir, err);
+ }
+
+ kfree(path);
+ kfree(fs_names);
+}
+
#ifdef CONFIG_ROOT_NFS
#define NFSROOT_TIMEOUT_MIN 5
@@ -346,7 +522,7 @@ static int __init mount_nodev_root(char *root_device_name)
fs_names = (void *)__get_free_page(GFP_KERNEL);
if (!fs_names)
return -EINVAL;
- num_fs = split_fs_names(fs_names, PAGE_SIZE);
+ num_fs = split_fs_names(fs_names, PAGE_SIZE, root_fs_names);
for (i = 0, fstype = fs_names; i < num_fs;
i++, fstype += strlen(fstype) + 1) {
@@ -482,6 +658,8 @@ void __init prepare_namespace(void)
if (root_wait)
wait_for_root(saved_root_name);
mount_root(saved_root_name);
+ if (root_image)
+ mount_root_image();
devtmpfs_mount();
if (init_pivot_root(".", ".")) {
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [RFC PATCH v2 4/4] init: support pinning the root image's fsverity digest
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
` (4 preceding siblings ...)
2026-07-27 10:48 ` [RFC PATCH v2 3/4] init: support mounting the root filesystem from an image file Eric Curtin
@ 2026-07-27 10:48 ` Eric Curtin
5 siblings, 0 replies; 10+ messages in thread
From: Eric Curtin @ 2026-07-27 10:48 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner
Cc: Jan Kara, Jonathan Corbet, Shuah Khan, Eric Biggers,
Theodore Y . Ts'o, Gao Xiang, Chao Yu, fsverity, linux-erofs,
linux-fsdevel, linux-doc, linux-kernel, Eric Curtin
When the root filesystem is mounted from an image file with rootimage=,
the carrier filesystem holding the image is typically writable and
therefore untrusted. Systems that seal their root images with fsverity
currently need an initramfs for the sole purpose of checking that the
image carries the expected fsverity digest before mounting it.
Add rootimageverity=<hash algorithm>:<hex digest>, which requires the
rootimage= file to have fsverity enabled with exactly this file digest
and fails the boot otherwise, using the same fsverity_get_digest()
interface that IMA and overlayfs already use for digest pinning.
Verification runs on the exact struct file that mount_root_image() then
hands to do_mount_root_file(): the fd-based mount from the preceding
patches means there is no separate path lookup afterwards that could
resolve to something else, and nothing between the check and the mount
that a userspace-less boot could race.
Combined with a trusted kernel command line (e.g. a signed unified
kernel image, or a TPM-measured bootloader configuration), this extends
the chain of trust to every byte of the root filesystem without any
userspace boot stage: the digest pins the image's Merkle tree, and
fsverity keeps verifying all data read from the image against it at
runtime, so post-boot tampering with the carrier filesystem is detected
as well. It is the file-backed counterpart of setting up a dm-verity
target for a partition-backed root via dm-mod.create=.
Two things this does not and cannot do:
- Establish trust in the carrier filesystem itself. Reaching the image
file at all means parsing carrier filesystem metadata (superblock,
directory entries, extents) before any of this runs, the same way a
dm-verity root still needs a trusted block layer underneath it -
this is meant to sit on top of an already-appropriately-trusted
carrier (e.g. one that is itself dm-verity/LUKS-backed, or a
well-audited always-read-only filesystem), not conjure trust in an
arbitrary writable one.
- Cover erofs images that pull in extra devices via
rootimageflags=device=device=..., since the pinned digest only ever
applies to the primary image file. Refuse to boot in that
combination rather than give a false sense of integrity.
Assisted-by: opencode:claude-fable-5
Signed-off-by: Eric Curtin <ericcurtin17@gmail.com>
---
.../admin-guide/kernel-parameters.txt | 22 +++++
init/do_mounts.c | 91 ++++++++++++++++++-
2 files changed, 108 insertions(+), 5 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index a1cd5973e497..dcb5a78fba55 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -6766,6 +6766,28 @@ Kernel parameters
(though still pinned alive by the image mount) after
boot.
+ rootimageverity= [KNL] Require the root image specified by
+ rootimage= to have fsverity enabled with this file
+ digest, given as <hash algorithm>:<hex digest>,
+ e.g. sha256:dd1b3fa9... The boot is aborted if the
+ image carries no or a different fsverity digest, or
+ if rootimageflags= adds extra devices via device=
+ (their contents would not be covered by the digest).
+ Because fsverity keeps verifying data read from the
+ image against its Merkle tree at runtime, a trusted
+ (e.g. signed or TPM-measured) kernel command line
+ extends the chain of trust to the complete root
+ filesystem contents without an initramfs, provided
+ the carrier filesystem holding the image is itself
+ from a trusted source (rootimage= verifies the image
+ file; it does not and cannot retroactively establish
+ trust in the carrier filesystem code and metadata
+ that had to be parsed to reach that file in the first
+ place - the same way a dm-verity root still needs a
+ trusted block layer under it). Requires
+ CONFIG_FS_VERITY and a carrier filesystem with
+ fsverity support.
+
rootwait [KNL] Wait (indefinitely) for root device to show up.
Useful for devices that are detected asynchronously
(e.g. USB and MMC devices).
diff --git a/init/do_mounts.c b/init/do_mounts.c
index 316421d8b65b..d4362fd758d8 100644
--- a/init/do_mounts.c
+++ b/init/do_mounts.c
@@ -24,6 +24,9 @@
#include <linux/nfs_fs_sb.h>
#include <linux/nfs_mount.h>
#include <linux/raid/detect.h>
+#include <linux/fsverity.h>
+#include <linux/hex.h>
+#include <crypto/hash_info.h>
#include <uapi/linux/mount.h>
#include "do_mounts.h"
@@ -156,10 +159,18 @@ static int __init root_image_srcdir_setup(char *str)
return 1;
}
+static char * __initdata root_image_verity;
+static int __init root_image_verity_setup(char *str)
+{
+ root_image_verity = str;
+ return 1;
+}
+
__setup("rootimage=", root_image_setup);
__setup("rootimagefstype=", root_image_fs_names_setup);
__setup("rootimageflags=", root_image_data_setup);
__setup("rootimagesrcdir=", root_image_srcdir_setup);
+__setup("rootimageverity=", root_image_verity_setup);
/* This can return zero length strings. Caller should check */
static int __init split_fs_names(char *page, size_t size, const char *names)
@@ -323,16 +334,73 @@ static int __init do_mount_root_file(struct file *file, const char *dir,
return ret;
}
+#ifdef CONFIG_FS_VERITY
+/*
+ * Require the root image to carry the fsverity file digest given by
+ * rootimageverity=<hash algorithm>:<hex digest>. @file must have been
+ * opened so that its fsverity information is loaded, and must be the
+ * exact file that do_mount_root_file() then mounts (see
+ * mount_root_image()): unlike checking a file and then separately
+ * mounting a path that is assumed, but not guaranteed, to name the same
+ * thing, there is no lookup left for anything to race or disagree with
+ * between this check and the mount.
+ *
+ * Any deviation fails the boot: with a trusted command line this pins
+ * the complete image contents, which fsverity keeps verifying against
+ * the image's Merkle tree as they are read.
+ */
+static void __init verify_root_image(struct file *file)
+{
+ u8 want[FS_VERITY_MAX_DIGEST_SIZE], got[FS_VERITY_MAX_DIGEST_SIZE];
+ enum hash_algo want_algo, got_algo;
+ int want_size, got_size, i;
+ char *hex;
+
+ hex = strchr(root_image_verity, ':');
+ if (!hex)
+ panic("VFS: rootimageverity= expects <algorithm>:<hex digest>");
+ *hex++ = '\0';
+ i = match_string(hash_algo_name, HASH_ALGO__LAST, root_image_verity);
+ if (i < 0)
+ panic("VFS: rootimageverity=: unknown hash algorithm \"%s\"",
+ root_image_verity);
+ want_algo = i;
+ want_size = hash_digest_size[want_algo];
+ if (strlen(hex) != 2 * want_size || hex2bin(want, hex, want_size))
+ panic("VFS: rootimageverity=: expected %d-byte hex digest",
+ want_size);
+
+ got_size = fsverity_get_digest(file_inode(file), got, NULL, &got_algo);
+ if (!got_size)
+ panic("VFS: root image does not have fsverity enabled");
+ if (got_algo != want_algo || got_size != want_size ||
+ memcmp(want, got, want_size))
+ panic("VFS: root image fsverity digest mismatch: expected %s:%*phN, got %s:%*phN",
+ hash_algo_name[want_algo], want_size, want,
+ hash_algo_name[got_algo], got_size, got);
+
+ pr_info("VFS: verified root image fsverity digest %s:%*phN\n",
+ hash_algo_name[want_algo], want_size, want);
+}
+#else /* !CONFIG_FS_VERITY */
+static void __init verify_root_image(struct file *file)
+{
+ panic("VFS: rootimageverity= requires CONFIG_FS_VERITY");
+}
+#endif /* !CONFIG_FS_VERITY */
+
/*
* Mount the actual root filesystem from the image file rootimage= on the
* filesystem that was just mounted from root= (the "carrier"), so that
* image-based systems can boot without an initramfs.
*
* Called with the carrier mounted at /root and the cwd there. The image
- * file is opened exactly once, and that same struct file is what actually
- * gets mounted via do_mount_root_file(), rather than a path that the
- * filesystem driver would then have to resolve again on its own with no
- * guarantee of landing on the same thing.
+ * file is opened exactly once; that same struct file is optionally
+ * fsverity-checked and is what actually gets mounted via
+ * do_mount_root_file(), so there is no window in which the checked file
+ * could be swapped for another before it is mounted, and no independent,
+ * second path lookup for a filesystem driver to disagree with about what
+ * "the image" refers to.
*
* On success the cwd is the image's root, ready for the pivot in
* prepare_namespace(). rootimagesrcdir= is mandatory: the carrier is
@@ -355,6 +423,16 @@ static void __init mount_root_image(void)
panic("VFS: rootimage= requires rootimagesrcdir= (use rootimagesrcdir=none to detach the carrier instead, making it unreachable after boot)");
if (strcmp(root_image_srcdir, "none") && root_image_srcdir[0] != '/')
panic("VFS: rootimagesrcdir= must be an absolute path or \"none\"");
+ /*
+ * A digest pinned via rootimageverity= only ever covers the
+ * primary image file; an erofs image that pulls in extra devices
+ * via rootimageflags=device=... would silently leave those
+ * unverified, so refuse rather than give a false sense of
+ * integrity.
+ */
+ if (root_image_verity && root_image_mount_data &&
+ strstr(root_image_mount_data, "device="))
+ panic("VFS: rootimageverity= does not cover extra devices added via rootimageflags=device=; refusing to boot");
path = kmalloc(PATH_MAX, GFP_KERNEL);
fs_names = kmalloc(PAGE_SIZE, GFP_KERNEL);
@@ -369,6 +447,9 @@ static void __init mount_root_image(void)
panic("VFS: unable to open root image %s: error %ld",
root_image, PTR_ERR(file));
+ if (root_image_verity)
+ verify_root_image(file);
+
err = init_mkdir("/image", 0700);
if (err < 0 && err != -EEXIST)
panic("VFS: unable to create /image: error %d", err);
@@ -401,7 +482,7 @@ static void __init mount_root_image(void)
/*
* do_mount_root_file() took its own reference to the file (see
* vfs_parse_fs_param_file()); this reference was only needed to
- * open it in the first place.
+ * open it and, if requested, check its fsverity digest.
*/
fput(file);
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-07-27 10:49 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-18 19:15 [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Eric Curtin
2026-07-18 19:15 ` [RFC PATCH 1/2] init: support mounting the root filesystem from an image file Eric Curtin
2026-07-18 19:15 ` [RFC PATCH 2/2] init: support pinning the root image's fsverity digest Eric Curtin
2026-07-27 8:55 ` [RFC PATCH 0/2] init: boot image-based systems without an initramfs (rootimage=) Christian Brauner
2026-07-27 10:31 ` Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 0/4] " Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 1/4] fs: allow in-kernel mounters to hand filesystems an already-open source file Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 2/4] erofs: use fs_context source_file for file-backed mounts when given Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 3/4] init: support mounting the root filesystem from an image file Eric Curtin
2026-07-27 10:48 ` [RFC PATCH v2 4/4] init: support pinning the root image's fsverity digest Eric Curtin
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.