deploy: Handle a read-only /boot

I'd like to encourage people to make OSTree-managed systems more
strictly read-only in multiple places.  Ideally everywhere is
read-only normally besides `/var/`, `/tmp/`, and `/run`.

`/boot` is a good example of something to make readonly.  Particularly
now that there's work on the `admin unlock` verb, we need to protect
the system better against things like `rpm -Uvh kernel.rpm` because
the RPM-packaged kernel won't understand how to do OSTree right.

In order to make this work of course, we *do* need to remount `/boot`
as writable when we're doing an upgrade that changes the kernel
configuration.  So the strategy is to detect whether it's read-only,
and if so, temporarily mount read-write, then remount read-only when
the upgrade is done.

We can generalize this in the future to also do `/etc` (and possibly
`/sysroot/ostree/` although that gets tricky).

One detail: In order to detect "is this path a mountpoint" is
nontrivial - I looked at copying the systemd code, but the right place
is to use `libmount` anyways.
This commit is contained in:
Colin Walters 2016-03-21 10:37:38 -04:00
parent b842429bf2
commit 8894bb3949
4 changed files with 109 additions and 1 deletions

View File

@ -159,6 +159,11 @@ libostree_1_la_CFLAGS += $(OT_INTERNAL_SOUP_CFLAGS)
libostree_1_la_LIBADD += $(OT_INTERNAL_SOUP_LIBS) libostree_1_la_LIBADD += $(OT_INTERNAL_SOUP_LIBS)
endif endif
if USE_LIBMOUNT
libostree_1_la_CFLAGS += $(OT_DEP_LIBMOUNT_CFLAGS)
libostree_1_la_LIBADD += $(OT_DEP_LIBMOUNT_LIBS)
endif
if USE_SELINUX if USE_SELINUX
libostree_1_la_CFLAGS += $(OT_DEP_SELINUX_CFLAGS) libostree_1_la_CFLAGS += $(OT_DEP_SELINUX_CFLAGS)
libostree_1_la_LIBADD += $(OT_DEP_SELINUX_LIBS) libostree_1_la_LIBADD += $(OT_DEP_SELINUX_LIBS)

View File

@ -192,6 +192,31 @@ AS_IF([ test x$with_selinux != xno ], [
if test x$with_selinux != xno; then OSTREE_FEATURES="$OSTREE_FEATURES +selinux"; fi if test x$with_selinux != xno; then OSTREE_FEATURES="$OSTREE_FEATURES +selinux"; fi
AM_CONDITIONAL(USE_SELINUX, test $with_selinux != no) AM_CONDITIONAL(USE_SELINUX, test $with_selinux != no)
dnl This is what is in RHEL7.2 right now, picking it arbitrarily
LIBMOUNT_DEPENDENCY="mount >= 2.23.0"
AC_ARG_WITH(libmount,
AS_HELP_STRING([--without-libmount], [Do not use libmount]),
:, with_libmount=maybe)
AS_IF([ test x$with_libmount != xno ], [
AC_MSG_CHECKING([for $LIBMOUNT_DEPENDENCY])
PKG_CHECK_EXISTS($LIBMOUNT_DEPENDENCY, have_libmount=yes, have_libmount=no)
AC_MSG_RESULT([$have_libmount])
AS_IF([ test x$have_libmount = xno && test x$with_libmount != xmaybe ], [
AC_MSG_ERROR([libmount is enabled but could not be found])
])
AS_IF([ test x$have_libmount = xyes], [
AC_DEFINE([HAVE_LIBMOUNT], 1, [Define if we have libmount.pc])
PKG_CHECK_MODULES(OT_DEP_LIBMOUNT, $LIBMOUNT_DEPENDENCY)
with_libmount=yes
], [
with_libmount=no
])
], [ with_libmount=no ])
if test x$with_libmount != xno; then OSTREE_FEATURES="$OSTREE_FEATURES +libmount"; fi
AM_CONDITIONAL(USE_LIBMOUNT, test $with_libmount != no)
# Enabled by default because I think people should use it. # Enabled by default because I think people should use it.
AC_ARG_ENABLE(rofiles-fuse, AC_ARG_ENABLE(rofiles-fuse,
[AS_HELP_STRING([--enable-rofiles-fuse], [AS_HELP_STRING([--enable-rofiles-fuse],
@ -260,6 +285,7 @@ echo "
libsoup (retrieve remote HTTP repositories): $with_soup libsoup (retrieve remote HTTP repositories): $with_soup
libsoup TLS client certs: $have_libsoup_client_certs libsoup TLS client certs: $have_libsoup_client_certs
SELinux: $with_selinux SELinux: $with_selinux
libmount: $with_libmount
libarchive (parse tar files directly): $with_libarchive libarchive (parse tar files directly): $with_libarchive
static deltas: yes (always enabled now) static deltas: yes (always enabled now)
man pages (xsltproc): $enable_man man pages (xsltproc): $enable_man

View File

@ -100,7 +100,10 @@ deployment lists. This happens when doing an upgrade that does not
include the kernel; think of a simple translation update. OSTree include the kernel; think of a simple translation update. OSTree
optimizes for this case because on some systems `/boot` may be on a optimizes for this case because on some systems `/boot` may be on a
separate medium such as flash storage not optimized for significant separate medium such as flash storage not optimized for significant
amounts of write traffic. amounts of write traffic. Related to this, modern OSTree has support
for having `/boot` be a read-only mount by default - it will
automatically remount read-write just for the portion of time
necessary to update the bootloader configuration.
To implement this, OSTree also maintains the directory To implement this, OSTree also maintains the directory
`/ostree/boot.<replaceable>bootversion</replaceable>`, which is a set `/ostree/boot.<replaceable>bootversion</replaceable>`, which is a set

View File

@ -22,6 +22,12 @@
#include <gio/gunixinputstream.h> #include <gio/gunixinputstream.h>
#include <gio/gunixoutputstream.h> #include <gio/gunixoutputstream.h>
#include <sys/mount.h>
#include <sys/statvfs.h>
#ifdef HAVE_LIBMOUNT
#include <libmount.h>
#endif
#include "ostree-sysroot-private.h" #include "ostree-sysroot-private.h"
#include "ostree-deployment-private.h" #include "ostree-deployment-private.h"
@ -1646,6 +1652,47 @@ cleanup_legacy_current_symlinks (OstreeSysroot *self,
return ret; return ret;
} }
static gboolean
is_ro_mount (const char *path)
{
#ifdef HAVE_LIBMOUNT
/* Dragging in all of this crud is apparently necessary just to determine
* whether something is a mount point.
*
* Systemd has a totally different implementation in
* src/basic/mount-util.c.
*/
struct libmnt_table *tb = mnt_new_table_from_file ("/proc/self/mountinfo");
struct libmnt_fs *fs;
struct libmnt_cache *cache;
gboolean is_mount = FALSE;
struct statvfs stvfsbuf;
if (!tb)
return FALSE;
/* to canonicalize all necessary paths */
cache = mnt_new_cache ();
mnt_table_set_cache (tb, cache);
fs = mnt_table_find_target(tb, path, MNT_ITER_BACKWARD);
is_mount = fs && mnt_fs_get_target (fs);
mnt_free_cache (cache);
mnt_free_table (tb);
if (!is_mount)
return FALSE;
/* We *could* parse the options, but it seems more reliable to
* introspect the actual mount at runtime.
*/
if (statvfs (path, &stvfsbuf) == 0)
return (stvfsbuf.f_flag & ST_RDONLY) != 0;
#endif
return FALSE;
}
/** /**
* ostree_sysroot_write_deployments: * ostree_sysroot_write_deployments:
* @self: Sysroot * @self: Sysroot
@ -1667,6 +1714,7 @@ ostree_sysroot_write_deployments (OstreeSysroot *self,
gboolean requires_new_bootversion = FALSE; gboolean requires_new_bootversion = FALSE;
gboolean found_booted_deployment = FALSE; gboolean found_booted_deployment = FALSE;
gboolean bootloader_is_atomic = FALSE; gboolean bootloader_is_atomic = FALSE;
gboolean boot_was_ro_mount = FALSE;
g_assert (self->loaded); g_assert (self->loaded);
@ -1754,6 +1802,20 @@ ostree_sysroot_write_deployments (OstreeSysroot *self,
glnx_unref_object OstreeRepo *repo = NULL; glnx_unref_object OstreeRepo *repo = NULL;
gboolean show_osname = FALSE; gboolean show_osname = FALSE;
if (self->booted_deployment)
boot_was_ro_mount = is_ro_mount ("/boot");
g_debug ("boot is ro: %s", boot_was_ro_mount ? "yes" : "no");
if (boot_was_ro_mount)
{
if (mount ("/boot", "/boot", NULL, MS_REMOUNT | MS_SILENT, NULL) < 0)
{
glnx_set_prefix_error_from_errno (error, "%s", "Remounting /boot read-write");
goto out;
}
}
if (!_ostree_sysroot_query_bootloader (self, &bootloader, cancellable, error)) if (!_ostree_sysroot_query_bootloader (self, &bootloader, cancellable, error))
goto out; goto out;
@ -1879,6 +1941,18 @@ ostree_sysroot_write_deployments (OstreeSysroot *self,
ret = TRUE; ret = TRUE;
out: out:
if (boot_was_ro_mount)
{
if (mount ("/boot", "/boot", NULL, MS_REMOUNT | MS_RDONLY | MS_SILENT, NULL) < 0)
{
/* Only make this a warning because we don't want to
* completely bomb out if some other process happened to
* jump in and open a file there.
*/
int errsv = errno;
g_printerr ("warning: Failed to remount /boot read-only: %s\n", strerror (errsv));
}
}
return ret; return ret;
} }