From e3e9e903fa64e6cde02fa1b5f41f124a0afddb3f Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 3 Oct 2018 14:57:19 +0000 Subject: [PATCH] WIP: Support mounting /sysroot (and /boot) read-only Add a magic file and an API for consumers to tell us it's OK for us to remount read-write (since it'll just affect our mount namespace). --- apidoc/ostree-sections.txt | 2 + src/libostree/libostree-devel.sym | 2 + src/libostree/ostree-sysroot-cleanup.c | 6 +++ src/libostree/ostree-sysroot-deploy.c | 27 ++++++++-- src/libostree/ostree-sysroot-private.h | 5 ++ src/libostree/ostree-sysroot.c | 71 +++++++++++++++++++++++++- src/libostree/ostree-sysroot.h | 11 ++++ src/ostree/ot-main.c | 30 +++++++++++ src/switchroot/ostree-remount.c | 70 +++++++++++++++++++------ 9 files changed, 203 insertions(+), 21 deletions(-) diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index 5dbafc5fdf..11e131710c 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -495,6 +495,7 @@ ostree_sepolicy_get_type OstreeSysroot ostree_sysroot_new ostree_sysroot_new_default +ostree_sysroot_initialize ostree_sysroot_get_path ostree_sysroot_load ostree_sysroot_load_if_changed @@ -504,6 +505,7 @@ ostree_sysroot_lock_async ostree_sysroot_lock_finish ostree_sysroot_unlock ostree_sysroot_unload +ostree_sysroot_set_mount_namespace_in_use ostree_sysroot_get_fd ostree_sysroot_ensure_initialized ostree_sysroot_get_bootversion diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 4ff2b86f9d..ab35fbbc70 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -19,6 +19,8 @@ /* Add new symbols here. Release commits should copy this section into -released.sym. */ LIBOSTREE_2018.10 { + ostree_sysroot_initialize; + ostree_sysroot_set_mount_namespace_in_use; } LIBOSTREE_2018.9; /* Stub section for the stable release *after* this development one; don't diff --git a/src/libostree/ostree-sysroot-cleanup.c b/src/libostree/ostree-sysroot-cleanup.c index 7a352e6b4b..eeca470b0f 100644 --- a/src/libostree/ostree-sysroot-cleanup.c +++ b/src/libostree/ostree-sysroot-cleanup.c @@ -454,6 +454,9 @@ ostree_sysroot_cleanup_prune_repo (OstreeSysroot *sysroot, OstreeRepo *repo = ostree_sysroot_repo (sysroot); const guint depth = 0; /* Historical default */ + if (!_ostree_sysroot_ensure_writable (sysroot, error)) + return FALSE; + /* Hold an exclusive lock by default across gathering refs and doing * the prune. */ @@ -536,6 +539,9 @@ _ostree_sysroot_cleanup_internal (OstreeSysroot *self, g_return_val_if_fail (OSTREE_IS_SYSROOT (self), FALSE); g_return_val_if_fail (self->loaded, FALSE); + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; + if (!cleanup_other_bootversions (self, cancellable, error)) return glnx_prefix_error (error, "Cleaning bootversions"); diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index b16f65b334..337a052195 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -55,6 +55,9 @@ #define OSTREE_DEPLOYMENT_FINALIZING_ID SD_ID128_MAKE(e8,64,6c,d6,3d,ff,46,25,b7,79,09,a8,e7,a4,09,94) #endif +static gboolean +is_ro_mount (const char *path); + /* * Like symlinkat() but overwrites (atomically) an existing * symlink. @@ -810,6 +813,9 @@ write_origin_file_internal (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error) { + if (!_ostree_sysroot_ensure_writable (sysroot, error)) + return FALSE; + GLNX_AUTO_PREFIX_ERROR ("Writing out origin file", error); GKeyFile *origin = new_origin ? new_origin : ostree_deployment_get_origin (deployment); @@ -2183,6 +2189,9 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot *self, { g_assert (self->loaded); + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; + /* Dealing with the staged deployment is quite tricky here. This function is * primarily concerned with writing out "finalized" deployments which have * bootloader entries. Originally, we simply dropped the staged deployment @@ -2337,7 +2346,6 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot *self, if (boot_was_ro_mount) { - /* TODO: Use new mount namespace. https://github.com/ostreedev/ostree/issues/1265 */ if (mount ("/boot", "/boot", NULL, MS_REMOUNT | MS_SILENT, NULL) < 0) return glnx_throw_errno_prefix (error, "Remounting /boot read-write"); } @@ -2349,8 +2357,10 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot *self, /* Note equivalent of try/finally here */ gboolean success = write_deployments_bootswap (self, new_deployments, opts, bootloader, &syncstats, cancellable, error); - /* Below here don't set GError until the if (!success) check */ - if (boot_was_ro_mount) + /* Below here don't set GError until the if (!success) check. + * Note we only bother remounting if a mount namespace isn't in use. + * */ + if (boot_was_ro_mount && !self->mount_namespace_in_use) { if (mount ("/boot", "/boot", NULL, MS_REMOUNT | MS_RDONLY | MS_SILENT, NULL) < 0) { @@ -2658,6 +2668,9 @@ ostree_sysroot_deploy_tree (OstreeSysroot *self, GCancellable *cancellable, GError **error) { + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; + g_autoptr(OstreeDeployment) deployment = NULL; if (!sysroot_initialize_deployment (self, osname, revision, origin, override_kernel_argv, &deployment, cancellable, error)) @@ -2757,6 +2770,9 @@ ostree_sysroot_stage_tree (OstreeSysroot *self, GCancellable *cancellable, GError **error) { + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; + OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (self); if (booted_deployment == NULL) return glnx_throw (error, "Cannot stage a deployment when not currently booted into an OSTree system"); @@ -2971,6 +2987,9 @@ ostree_sysroot_deployment_set_kargs (OstreeSysroot *self, GCancellable *cancellable, GError **error) { + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; + /* For now; instead of this do a redeployment */ g_assert (!ostree_deployment_is_staged (deployment)); @@ -3018,6 +3037,8 @@ ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, GCancellable *cancellable, GError **error) { + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h index 9da6d4c9e1..8665ed71de 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -53,6 +53,7 @@ struct OstreeSysroot { GLnxLockFile lock; gboolean loaded; + gboolean mount_namespace_in_use; /* TRUE if caller has told us they used CLONE_NEWNS */ gboolean root_is_ostree_booted; /* TRUE if sysroot is / and we are booted via ostree */ /* The device/inode for /, used to detect booted deployment */ dev_t root_device; @@ -79,6 +80,10 @@ struct OstreeSysroot { #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/" #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development" +gboolean +_ostree_sysroot_ensure_writable (OstreeSysroot *self, + GError **error); + void _ostree_sysroot_emit_journal_msg (OstreeSysroot *self, const char *msg); diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index 84c1230147..46bf02bbab 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -226,6 +226,24 @@ ostree_sysroot_new_default (void) return ostree_sysroot_new (NULL); } +/** + * ostree_sysroot_set_mount_namespace_in_use: + * + * If this function is invoked, then libostree will assume that + * a private Linux mount namespace has been created by the process. + * The primary use case for this is to have e.g. /sysroot mounted + * read-only by default. + * + * If this function has been called, then libostree will automatically + * remount as writable any mount points on which it operates. This + * currently is just /sysroot. + */ +void +ostree_sysroot_set_mount_namespace_in_use (OstreeSysroot *self) +{ + self->mount_namespace_in_use = TRUE; +} + /** * ostree_sysroot_get_path: * @self: @@ -251,6 +269,37 @@ ensure_sysroot_fd (OstreeSysroot *self, return TRUE; } +/* Remount /sysroot read-write if necessary */ +gboolean +_ostree_sysroot_ensure_writable (OstreeSysroot *self, + GError **error) +{ + /* If we aren't operating on a booted system, then we don't + * do anything with mounts. Also, if the caller hasn't + * explicitly told us they made a mount namespace, we don't + * do any remounts. + */ + if (!self->root_is_ostree_booted || + !self->mount_namespace_in_use) + return TRUE; + + if (!ensure_sysroot_fd (self, error)) + return FALSE; + + /* Check if /sysroot is a read-only mountpoint */ + struct statvfs stvfsbuf; + if (statvfs ("/sysroot", &stvfsbuf) <= 0) + return glnx_throw_errno_prefix (error, "fstatvfs(/sysroot)"); + if ((stvfsbuf.f_flag & ST_RDONLY) != 0) + return TRUE; + + /* OK, let's remount writable. */ + if (mount ("/sysroot", "/sysroot", NULL, MS_REMOUNT | MS_SILENT, NULL) < 0) + return glnx_throw_errno_prefix (error, "Remounting /sysroot read-write"); + + return TRUE; +} + /** * ostree_sysroot_get_fd: * @self: Sysroot @@ -771,6 +820,24 @@ ostree_sysroot_load (OstreeSysroot *self, return ostree_sysroot_load_if_changed (self, NULL, cancellable, error); } +/** + * ostree_sysroot_initialize: + * @self: sysroot + * + * Subset of ostree_sysroot_load(); performs basic initialization. + * Notably, one can invoke `ostree_sysroot_get_fd()` after calling + * this function. + * + * It is not necessary to call this function if ostree_sysroot_load() + * is invoked. + */ +gboolean +ostree_sysroot_initialize (OstreeSysroot *self, + GError **error) +{ + return ensure_sysroot_fd (self, error); +} + static gboolean ensure_repo (OstreeSysroot *self, GError **error) @@ -861,7 +928,7 @@ ostree_sysroot_load_if_changed (OstreeSysroot *self, GCancellable *cancellable, GError **error) { - if (!ensure_sysroot_fd (self, error)) + if (!ostree_sysroot_initialize (self, error)) return FALSE; /* Here we also lazily initialize the repository. We didn't do this @@ -1482,7 +1549,7 @@ ostree_sysroot_init_osname (OstreeSysroot *self, GCancellable *cancellable, GError **error) { - if (!ensure_sysroot_fd (self, error)) + if (!_ostree_sysroot_ensure_writable (self, error)) return FALSE; const char *deploydir = glnx_strjoina ("ostree/deploy/", osname); diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h index 502cd75020..4dd33a6ebc 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -41,12 +41,19 @@ OstreeSysroot* ostree_sysroot_new (GFile *path); _OSTREE_PUBLIC OstreeSysroot* ostree_sysroot_new_default (void); +_OSTREE_PUBLIC +void ostree_sysroot_set_mount_namespace_in_use (OstreeSysroot *self); + _OSTREE_PUBLIC GFile *ostree_sysroot_get_path (OstreeSysroot *self); _OSTREE_PUBLIC int ostree_sysroot_get_fd (OstreeSysroot *self); +_OSTREE_PUBLIC +gboolean ostree_sysroot_initialize (OstreeSysroot *self, + GError **error); + _OSTREE_PUBLIC gboolean ostree_sysroot_load (OstreeSysroot *self, GCancellable *cancellable, @@ -90,6 +97,10 @@ GFile * ostree_sysroot_get_deployment_origin_path (GFile *deployment_path); _OSTREE_PUBLIC gboolean ostree_sysroot_lock (OstreeSysroot *self, GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sysroot_lock_with_mount_namespace (OstreeSysroot *self, GError **error); + _OSTREE_PUBLIC gboolean ostree_sysroot_try_lock (OstreeSysroot *self, gboolean *out_acquired, diff --git a/src/ostree/ot-main.c b/src/ostree/ot-main.c index a1449aef25..7949faf5e1 100644 --- a/src/ostree/ot-main.c +++ b/src/ostree/ot-main.c @@ -27,6 +27,7 @@ #include #include +#include #include "ot-main.h" #include "ostree.h" @@ -423,10 +424,39 @@ ostree_admin_option_context_parse (GOptionContext *context, sysroot_path = g_file_new_for_path (opt_sysroot); g_autoptr(OstreeSysroot) sysroot = ostree_sysroot_new (sysroot_path); + if (!ostree_sysroot_initialize (sysroot, error)) + return FALSE; g_signal_connect (sysroot, "journal-msg", G_CALLBACK (on_sysroot_journal_msg), NULL); if ((flags & OSTREE_ADMIN_BUILTIN_FLAG_UNLOCKED) == 0) { + /* If we're requested to lock the sysroot, first find + * out if it's a read-only mount point, and if so, + * create a new mount namespace and tell the sysroot + * that we've done so. See the docs for + * ostree_sysroot_set_mount_namespace_in_use(). + */ + int sysroot_fd = ostree_sysroot_get_fd (sysroot); + g_assert_cmpint (sysroot_fd, !=, -1); + struct stat stbuf; + if (!glnx_fstat (sysroot_fd, &stbuf, error)) + return FALSE; + /* We really expect this to always be root but...maybe someone + * is running ostree as non-root on a guestmount or so. + */ + if (getuid () == stbuf.st_uid) + { + struct statvfs stvfs; + if (fstatvfs (sysroot_fd, &stvfs) < 0) + return glnx_throw_errno_prefix (error, "fstatvfs"); + if (stvfs.f_flag & ST_RDONLY) + { + if (unshare (CLONE_NEWNS) < 0) + return glnx_throw_errno_prefix (error, "preparing writable sysroot: unshare (CLONE_NEWNS)"); + ostree_sysroot_set_mount_namespace_in_use (sysroot); + } + } + /* Released when sysroot is finalized, or on process exit */ if (!ot_admin_sysroot_lock (sysroot, error)) return FALSE; diff --git a/src/switchroot/ostree-remount.c b/src/switchroot/ostree-remount.c index 5e6d23d3ae..bb8e1e1140 100644 --- a/src/switchroot/ostree-remount.c +++ b/src/switchroot/ostree-remount.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -40,7 +41,8 @@ #include "ostree-mount-util.h" static void -do_remount (const char *target) +do_remount (const char *target, + bool writable) { struct stat stbuf; if (lstat (target, &stbuf) < 0) @@ -54,20 +56,18 @@ do_remount (const char *target) struct statvfs stvfsbuf; if (statvfs (target, &stvfsbuf) == -1) return; - /* If no read-only flag, skip it */ - if ((stvfsbuf.f_flag & ST_RDONLY) == 0) + + const bool currently_writable = ((stvfsbuf.f_flag & ST_RDONLY) == 0); + if (writable == currently_writable) return; - /* It's a mounted, read-only fs; remount it */ - if (mount (target, target, NULL, MS_REMOUNT | MS_SILENT, NULL) < 0) - { - /* Also ignore EINVAL - if the target isn't a mountpoint - * already, then assume things are OK. - */ - if (errno != EINVAL) - err (EXIT_FAILURE, "failed to remount %s", target); - } - else - printf ("Remounted: %s\n", target); + + int mnt_flags = MS_REMOUNT | MS_SILENT; + if (!writable) + mnt_flags |= MS_RDONLY; + if (mount (target, target, NULL, mnt_flags, NULL) < 0) + err (EXIT_FAILURE, "failed to remount %s", target); + + printf ("Remounted %s: %s\n", writable ? "rw" : "ro", target); } int @@ -95,8 +95,46 @@ main(int argc, char *argv[]) exit (EXIT_SUCCESS); } - do_remount ("/sysroot"); - do_remount ("/var"); + do_remount ("/var", true); + + /* We could also parse the ostree repo config, but...this service + * so far doesn't link to libostree. + */ + struct stat stbuf; + static const char sysroot_ro_path[] = "/sysroot/ostree/.sysroot-ro"; + bool sysroot_readonly = lstat (sysroot_ro_path, &stbuf) == 0; + if (!sysroot_readonly) + do_remount ("/sysroot", !sysroot_readonly); + else + { + /* Since /sysroot is the real physical root, we can't simply + * remount it read-only here, as that'd affect e.g. /etc in + * and also /var if it's not a separate mount. Instead, we make + * new read-only bind mount to it, then move it over. + */ + /* This temporary lives in /etc since it needs to be on the same mount. */ + static const char tmp_sysroot[] = "/etc/ostree-sysroot.tmp"; + static const char sysroot[] = "/sysroot"; + if (mkdir (tmp_sysroot, 0755) < 0) + err (EXIT_FAILURE, "mkdir(%s)", tmp_sysroot); + if (mount (sysroot, tmp_sysroot, NULL, MS_BIND | MS_PRIVATE, NULL) < 0) + err (EXIT_FAILURE, "failed to bind mount %s %s", sysroot, tmp_sysroot); + if (mount (tmp_sysroot, tmp_sysroot, NULL, MS_BIND | MS_RDONLY | MS_REMOUNT, NULL) < 0) + err (EXIT_FAILURE, "failed to remount ro %s", tmp_sysroot); + if (umount (sysroot) < 0) + err (EXIT_FAILURE, "while remounting %s read-only: umount", sysroot); + /* Now we briefly make the sysroot private so that we can move the mount */ + if (mount ("none", "/", NULL, MS_PRIVATE, NULL) < 0) + err (EXIT_FAILURE, "making / private temporarily"); + /* Do the move */ + if (mount (tmp_sysroot, sysroot, NULL, MS_MOVE, NULL) < 0) + err (EXIT_FAILURE, "failed to move read-only %s mount", sysroot); + /* Make / shared again */ + if (mount ("none", "/", NULL, MS_SHARED, NULL) < 0) + err (EXIT_FAILURE, "making / shared again"); + if (rmdir (tmp_sysroot) < 0) + err (EXIT_FAILURE, "rmdir(%s)", tmp_sysroot); + } exit (EXIT_SUCCESS); }