/
githubmirror
/
ostree
Обзор
Документация
Войти
/
githubmirror
/
ostree
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/switchroot/ostree-prepare-root.c
514 строк
20 KB
Ricardo Salveti
prepare-root: create /run/systemd/volatile-root for composefs
03 июл 2026, 05:57
03 июл 2026, 05:57
62109dc
Код
Авторство
О чём код?
/* -*- c-file-style: "gnu" -*- * Switch to new root directory and start init. * * Copyright 2011,2012,2013 Colin Walters <walters@verbum.org> * * Based on code from util-linux/sys-utils/switch_root.c, * Copyright 2002-2009 Red Hat, Inc. All rights reserved. * Authors: * Peter Jones <pjones@redhat.com> * Jeremy Katz <katzj@redhat.com> * * Relicensed with permission to LGPLv2+. * * SPDX-License-Identifier: LGPL-2.0+ * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see <https://www.gnu.org/licenses/>. */ /* The high level goal of ostree-prepare-root.service is to run inside * the initial ram disk (if one is in use) and set up the `/` mountpoint * to be the deployment root, using the ostree= kernel commandline * argument to find the target deployment root. * * It's really the heart of how ostree works - basically multiple * hardlinked chroot() targets are maintained, this one does the equivalent * of chroot(). * * # ostree-prepare-root.service * * If using systemd, an excellent reference is `man bootup`. This * service runs Before=initrd-root-fs.target. At this point it's * assumed that the block storage and root filesystem are mounted at * /sysroot - i.e. /sysroot points to the *physical* root before * this service runs. After, `/` is the deployment root, and /sysroot is * the physical root. * * # Running as pid 1 * * See ostree-prepare-root-static.c for this. */ #include "config.h" #include <ctype.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <libglnx.h> #include <linux/btrfs.h> #include <linux/magic.h> #include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mount.h> #include <sys/param.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/sysmacros.h> #include <sys/types.h> #include <sys/vfs.h> #include <unistd.h> #include <ostree-core.h> #include <ostree-repo-private.h> #include "ot-keyfile-utils.h" #include "otcore.h" #define SYSROOT_KEY "sysroot" #define READONLY_KEY "readonly" #define OSTREE_PREPARE_ROOT_DEPLOYMENT_MSG \ SD_ID128_MAKE (71, 70, 33, 6a, 73, ba, 46, 01, ba, d3, 1a, f8, 88, aa, 0d, f7) // A temporary mount point #define TMP_SYSROOT "/sysroot.tmp" #include "ostree-mount-util.h" static GOptionEntry options[] = { { NULL } }; static bool sysroot_is_configured_ro (const char *sysroot) { g_autoptr (GError) local_error = NULL; g_autofree char *repo_config_path = g_build_filename (sysroot, "ostree/repo/config", NULL); g_autoptr (GKeyFile) repo_config = g_key_file_new (); if (!g_key_file_load_from_file (repo_config, repo_config_path, G_KEY_FILE_NONE, &local_error)) { g_printerr ("Failed to load %s: %s", repo_config_path, local_error->message); return false; } return g_key_file_get_boolean (repo_config, SYSROOT_KEY, READONLY_KEY, NULL); } static char * resolve_deploy_path (const char *kernel_cmdline, const char *root_mountpoint) { char destpath[PATH_MAX]; struct stat stbuf; char *deploy_path; g_autoptr (GError) error = NULL; g_autofree char *ostree_target = NULL; if (!otcore_get_ostree_target (kernel_cmdline, NULL, &ostree_target, &error)) errx (EXIT_FAILURE, "Failed to determine ostree target: %s", error->message); if (!ostree_target) errx (EXIT_FAILURE, "No ostree target found"); if (snprintf (destpath, sizeof (destpath), "%s/%s", root_mountpoint, ostree_target) < 0) err (EXIT_FAILURE, "failed to assemble ostree target path"); if (lstat (destpath, &stbuf) < 0) err (EXIT_FAILURE, "Couldn't find specified OSTree root '%s'", destpath); if (!S_ISLNK (stbuf.st_mode)) errx (EXIT_FAILURE, "OSTree target is not a symbolic link: %s", destpath); deploy_path = realpath (destpath, NULL); if (deploy_path == NULL) err (EXIT_FAILURE, "realpath(%s) failed", destpath); if (stat (deploy_path, &stbuf) < 0) err (EXIT_FAILURE, "stat(%s) failed", deploy_path); /* Quiet logs if there's no journal */ const char *resolved_path = deploy_path + strlen (root_mountpoint); ot_journal_send ("MESSAGE=Resolved OSTree target to: %s", deploy_path, "MESSAGE_ID=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL (OSTREE_PREPARE_ROOT_DEPLOYMENT_MSG), "DEPLOYMENT_PATH=%s", resolved_path, "DEPLOYMENT_DEVICE=%" PRIu64, (uint64_t)stbuf.st_dev, "DEPLOYMENT_INODE=%" PRIu64, (uint64_t)stbuf.st_ino, NULL); return deploy_path; } /* Resolve the block device backing a mounted btrfs filesystem, mirroring * systemd's btrfs_get_block_device_fd(): btrfs mounts expose an anonymous * st_dev, so the backing device has to be queried via ioctl. Multi-device * filesystems are rejected (as systemd does) since no single backing device * can be determined for them. Returns 0 if no device was found. */ static dev_t get_btrfs_block_device (const char *path) { glnx_autofd int fd = open (path, O_RDONLY | O_CLOEXEC | O_NONBLOCK | O_NOCTTY); if (fd < 0) return 0; struct statfs sfs; if (fstatfs (fd, &sfs) < 0 || sfs.f_type != BTRFS_SUPER_MAGIC) return 0; struct btrfs_ioctl_fs_info_args fsi = { 0 }; if (ioctl (fd, BTRFS_IOC_FS_INFO, &fsi) < 0) return 0; if (fsi.num_devices != 1) return 0; for (uint64_t id = 1; id <= fsi.max_id; id++) { struct btrfs_ioctl_dev_info_args di = { .devid = id }; struct stat st; if (ioctl (fd, BTRFS_IOC_DEV_INFO, &di) < 0) { if (errno == ENODEV) continue; return 0; } if (stat ((char *)di.path, &st) < 0) return 0; if (!S_ISBLK (st.st_mode) || major (st.st_rdev) == 0) return 0; return st.st_rdev; } return 0; } /* systemd resolves the block device backing an overlay root via the * /run/systemd/volatile-root symlink (see blockdev_get_root()). With * composefs both / and /usr are overlays with an anonymous st_dev, so tools * like systemd-gpt-auto-generator would otherwise be unable to find the * backing disk (breaking e.g. ESP automounting). Point it at the physical * root device. The symlink target must use the /dev/block/MAJ:MIN form, as * systemd never stats the path. * * Prefer /dev/gpt-auto-root when it exists: on systemd-based initrds udev * has already resolved the root partition for us. Otherwise (e.g. initrds * without systemd) fall back to the device backing the root mountpoint, * where btrfs needs special handling due to its anonymous st_dev. */ static void create_volatile_root_symlink (const char *root_mountpoint) { dev_t dev = 0; struct stat st; if (stat ("/dev/gpt-auto-root", &st) == 0 && S_ISBLK (st.st_mode)) dev = st.st_rdev; else if (stat (root_mountpoint, &st) == 0) { if (major (st.st_dev) > 0) dev = st.st_dev; else dev = get_btrfs_block_device (root_mountpoint); } if (dev == 0) return; g_autofree char *devpath = g_strdup_printf ("/dev/block/%u:%u", major (dev), minor (dev)); (void)mkdir ("/run/systemd", 0755); if (symlink (devpath, "/run/systemd/volatile-root") < 0 && errno != EEXIST) g_printerr ("failed to create /run/systemd/volatile-root: %s\n", strerror (errno)); } int main (int argc, char *argv[]) { char srcpath[PATH_MAX]; struct stat stbuf; g_autoptr (GError) error = NULL; g_autoptr (GOptionContext) context = g_option_context_new ("SYSROOT [KERNEL_CMDLINE]"); g_option_context_add_main_entries (context, options, NULL); if (!g_option_context_parse (context, &argc, &argv, &error)) errx (EXIT_FAILURE, "Error parsing options: %s", error->message); if (argc < 2) err (EXIT_FAILURE, "usage: ostree-prepare-root SYSROOT [KERNEL_CMDLINE]"); const char *root_arg = argv[1]; g_autofree char *kernel_cmdline = NULL; if (argc < 3) { kernel_cmdline = read_proc_cmdline (); } else { // Duplicate argv[2] so g_autofree can safely manage it. kernel_cmdline = g_strdup (argv[2]); } if (!kernel_cmdline) errx (EXIT_FAILURE, "Failed to read kernel cmdline"); // Since several APIs want to operate in terms of file descriptors, let's // open the initramfs now. Currently this is just used for the config parser. glnx_autofd int initramfs_rootfs_fd = -1; if (!glnx_opendirat (AT_FDCWD, "/", FALSE, &initramfs_rootfs_fd, &error)) errx (EXIT_FAILURE, "Failed to open /: %s", error->message); g_autoptr (GKeyFile) config = otcore_load_config (initramfs_rootfs_fd, PREPARE_ROOT_CONFIG_PATH, &error); if (!config) errx (EXIT_FAILURE, "Failed to parse config: %s", error->message); gboolean sysroot_readonly = FALSE; // We always parse the composefs config, because we want to detect and error // out if it's enabled, but not supported at compile time. g_autoptr (RootConfig) rootfs_config = otcore_load_rootfs_config (kernel_cmdline, config, TRUE, &error); if (!rootfs_config) errx (EXIT_FAILURE, "%s", error->message); // If composefs is enabled, that also implies sysroot.readonly=true because it's // the new default we want to use (not because it's actually required) const bool sysroot_readonly_default = rootfs_config->composefs_enabled == OT_TRISTATE_YES; if (!ot_keyfile_get_boolean_with_default (config, SYSROOT_KEY, READONLY_KEY, sysroot_readonly_default, &sysroot_readonly, &error)) errx (EXIT_FAILURE, "Failed to parse sysroot.readonly value: %s", error->message); /* This is the final target where we should prepare the rootfs. The usual * case with systemd in the initramfs is that root_mountpoint = "/sysroot". * In the fastboot embedded case we're pid1 and will setup / ourself, and * then root_mountpoint = "/". * */ const char *root_mountpoint = realpath (root_arg, NULL); if (root_mountpoint == NULL) err (EXIT_FAILURE, "realpath(\"%s\")", root_arg); g_autofree char *deploy_path = resolve_deploy_path (kernel_cmdline, root_mountpoint); const char *deploy_directory_name = glnx_basename (deploy_path); // Note that realpath() should have stripped any trailing `/` which shouldn't // be in the karg to start with, but we assert here to be sure we have a non-empty // filename. g_assert (deploy_directory_name && *deploy_directory_name); /* These are global state directories underneath /run */ if (mkdirat (AT_FDCWD, OTCORE_RUN_OSTREE, 0755) < 0) err (EXIT_FAILURE, "Failed to create %s", OTCORE_RUN_OSTREE); if (mkdirat (AT_FDCWD, OTCORE_RUN_OSTREE_PRIVATE, 0) < 0) err (EXIT_FAILURE, "Failed to create %s", OTCORE_RUN_OSTREE_PRIVATE); /* Fall back to querying the repository configuration in the target disk. * This is an operating system builder choice. More info: * https://github.com/ostreedev/ostree/pull/1767 * However, we only do this if composefs is not enabled, because we don't * want to parse the target root filesystem before verifying its integrity. */ if (!sysroot_readonly && rootfs_config->composefs_enabled != OT_TRISTATE_YES) { sysroot_readonly = sysroot_is_configured_ro (root_arg); // Encourage porting to the new config file if (sysroot_readonly) g_print ("Found legacy sysroot.readonly flag, not configured in %s\n", PREPARE_ROOT_CONFIG_PATH); } const bool sysroot_currently_writable = !path_is_on_readonly_fs (root_arg); g_print ("sysroot.readonly configuration value: %d (fs writable: %d)\n", (int)sysroot_readonly, (int)sysroot_currently_writable); if (rootfs_config->root_transient) { g_print ("root.transient: %d (ro: %d)\n", (int)rootfs_config->root_transient, (int)rootfs_config->root_transient_ro); } /* Remount root MS_PRIVATE here to avoid errors due to the kernel-enforced * constraint that disallows MS_SHARED mounts to be moved. * * Kernel docs: Documentation/filesystems/sharedsubtree.txt */ if (mount (NULL, "/", NULL, MS_REC | MS_PRIVATE | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to make \"/\" private mount"); if (mkdir (TMP_SYSROOT, 0755) < 0 && errno != EEXIST) err (EXIT_FAILURE, "couldn't create temporary sysroot %s", TMP_SYSROOT); /* Run in the deploy_path dir so we can use relative paths below */ if (chdir (deploy_path) < 0) err (EXIT_FAILURE, "failed to chdir to deploy_path"); GVariantBuilder metadata_builder; g_variant_builder_init (&metadata_builder, G_VARIANT_TYPE ("a{sv}")); // Tracks if we did successfully enable it at runtime bool using_composefs = false; if (!otcore_mount_rootfs (rootfs_config, &metadata_builder, root_mountpoint, deploy_path, TMP_SYSROOT, &using_composefs, &error)) errx (EXIT_FAILURE, "Failed to mount composefs: %s", error->message); if (!using_composefs) { if (rootfs_config->root_transient) { errx (EXIT_FAILURE, "Must enable composefs with root.transient"); } g_print ("Using legacy ostree bind mount for /\n"); /* The deploy root starts out bind mounted to sysroot.tmp */ if (mount (deploy_path, TMP_SYSROOT, NULL, MS_BIND | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to make initial bind mount %s", deploy_path); } else create_volatile_root_symlink (root_mountpoint); /* Pass on the state for use by ostree-prepare-root */ g_variant_builder_add (&metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_SYSROOT_RO, g_variant_new_boolean (sysroot_readonly)); /* /boot is handled by ostree-system-generator which generates a boot.mount * unit when /boot is on the same partition. This works for all boot scenarios * including soft-reboot. See ostree-impl-system-generator.c */ /* Prepare /etc. * No action required if sysroot is writable. Otherwise, a bind-mount for * the deployment needs to be created and remounted as read/write. */ if (sysroot_readonly || using_composefs || rootfs_config->root_transient) { if (!otcore_mount_etc (config, &metadata_builder, TMP_SYSROOT, &error)) errx (EXIT_FAILURE, "Failed to mount etc: %s", error->message); } /* Prepare /usr. * It may be either just a read-only bind-mount, or a persistent overlayfs if set up * with ostree admin unlock --hotfix. * Note however that root.transient as handled above is effectively a generalization of unlock * --hotfix. * Also, hotfixes are incompatible with signed composefs use for security reasons. */ if (lstat (OTCORE_HOTFIX_USR_OVL_WORK, &stbuf) == 0 && !(using_composefs && rootfs_config->is_signed)) { /* Do we have a persistent overlayfs for /usr? If so, mount it now. */ const char usr_ovl_options[] = "lowerdir=" TMP_SYSROOT "/usr,upperdir=.usr-ovl-upper,workdir=" OTCORE_HOTFIX_USR_OVL_WORK; unsigned long mflags = MS_SILENT; // Propagate readonly state if (!sysroot_currently_writable) mflags |= MS_RDONLY; if (mount ("overlay", TMP_SYSROOT "/usr", "overlay", mflags, usr_ovl_options) < 0) err (EXIT_FAILURE, "failed to mount /usr overlayfs"); } else if (!using_composefs) { /* Otherwise, a read-only bind mount for /usr. (Not needed for composefs) */ if (mount (TMP_SYSROOT "/usr", TMP_SYSROOT "/usr", NULL, MS_BIND | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to bind mount (class:readonly) /usr"); if (mount (TMP_SYSROOT "/usr", TMP_SYSROOT "/usr", NULL, MS_BIND | MS_REMOUNT | MS_RDONLY | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to bind mount (class:readonly) /usr"); } /* Prepare /sysroot. * The future / (currently at /sysroot.tmp) is an overlayfs or composefs that uses * the physical root (currently at /sysroot), and we want to mount the physical root * on top of the future / (at /sysroot.tmp/sysroot). * If we MS_MOVE /sysroot to /sysroot.tmp/sysroot, we end up with a mount cycle, * and systemd fails to unmount sysroot.mount. * To avoid the mount cycle, bind-mount the physical root and then detach it. */ if (mount (root_mountpoint, TMP_SYSROOT "/sysroot", NULL, MS_BIND | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to MS_BIND '%s' to 'sysroot'", root_mountpoint); if (umount2 (root_mountpoint, MNT_DETACH) < 0) err (EXIT_FAILURE, "failed to MS_DETACH '%s'", root_mountpoint); /* Resolve deploy path again so we can use paths relative to the physical root bind-mount */ g_autofree char *deploy_path2 = resolve_deploy_path (kernel_cmdline, TMP_SYSROOT "/sysroot"); if (chdir (deploy_path2) < 0) err (EXIT_FAILURE, "failed to chdir to deploy_path2"); /* Prepare /var. * When a read-only sysroot is configured, this adds a dedicated bind-mount (to itself) * so that the stateroot location stays writable. */ if (sysroot_readonly) { /* Bind-mount /var (at stateroot path), and remount as writable. */ if (mount ("../../var", "../../var", NULL, MS_BIND | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to prepare /var bind-mount at %s", srcpath); if (mount ("../../var", "../../var", NULL, MS_BIND | MS_REMOUNT | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to make writable /var bind-mount at %s", srcpath); } /* When running under systemd, /var will be handled by a 'var.mount' unit outside * of initramfs. * Systemd auto-detection can be overridden by a marker file under /run. */ #ifdef HAVE_SYSTEMD_AND_LIBMOUNT bool mount_var = false; #else bool mount_var = true; #endif if (lstat (INITRAMFS_MOUNT_VAR, &stbuf) == 0) mount_var = true; /* If required, bind-mount `/var` in the deployment to the "stateroot", which is * the shared persistent directory for a set of deployments. More info: * https://ostreedev.github.io/ostree/deployment/#stateroot-aka-osname-group-of-deployments-that-share-var */ if (mount_var) { if (mount ("../../var", TMP_SYSROOT "/var", NULL, MS_BIND | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to bind mount ../../var to var"); /* To avoid having submounts of /var propagate into $stateroot/var, the * mount is made with slave+shared propagation. See the comment in * ostree-impl-system-generator.c when /var isn't mounted in the * initramfs for further explanation. */ if (mount (NULL, TMP_SYSROOT "/var", NULL, MS_SLAVE | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to change /var to slave mount"); if (mount (NULL, TMP_SYSROOT "/var", NULL, MS_SHARED | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to change /var to slave+shared mount"); } /* This can be used by other things to signal ostree is in use */ { g_autoptr (GVariant) metadata = g_variant_ref_sink (g_variant_builder_end (&metadata_builder)); const guint8 *buf = g_variant_get_data (metadata) ?: (guint8 *)""; if (!glnx_file_replace_contents_at (AT_FDCWD, OTCORE_RUN_BOOTED, buf, g_variant_get_size (metadata), 0, NULL, &error)) errx (EXIT_FAILURE, "Writing %s: %s", OTCORE_RUN_BOOTED, error->message); } /* Now we have our ready made-up deploy root at /sysroot.tmp, * we just need to move it to /sysroot (root_mountpoint). * systemd will be responsible for moving /sysroot to /. */ if (mount (TMP_SYSROOT, root_mountpoint, NULL, MS_MOVE | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to MS_MOVE %s to %s", TMP_SYSROOT, root_mountpoint); if (chdir (root_mountpoint) < 0) err (EXIT_FAILURE, "failed to chdir to %s", root_mountpoint); if (rmdir (TMP_SYSROOT) < 0) err (EXIT_FAILURE, "couldn't remove temporary sysroot %s", TMP_SYSROOT); /* Now that we've set up all the mount points, if configured we remount the physical * rootfs as read-only; what is visibly mutable to the OS by default is just /etc and /var. * But ostree knows how to mount /boot and /sysroot read-write to perform operations. */ if (sysroot_readonly) { if (mount ("sysroot", "sysroot", NULL, MS_BIND | MS_REMOUNT | MS_RDONLY | MS_SILENT, NULL) < 0) err (EXIT_FAILURE, "failed to make /sysroot read-only"); } exit (EXIT_SUCCESS); }