From 02d24d2a3854185cb1f4174f04efa59f1c6d53e9 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 23 May 2023 09:26:26 +0200 Subject: [PATCH 01/14] Add ot_keyfile_get_tristate_with_default() helper This parses keys like yes/no/maybe. The introduced OtTristate type is compatible with the existing _OstreeFeatureSupport type. --- src/libotutil/ot-keyfile-utils.c | 44 ++++++++++++++++++++++++++++++++ src/libotutil/ot-keyfile-utils.h | 11 ++++++++ 2 files changed, 55 insertions(+) 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); From c6ed5cc7b2e09730fda8d7c0b2502774796020e8 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 16 May 2023 16:01:33 +0200 Subject: [PATCH 02/14] fsverity: Add _ostree_fsverity_sign helper This code signs a fsverity digest (using openssl) such that the resulting signature can be used with the FS_IOC_ENABLE_VERITY ioctl. --- src/libostree/ostree-repo-private.h | 3 + src/libostree/ostree-repo-verity.c | 141 ++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h index f386585481..5395de40f4 100644 --- a/src/libostree/ostree-repo-private.h +++ b/src/libostree/ostree-repo-private.h @@ -393,6 +393,9 @@ gboolean _ostree_tmpf_fsverity_core (GLnxTmpfile *tmpf, _OstreeFeatureSupport fs gboolean *supported, GError **error); gboolean _ostree_tmpf_fsverity (OstreeRepo *self, GLnxTmpfile *tmpf, 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); diff --git a/src/libostree/ostree-repo-verity.c b/src/libostree/ostree-repo-verity.c index 8c199e858e..fcbdaccd9c 100644 --- a/src/libostree/ostree-repo-verity.c +++ b/src/libostree/ostree-repo-verity.c @@ -29,6 +29,19 @@ #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) { @@ -167,3 +180,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 From 9ba98cd8e98ae9501dc1c03ff69920b94f8cd429 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 16 May 2023 16:31:34 +0200 Subject: [PATCH 03/14] fsverity: Support passing a signature when enabling fs-verity The composefs code will need this. --- src/libostree/ostree-repo-commit.c | 2 +- src/libostree/ostree-repo-private.h | 5 +++-- src/libostree/ostree-repo-verity.c | 10 +++++----- src/libostree/ostree-sysroot-deploy.c | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c index 5cda047c6b..0900205fee 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, diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h index 5395de40f4..98719f5a67 100644 --- a/src/libostree/ostree-repo-private.h +++ b/src/libostree/ostree-repo-private.h @@ -390,9 +390,10 @@ gboolean _ostree_repo_maybe_regenerate_summary (OstreeRepo *self, GCancellable * gboolean _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); -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); diff --git a/src/libostree/ostree-repo-verity.c b/src/libostree/ostree-repo-verity.c index fcbdaccd9c..5b2a621a7d 100644 --- a/src/libostree/ostree-repo-verity.c +++ b/src/libostree/ostree-repo-verity.c @@ -82,7 +82,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) @@ -106,8 +106,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) { @@ -133,7 +133,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); @@ -156,7 +156,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) diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 425abe8bd6..32bbd3358e 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)) From e2956e2c08fb3e11e249b540db3a5b760dd04fac Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 9 Jun 2022 17:15:29 +0200 Subject: [PATCH 04/14] lib: Add (private) API for checking out commits into a composefs image This supports checking out a commit into a tree which is then converted into a composefs image containing fs-verity digests for all the regular files, and payloads that are relative to a the `repo/objects` directory of a bare ostree repo. Some specal files are always created in the image. This ensures that various directories (usr, etc, boot, var, sysroot) exists in the created image, even if they were not in the source commit. These are needed (as bindmount targets) if you want to boot from the image. In the non-composefs case these are just created as needed in the checked out deploydir, but we can't do that here. This is all controlled by the new ex-integrity config section, which has the following layout: ``` [ex-integrity] fsverity=yes/no/maybe composefs=yes/no/maybe composefs-apply-sig=yes/no composefs-add-metadata=yes/no composefs-keyfiile=/a/path composefs-certfile=/a/path ``` The `fsverity` key overrides the old `ex-fsverity` section if specified. The default for all these is for the new behaviour to be disabled. Additionally, enabling composefs implies fsverity defaults to `maybe`, to avoid having to set both. --- Makefile-libostree.am | 6 + configure.ac | 29 +- src/libostree/ostree-repo-composefs.c | 560 ++++++++++++++++++++++++++ src/libostree/ostree-repo-private.h | 21 + src/libostree/ostree-repo-verity.c | 54 ++- src/libostree/ostree-repo.c | 3 + 6 files changed, 658 insertions(+), 15 deletions(-) create mode 100644 src/libostree/ostree-repo-composefs.c diff --git a/Makefile-libostree.am b/Makefile-libostree.am index d18714ae4f..0e984c7d5e 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 \ @@ -266,6 +267,11 @@ 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_CFLAGS += $(OT_DEP_COMPOSEFS_CFLAGS) +libostree_1_la_LIBADD += $(OT_DEP_COMPOSEFS_LIBS) +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/configure.ac b/configure.ac index ce449cb013..95f0047d87 100644 --- a/configure.ac +++ b/configure.ac @@ -280,6 +280,32 @@ AS_IF([test x$have_gpgme = xyes], ) AM_CONDITIONAL(USE_GPGME, test "x$have_gpgme" = xyes) +COMPOSEFS_DEPENDENCY="composefs >= 0.1.2" +AC_ARG_WITH(composefs, + AS_HELP_STRING([--without-composefs], [Do not use composefs]), + :, with_composefs=maybe) + +AS_IF([ test x$with_composefs != xno ], [ + AC_MSG_CHECKING([for $COMPOSEFS_DEPENDENCY]) + PKG_CHECK_EXISTS($COMPOSEFS_DEPENDENCY, have_composefs=yes, have_composefs=no) + AC_MSG_RESULT([$have_composefs]) + AS_IF([ test x$have_composefs = xno && test x$with_composefs != xmaybe ], [ + AC_MSG_ERROR([composefs is enabled but could not be found]) + ]) + AS_IF([ test x$have_composefs = xyes], [ + AC_DEFINE([HAVE_COMPOSEFS], 1, [Define if we have composefs.pc]) + PKG_CHECK_MODULES(OT_DEP_COMPOSEFS, $COMPOSEFS_DEPENDENCY) + save_LIBS=$LIBS + LIBS=$OT_DEP_COMPOSEFS_LIBS + AC_CHECK_FUNCS(lcfs_node_new) + LIBS=$save_LIBS + with_composefs=yes + ], [ + with_composefs=no + ]) +], [ with_composefs=no ]) +if test x$with_composefs != xno; then OSTREE_FEATURES="$OSTREE_FEATURES composefs"; fi +AM_CONDITIONAL(USE_COMPOSEFS, test $with_composefs != no) LIBSODIUM_DEPENDENCY="1.0.14" AC_ARG_WITH(ed25519_libsodium, @@ -675,7 +701,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-composefs.c b/src/libostree/ostree-repo-composefs.c new file mode 100644 index 0000000000..3df1cf9835 --- /dev/null +++ b/src/libostree/ostree-repo-composefs.c @@ -0,0 +1,560 @@ +/* + * 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 +} diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h index 98719f5a67..1512137252 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,6 +392,7 @@ 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, GBytes *signature, gboolean *supported, GError **error); @@ -446,4 +451,20 @@ 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); + +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 5b2a621a7d..512386a96f 100644 --- a/src/libostree/ostree-repo-verity.c +++ b/src/libostree/ostree-repo-verity.c @@ -46,33 +46,59 @@ 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; } 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, From 0c3d9894be1f1fa0a825aa7b16c119b72c4edc7e Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Mon, 15 May 2023 15:18:16 +0200 Subject: [PATCH 05/14] Commit: Add composefs digest and sig to the commit metadata If `composefs-apply-sig` is enabled (default no) we add an ostree.composefs digest to the commit metadata. This can be verified on deploy. This is a separate option from the generic `composefs` option which controls whether composefs is used during deploy. It is separate because we want to not have to force use of fs-verity, etc during the build. If the `composefs-certfile` and `composefs-keyfile` keys in the ex-integrity group are set, then the commit metadata also gets a ostree.composefs-sig containing the signature of the composefs file. --- src/libostree/ostree-repo-commit.c | 33 +++++++++---- src/libostree/ostree-repo-composefs.c | 71 +++++++++++++++++++++++++++ src/libostree/ostree-repo-private.h | 4 ++ 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c index 0900205fee..fe92486199 100644 --- a/src/libostree/ostree-repo-commit.c +++ b/src/libostree/ostree-repo-commit.c @@ -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 index 3df1cf9835..f33f5d3055 100644 --- a/src/libostree/ostree-repo-composefs.c +++ b/src/libostree/ostree-repo-composefs.c @@ -558,3 +558,74 @@ ostree_repo_checkout_composefs (OstreeRepo *self, OstreeComposefsTarget *target, 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 1512137252..76f3152bcb 100644 --- a/src/libostree/ostree-repo-private.h +++ b/src/libostree/ostree-repo-private.h @@ -465,6 +465,10 @@ gboolean ostree_repo_checkout_composefs (OstreeRepo *self, OstreeComposefsTarget 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 From c988ff79387b5b61a1134c5f0d91010b0a280fd6 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 30 Jun 2022 14:47:44 +0200 Subject: [PATCH 06/14] deploy: Write a .ostree.cfs composefs image in the deploy dir This can be used as a composefs source for the root fs instead of the checkout by pointing the basedir to /ostree/repo/objects. We only write the file is `composefs` is enabled. We enable ensure_rootfs_dirs when building the image which adds the required root dirs to the image. In particular, this includes /etc which often isn't in ostree commits in use. We also create an (empty) .ostree.mnt directory, where composefs will mount the erofs image that will be used as overlayfs lowerdir for the root overlayfs mount. This way we can find the deploy dir from the root overlayfs mount options. If the commit has composefs digests recorded we verify those with the created file. It also applies the fs-verity signature if it is recorded, unless this is disabled with the ex-integrity.composefs-apply-sign=false option. --- src/libostree/ostree-sysroot-deploy.c | 109 +++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 32bbd3358e..a4853bb010 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -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,78 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy cancellable, error)) return FALSE; +#ifdef HAVE_COMPOSEFS + if (repo->composefs_wanted != OT_TRISTATE_NO) + { + g_autoptr (GBytes) sig = NULL; + 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 (apply_composefs_signature && metadata_composefs_sig) + sig = g_variant_get_data_as_bytes (metadata_composefs_sig); + + if (!_ostree_tmpf_fsverity (repo, &tmpf, sig, 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 +3099,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; From 3fcebe454ed78df378b656af2013e43c648dbe46 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Mon, 29 May 2023 12:25:55 +0200 Subject: [PATCH 07/14] composefs deploy: Store cfs signature in .ostree.cfs.sig file In many cases, such as when using osbuild, we are not preparing the final deployment but rather a rootfs tree that will eventually be copied to the final location. In that case we don't want to apply the signature directly but when the deployment is copied in place. To make this situateion workable we also write the signature to a file next to the composefs image file. Then whatever mechanism that does the final copy can apply the signature. --- src/libostree/ostree-sysroot-deploy.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index a4853bb010..cbb56c8798 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -699,10 +699,21 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy if (!glnx_fchmod (tmpf.fd, 0644, error)) return FALSE; - if (apply_composefs_signature && metadata_composefs_sig) - sig = g_variant_get_data_as_bytes (metadata_composefs_sig); + if (metadata_composefs_sig) + { + g_autofree char *composefs_sig_path + = g_strdup_printf ("%s/.ostree.cfs.sig", checkout_target_name); + + sig = g_variant_get_data_as_bytes (metadata_composefs_sig); + + /* Write signature to file so it can be applied later if needed */ + 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; + } - if (!_ostree_tmpf_fsverity (repo, &tmpf, sig, error)) + if (!_ostree_tmpf_fsverity (repo, &tmpf, apply_composefs_signature ? sig : NULL, error)) return FALSE; if (!glnx_link_tmpfile_at (&tmpf, GLNX_LINK_TMPFILE_REPLACE, osdeploy_dfd, composefs_cfs_path, From bba3109fe2a746b59424369f839c8380fb9b35a7 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 18 May 2023 10:53:22 +0200 Subject: [PATCH 08/14] switchroot: Make read_proc_cmdline_ostree() take a key argument This changes it into read_proc_cmdline_key(), as this will later be used to read additional keys. --- src/switchroot/ostree-mount-util.h | 7 ++++--- src/switchroot/ostree-prepare-root.c | 2 +- src/switchroot/ostree-system-generator.c | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/switchroot/ostree-mount-util.h b/src/switchroot/ostree-mount-util.h index 9d79f0cb9f..e3b0037a0f 100644 --- a/src/switchroot/ostree-mount-util.h +++ b/src/switchroot/ostree-mount-util.h @@ -73,11 +73,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 +91,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 diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index f24a568101..7f1489593e 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -135,7 +135,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/..."); 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); From 11d7587e403c897ad918ed8bc6d437e17f86c4c0 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 30 Jun 2022 14:53:13 +0200 Subject: [PATCH 09/14] prepare-root: Support using composefs as root filesystem This changes ostree-prepare-root to use the .ostree.cfs image as a composefs filesystem, instead of the checkout. By default, composefs is used if support is built in and the .ostree.cfs file exists in the deploy dir, otherwise we fall back to the old method. However, if the ot-composefs kernel option is specified this can be tweaked as per: * off: Never use composefsz * maybe: Use if possible * on: Fail if not possible * signed: Fail if the cfs image is not fs-verity signed with a key in the keyring. * digest=....: Fail if the cfs image does not match the specified digest. The final layout when composefs is active is: / ro overlayfs mount for composefs /sysroot "real" root /etc rw bind mount to $deploydir/etc /var rw bind mount to $vardir We also specify the $deploydir/.ostree-mnt directory as the (internal) mountpoint for the erofs mount for composefs. This can be used to map the root fs back to the deploy id/dir in use, A further note: I didn't test the .usr-ovl-work overlayfs case, but a comment mentions that you can't mount overlayfs on top of a readonly mount. That seems incompatible with composefs. If this is needed we have to merge that with the overlayfs that composefs itself sets up, which is possible with the libcomposefs APIs. --- Makefile-switchroot.am | 13 +++-- src/switchroot/ostree-prepare-root.c | 81 +++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/Makefile-switchroot.am b/Makefile-switchroot.am index 104ec0cdf3..8c486e4935 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. @@ -43,10 +45,10 @@ if BUILDOPT_USE_STATIC_COMPILER ostree_boot_SCRIPTS += ostree-prepare-root 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 + $(STATIC_COMPILER) -o $@ -static $(top_srcdir)/src/switchroot/ostree-prepare-root.c $(ostree_prepare_root_CPPFLAGS) $(AM_CFLAGS) $(DEFAULT_INCLUDES) $(OT_DEP_COMPOSEFS_CFLAGS) $(OT_DEP_COMPOSEFS_LIBS) -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 endif ostree_remount_SOURCES = \ @@ -56,9 +58,14 @@ 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_CFLAGS += $(OT_DEP_COMPOSEFS_CFLAGS) +ostree_prepare_root_LDADD += $(OT_DEP_COMPOSEFS_LIBS) +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/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index 7f1489593e..3dbca634a9 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -86,8 +86,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) { @@ -227,6 +240,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,9 +295,47 @@ 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, + }; + + 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_image (".ostree.cfs", "/sysroot.tmp", &cfs_options) < 0) + { + if (composefs_mode > OSTREE_COMPOSEFS_MODE_MAYBE) + err (EXIT_FAILURE, "Failed to mount composefs"); + } + else + using_composefs = 1; +#else + err (EXIT_FAILURE, "Composefs not supported"); +#endif + } + if (!using_composefs) { /* The deploy root starts out bind mounted to sysroot.tmp */ From d47a90347ba4e72ae223d4eb59287a42d32a5df0 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 16 May 2023 10:17:32 +0200 Subject: [PATCH 10/14] sysroot: Ensure deployment detection works when using composefs In the case of composefs, we cannot compare the devino of the rootfs and the deploy dir, because the root is the composefs mount, not a bind mount. Instead we check the devino of the etc subdir of the deploy, because this is a bind mount even when using composefs. --- src/libostree/ostree-sysroot-private.h | 4 +++- src/libostree/ostree-sysroot.c | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) 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; From f9bdc66649a90e8834d5fa9adb65ede9b2453b8b Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Fri, 26 May 2023 12:53:57 +0200 Subject: [PATCH 11/14] ostree-remount: Don't skip remount if root is composefs When using composefs the root fs will always be read-only, but in this case we should still continue remounting /sysroot. So, we record a /run/ostree-composefs-root.stamp file in ostree-prepare-root if composefs is used, and then react to it in ostree-remount. --- src/switchroot/ostree-mount-util.h | 1 + src/switchroot/ostree-prepare-root.c | 9 ++++++++- src/switchroot/ostree-remount.c | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/switchroot/ostree-mount-util.h b/src/switchroot/ostree-mount-util.h index e3b0037a0f..b6ee2f56c6 100644 --- a/src/switchroot/ostree-mount-util.h +++ b/src/switchroot/ostree-mount-util.h @@ -32,6 +32,7 @@ #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) diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index 3dbca634a9..a65da45bab 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -330,7 +330,14 @@ main (int argc, char *argv[]) err (EXIT_FAILURE, "Failed to mount composefs"); } else - using_composefs = 1; + { + 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; + } #else err (EXIT_FAILURE, "Composefs not supported"); #endif 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. From e3be4ee52acc4079eaae6e87ffe7f210763e76ad Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 23 May 2023 10:31:23 +0200 Subject: [PATCH 12/14] Update submodule: composefs Instead of using pkg-config, etc we just include composefs. In the end the library is just 5 c source files, and it is set up to be easy to use as a submodule. For now, composefs support is disabled by default. --- .gitmodules | 3 +++ Makefile-libostree.am | 5 ++--- Makefile-switchroot.am | 7 +++--- Makefile.am | 9 ++++++++ autogen.sh | 1 + composefs | 1 + configure.ac | 51 ++++++++++++++++++++++-------------------- 7 files changed, 46 insertions(+), 31 deletions(-) create mode 160000 composefs 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 0e984c7d5e..8edd7f4de1 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -185,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)\" @@ -268,8 +268,7 @@ libostree_1_la_LIBADD += $(OT_DEP_LIBSODIUM_LIBS) endif # USE_LIBSODIUM if USE_COMPOSEFS -libostree_1_la_CFLAGS += $(OT_DEP_COMPOSEFS_CFLAGS) -libostree_1_la_LIBADD += $(OT_DEP_COMPOSEFS_LIBS) +libostree_1_la_LIBADD += libcomposefs.la endif # USE_COMPOSEFS # XXX: work around clang being passed -fstack-clash-protection which it doesn't understand diff --git a/Makefile-switchroot.am b/Makefile-switchroot.am index 8c486e4935..8063d9e0cb 100644 --- a/Makefile-switchroot.am +++ b/Makefile-switchroot.am @@ -45,10 +45,10 @@ if BUILDOPT_USE_STATIC_COMPILER ostree_boot_SCRIPTS += ostree-prepare-root 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) $(OT_DEP_COMPOSEFS_CFLAGS) $(OT_DEP_COMPOSEFS_LIBS) -DOSTREE_PREPARE_ROOT_STATIC=1 + $(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 = \ @@ -59,8 +59,7 @@ ostree_remount_CPPFLAGS = $(AM_CPPFLAGS) $(OT_INTERNAL_GIO_UNIX_CFLAGS) -Isrc/sw ostree_remount_LDADD = $(AM_LDFLAGS) $(OT_INTERNAL_GIO_UNIX_LIBS) libglnx.la if USE_COMPOSEFS -ostree_prepare_root_CFLAGS += $(OT_DEP_COMPOSEFS_CFLAGS) -ostree_prepare_root_LDADD += $(OT_DEP_COMPOSEFS_LIBS) +ostree_prepare_root_LDADD += libcomposefs.la endif if BUILDOPT_SYSTEMD 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 95f0047d87..f7ae2d64b7 100644 --- a/configure.ac +++ b/configure.ac @@ -280,31 +280,34 @@ AS_IF([test x$have_gpgme = xyes], ) AM_CONDITIONAL(USE_GPGME, test "x$have_gpgme" = xyes) -COMPOSEFS_DEPENDENCY="composefs >= 0.1.2" +# 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([--without-composefs], [Do not use composefs]), - :, with_composefs=maybe) - -AS_IF([ test x$with_composefs != xno ], [ - AC_MSG_CHECKING([for $COMPOSEFS_DEPENDENCY]) - PKG_CHECK_EXISTS($COMPOSEFS_DEPENDENCY, have_composefs=yes, have_composefs=no) - AC_MSG_RESULT([$have_composefs]) - AS_IF([ test x$have_composefs = xno && test x$with_composefs != xmaybe ], [ - AC_MSG_ERROR([composefs is enabled but could not be found]) - ]) - AS_IF([ test x$have_composefs = xyes], [ - AC_DEFINE([HAVE_COMPOSEFS], 1, [Define if we have composefs.pc]) - PKG_CHECK_MODULES(OT_DEP_COMPOSEFS, $COMPOSEFS_DEPENDENCY) - save_LIBS=$LIBS - LIBS=$OT_DEP_COMPOSEFS_LIBS - AC_CHECK_FUNCS(lcfs_node_new) - LIBS=$save_LIBS - with_composefs=yes - ], [ - with_composefs=no - ]) -], [ with_composefs=no ]) -if test x$with_composefs != xno; then OSTREE_FEATURES="$OSTREE_FEATURES composefs"; fi + 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" From 6d2dc959686e530dfd0b4e1ef8c4b480d9a01e1b Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 23 May 2023 16:37:16 +0200 Subject: [PATCH 13/14] CI: Build with composefs on some versions This enables --with-composefs on: * Fedora Latest * Debian Testing * Ubuntu Latest These all should have new enough version of dependencies. --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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. From 733380394922f441f743ee57d626ad631f418d5c Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Wed, 31 May 2023 18:35:44 +0200 Subject: [PATCH 14/14] composefs: When using signatures, delay application until first boot We can't safely apply the fs-verity with signature until we have booted with the new initrd, because the public key that matches the signature is loaded from it. So, instead we save the .sig file next to the compoosefs, and on the first boot we detect that it is there, and the composefs file isn't fs-verity, so we apply it. Things get a bit more complex due to having to temporarily make /sysroot read-write for the fsverity operation too. --- src/libostree/ostree-sysroot-deploy.c | 19 ++++---- src/switchroot/ostree-mount-util.h | 67 +++++++++++++++++++++++++++ src/switchroot/ostree-prepare-root.c | 60 +++++++++++++++++++++--- 3 files changed, 132 insertions(+), 14 deletions(-) diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index cbb56c8798..c0ce1e94c3 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -649,7 +649,6 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy #ifdef HAVE_COMPOSEFS if (repo->composefs_wanted != OT_TRISTATE_NO) { - g_autoptr (GBytes) sig = NULL; gboolean apply_composefs_signature; g_autofree guchar *fsverity_digest = NULL; g_auto (GLnxTmpfile) tmpf = { @@ -699,22 +698,26 @@ checkout_deployment_tree (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploy if (!glnx_fchmod (tmpf.fd, 0644, error)) return FALSE; - if (metadata_composefs_sig) + 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); - sig = g_variant_get_data_as_bytes (metadata_composefs_sig); - - /* Write signature to file so it can be applied later if needed */ 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; } - - if (!_ostree_tmpf_fsverity (repo, &tmpf, apply_composefs_signature ? sig : NULL, 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)) diff --git a/src/switchroot/ostree-mount-util.h b/src/switchroot/ostree-mount-util.h index b6ee2f56c6..fdf1a43476 100644 --- a/src/switchroot/ostree-mount-util.h +++ b/src/switchroot/ostree-mount-util.h @@ -24,12 +24,18 @@ #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" @@ -123,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 a65da45bab..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 @@ -307,6 +312,47 @@ main (int argc, char *argv[]) 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; @@ -324,12 +370,7 @@ main (int argc, char *argv[]) cfs_options.expected_digest = composefs_digest; } - if (lcfs_mount_image (".ostree.cfs", "/sysroot.tmp", &cfs_options) < 0) - { - if (composefs_mode > OSTREE_COMPOSEFS_MODE_MAYBE) - err (EXIT_FAILURE, "Failed to mount composefs"); - } - else + 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) @@ -338,6 +379,10 @@ main (int argc, char *argv[]) using_composefs = 1; } + + close (cfs_fd); + + nocfs: #else err (EXIT_FAILURE, "Composefs not supported"); #endif @@ -345,6 +390,9 @@ main (int argc, char *argv[]) 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);