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
This commit is contained in:
Colin Walters committed 2019-12-11 15:33:57 +00:00
1 parent 20daf96880
commit 5af403be0c
12 files changed
+330 -67

No files matched your search

+156 -44
View File
@@ -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);