From 20daf9688006f17e11af7692d2e43292b36df71f Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 5 Sep 2019 00:52:15 +0000 Subject: [PATCH 1/2] finalize-staged: Use the core option parsing to load sysroot Prep for using the default mount namespace handling there that will land as part of the read-only `/sysroot` and `/boot` work. See https://github.com/ostreedev/ostree/issues/1265 --- src/ostree/ot-admin-builtin-finalize-staged.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ostree/ot-admin-builtin-finalize-staged.c b/src/ostree/ot-admin-builtin-finalize-staged.c index 6740f82aa9..3cea1bdb3e 100644 --- a/src/ostree/ot-admin-builtin-finalize-staged.c +++ b/src/ostree/ot-admin-builtin-finalize-staged.c @@ -34,6 +34,10 @@ #include "ostree-cmdprivate.h" #include "ostree.h" +static GOptionEntry options[] = { + { NULL } +}; + /* Called by ostree-finalize-staged.service, and in turn * invokes a cmdprivate function inside the shared library. */ @@ -46,11 +50,13 @@ ot_admin_builtin_finalize_staged (int argc, char **argv, OstreeCommandInvocation if (fstatat (AT_FDCWD, "/run/ostree-booted", &stbuf, 0) < 0) return TRUE; - g_autoptr(GFile) sysroot_file = g_file_new_for_path ("/"); - g_autoptr(OstreeSysroot) sysroot = ostree_sysroot_new (sysroot_file); - - if (!ostree_sysroot_load (sysroot, cancellable, error)) + g_autoptr(GOptionContext) context = g_option_context_new (""); + g_autoptr(OstreeSysroot) sysroot = NULL; + if (!ostree_admin_option_context_parse (context, options, &argc, &argv, + OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER, + invocation, &sysroot, cancellable, error)) return FALSE; + if (!ostree_cmd__private__()->ostree_finalize_staged (sysroot, cancellable, error)) return FALSE; From 5af403be0cc64df50ad21cef05f3268ead256d6d Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 3 Oct 2018 14:57:19 +0000 Subject: [PATCH 2/2] Support mounting /sysroot (and /boot) read-only We want to support extending the read-only state to cover `/sysroot` and `/boot`, since conceptually all of the data there should only be written via libostree. Or at least for `/boot` should *mostly* just be written by ostree. This change needs to be opt-in though to avoid breaking anyone. Add a `sysroot/readonly` key to the repository config which instructs `ostree-remount.service` to ensure `/sysroot` is read-only. This requires a bit of a dance because `/sysroot` is actually the same filesystem as `/`; so we make `/etc` a writable bind mount in this case. We also need to handle `/var` in the "OSTree default" case of a bind mount; the systemd generator now looks at the writability state of `/sysroot` and uses that to determine whether it should have the `var.mount` unit happen before or after `ostree-remount.service.` Also add an API to instruct the libostree shared library that the caller has created a new mount namespace. This way we can freely remount read-write. This approach extends upon in a much better way previous work we did to support remounting `/boot` read-write. Closes: https://github.com/ostreedev/ostree/issues/1265 --- Makefile-switchroot.am | 3 +- apidoc/ostree-sections.txt | 3 + src/boot/ostree-remount.service | 6 +- src/libostree/libostree-devel.sym | 3 + src/libostree/ostree-impl-system-generator.c | 3 +- src/libostree/ostree-sysroot-cleanup.c | 8 +- src/libostree/ostree-sysroot-deploy.c | 29 ++- src/libostree/ostree-sysroot-private.h | 13 +- src/libostree/ostree-sysroot.c | 200 +++++++++++++++---- src/libostree/ostree-sysroot.h | 14 ++ src/ostree/ot-main.c | 37 ++++ src/switchroot/ostree-remount.c | 78 +++++++- 12 files changed, 330 insertions(+), 67 deletions(-) diff --git a/Makefile-switchroot.am b/Makefile-switchroot.am index b81b843feb..ad370eb79d 100644 --- a/Makefile-switchroot.am +++ b/Makefile-switchroot.am @@ -55,7 +55,8 @@ ostree_remount_SOURCES = \ src/switchroot/ostree-mount-util.h \ src/switchroot/ostree-remount.c \ $(NULL) -ostree_remount_CPPFLAGS = $(AM_CPPFLAGS) -Isrc/switchroot +ostree_remount_CPPFLAGS = $(AM_CPPFLAGS) $(OT_INTERNAL_GIO_UNIX_CFLAGS) -Isrc/switchroot -I$(srcdir)/libglnx +ostree_remount_LDADD = $(AM_LDFLAGS) $(OT_INTERNAL_GIO_UNIX_LIBS) libglnx.la if BUILDOPT_SYSTEMD ostree_prepare_root_CPPFLAGS += -DHAVE_SYSTEMD=1 diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index f99c4df55f..1ef4bbf638 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -499,6 +499,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 @@ -508,6 +509,8 @@ ostree_sysroot_lock_async ostree_sysroot_lock_finish ostree_sysroot_unlock ostree_sysroot_unload +ostree_sysroot_set_mount_namespace_in_use +ostree_sysroot_is_booted ostree_sysroot_get_fd ostree_sysroot_ensure_initialized ostree_sysroot_get_bootversion diff --git a/src/boot/ostree-remount.service b/src/boot/ostree-remount.service index b98110c2d6..4c3ed94a42 100644 --- a/src/boot/ostree-remount.service +++ b/src/boot/ostree-remount.service @@ -22,12 +22,12 @@ DefaultDependencies=no ConditionKernelCommandLine=ostree OnFailure=emergency.target Conflicts=umount.target -After=-.mount +# Run after core mounts +After=-.mount var.mount After=systemd-remount-fs.service +# But we run *before* most other core bootup services that need write access to /etc and /var Before=local-fs.target umount.target -# Other early boot units that need to write to /var Before=systemd-random-seed.service plymouth-read-write.service systemd-journal-flush.service -# tmpfiles.d usually needs write access to a few places Before=systemd-tmpfiles-setup.service [Service] diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index a5d15e93e3..d1666176d8 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -19,6 +19,9 @@ /* Add new symbols here. Release commits should copy this section into -released.sym. */ LIBOSTREE_2019.7 { + ostree_sysroot_initialize; + ostree_sysroot_is_booted; + ostree_sysroot_set_mount_namespace_in_use; } LIBOSTREE_2019.6; /* Stub section for the stable release *after* this development one; don't diff --git a/src/libostree/ostree-impl-system-generator.c b/src/libostree/ostree-impl-system-generator.c index ce40a698c4..cb9170b330 100644 --- a/src/libostree/ostree-impl-system-generator.c +++ b/src/libostree/ostree-impl-system-generator.c @@ -28,6 +28,7 @@ #ifdef HAVE_LIBMOUNT #include #endif +#include #include #include "otutil.h" @@ -189,8 +190,6 @@ _ostree_impl_system_generator (const char *ostree_cmdline, "[Unit]\n" "Documentation=man:ostree(1)\n" "ConditionKernelCommandLine=!systemd.volatile\n" - /* We need /sysroot mounted writable first */ - "After=ostree-remount.service\n" "Before=local-fs.target\n" "\n" "[Mount]\n" diff --git a/src/libostree/ostree-sysroot-cleanup.c b/src/libostree/ostree-sysroot-cleanup.c index ef95d13c6b..71d978e005 100644 --- a/src/libostree/ostree-sysroot-cleanup.c +++ b/src/libostree/ostree-sysroot-cleanup.c @@ -455,6 +455,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. */ @@ -535,7 +538,10 @@ _ostree_sysroot_cleanup_internal (OstreeSysroot *self, GError **error) { g_return_val_if_fail (OSTREE_IS_SYSROOT (self), FALSE); - g_return_val_if_fail (self->loaded, FALSE); + g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_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 a09c354b93..0dcda7328f 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -56,6 +56,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. @@ -806,6 +809,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); @@ -2217,7 +2223,10 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot *self, GCancellable *cancellable, GError **error) { - g_assert (self->loaded); + g_assert (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_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 @@ -2374,7 +2383,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"); } @@ -2408,8 +2416,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) { @@ -2716,6 +2726,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)) @@ -2817,6 +2830,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"); @@ -3043,6 +3059,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)); @@ -3090,6 +3109,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 858673c5d3..96670b137b 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -40,6 +40,12 @@ typedef enum { OSTREE_SYSROOT_DEBUG_TEST_STAGED_PATH = 1 << 3, } OstreeSysrootDebugFlags; +typedef enum { + OSTREE_SYSROOT_LOAD_STATE_NONE, /* ostree_sysroot_new() was called */ + OSTREE_SYSROOT_LOAD_STATE_INIT, /* We've loaded basic sysroot state and have an fd */ + OSTREE_SYSROOT_LOAD_STATE_LOADED, /* We've loaded all of the deployments */ +} OstreeSysrootLoadState; + /** * OstreeSysroot: * Internal struct @@ -51,7 +57,8 @@ struct OstreeSysroot { int sysroot_fd; GLnxLockFile lock; - gboolean loaded; + OstreeSysrootLoadState loadstate; + 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 +86,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 1c9dbf379c..23a069752d 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -226,6 +226,33 @@ 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 when a function which requires + * writable access is invoked, libostree will automatically remount as writable + * any mount points on which it operates. This currently is just `/sysroot` and + * `/boot`. + * + * If you invoke this function, it must be before ostree_sysroot_load(); it may + * be invoked before or after ostree_sysroot_initialize(). + * + * Since: 2019.7 + */ +void +ostree_sysroot_set_mount_namespace_in_use (OstreeSysroot *self) +{ + /* Must be before we're loaded, as otherwise we'd have to close/reopen all our + fds, e.g. the repo */ + g_return_if_fail (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED); + self->mount_namespace_in_use = TRUE; +} + /** * ostree_sysroot_get_path: * @self: @@ -238,6 +265,7 @@ ostree_sysroot_get_path (OstreeSysroot *self) return self->path; } +/* Open a directory file descriptor for the sysroot if we haven't yet */ static gboolean ensure_sysroot_fd (OstreeSysroot *self, GError **error) @@ -251,13 +279,51 @@ ensure_sysroot_fd (OstreeSysroot *self, return TRUE; } +/* Remount /sysroot read-write if necessary */ +gboolean +_ostree_sysroot_ensure_writable (OstreeSysroot *self, + GError **error) +{ + /* Do nothing if no mount namespace is in use */ + if (!self->mount_namespace_in_use) + return TRUE; + + /* If a mount namespace is in use, ensure we're initialized */ + if (!ostree_sysroot_initialize (self, error)) + return FALSE; + + /* If we aren't operating on a booted system, then we don't + * do anything with mounts. + */ + if (!self->root_is_ostree_booted) + return TRUE; + + /* 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_RELATIME, "") < 0) + return glnx_throw_errno_prefix (error, "Remounting /sysroot read-write"); + + /* Reopen our fd */ + glnx_close_fd (&self->sysroot_fd); + if (!ensure_sysroot_fd (self, error)) + return FALSE; + + return TRUE; +} + /** * ostree_sysroot_get_fd: * @self: Sysroot * - * Access a file descriptor that refers to the root directory of this - * sysroot. ostree_sysroot_load() must have been invoked prior to - * calling this function. + * Access a file descriptor that refers to the root directory of this sysroot. + * ostree_sysroot_initialize() (or ostree_sysroot_load()) must have been invoked + * prior to calling this function. * * Returns: A file descriptor valid for the lifetime of @self */ @@ -268,6 +334,22 @@ ostree_sysroot_get_fd (OstreeSysroot *self) return self->sysroot_fd; } +/** + * ostree_sysroot_is_booted: + * @self: Sysroot + * + * Can only be invoked after `ostree_sysroot_initialize()`. + * + * Returns: %TRUE iff the sysroot points to a booted deployment + * Since: 2019.7 + */ +gboolean +ostree_sysroot_is_booted (OstreeSysroot *self) +{ + g_return_val_if_fail (self->loadstate >= OSTREE_SYSROOT_LOAD_STATE_INIT, FALSE); + return self->root_is_ostree_booted; +} + gboolean _ostree_sysroot_bump_mtime (OstreeSysroot *self, GError **error) @@ -798,6 +880,57 @@ ensure_repo (OstreeSysroot *self, return TRUE; } +/** + * 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. + * + * Since: 2019.7 + */ +gboolean +ostree_sysroot_initialize (OstreeSysroot *self, + GError **error) +{ + if (!ensure_sysroot_fd (self, error)) + return FALSE; + + if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_INIT) + { + /* Gather some global state; first if we have the global ostree-booted flag; + * we'll use it to sanity check that we found a booted deployment for example. + * Second, we also find out whether sysroot == /. + */ + if (!glnx_fstatat_allow_noent (AT_FDCWD, "/run/ostree-booted", NULL, 0, error)) + return FALSE; + const gboolean ostree_booted = (errno == 0); + + { struct stat root_stbuf; + if (!glnx_fstatat (AT_FDCWD, "/", &root_stbuf, 0, error)) + return FALSE; + self->root_device = root_stbuf.st_dev; + self->root_inode = root_stbuf.st_ino; + } + + struct stat self_stbuf; + if (!glnx_fstatat (AT_FDCWD, gs_file_get_path_cached (self->path), &self_stbuf, 0, error)) + return FALSE; + + const gboolean root_is_sysroot = + (self->root_device == self_stbuf.st_dev && + self->root_inode == self_stbuf.st_ino); + + self->root_is_ostree_booted = (ostree_booted && root_is_sysroot); + self->loadstate = OSTREE_SYSROOT_LOAD_STATE_INIT; + } + + return TRUE; +} + /* Reload the staged deployment from the file in /run */ gboolean _ostree_sysroot_reload_staged (OstreeSysroot *self, @@ -870,7 +1003,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 @@ -880,34 +1013,6 @@ ostree_sysroot_load_if_changed (OstreeSysroot *self, if (!ensure_repo (self, error)) return FALSE; - /* Gather some global state; first if we have the global ostree-booted flag; - * we'll use it to sanity check that we found a booted deployment for example. - * Second, we also find out whether sysroot == /. - */ - if (!self->loaded) - { - if (!glnx_fstatat_allow_noent (AT_FDCWD, "/run/ostree-booted", NULL, 0, error)) - return FALSE; - const gboolean ostree_booted = (errno == 0); - - { struct stat root_stbuf; - if (!glnx_fstatat (AT_FDCWD, "/", &root_stbuf, 0, error)) - return FALSE; - self->root_device = root_stbuf.st_dev; - self->root_inode = root_stbuf.st_ino; - } - - struct stat self_stbuf; - if (!glnx_fstat (self->sysroot_fd, &self_stbuf, error)) - return FALSE; - - const gboolean root_is_sysroot = - (self->root_device == self_stbuf.st_dev && - self->root_inode == self_stbuf.st_ino); - - self->root_is_ostree_booted = (ostree_booted && root_is_sysroot); - } - int bootversion = 0; if (!read_current_bootversion (self, &bootversion, cancellable, error)) return FALSE; @@ -990,8 +1095,8 @@ ostree_sysroot_load_if_changed (OstreeSysroot *self, ostree_deployment_set_index (deployment, i); } - /* Determine whether we're "physical" or not, the first time we initialize */ - if (!self->loaded) + /* Determine whether we're "physical" or not, the first time we load deployments */ + if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED) { /* If we have a booted deployment, the sysroot is / and we're definitely * not physical. @@ -1009,13 +1114,14 @@ ostree_sysroot_load_if_changed (OstreeSysroot *self, self->is_physical = TRUE; } /* Otherwise, the default is FALSE */ + + self->loadstate = OSTREE_SYSROOT_LOAD_STATE_LOADED; } self->bootversion = bootversion; self->subbootversion = subbootversion; self->deployments = deployments; deployments = NULL; /* Transfer ownership */ - self->loaded = TRUE; self->loaded_ts = stbuf.st_mtim; if (out_changed) @@ -1044,7 +1150,7 @@ ostree_sysroot_get_subbootversion (OstreeSysroot *self) OstreeDeployment * ostree_sysroot_get_booted_deployment (OstreeSysroot *self) { - g_return_val_if_fail (self->loaded, NULL); + g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL); return self->booted_deployment; } @@ -1060,7 +1166,7 @@ ostree_sysroot_get_booted_deployment (OstreeSysroot *self) OstreeDeployment * ostree_sysroot_get_staged_deployment (OstreeSysroot *self) { - g_return_val_if_fail (self->loaded, NULL); + g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL); return self->staged_deployment; } @@ -1074,7 +1180,7 @@ ostree_sysroot_get_staged_deployment (OstreeSysroot *self) GPtrArray * ostree_sysroot_get_deployments (OstreeSysroot *self) { - g_return_val_if_fail (self->loaded, NULL); + g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL); GPtrArray *copy = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref); for (guint i = 0; i < self->deployments->len; i++) @@ -1163,8 +1269,8 @@ ostree_sysroot_get_repo (OstreeSysroot *self, * @self: Sysroot * * This function is a variant of ostree_sysroot_get_repo() that cannot fail, and - * returns a cached repository. Can only be called after ostree_sysroot_load() - * has been invoked successfully. + * returns a cached repository. Can only be called after ostree_sysroot_initialize() + * or ostree_sysroot_load() has been invoked successfully. * * Returns: (transfer none): The OSTree repository in sysroot @self. * @@ -1173,7 +1279,7 @@ ostree_sysroot_get_repo (OstreeSysroot *self, OstreeRepo * ostree_sysroot_repo (OstreeSysroot *self) { - g_return_val_if_fail (self->loaded, NULL); + g_return_val_if_fail (self->loadstate >= OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL); g_assert (self->repo); return self->repo; } @@ -1368,6 +1474,10 @@ ostree_sysroot_lock (OstreeSysroot *self, { if (!ensure_sysroot_fd (self, error)) return FALSE; + + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; + return glnx_make_lock_file (self->sysroot_fd, OSTREE_SYSROOT_LOCKFILE, LOCK_EX, &self->lock, error); } @@ -1391,12 +1501,14 @@ ostree_sysroot_try_lock (OstreeSysroot *self, gboolean *out_acquired, GError **error) { - g_autoptr(GError) local_error = NULL; - if (!ensure_sysroot_fd (self, error)) return FALSE; + if (!_ostree_sysroot_ensure_writable (self, error)) + return FALSE; + /* Note use of LOCK_NB */ + g_autoptr(GError) local_error = NULL; if (!glnx_make_lock_file (self->sysroot_fd, OSTREE_SYSROOT_LOCKFILE, LOCK_EX | LOCK_NB, &self->lock, &local_error)) { @@ -1509,7 +1621,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..af8192fc40 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -41,12 +41,22 @@ 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 +gboolean ostree_sysroot_is_booted (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 +100,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 4b72f3995b..a044cef23f 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" @@ -434,10 +435,46 @@ 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 check if we're operating + * on a booted (not physical) sysroot. Then find out if the /sysroot + * subdir is 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(). + * + * This is a conservative approach; we could just always + * unshare() too. + */ + if (ostree_sysroot_is_booted (sysroot)) + { + int sysroot_fd = ostree_sysroot_get_fd (sysroot); + g_assert_cmpint (sysroot_fd, !=, -1); + + glnx_autofd int sysroot_subdir_fd = glnx_opendirat_with_errno (sysroot_fd, "sysroot", TRUE); + if (sysroot_subdir_fd < 0) + { + if (errno != ENOENT) + return glnx_throw_errno_prefix (error, "opendirat"); + } + else if (getuid () == 0) + { + struct statvfs stvfs; + if (fstatvfs (sysroot_subdir_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..326b104f03 100644 --- a/src/switchroot/ostree-remount.c +++ b/src/switchroot/ostree-remount.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -37,10 +38,14 @@ #include #include +#include + #include "ostree-mount-util.h" +#include "glnx-backport-autocleanups.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 +59,41 @@ 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) + + int mnt_flags = MS_REMOUNT | MS_SILENT; + if (!writable) + mnt_flags |= MS_RDONLY; + if (mount (target, target, NULL, mnt_flags, 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); + if (errno != EINVAL) + err (EXIT_FAILURE, "failed to remount(%s) %s", writable ? "rw" : "ro", target); + else + return; } - else - printf ("Remounted: %s\n", target); + + printf ("Remounted %s: %s\n", writable ? "rw" : "ro", target); +} + +static bool +sysroot_is_configured_ro (void) +{ + struct stat stbuf; + static const char config_path[] = "/ostree/repo/config"; + if (stat (config_path, &stbuf) != 0) + return false; + + g_autoptr(GKeyFile) keyfile = g_key_file_new (); + if (!g_key_file_load_from_file (keyfile, config_path, 0, NULL)) + return false; + + return g_key_file_get_boolean (keyfile, "sysroot", "readonly", NULL); } int @@ -95,8 +121,38 @@ main(int argc, char *argv[]) exit (EXIT_SUCCESS); } - do_remount ("/sysroot"); - do_remount ("/var"); + /* Query the repository configuration - this is an operating system builder + * choice. + * */ + const bool sysroot_readonly = sysroot_is_configured_ro (); + + /* Mount the sysroot read-only if we're configured to do so. + * Note we only get here if / is already writable. + */ + do_remount ("/sysroot", !sysroot_readonly); + + if (sysroot_readonly) + { + /* Now, /etc is not normally a bind mount, but remounting the + * sysroot above made it read-only since it's on the same filesystem. + * Make it a self-bind mount, so we can then mount it read-write. + */ + if (mount ("/etc", "/etc", NULL, MS_BIND, NULL) < 0) + err (EXIT_FAILURE, "failed to make /etc a bind mount"); + do_remount ("/etc", true); + } + + /* If /var was created as as an OSTree default bind mount (instead of being a separate filesystem) + * then remounting the root mount read-only also remounted it. + * So just like /etc, we need to make it read-write by default. + * If it was a separate filesystem, we expect it to be writable anyways, + * so it doesn't hurt to remount it if so. + * + * And if we started out with a writable system root, then we need + * to ensure that the /var bind mount created by the systemd generator + * is writable too. + */ + do_remount ("/var", true); exit (EXIT_SUCCESS); }