diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 966f641679..45060ab6bf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -73,7 +73,7 @@ jobs: - name: Build run: | env NOCONFIGURE=1 ./autogen.sh && - ./configure --with-curl --with-selinux --with-dracut=yesbutnoconf && + ./configure --with-curl --with-selinux --with-dracut=yesbutnoconf --with-composefs && make -j 4 && make install DESTDIR=$(pwd)/install && tar -c -C install --zstd -f inst.tar.zst . - name: Upload binary uses: actions/upload-artifact@v2 @@ -193,6 +193,8 @@ jobs: pre-checkout-setup: | apt-get update apt-get install -y git + configure-options: >- + --with-composefs # A build using libsoup3. After bookworm is released, this can # be switched to Debian Stable. diff --git a/.gitmodules b/.gitmodules index 1da1f9b297..0de0c83106 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "bsdiff"] path = bsdiff url = https://github.com/mendsley/bsdiff +[submodule "composefs"] + path = composefs + url = https://github.com/containers/composefs.git diff --git a/Makefile-libostree.am b/Makefile-libostree.am index d18714ae4f..8edd7f4de1 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -87,6 +87,7 @@ libostree_1_la_SOURCES = \ src/libostree/ostree-repo.c \ src/libostree/ostree-repo-checkout.c \ src/libostree/ostree-repo-commit.c \ + src/libostree/ostree-repo-composefs.c \ src/libostree/ostree-repo-pull.c \ src/libostree/ostree-repo-pull-private.h \ src/libostree/ostree-repo-pull-verify.c \ @@ -184,7 +185,7 @@ EXTRA_DIST += \ $(top_srcdir)/src/libostree/libostree-released.sym \ $(NULL) -libostree_1_la_CFLAGS = $(AM_CFLAGS) -I$(srcdir)/bsdiff -I$(srcdir)/libglnx -I$(srcdir)/src/libotutil -I$(srcdir)/src/libostree -I$(builddir)/src/libostree \ +libostree_1_la_CFLAGS = $(AM_CFLAGS) -I$(srcdir)/bsdiff -I$(srcdir)/libglnx -I$(srcdir)/composefs -I$(srcdir)/src/libotutil -I$(srcdir)/src/libostree -I$(builddir)/src/libostree \ $(OT_INTERNAL_GIO_UNIX_CFLAGS) $(OT_INTERNAL_GPGME_CFLAGS) $(OT_DEP_LZMA_CFLAGS) $(OT_DEP_ZLIB_CFLAGS) $(OT_DEP_CRYPTO_CFLAGS) \ -fvisibility=hidden '-D_OSTREE_PUBLIC=__attribute__((visibility("default"))) extern' \ -DPKGLIBEXECDIR=\"$(pkglibexecdir)\" @@ -266,6 +267,10 @@ libostree_1_la_CFLAGS += $(OT_DEP_LIBSODIUM_CFLAGS) libostree_1_la_LIBADD += $(OT_DEP_LIBSODIUM_LIBS) endif # USE_LIBSODIUM +if USE_COMPOSEFS +libostree_1_la_LIBADD += libcomposefs.la +endif # USE_COMPOSEFS + # XXX: work around clang being passed -fstack-clash-protection which it doesn't understand # See: https://bugzilla.redhat.com/show_bug.cgi?id=1672012 INTROSPECTION_SCANNER_ENV = CC=gcc diff --git a/Makefile-switchroot.am b/Makefile-switchroot.am index 104ec0cdf3..8063d9e0cb 100644 --- a/Makefile-switchroot.am +++ b/Makefile-switchroot.am @@ -27,7 +27,9 @@ ostree_prepare_root_SOURCES = \ src/switchroot/ostree-mount-util.h \ src/switchroot/ostree-prepare-root.c \ $(NULL) +ostree_prepare_root_CFLAGS = ostree_prepare_root_CPPFLAGS = $(AM_CPPFLAGS) +ostree_prepare_root_LDADD = if BUILDOPT_USE_STATIC_COMPILER # ostree-prepare-root can be used as init in a system without a populated /lib. @@ -46,7 +48,7 @@ ostree-prepare-root : $(ostree_prepare_root_SOURCES) $(STATIC_COMPILER) -o $@ -static $(top_srcdir)/src/switchroot/ostree-prepare-root.c $(ostree_prepare_root_CPPFLAGS) $(AM_CFLAGS) $(DEFAULT_INCLUDES) -DOSTREE_PREPARE_ROOT_STATIC=1 else ostree_boot_PROGRAMS += ostree-prepare-root -ostree_prepare_root_CFLAGS = $(AM_CFLAGS) -Isrc/switchroot +ostree_prepare_root_CFLAGS += $(AM_CFLAGS) -Isrc/switchroot -I$(srcdir)/composefs endif ostree_remount_SOURCES = \ @@ -56,9 +58,13 @@ ostree_remount_SOURCES = \ 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 USE_COMPOSEFS +ostree_prepare_root_LDADD += libcomposefs.la +endif + if BUILDOPT_SYSTEMD ostree_prepare_root_CPPFLAGS += -DHAVE_SYSTEMD=1 -ostree_prepare_root_LDADD = $(AM_LDFLAGS) $(LIBSYSTEMD_LIBS) +ostree_prepare_root_LDADD += $(AM_LDFLAGS) $(LIBSYSTEMD_LIBS) endif # This is the "new mode" of using a generator for /var; see diff --git a/Makefile.am b/Makefile.am index cccf6ae7ea..ca7dec9e4f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -117,6 +117,15 @@ include bsdiff/Makefile-bsdiff.am.inc EXTRA_DIST += bsdiff/Makefile-bsdiff.am noinst_LTLIBRARIES += libbsdiff.la +COMPOSEFSDIR=$(srcdir)/composefs/libcomposefs +LCFS_DEP_CRYPTO_CFLAGS=$(OT_DEP_CRYPTO_CFLAGS) +LCFS_DEP_CRYPTO_LIBS=$(OT_DEP_CRYPTO_LIBS) +include composefs/libcomposefs/Makefile-lib.am.inc +EXTRA_DIST += composefs/libcomposefs/Makefile-lib.am +if USE_COMPOSEFS +noinst_LTLIBRARIES += libcomposefs.la +endif + include Makefile-otutil.am include Makefile-libostree.am include Makefile-ostree.am diff --git a/autogen.sh b/autogen.sh index 689bba8f89..ea2412c4e9 100755 --- a/autogen.sh +++ b/autogen.sh @@ -35,6 +35,7 @@ fi # changing this, please also change Makefile.am. sed -e 's,$(libglnx_srcpath),libglnx,g' < libglnx/Makefile-libglnx.am >libglnx/Makefile-libglnx.am.inc sed -e 's,$(libbsdiff_srcpath),bsdiff,g' < bsdiff/Makefile-bsdiff.am >bsdiff/Makefile-bsdiff.am.inc +sed -e 's,$(COMPOSEFSDIR),composefs/libcomposefs,g' < composefs/libcomposefs/Makefile-lib.am >composefs/libcomposefs/Makefile-lib.am.inc # FIXME - figure out how to get aclocal to find this by default ln -sf ../libglnx/libglnx.m4 buildutil/libglnx.m4 diff --git a/composefs b/composefs new file mode 160000 index 0000000000..e5ab5e2dc6 --- /dev/null +++ b/composefs @@ -0,0 +1 @@ +Subproject commit e5ab5e2dc6aa6bd2daab3052553b787efe16fc7d diff --git a/configure.ac b/configure.ac index ce449cb013..f7ae2d64b7 100644 --- a/configure.ac +++ b/configure.ac @@ -280,6 +280,35 @@ AS_IF([test x$have_gpgme = xyes], ) AM_CONDITIONAL(USE_GPGME, test "x$have_gpgme" = xyes) +# These are needed by libcomposefs +AC_MSG_CHECKING([for new mount API (fsconfig)]) +AC_COMPILE_IFELSE( + [AC_LANG_SOURCE([[ + #include + int cmd = FSCONFIG_CMD_CREATE; + ]])], + [AC_MSG_RESULT(yes) + AC_DEFINE([HAVE_FSCONFIG_CMD_CREATE_SYS_MOUNT_H], 1, [Define if FSCONFIG_CMD_CREATE is available in sys/mount.h])], + [AC_MSG_RESULT(no)]) +AC_COMPILE_IFELSE( + [AC_LANG_SOURCE([[ + /* also make sure it doesn't conflict with since it is always used. */ + #include + #include + int cmd = FSCONFIG_CMD_CREATE; + ]])], + [AC_MSG_RESULT(yes) + AC_DEFINE([HAVE_FSCONFIG_CMD_CREATE_LINUX_MOUNT_H], 1, [Define if FSCONFIG_CMD_CREATE is available in linux/mount.h])], + [AC_MSG_RESULT(no)]) + +AC_ARG_WITH(composefs, + AS_HELP_STRING([--with-composefs], [Support composefs]), + :, with_composefs=no) + +if test x$with_composefs != xno; then OSTREE_FEATURES="$OSTREE_FEATURES composefs"; + AC_DEFINE([HAVE_COMPOSEFS], 1, [Define if we have libcomposefs]) +fi +AM_CONDITIONAL(USE_COMPOSEFS, test $with_composefs != no) LIBSODIUM_DEPENDENCY="1.0.14" AC_ARG_WITH(ed25519_libsodium, @@ -675,7 +704,8 @@ echo " gjs-based tests: $have_gjs dracut: $with_dracut mkinitcpio: $with_mkinitcpio - Static compiler for ostree-prepare-root: $with_static_compiler" + Static compiler for ostree-prepare-root: $with_static_compiler + Composefs: $with_composefs" AS_IF([test x$with_builtin_grub2_mkconfig = xyes], [ echo " builtin grub2-mkconfig (instead of system): $with_builtin_grub2_mkconfig" ], [ diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c index 5cda047c6b..fe92486199 100644 --- a/src/libostree/ostree-repo-commit.c +++ b/src/libostree/ostree-repo-commit.c @@ -184,7 +184,7 @@ _ostree_repo_commit_tmpf_final (OstreeRepo *self, const char *checksum, OstreeOb if (!_ostree_repo_ensure_loose_objdir_at (dest_dfd, tmpbuf, cancellable, error)) return FALSE; - if (!_ostree_tmpf_fsverity (self, tmpf, error)) + if (!_ostree_tmpf_fsverity (self, tmpf, NULL, error)) return FALSE; if (!glnx_link_tmpfile_at (tmpf, GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST, dest_dfd, tmpbuf, @@ -398,14 +398,9 @@ compare_ascii_checksums_for_sorting (gconstpointer a_pp, gconstpointer b_pp) /* * Create sizes metadata GVariant and add it to the metadata variant given. */ -static GVariant * -add_size_index_to_metadata (OstreeRepo *self, GVariant *original_metadata) +static void +add_size_index_to_metadata (OstreeRepo *self, GVariantBuilder *builder) { - g_autoptr (GVariantBuilder) builder = NULL; - - /* original_metadata may be NULL */ - builder = ot_util_variant_builder_from_variant (original_metadata, G_VARIANT_TYPE ("a{sv}")); - if (self->object_sizes && g_hash_table_size (self->object_sizes) > 0) { GVariantBuilder index_builder; @@ -443,8 +438,6 @@ add_size_index_to_metadata (OstreeRepo *self, GVariant *original_metadata) /* Clear the object sizes hash table for a subsequent commit. */ g_hash_table_remove_all (self->object_sizes); } - - return g_variant_ref_sink (g_variant_builder_end (builder)); } static gboolean @@ -2912,6 +2905,23 @@ ostree_repo_write_commit (OstreeRepo *self, const char *parent, const char *subj out_commit, cancellable, error); } +static GVariant * +add_auto_metadata (OstreeRepo *self, GVariant *original_metadata, OstreeRepoFile *repo_root, + GCancellable *cancellable, GError **error) +{ + g_autoptr (GVariantBuilder) builder = NULL; + + /* original_metadata may be NULL */ + builder = ot_util_variant_builder_from_variant (original_metadata, G_VARIANT_TYPE ("a{sv}")); + + add_size_index_to_metadata (self, builder); + + if (!ostree_repo_commit_add_composefs_metadata (self, builder, repo_root, cancellable, error)) + return NULL; + + return g_variant_ref_sink (g_variant_builder_end (builder)); +} + /** * ostree_repo_write_commit_with_time: * @self: Repo @@ -2938,7 +2948,10 @@ ostree_repo_write_commit_with_time (OstreeRepo *self, const char *parent, const OstreeRepoFile *repo_root = OSTREE_REPO_FILE (root); /* Add sizes information to our metadata object */ - g_autoptr (GVariant) new_metadata = add_size_index_to_metadata (self, metadata); + g_autoptr (GVariant) new_metadata + = add_auto_metadata (self, metadata, repo_root, cancellable, error); + if (new_metadata == NULL) + return FALSE; g_autoptr (GVariant) commit = g_variant_new ( "(@a{sv}@ay@a(say)sst@ay@ay)", new_metadata ? new_metadata : create_empty_gvariant_dict (), diff --git a/src/libostree/ostree-repo-composefs.c b/src/libostree/ostree-repo-composefs.c new file mode 100644 index 0000000000..f33f5d3055 --- /dev/null +++ b/src/libostree/ostree-repo-composefs.c @@ -0,0 +1,631 @@ +/* + * Copyright (C) Red Hat, Inc. + * + * 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 . + */ + +#include "config.h" + +#include +#include +#include + +#include "ostree-core-private.h" +#include "ostree-repo-file.h" +#include "ostree-repo-private.h" + +#ifdef HAVE_COMPOSEFS +#include +#endif + +#ifdef HAVE_LINUX_FSVERITY_H +#include +#endif + +gboolean +_ostree_repo_parse_composefs_config (OstreeRepo *self, GError **error) +{ + /* Currently experimental */ + OtTristate use_composefs; + + if (!ot_keyfile_get_tristate_with_default (self->config, _OSTREE_INTEGRITY_SECTION, "composefs", + OT_TRISTATE_NO, &use_composefs, error)) + return FALSE; + + self->composefs_wanted = use_composefs; +#ifdef HAVE_COMPOSEFS + self->composefs_supported = TRUE; +#else + self->composefs_supported = FALSE; +#endif + + if (use_composefs == OT_TRISTATE_YES && !self->composefs_supported) + return glnx_throw (error, "composefs required, but libostree compiled without support"); + + return TRUE; +} + +struct OstreeComposefsTarget +{ +#ifdef HAVE_COMPOSEFS + struct lcfs_node_s *dest; +#endif + int ref_count; +}; + +/** + * ostree_composefs_target_new: + * + * Creates a #OstreeComposefsTarget which can be used with + * ostree_repo_checkout_composefs() to create a composefs image based + * on a set of checkouts. + * + * Returns: (transfer full): a new of #OstreeComposefsTarget + */ +OstreeComposefsTarget * +ostree_composefs_target_new (void) +{ + OstreeComposefsTarget *target; + + target = g_slice_new0 (OstreeComposefsTarget); + +#ifdef HAVE_COMPOSEFS + target->dest = lcfs_node_new (); + lcfs_node_set_mode (target->dest, 0755 | S_IFDIR); +#endif + + target->ref_count = 1; + + return target; +} + +/** + * ostree_composefs_target_ref: + * @target: an #OstreeComposefsTarget + * + * Increase the reference count on the given @target. + * + * Returns: (transfer full): a copy of @target, for convenience + */ +OstreeComposefsTarget * +ostree_composefs_target_ref (OstreeComposefsTarget *target) +{ + gint refcount; + g_return_val_if_fail (target != NULL, NULL); + refcount = g_atomic_int_add (&target->ref_count, 1); + g_assert (refcount > 0); + return target; +} + +/** + * ostree_composefs_target_unref: + * @target: (transfer full): an #OstreeComposefsTarget + * + * Decrease the reference count on the given @target and free it if the + * reference count reaches 0. + */ +void +ostree_composefs_target_unref (OstreeComposefsTarget *target) +{ + g_return_if_fail (target != NULL); + g_return_if_fail (target->ref_count > 0); + + if (g_atomic_int_dec_and_test (&target->ref_count)) + { +#ifdef HAVE_COMPOSEFS + g_clear_pointer (&target->dest, lcfs_node_unref); +#endif + g_slice_free (OstreeComposefsTarget, target); + } +} + +G_DEFINE_BOXED_TYPE (OstreeComposefsTarget, ostree_composefs_target, ostree_composefs_target_ref, + ostree_composefs_target_unref); + +#ifdef HAVE_COMPOSEFS + +static ssize_t +_composefs_read_cb (void *_file, void *buf, size_t count) +{ + GInputStream *in = _file; + gsize bytes_read; + + if (!g_input_stream_read_all (in, buf, count, &bytes_read, NULL, NULL)) + { + errno = EIO; + return -1; + } + + return bytes_read; +} + +static ssize_t +_composefs_write_cb (void *file, void *buf, size_t len) +{ + int fd = GPOINTER_TO_INT (file); + const char *content = buf; + ssize_t res = 0; + + while (len > 0) + { + res = write (fd, content, len); + if (res < 0 && errno == EINTR) + continue; + + if (res <= 0) + { + if (res == 0) /* Unexpected short write, should not happen when writing to a file */ + errno = ENOSPC; + return -1; + } + + break; + } + + return res; +} + +#endif + +/** + * ostree_composefs_target_write: + * @target: an #OstreeComposefsTarget + * @fd: Write image here (or -1 to not write) + * @out_fsverity_digest: (out) (array fixed-size=32) (nullable): Return location for the fsverity + * binary digest, or %NULL to not compute it + * @cancellable: Cancellable + * @error: Error + * + * Writes a composefs image file to the filesystem at the + * path specified by @destination_dfd and destination_path (if not %NULL) + * and (optionally) computes the fsverity digest of the image. + * + * Returns: %TRUE on success, %FALSE on failure + */ +gboolean +ostree_composefs_target_write (OstreeComposefsTarget *target, int fd, guchar **out_fsverity_digest, + GCancellable *cancellable, GError **error) +{ +#ifdef HAVE_COMPOSEFS + g_autoptr (GOutputStream) tmp_out = NULL; + g_autoptr (GOutputStream) out = NULL; + struct lcfs_node_s *root; + g_autofree guchar *fsverity_digest = NULL; + struct lcfs_write_options_s options = { + LCFS_FORMAT_EROFS, + }; + + root = lcfs_node_lookup_child (target->dest, "root"); + if (root == NULL) + root = target->dest; /* Nothing was checked out, use an empty dir */ + + if (out_fsverity_digest) + { + fsverity_digest = g_malloc (OSTREE_SHA256_DIGEST_LEN); + options.digest_out = fsverity_digest; + } + + if (fd != -1) + { + options.file = GINT_TO_POINTER (fd); + options.file_write_cb = _composefs_write_cb; + } + + if (lcfs_write_to (root, &options) != 0) + return glnx_throw_errno (error); + + if (out_fsverity_digest) + *out_fsverity_digest = g_steal_pointer (&fsverity_digest); + + return TRUE; +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Composeefs is not supported in this ostree build"); + return FALSE; +#endif +} + +#ifdef HAVE_COMPOSEFS +static gboolean +_ostree_composefs_set_xattrs (struct lcfs_node_s *node, GVariant *xattrs, GCancellable *cancellable, + GError **error) +{ + const guint n = g_variant_n_children (xattrs); + for (guint i = 0; i < n; i++) + { + const guint8 *name; + g_autoptr (GVariant) value = NULL; + g_variant_get_child (xattrs, i, "(^&ay@ay)", &name, &value); + + gsize value_len; + const guint8 *value_data = g_variant_get_fixed_array (value, &value_len, 1); + + if (lcfs_node_set_xattr (node, (char *)name, (char *)value_data, value_len) != 0) + return glnx_throw_errno_prefix (error, "Setting composefs xattrs for %s", name); + } + + return TRUE; +} + +static gboolean +checkout_one_composefs_file_at (OstreeRepo *repo, const char *checksum, struct lcfs_node_s *parent, + const char *destination_name, GCancellable *cancellable, + GError **error) +{ + g_autoptr (GInputStream) input = NULL; + g_autoptr (GVariant) xattrs = NULL; + struct lcfs_node_s *existing; + + /* Validate this up front to prevent path traversal attacks */ + if (!ot_util_filename_validate (destination_name, error)) + return FALSE; + + existing = lcfs_node_lookup_child (parent, destination_name); + if (existing != NULL) + return glnx_throw (error, "Target checkout file already exist"); + + g_autoptr (GFileInfo) source_info = NULL; + if (!ostree_repo_load_file (repo, checksum, &input, &source_info, &xattrs, cancellable, error)) + return FALSE; + + const guint32 source_mode = g_file_info_get_attribute_uint32 (source_info, "unix::mode"); + const guint32 source_uid = g_file_info_get_attribute_uint32 (source_info, "unix::uid"); + const guint32 source_gid = g_file_info_get_attribute_uint32 (source_info, "unix::gid"); + const guint64 source_size = g_file_info_get_size (source_info); + const char *source_symlink_target = g_file_info_get_symlink_target (source_info); + const gboolean is_symlink + = (g_file_info_get_file_type (source_info) == G_FILE_TYPE_SYMBOLIC_LINK); + + struct lcfs_node_s *node = lcfs_node_new (); + if (node == NULL) + return glnx_throw (error, "Out of memory"); + + /* Takes ownership on success */ + if (lcfs_node_add_child (parent, node, destination_name) != 0) + { + lcfs_node_unref (node); + return glnx_throw_errno (error); + } + + lcfs_node_set_mode (node, source_mode); + lcfs_node_set_uid (node, source_uid); + lcfs_node_set_gid (node, source_gid); + lcfs_node_set_size (node, source_size); + if (is_symlink) + { + if (lcfs_node_set_payload (node, source_symlink_target) != 0) + return glnx_throw_errno (error); + } + else if (source_size != 0) + { + char loose_path_buf[_OSTREE_LOOSE_PATH_MAX]; + _ostree_loose_path (loose_path_buf, checksum, OSTREE_OBJECT_TYPE_FILE, OSTREE_REPO_MODE_BARE); + if (lcfs_node_set_payload (node, loose_path_buf) != 0) + return glnx_throw_errno (error); + + guchar *known_digest = NULL; + +#ifdef HAVE_LINUX_FSVERITY_H + /* First try to get the digest directly from the bare repo file. + * This is the typical case when we're pulled into the target + * system repo with verity on and are recreating the composefs + * image during deploy. */ + char buf[sizeof (struct fsverity_digest) + OSTREE_SHA256_DIGEST_LEN]; + + if (G_IS_UNIX_INPUT_STREAM (input)) + { + int content_fd = g_unix_input_stream_get_fd (G_UNIX_INPUT_STREAM (input)); + struct fsverity_digest *d = (struct fsverity_digest *)&buf; + d->digest_size = OSTREE_SHA256_DIGEST_LEN; + + if (ioctl (content_fd, FS_IOC_MEASURE_VERITY, d) == 0 + && d->digest_size == OSTREE_SHA256_DIGEST_LEN + && d->digest_algorithm == FS_VERITY_HASH_ALG_SHA256) + known_digest = d->digest; + } +#endif + + if (known_digest) + lcfs_node_set_fsverity_digest (node, known_digest); + else if (lcfs_node_set_fsverity_from_content (node, input, _composefs_read_cb) != 0) + return glnx_throw_errno (error); + } + + if (xattrs) + { + if (!_ostree_composefs_set_xattrs (node, xattrs, cancellable, error)) + return FALSE; + } + + g_clear_object (&input); + + return TRUE; +} + +static gboolean +checkout_composefs_recurse (OstreeRepo *self, const char *dirtree_checksum, + const char *dirmeta_checksum, struct lcfs_node_s *parent, + const char *name, GCancellable *cancellable, GError **error) +{ + g_autoptr (GVariant) dirtree = NULL; + g_autoptr (GVariant) dirmeta = NULL; + g_autoptr (GVariant) xattrs = NULL; + struct lcfs_node_s *directory; + + if (!ostree_repo_load_variant (self, OSTREE_OBJECT_TYPE_DIR_TREE, dirtree_checksum, &dirtree, + error)) + return FALSE; + if (!ostree_repo_load_variant (self, OSTREE_OBJECT_TYPE_DIR_META, dirmeta_checksum, &dirmeta, + error)) + return FALSE; + + /* Parse OSTREE_OBJECT_TYPE_DIR_META */ + guint32 uid, gid, mode; + g_variant_get (dirmeta, "(uuu@a(ayay))", &uid, &gid, &mode, &xattrs); + uid = GUINT32_FROM_BE (uid); + gid = GUINT32_FROM_BE (gid); + mode = GUINT32_FROM_BE (mode); + + directory = lcfs_node_lookup_child (parent, name); + if (directory != NULL && lcfs_node_get_mode (directory) != 0) + { + return glnx_throw (error, "Target checkout directory already exist"); + } + else + { + directory = lcfs_node_new (); + if (directory == NULL) + return glnx_throw (error, "Out of memory"); + + /* Takes ownership on success */ + if (lcfs_node_add_child (parent, directory, name) != 0) + { + lcfs_node_unref (directory); + return glnx_throw_errno (error); + } + } + + lcfs_node_set_mode (directory, mode); + lcfs_node_set_uid (directory, uid); + lcfs_node_set_gid (directory, gid); + + /* Set the xattrs if we created the dir */ + if (xattrs && !_ostree_composefs_set_xattrs (directory, xattrs, cancellable, error)) + return FALSE; + + /* Process files in this subdir */ + { + g_autoptr (GVariant) dir_file_contents = g_variant_get_child_value (dirtree, 0); + GVariantIter viter; + g_variant_iter_init (&viter, dir_file_contents); + const char *fname; + g_autoptr (GVariant) contents_csum_v = NULL; + while (g_variant_iter_loop (&viter, "(&s@ay)", &fname, &contents_csum_v)) + { + char tmp_checksum[OSTREE_SHA256_STRING_LEN + 1]; + _ostree_checksum_inplace_from_bytes_v (contents_csum_v, tmp_checksum); + + if (!checkout_one_composefs_file_at (self, tmp_checksum, directory, fname, cancellable, + error)) + return FALSE; + } + contents_csum_v = NULL; /* iter_loop freed it */ + } + + /* Process subdirectories */ + { + g_autoptr (GVariant) dir_subdirs = g_variant_get_child_value (dirtree, 1); + const char *dname; + g_autoptr (GVariant) subdirtree_csum_v = NULL; + g_autoptr (GVariant) subdirmeta_csum_v = NULL; + GVariantIter viter; + g_variant_iter_init (&viter, dir_subdirs); + while ( + g_variant_iter_loop (&viter, "(&s@ay@ay)", &dname, &subdirtree_csum_v, &subdirmeta_csum_v)) + { + /* Validate this up front to prevent path traversal attacks. Note that + * we don't validate at the top of this function like we do for + * checkout_one_file_at() becuase I believe in some cases this function + * can be called *initially* with user-specified paths for the root + * directory. + */ + if (!ot_util_filename_validate (dname, error)) + return FALSE; + + char subdirtree_checksum[OSTREE_SHA256_STRING_LEN + 1]; + _ostree_checksum_inplace_from_bytes_v (subdirtree_csum_v, subdirtree_checksum); + char subdirmeta_checksum[OSTREE_SHA256_STRING_LEN + 1]; + _ostree_checksum_inplace_from_bytes_v (subdirmeta_csum_v, subdirmeta_checksum); + if (!checkout_composefs_recurse (self, subdirtree_checksum, subdirmeta_checksum, directory, + dname, cancellable, error)) + return FALSE; + } + } + + return TRUE; +} + +/* Begin a checkout process */ +static gboolean +checkout_composefs_tree (OstreeRepo *self, OstreeComposefsTarget *target, OstreeRepoFile *source, + GFileInfo *source_info, GCancellable *cancellable, GError **error) +{ + if (g_file_info_get_file_type (source_info) != G_FILE_TYPE_DIRECTORY) + return glnx_throw (error, "Root checkout of composefs must be directory"); + + /* Cache any directory metadata we read during this operation; + * see commit b7afe91e21143d7abb0adde440683a52712aa246 + */ + g_auto (OstreeRepoMemoryCacheRef) memcache_ref; + _ostree_repo_memory_cache_ref_init (&memcache_ref, self); + + g_assert_cmpint (g_file_info_get_file_type (source_info), ==, G_FILE_TYPE_DIRECTORY); + + const char *dirtree_checksum = ostree_repo_file_tree_get_contents_checksum (source); + const char *dirmeta_checksum = ostree_repo_file_tree_get_metadata_checksum (source); + return checkout_composefs_recurse (self, dirtree_checksum, dirmeta_checksum, target->dest, "root", + cancellable, error); +} + +static struct lcfs_node_s * +ensure_lcfs_dir (struct lcfs_node_s *parent, const char *name, GError **error) +{ + struct lcfs_node_s *node; + + node = lcfs_node_lookup_child (parent, name); + if (node != NULL) + return node; + + node = lcfs_node_new (); + lcfs_node_set_mode (node, 0755 | S_IFDIR); + if (lcfs_node_add_child (parent, node, name) != 0) + { + lcfs_node_unref (node); + glnx_throw_errno (error); + return NULL; + } + + return node; +} +#endif + +/** + * ostree_repo_checkout_composefs: + * @self: Repo + * @target: A target for the checkout + * @source: Source tree + * @cancellable: Cancellable + * @error: Error + * + * Check out @source into @target, which is an in-memory + * representation of a composefs image. The @target can be reused + * multiple times to layer multiple checkouts before writing out the + * image to disk using ostree_composefs_target_write(). + * + * There are various options specified by @options that affect + * how the image is created. + * + * Returns: %TRUE on success, %FALSE on failure + */ +gboolean +ostree_repo_checkout_composefs (OstreeRepo *self, OstreeComposefsTarget *target, + OstreeRepoFile *source, GCancellable *cancellable, GError **error) +{ +#ifdef HAVE_COMPOSEFS + char *root_dirs[] = { "usr", "etc", "boot", "var", "sysroot" }; + int i; + struct lcfs_node_s *root, *dir; + + g_autoptr (GFileInfo) target_info + = g_file_query_info (G_FILE (source), OSTREE_GIO_FAST_QUERYINFO, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, error); + if (!target_info) + return FALSE; + + if (!checkout_composefs_tree (self, target, source, target_info, cancellable, error)) + return FALSE; + + /* We need a root dir */ + root = ensure_lcfs_dir (target->dest, "root", error); + if (root == NULL) + return FALSE; + + /* To work as a rootfs we need some root directories to use as bind-mounts */ + for (i = 0; i < G_N_ELEMENTS (root_dirs); i++) + { + dir = ensure_lcfs_dir (root, root_dirs[i], error); + if (dir == NULL) + return FALSE; + } + + return TRUE; +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Composeefs is not supported in this ostree build"); + return FALSE; +#endif +} + +#ifdef HAVE_COMPOSEFS +static gboolean +ostree_repo_commit_add_composefs_sig (OstreeRepo *self, GVariantBuilder *builder, + guchar *fsverity_digest, GCancellable *cancellable, + GError **error) +{ + g_autofree char *certfile = NULL; + g_autofree char *keyfile = NULL; + g_autoptr (GBytes) sig = NULL; + + certfile + = g_key_file_get_string (self->config, _OSTREE_INTEGRITY_SECTION, "composefs-certfile", NULL); + keyfile + = g_key_file_get_string (self->config, _OSTREE_INTEGRITY_SECTION, "composefs-keyfile", NULL); + + if (certfile == NULL && keyfile == NULL) + return TRUE; + + if (certfile == NULL) + return glnx_throw (error, "Error signing compoosefs: keyfile specified but certfile is not"); + + if (keyfile == NULL) + return glnx_throw (error, "Error signing compoosefs: certfile specified but keyfile is not"); + + if (!_ostree_fsverity_sign (certfile, keyfile, fsverity_digest, &sig, cancellable, error)) + return FALSE; + + g_variant_builder_add (builder, "{sv}", "ostree.composefs-sig", ot_gvariant_new_ay_bytes (sig)); + + return TRUE; +} +#endif + +gboolean +ostree_repo_commit_add_composefs_metadata (OstreeRepo *self, GVariantBuilder *builder, + OstreeRepoFile *repo_root, GCancellable *cancellable, + GError **error) +{ + gboolean add_metadata; + + if (!ot_keyfile_get_boolean_with_default (self->config, _OSTREE_INTEGRITY_SECTION, + "composefs-add-metadata", FALSE, &add_metadata, error)) + return FALSE; + + if (add_metadata) + { +#ifdef HAVE_COMPOSEFS + /* Create a composefs image and put in deploy dir as .ostree.cfs */ + g_autoptr (OstreeComposefsTarget) target = ostree_composefs_target_new (); + + if (!ostree_repo_checkout_composefs (self, target, repo_root, cancellable, error)) + return FALSE; + + g_autofree guchar *fsverity_digest = NULL; + if (!ostree_composefs_target_write (target, -1, &fsverity_digest, cancellable, error)) + return FALSE; + + g_variant_builder_add (builder, "{sv}", "ostree.composefs", + ot_gvariant_new_bytearray (fsverity_digest, OSTREE_SHA256_DIGEST_LEN)); + + if (!ostree_repo_commit_add_composefs_sig (self, builder, fsverity_digest, cancellable, + error)) + return FALSE; +#else + return glnx_throw (error, "composefs required, but libostree compiled without support"); +#endif + } + + return TRUE; +} diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h index f386585481..76f3152bcb 100644 --- a/src/libostree/ostree-repo-private.h +++ b/src/libostree/ostree-repo-private.h @@ -65,6 +65,8 @@ G_BEGIN_DECLS #define OSTREE_COMMIT_TIMESTAMP "ostree.commit.timestamp" #define OSTREE_COMMIT_VERSION "ostree.commit.version" +#define _OSTREE_INTEGRITY_SECTION "ex-integrity" + typedef enum { OSTREE_REPO_TEST_ERROR_PRE_COMMIT = (1 << 0), @@ -176,6 +178,8 @@ struct OstreeRepo gboolean txn_locked; _OstreeFeatureSupport fs_verity_wanted; _OstreeFeatureSupport fs_verity_supported; + OtTristate composefs_wanted; + gboolean composefs_supported; GMutex cache_lock; guint dirmeta_cache_refcount; @@ -388,11 +392,16 @@ gboolean _ostree_repo_maybe_regenerate_summary (OstreeRepo *self, GCancellable * GError **error); gboolean _ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error); +gboolean _ostree_repo_parse_composefs_config (OstreeRepo *self, GError **error); gboolean _ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, _OstreeFeatureSupport fsverity_requested, - gboolean *supported, GError **error); + GBytes *signature, gboolean *supported, GError **error); -gboolean _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GError **error); +gboolean _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GBytes *signature, + GError **error); +gboolean _ostree_fsverity_sign (const char *certfile, const char *keyfile, + const guchar *fsverity_digest, GBytes **data_out, + GCancellable *cancellable, GError **error); gboolean _ostree_repo_verify_bindings (const char *collection_id, const char *ref_name, GVariant *commit, GError **error); @@ -442,4 +451,24 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoAutoTransaction, _ostree_repo_auto_tran * should not be made into public API, even if the rest is */ OstreeRepoAutoTransaction *_ostree_repo_auto_transaction_new (OstreeRepo *repo); +typedef struct OstreeComposefsTarget OstreeComposefsTarget; + +GType ostree_composefs_target_get_type (void) G_GNUC_CONST; +OstreeComposefsTarget *ostree_composefs_target_new (void); +OstreeComposefsTarget *ostree_composefs_target_ref (OstreeComposefsTarget *target); +void ostree_composefs_target_unref (OstreeComposefsTarget *target); +gboolean ostree_composefs_target_write (OstreeComposefsTarget *target, int fd, + guchar **out_fsverity_digest, GCancellable *cancellable, + GError **error); + +gboolean ostree_repo_checkout_composefs (OstreeRepo *self, OstreeComposefsTarget *target, + OstreeRepoFile *source, GCancellable *cancellable, + GError **error); + +gboolean ostree_repo_commit_add_composefs_metadata (OstreeRepo *self, GVariantBuilder *builder, + OstreeRepoFile *repo_root, + GCancellable *cancellable, GError **error); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeComposefsTarget, ostree_composefs_target_unref) + G_END_DECLS diff --git a/src/libostree/ostree-repo-verity.c b/src/libostree/ostree-repo-verity.c index 8c199e858e..512386a96f 100644 --- a/src/libostree/ostree-repo-verity.c +++ b/src/libostree/ostree-repo-verity.c @@ -29,37 +29,76 @@ #include #endif +#if defined(HAVE_OPENSSL) +#include +#include +#include +#include +#include + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (X509, X509_free); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (EVP_PKEY, EVP_PKEY_free); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (BIO, BIO_free); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PKCS7, PKCS7_free); +#endif + gboolean _ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error) { /* Currently experimental */ - static const char fsverity_key[] = "ex-fsverity"; - self->fs_verity_wanted = _OSTREE_FEATURE_NO; + OtTristate use_composefs; + OtTristate use_fsverity; + #ifdef HAVE_LINUX_FSVERITY_H self->fs_verity_supported = _OSTREE_FEATURE_MAYBE; #else self->fs_verity_supported = _OSTREE_FEATURE_NO; #endif - gboolean fsverity_required = FALSE; - if (!ot_keyfile_get_boolean_with_default (self->config, fsverity_key, "required", FALSE, - &fsverity_required, error)) + + /* Composefs use implies fsverity default of maybe */ + if (!ot_keyfile_get_tristate_with_default (self->config, _OSTREE_INTEGRITY_SECTION, "composefs", + OT_TRISTATE_NO, &use_composefs, error)) + return FALSE; + + if (!ot_keyfile_get_tristate_with_default (self->config, _OSTREE_INTEGRITY_SECTION, "fsverity", + (use_composefs != OT_TRISTATE_NO) ? OT_TRISTATE_MAYBE + : OT_TRISTATE_NO, + &use_fsverity, error)) return FALSE; - if (fsverity_required) + + if (use_fsverity != OT_TRISTATE_NO) { - self->fs_verity_wanted = _OSTREE_FEATURE_YES; - if (self->fs_verity_supported == _OSTREE_FEATURE_NO) - return glnx_throw (error, "fsverity required, but libostree compiled without support"); + self->fs_verity_wanted = (_OstreeFeatureSupport)use_fsverity; } else { - gboolean fsverity_opportunistic = FALSE; - if (!ot_keyfile_get_boolean_with_default (self->config, fsverity_key, "opportunistic", FALSE, - &fsverity_opportunistic, error)) + /* Fall back to old configuration key */ + static const char fsverity_section[] = "ex-fsverity"; + + self->fs_verity_wanted = _OSTREE_FEATURE_NO; + gboolean fsverity_required = FALSE; + if (!ot_keyfile_get_boolean_with_default (self->config, fsverity_section, "required", FALSE, + &fsverity_required, error)) return FALSE; - if (fsverity_opportunistic) - self->fs_verity_wanted = _OSTREE_FEATURE_MAYBE; + if (fsverity_required) + { + self->fs_verity_wanted = _OSTREE_FEATURE_YES; + } + else + { + gboolean fsverity_opportunistic = FALSE; + if (!ot_keyfile_get_boolean_with_default (self->config, fsverity_section, "opportunistic", + FALSE, &fsverity_opportunistic, error)) + return FALSE; + if (fsverity_opportunistic) + self->fs_verity_wanted = _OSTREE_FEATURE_MAYBE; + } } + if (self->fs_verity_wanted == _OSTREE_FEATURE_YES + && self->fs_verity_supported == _OSTREE_FEATURE_NO) + return glnx_throw (error, "fsverity required, but libostree compiled without support"); + return TRUE; } @@ -69,7 +108,7 @@ _ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error) * */ gboolean _ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, _OstreeFeatureSupport fsverity_requested, - gboolean *supported, GError **error) + GBytes *signature, gboolean *supported, GError **error) { /* Set this by default to simplify the code below */ if (supported) @@ -93,8 +132,8 @@ _ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, _OstreeFeatureSupport fsverity_re arg.block_size = 4096; /* FIXME query */ arg.salt_size = 0; /* TODO store salt in ostree repo config */ arg.salt_ptr = 0; - arg.sig_size = 0; /* We don't currently expect use of in-kernel signature verification */ - arg.sig_ptr = 0; + arg.sig_size = signature ? g_bytes_get_size (signature) : 0; + arg.sig_ptr = signature ? (guint64)g_bytes_get_data (signature, NULL) : 0; if (ioctl (tmpf->fd, FS_IOC_ENABLE_VERITY, &arg) < 0) { @@ -120,7 +159,7 @@ _ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, _OstreeFeatureSupport fsverity_re * as well as to support "opportunistic" use (requested and if filesystem supports). * */ gboolean -_ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GError **error) +_ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GBytes *signature, GError **error) { #ifdef HAVE_LINUX_FSVERITY_H g_mutex_lock (&self->txn_lock); @@ -143,7 +182,7 @@ _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GError **error) } gboolean supported = FALSE; - if (!_ostree_tmpf_fsverity_core (tmpf, fsverity_wanted, &supported, error)) + if (!_ostree_tmpf_fsverity_core (tmpf, fsverity_wanted, signature, &supported, error)) return FALSE; if (!supported) @@ -167,3 +206,131 @@ _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, GError **error) #endif return TRUE; } + +#if defined(HAVE_OPENSSL) +static gboolean +read_pem_x509_certificate (const char *certfile, X509 **cert_ret, GError **error) +{ + g_autoptr (BIO) bio = NULL; + X509 *cert; + + errno = 0; + bio = BIO_new_file (certfile, "r"); + if (!bio) + return glnx_throw_errno_prefix (error, "Error loading composefs certfile '%s'", certfile); + + cert = PEM_read_bio_X509 (bio, NULL, NULL, NULL); + if (!cert) + return glnx_throw (error, "Error parsing composefs certfile '%s'", certfile); + + *cert_ret = cert; + return TRUE; +} + +static gboolean +read_pem_pkcs8_private_key (const char *keyfile, EVP_PKEY **pkey_ret, GError **error) +{ + g_autoptr (BIO) bio; + EVP_PKEY *pkey; + + errno = 0; + bio = BIO_new_file (keyfile, "r"); + if (!bio) + return glnx_throw_errno_prefix (error, "Error loading composefs keyfile '%s'", keyfile); + + pkey = PEM_read_bio_PrivateKey (bio, NULL, NULL, NULL); + if (!pkey) + return glnx_throw (error, "Error parsing composefs keyfile '%s'", keyfile); + + *pkey_ret = pkey; + return TRUE; +} + +static gboolean +sign_pkcs7 (const void *data_to_sign, size_t data_size, EVP_PKEY *pkey, X509 *cert, + const EVP_MD *md, BIO **res, GError **error) +{ + int pkcs7_flags = PKCS7_BINARY | PKCS7_DETACHED | PKCS7_NOATTR | PKCS7_NOCERTS | PKCS7_PARTIAL; + g_autoptr (BIO) bio = NULL; + g_autoptr (BIO) bio_res = NULL; + g_autoptr (PKCS7) p7 = NULL; + + bio = BIO_new_mem_buf ((void *)data_to_sign, data_size); + if (!bio) + return glnx_throw (error, "Can't allocate buffer"); + + p7 = PKCS7_sign (NULL, NULL, NULL, bio, pkcs7_flags); + if (!p7) + return glnx_throw (error, "Can't initialize PKCS#7"); + + if (!PKCS7_sign_add_signer (p7, cert, pkey, md, pkcs7_flags)) + return glnx_throw (error, "Can't add signer to PKCS#7"); + + if (PKCS7_final (p7, bio, pkcs7_flags) != 1) + return glnx_throw (error, "Can't finalize PKCS#7"); + + bio_res = BIO_new (BIO_s_mem ()); + if (!bio_res) + return glnx_throw (error, "Can't allocate buffer"); + + if (i2d_PKCS7_bio (bio_res, p7) != 1) + return glnx_throw (error, "Can't DER-encode PKCS#7 signature object"); + + *res = g_steal_pointer (&bio_res); + return TRUE; +} + +gboolean +_ostree_fsverity_sign (const char *certfile, const char *keyfile, const guchar *fsverity_digest, + GBytes **data_out, GCancellable *cancellable, GError **error) +{ + g_autofree struct fsverity_formatted_digest *d = NULL; + gsize d_size; + g_autoptr (X509) cert = NULL; + g_autoptr (EVP_PKEY) pkey = NULL; + g_autoptr (BIO) bio_sig = NULL; + const EVP_MD *md; + guchar *sig; + long sig_size; + + if (certfile == NULL) + return glnx_throw (error, "certfile not specified"); + + if (keyfile == NULL) + return glnx_throw (error, "keyfile not specified"); + + if (!read_pem_x509_certificate (certfile, &cert, error)) + return FALSE; + + if (!read_pem_pkcs8_private_key (keyfile, &pkey, error)) + return FALSE; + + md = EVP_sha256 (); + if (md == NULL) + return glnx_throw (error, "No sha256 support in openssl"); + + d_size = sizeof (struct fsverity_formatted_digest) + OSTREE_SHA256_DIGEST_LEN; + d = g_malloc0 (d_size); + + memcpy (d->magic, "FSVerity", 8); + d->digest_algorithm = GUINT16_TO_LE (FS_VERITY_HASH_ALG_SHA256); + d->digest_size = GUINT16_TO_LE (OSTREE_SHA256_DIGEST_LEN); + memcpy (d->digest, fsverity_digest, OSTREE_SHA256_DIGEST_LEN); + + if (!sign_pkcs7 (d, d_size, pkey, cert, md, &bio_sig, error)) + return FALSE; + + sig_size = BIO_get_mem_data (bio_sig, &sig); + + *data_out = g_bytes_new (sig, sig_size); + + return TRUE; +} +#else +gboolean +_ostree_fsverity_sign (const char *certfile, const char *keyfile, const guchar *fsverity_digest, + GBytes **data_out, GCancellable *cancellable, GError **error) +{ + return glnx_throw (error, "fsverity signature support not built"); +} +#endif diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c index 6e58253d5f..8633701f40 100644 --- a/src/libostree/ostree-repo.c +++ b/src/libostree/ostree-repo.c @@ -3154,6 +3154,9 @@ reload_core_config (OstreeRepo *self, GCancellable *cancellable, GError **error) if (!_ostree_repo_parse_fsverity_config (self, error)) return FALSE; + if (!_ostree_repo_parse_composefs_config (self, error)) + return FALSE; + { g_clear_pointer (&self->collection_id, g_free); if (!ot_keyfile_get_value_with_default (self->config, "core", "collection-id", NULL, diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 425abe8bd6..c0ce1e94c3 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -163,7 +163,7 @@ install_into_boot (OstreeRepo *repo, OstreeSePolicy *sepolicy, int src_dfd, cons _OstreeFeatureSupport boot_verity = _OSTREE_FEATURE_NO; if (repo->fs_verity_wanted != _OSTREE_FEATURE_NO) boot_verity = _OSTREE_FEATURE_MAYBE; - if (!_ostree_tmpf_fsverity_core (&tmp_dest, boot_verity, NULL, error)) + if (!_ostree_tmpf_fsverity_core (&tmp_dest, boot_verity, NULL, NULL, error)) return FALSE; if (!glnx_link_tmpfile_at (&tmp_dest, GLNX_LINK_TMPFILE_NOREPLACE, dest_dfd, dest_subpath, error)) @@ -582,13 +582,45 @@ merge_configuration_from (OstreeSysroot *sysroot, OstreeDeployment *merge_deploy return TRUE; } +#ifdef HAVE_COMPOSEFS +static gboolean +compare_verity_digests (GVariant *metadata_composefs, const guchar *fsverity_digest, GError **error) +{ + const guchar *expected_digest; + + if (metadata_composefs == NULL) + return TRUE; + + if (g_variant_n_children (metadata_composefs) != OSTREE_SHA256_DIGEST_LEN) + return glnx_throw (error, "Expected composefs fs-verity in metadata has the wrong size"); + + expected_digest = g_variant_get_data (metadata_composefs); + if (memcmp (fsverity_digest, expected_digest, OSTREE_SHA256_DIGEST_LEN) != 0) + { + char actual_checksum[OSTREE_SHA256_STRING_LEN + 1]; + char expected_checksum[OSTREE_SHA256_STRING_LEN + 1]; + + ostree_checksum_inplace_from_bytes (fsverity_digest, actual_checksum); + ostree_checksum_inplace_from_bytes (expected_digest, expected_checksum); + + return glnx_throw (error, + "Generated composefs image digest (%s) doesn't match expected digest (%s)", + actual_checksum, expected_checksum); + } + + return TRUE; +} + +#endif + /* Look up @revision in the repository, and check it out in * /ostree/deploy/OS/deploy/${treecsum}.${deployserial}. * A dfd for the result is returned in @out_deployment_dfd. */ static gboolean checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployment *deployment, - int *out_deployment_dfd, GCancellable *cancellable, GError **error) + const char *revision, int *out_deployment_dfd, GCancellable *cancellable, + GError **error) { GLNX_AUTO_PREFIX_ERROR ("Checking out deployment tree", error); /* Find the directory with deployments for this stateroot */ @@ -614,6 +646,92 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy cancellable, error)) return FALSE; +#ifdef HAVE_COMPOSEFS + if (repo->composefs_wanted != OT_TRISTATE_NO) + { + gboolean apply_composefs_signature; + g_autofree guchar *fsverity_digest = NULL; + g_auto (GLnxTmpfile) tmpf = { + 0, + }; + g_autoptr (GVariant) commit_variant = NULL; + + if (!ostree_repo_load_commit (repo, revision, &commit_variant, NULL, error)) + return FALSE; + + if (!ot_keyfile_get_boolean_with_default (repo->config, _OSTREE_INTEGRITY_SECTION, + "composefs-apply-sig", TRUE, + &apply_composefs_signature, error)) + return FALSE; + + g_autoptr (GVariant) metadata = g_variant_get_child_value (commit_variant, 0); + g_autoptr (GVariant) metadata_composefs + = g_variant_lookup_value (metadata, "ostree.composefs", G_VARIANT_TYPE_BYTESTRING); + g_autoptr (GVariant) metadata_composefs_sig + = g_variant_lookup_value (metadata, "ostree.composefs-sig", G_VARIANT_TYPE_BYTESTRING); + + /* Create a composefs image and put in deploy dir as .ostree.cfs */ + g_autoptr (OstreeComposefsTarget) target = ostree_composefs_target_new (); + + g_autoptr (GFile) commit_root = NULL; + if (!ostree_repo_read_commit (repo, csum, &commit_root, NULL, cancellable, error)) + return FALSE; + + if (!ostree_repo_checkout_composefs (repo, target, (OstreeRepoFile *)commit_root, cancellable, + error)) + return FALSE; + + g_autofree char *composefs_cfs_path + = g_strdup_printf ("%s/.ostree.cfs", checkout_target_name); + + if (!glnx_open_tmpfile_linkable_at (osdeploy_dfd, checkout_target_name, O_WRONLY | O_CLOEXEC, + &tmpf, error)) + return FALSE; + + if (!ostree_composefs_target_write (target, tmpf.fd, &fsverity_digest, cancellable, error)) + return FALSE; + + /* If the commit specified a composefs digest, verify it */ + if (!compare_verity_digests (metadata_composefs, fsverity_digest, error)) + return FALSE; + + if (!glnx_fchmod (tmpf.fd, 0644, error)) + return FALSE; + + if (metadata_composefs_sig && apply_composefs_signature) + { + /* We can't apply the signature during deploy, because the corresponding public key for + this commit is not loaded into the keyring. So, we delay fs-verity application to the + first boot. */ + + g_autofree char *composefs_sig_path + = g_strdup_printf ("%s/.ostree.cfs.sig", checkout_target_name); + g_autoptr (GBytes) sig = g_variant_get_data_as_bytes (metadata_composefs_sig); + + if (!glnx_file_replace_contents_at (osdeploy_dfd, composefs_sig_path, + g_bytes_get_data (sig, NULL), g_bytes_get_size (sig), + 0, cancellable, error)) + return FALSE; + } + else + { + if (!_ostree_tmpf_fsverity (repo, &tmpf, NULL, error)) + return FALSE; + } + + if (!glnx_link_tmpfile_at (&tmpf, GLNX_LINK_TMPFILE_REPLACE, osdeploy_dfd, composefs_cfs_path, + error)) + return FALSE; + + /* This is where the erofs image will be temporarily mounted */ + g_autofree char *composefs_mnt_path + = g_strdup_printf ("%s/.ostree.mnt", checkout_target_name); + + if (!glnx_shutil_mkdir_p_at (osdeploy_dfd, composefs_mnt_path, 0775, cancellable, error)) + return FALSE; + } +#endif + return glnx_opendirat (osdeploy_dfd, checkout_target_name, TRUE, out_deployment_dfd, error); } @@ -2995,7 +3113,8 @@ sysroot_initialize_deployment (OstreeSysroot *self, const char *osname, const ch /* Check out the userspace tree onto the filesystem */ glnx_autofd int deployment_dfd = -1; - if (!checkout_deployment_tree (self, repo, new_deployment, &deployment_dfd, cancellable, error)) + if (!checkout_deployment_tree (self, repo, new_deployment, revision, &deployment_dfd, cancellable, + error)) return FALSE; g_autoptr (OstreeKernelLayout) kernel_layout = NULL; diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h index 6c1c5d3e9c..78e736fca8 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -70,9 +70,11 @@ struct OstreeSysroot 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 */ + /* The device/inode for / and /etc, used to detect booted deployment */ dev_t root_device; ino_t root_inode; + dev_t etc_device; + ino_t etc_inode; gboolean is_physical; /* TRUE if we're pointed at physical storage root and not a deployment */ GPtrArray *deployments; diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index ca8b327717..946bd35359 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -799,14 +799,26 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment * if (looking_for_booted_deployment) { struct stat stbuf; + struct stat etc_stbuf = {}; if (!glnx_fstat (deployment_dfd, &stbuf, error)) return FALSE; + + /* We look for either the root or the etc subdir of the + * deployment. We need to do this, because when using composefs, + * the root is not a bind mount of the deploy dir, but the etc + * dir is. + */ + + if (!glnx_fstatat_allow_noent (deployment_dfd, "etc", &etc_stbuf, 0, error)) + return FALSE; + /* A bit ugly, we're assigning to a sysroot-owned variable from deep in * this parsing code. But eh, if something fails the sysroot state can't * be relied on anyways. */ is_booted_deployment - = (stbuf.st_dev == self->root_device && stbuf.st_ino == self->root_inode); + = (stbuf.st_dev == self->root_device && stbuf.st_ino == self->root_inode) + || (etc_stbuf.st_dev == self->etc_device && etc_stbuf.st_ino == self->etc_inode); } g_autoptr (OstreeDeployment) ret_deployment @@ -1003,6 +1015,17 @@ ostree_sysroot_initialize (OstreeSysroot *self, GError **error) self->root_inode = root_stbuf.st_ino; } + { + struct stat etc_stbuf; + if (!glnx_fstatat_allow_noent (AT_FDCWD, "/etc", &etc_stbuf, 0, error)) + return FALSE; + if (errno != ENOENT) + { + self->etc_device = etc_stbuf.st_dev; + self->etc_inode = etc_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; diff --git a/src/libotutil/ot-keyfile-utils.c b/src/libotutil/ot-keyfile-utils.c index 141b2f7a69..a1250dc64f 100644 --- a/src/libotutil/ot-keyfile-utils.c +++ b/src/libotutil/ot-keyfile-utils.c @@ -60,6 +60,50 @@ ot_keyfile_get_boolean_with_default (GKeyFile *keyfile, const char *section, con return TRUE; } +gboolean +ot_keyfile_get_tristate_with_default (GKeyFile *keyfile, const char *section, const char *value, + OtTristate default_value, OtTristate *out_tri, GError **error) +{ + g_return_val_if_fail (keyfile != NULL, FALSE); + g_return_val_if_fail (section != NULL, FALSE); + g_return_val_if_fail (value != NULL, FALSE); + + GError *temp_error = NULL; + g_autofree char *ret_value = g_key_file_get_value (keyfile, section, value, &temp_error); + if (temp_error) + { + if (is_notfound (temp_error)) + { + g_clear_error (&temp_error); + g_assert (ret_value == NULL); + *out_tri = default_value; + return TRUE; + } + + g_propagate_error (error, temp_error); + return FALSE; + } + + ret_value = g_strstrip (ret_value); + + if (strcmp (ret_value, "yes") == 0 || strcmp (ret_value, "true") == 0 + || strcmp (ret_value, "1") == 0) + *out_tri = OT_TRISTATE_YES; + else if (strcmp (ret_value, "no") == 0 || strcmp (ret_value, "false") == 0 + || strcmp (ret_value, "0") == 0) + *out_tri = OT_TRISTATE_NO; + else if (strcmp (ret_value, "maybe") == 0) + *out_tri = OT_TRISTATE_MAYBE; + else + { + g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, + "Invalid tri-state value: %s", ret_value); + return FALSE; + } + + return TRUE; +} + gboolean ot_keyfile_get_value_with_default (GKeyFile *keyfile, const char *section, const char *value, const char *default_value, char **out_value, GError **error) diff --git a/src/libotutil/ot-keyfile-utils.h b/src/libotutil/ot-keyfile-utils.h index ae70f1c752..eb97c8d7c6 100644 --- a/src/libotutil/ot-keyfile-utils.h +++ b/src/libotutil/ot-keyfile-utils.h @@ -23,12 +23,23 @@ #include +typedef enum +{ + OT_TRISTATE_NO, + OT_TRISTATE_MAYBE, + OT_TRISTATE_YES, +} OtTristate; + G_BEGIN_DECLS gboolean ot_keyfile_get_boolean_with_default (GKeyFile *keyfile, const char *section, const char *value, gboolean default_value, gboolean *out_bool, GError **error); +gboolean ot_keyfile_get_tristate_with_default (GKeyFile *keyfile, const char *section, + const char *value, OtTristate default_value, + OtTristate *out_tri, GError **error); + gboolean ot_keyfile_get_value_with_default (GKeyFile *keyfile, const char *section, const char *value, const char *default_value, char **out_value, GError **error); diff --git a/src/switchroot/ostree-mount-util.h b/src/switchroot/ostree-mount-util.h index 9d79f0cb9f..fdf1a43476 100644 --- a/src/switchroot/ostree-mount-util.h +++ b/src/switchroot/ostree-mount-util.h @@ -24,14 +24,21 @@ #include #include #include +#include #include #include #include +#include #include #include +#ifdef HAVE_LINUX_FSVERITY_H +#include +#endif + #define INITRAMFS_MOUNT_VAR "/run/ostree/initramfs-mount-var" #define _OSTREE_SYSROOT_READONLY_STAMP "/run/ostree-sysroot-ro.stamp" +#define _OSTREE_COMPOSEFS_ROOT_STAMP "/run/ostree-composefs-root.stamp" static inline int path_is_on_readonly_fs (const char *path) @@ -73,11 +80,12 @@ read_proc_cmdline (void) } static inline char * -read_proc_cmdline_ostree (void) +read_proc_cmdline_key (const char *key) { char *cmdline = NULL; const char *iter; char *ret = NULL; + size_t key_len = strlen (key); cmdline = read_proc_cmdline (); if (!cmdline) @@ -90,9 +98,9 @@ read_proc_cmdline_ostree (void) const char *next_nonspc = next; while (next_nonspc && *next_nonspc == ' ') next_nonspc += 1; - if (strncmp (iter, "ostree=", strlen ("ostree=")) == 0) + if (strncmp (iter, key, key_len) == 0 && iter[key_len] == '=') { - const char *start = iter + strlen ("ostree="); + const char *start = iter + key_len + 1; if (next) ret = strndup (start, next - start); else @@ -121,4 +129,65 @@ touch_run_ostree (void) (void)close (fd); } +static inline unsigned char * +read_file (const char *path, size_t *out_len) +{ + int fd; + + fd = open (path, O_RDONLY); + if (fd < 0) + { + if (errno == ENOENT) + return NULL; + err (EXIT_FAILURE, "failed to open %s", path); + } + + struct stat stbuf; + if (fstat (fd, &stbuf)) + err (EXIT_FAILURE, "fstat(%s) failed", path); + + size_t file_size = stbuf.st_size; + unsigned char *buf = malloc (file_size); + if (buf == NULL) + err (EXIT_FAILURE, "Out of memory"); + + size_t file_read = 0; + while (file_read < file_size) + { + ssize_t bytes_read; + do + bytes_read = read (fd, buf + file_read, file_size - file_read); + while (bytes_read == -1 && errno == EINTR); + if (bytes_read == -1) + err (EXIT_FAILURE, "read_file(%s) failed", path); + if (bytes_read == 0) + break; + + file_read += bytes_read; + } + + close (fd); + + *out_len = file_read; + return buf; +} + +static inline void +fsverity_sign (int fd, unsigned char *signature, size_t signature_len) +{ +#ifdef HAVE_LINUX_FSVERITY_H + struct fsverity_enable_arg arg = { + 0, + }; + arg.version = 1; + arg.hash_algorithm = FS_VERITY_HASH_ALG_SHA256; + arg.block_size = 4096; + arg.sig_size = signature_len; + arg.sig_ptr = (uint64_t)signature; + + if (ioctl (fd, FS_IOC_ENABLE_VERITY, &arg) < 0) + err (EXIT_FAILURE, "failed to fs-verity sign file"); +#endif +} + #endif /* __OSTREE_MOUNT_UTIL_H_ */ diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index f24a568101..136502cc06 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -66,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,10 @@ #include #include +/* We can't include both linux/fs.h and sys/mount.h, so define these directly */ +#define FS_VERITY_FL 0x00100000 /* Verity protected inode */ +#define FS_IOC_GETFLAGS _IOR ('f', 1, long) + #if defined(HAVE_LIBSYSTEMD) && !defined(OSTREE_PREPARE_ROOT_STATIC) #define USE_LIBSYSTEMD #endif @@ -86,8 +91,21 @@ // A temporary mount point #define TMP_SYSROOT "/sysroot.tmp" +#ifdef HAVE_COMPOSEFS +#include +#endif + #include "ostree-mount-util.h" +typedef enum +{ + OSTREE_COMPOSEFS_MODE_OFF, /* Never use composefs */ + OSTREE_COMPOSEFS_MODE_MAYBE, /* Use if supported and image exists in deploy */ + OSTREE_COMPOSEFS_MODE_ON, /* Always use (and fail if not working) */ + OSTREE_COMPOSEFS_MODE_SIGNED, /* Always use and require it to be signed */ + OSTREE_COMPOSEFS_MODE_DIGEST, /* Always use and require specific digest */ +} OstreeComposefsMode; + static inline bool sysroot_is_configured_ro (const char *sysroot) { @@ -135,7 +153,7 @@ resolve_deploy_path (const char *root_mountpoint) struct stat stbuf; char *ostree_target, *deploy_path; - ostree_target = read_proc_cmdline_ostree (); + ostree_target = read_proc_cmdline_key ("ostree"); if (!ostree_target) errx (EXIT_FAILURE, "No OSTree target; expected ostree=/ostree/boot.N/..."); @@ -227,6 +245,34 @@ main (int argc, char *argv[]) err (EXIT_FAILURE, "failed to umount proc from /proc"); } + OstreeComposefsMode composefs_mode = OSTREE_COMPOSEFS_MODE_MAYBE; + char *ot_composefs = read_proc_cmdline_key ("ot-composefs"); + char *composefs_digest = NULL; + if (ot_composefs) + { + if (strcmp (ot_composefs, "off") == 0) + composefs_mode = OSTREE_COMPOSEFS_MODE_OFF; + else if (strcmp (ot_composefs, "maybe") == 0) + composefs_mode = OSTREE_COMPOSEFS_MODE_MAYBE; + else if (strcmp (ot_composefs, "on") == 0) + composefs_mode = OSTREE_COMPOSEFS_MODE_ON; + else if (strcmp (ot_composefs, "signed") == 0) + composefs_mode = OSTREE_COMPOSEFS_MODE_SIGNED; + else if (strncmp (ot_composefs, "digest=", strlen ("digest=")) == 0) + { + composefs_mode = OSTREE_COMPOSEFS_MODE_DIGEST; + composefs_digest = ot_composefs + strlen ("digest="); + } + else + err (EXIT_FAILURE, "Unsupported ot-composefs option: '%s'", ot_composefs); + } + +#ifndef HAVE_COMPOSEFS + if (composefs_mode == OSTREE_COMPOSEFS_MODE_MAYBE) + composefs_mode = OSTREE_COMPOSEFS_MODE_OFF; + (void)composefs_digest; +#endif + /* Query the repository configuration - this is an operating system builder * choice. More info: https://github.com/ostreedev/ostree/pull/1767 */ @@ -254,11 +300,99 @@ main (int argc, char *argv[]) if (chdir (deploy_path) < 0) err (EXIT_FAILURE, "failed to chdir to deploy_path"); - /* Currently always false */ bool using_composefs = false; + /* We construct the new sysroot in /sysroot.tmp, which is either the composfs + mount or a bind mount of the deploy-dir */ + if (composefs_mode != OSTREE_COMPOSEFS_MODE_OFF) + { +#ifdef HAVE_COMPOSEFS + const char *objdirs[] = { "/sysroot/ostree/repo/objects" }; + struct lcfs_mount_options_s cfs_options = { + objdirs, + 1, + }; + int cfs_fd; + unsigned cfs_flags; + + cfs_fd = open (".ostree.cfs", O_RDONLY); + if (cfs_fd < 0) + { + if (errno == ENOENT) + goto nocfs; + + err (EXIT_FAILURE, "failed to open .ostree.cfs"); + } + + /* Check if file is already fsverity */ + if (ioctl (cfs_fd, FS_IOC_GETFLAGS, &cfs_flags) < 0) + err (EXIT_FAILURE, "failed to get .ostree.cfs flags"); + + /* It is not, apply signature (if it exists) */ + if ((cfs_flags & FS_VERITY_FL) == 0) + { + unsigned char *signature; + size_t signature_len; + + signature = read_file (".ostree.cfs.sig", &signature_len); + if (signature != NULL) + { + /* If we're read-only we temporarily make it read-write to sign the image */ + if (!sysroot_currently_writable + && mount (root_mountpoint, root_mountpoint, NULL, MS_REMOUNT | MS_SILENT, NULL) + < 0) + err (EXIT_FAILURE, "failed to remount rootfs writable (for signing)"); + + fsverity_sign (cfs_fd, signature, signature_len); + free (signature); + + if (!sysroot_currently_writable + && mount (root_mountpoint, root_mountpoint, NULL, + MS_REMOUNT | MS_RDONLY | MS_SILENT, NULL) + < 0) + err (EXIT_FAILURE, "failed to remount rootfs back read-only (after signing)"); + } + } + + cfs_options.flags = LCFS_MOUNT_FLAGS_READONLY; + + if (snprintf (srcpath, sizeof (srcpath), "%s/.ostree.mnt", deploy_path) < 0) + err (EXIT_FAILURE, "failed to assemble /boot/loader path"); + cfs_options.image_mountdir = srcpath; + + if (composefs_mode == OSTREE_COMPOSEFS_MODE_SIGNED) + { + cfs_options.flags |= LCFS_MOUNT_FLAGS_REQUIRE_SIGNATURE | LCFS_MOUNT_FLAGS_REQUIRE_VERITY; + } + else if (composefs_mode == OSTREE_COMPOSEFS_MODE_DIGEST) + { + cfs_options.flags |= LCFS_MOUNT_FLAGS_REQUIRE_VERITY; + cfs_options.expected_digest = composefs_digest; + } + + if (lcfs_mount_fd (cfs_fd, TMP_SYSROOT, &cfs_options) == 0) + { + int fd = open (_OSTREE_COMPOSEFS_ROOT_STAMP, O_WRONLY | O_CREAT | O_CLOEXEC, 0644); + if (fd < 0) + err (EXIT_FAILURE, "failed to create %s", _OSTREE_COMPOSEFS_ROOT_STAMP); + (void)close (fd); + + using_composefs = 1; + } + + close (cfs_fd); + + nocfs: +#else + err (EXIT_FAILURE, "Composefs not supported"); +#endif + } + if (!using_composefs) { + if (composefs_mode > OSTREE_COMPOSEFS_MODE_MAYBE) + err (EXIT_FAILURE, "Failed to mount composefs"); + /* 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); diff --git a/src/switchroot/ostree-remount.c b/src/switchroot/ostree-remount.c index ba5a16b2a5..80f8ad346a 100644 --- a/src/switchroot/ostree-remount.c +++ b/src/switchroot/ostree-remount.c @@ -95,7 +95,12 @@ main (int argc, char *argv[]) if (mount ("none", "/sysroot", NULL, MS_REC | MS_PRIVATE, NULL) < 0) perror ("warning: While remounting /sysroot MS_PRIVATE"); - if (path_is_on_readonly_fs ("/")) + bool root_is_composefs = false; + struct stat stbuf; + if (fstatat (AT_FDCWD, _OSTREE_COMPOSEFS_ROOT_STAMP, &stbuf, 0) == 0) + root_is_composefs = true; + + if (path_is_on_readonly_fs ("/") && !root_is_composefs) { /* If / isn't writable, don't do any remounts; we don't want * to clear the readonly flag in that case. diff --git a/src/switchroot/ostree-system-generator.c b/src/switchroot/ostree-system-generator.c index 33e3d78303..c045c541a2 100644 --- a/src/switchroot/ostree-system-generator.c +++ b/src/switchroot/ostree-system-generator.c @@ -63,7 +63,7 @@ main (int argc, char *argv[]) * exit so that we don't error, but at the same time work where switchroot * is PID 1 (and so hasn't created /run/ostree-booted). */ - char *ostree_cmdline = read_proc_cmdline_ostree (); + char *ostree_cmdline = read_proc_cmdline_key ("ostree"); if (!ostree_cmdline) exit (EXIT_SUCCESS);