diff --git a/src/libostree/ostree-bootconfig-parser-private.h b/src/libostree/ostree-bootconfig-parser-private.h index 16ccb0fcdc..1450fa9921 100644 --- a/src/libostree/ostree-bootconfig-parser-private.h +++ b/src/libostree/ostree-bootconfig-parser-private.h @@ -8,4 +8,6 @@ G_BEGIN_DECLS const char *_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self); +GVariant *_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self); + G_END_DECLS diff --git a/src/libostree/ostree-bootconfig-parser.c b/src/libostree/ostree-bootconfig-parser.c index 2e08256d72..4e7c823a59 100644 --- a/src/libostree/ostree-bootconfig-parser.c +++ b/src/libostree/ostree-bootconfig-parser.c @@ -339,6 +339,60 @@ ostree_bootconfig_parser_write (OstreeBootconfigParser *self, GFile *output, cancellable, error); } +/* Standard BLS keys that are managed by ostree's own deployment code. + * These are rebuilt from scratch during staged deployment finalization + * (title, version, linux, initrd from the deployment; options from the + * serialized kargs), so they must NOT be duplicated into bootconfig-extra. + */ +static const char *const standard_bls_keys[] + = { "title", "version", "options", "linux", "initrd", "devicetree", NULL }; + +static gboolean +is_standard_bls_key (const char *key) +{ + for (const char *const *p = standard_bls_keys; *p != NULL; p++) + { + if (strcmp (key, *p) == 0) + return TRUE; + } + return FALSE; +} + +/** + * _ostree_bootconfig_parser_get_extra_keys_variant: + * @self: Parser + * + * Returns a GVariant of type "a{ss}" containing all bootconfig keys + * that are not part of the standard BLS set managed by ostree. These + * are extension keys set by consumers like bootc (e.g. + * "x-options-source-tuned") that need to survive the staged deployment + * serialization roundtrip. + * + * Returns: (transfer full) (nullable): A new floating GVariant, or NULL if + * there are no extra keys + */ +GVariant * +_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self) +{ + g_auto (GVariantBuilder) builder = OT_VARIANT_BUILDER_INITIALIZER; + gboolean has_entries = FALSE; + + g_variant_builder_init (&builder, (GVariantType *)"a{ss}"); + + GLNX_HASH_TABLE_FOREACH_KV (self->options, const char *, k, const char *, v) + { + if (is_standard_bls_key (k)) + continue; + g_variant_builder_add (&builder, "{ss}", k, v); + has_entries = TRUE; + } + + if (!has_entries) + return NULL; + + return g_variant_builder_end (&builder); +} + static void ostree_bootconfig_parser_finalize (GObject *object) { diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 1dbaaa7e9d..77b601ff42 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -3884,6 +3884,33 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname, g_variant_builder_add (builder, "{sv}", "overlay-initrds", g_variant_new_strv ((const char *const *)opts->overlay_initrds, -1)); + /* Serialize any extension BLS keys (e.g. x-options-source-tuned). + * These are custom keys set by consumers like bootc and need to survive + * the staging roundtrip so they are preserved during finalization at shutdown. + * + * First check the new deployment's bootconfig (in case the caller set keys + * on it directly). If none found, fall back to the merge deployment's + * bootconfig, which carries the keys from the currently deployed BLS entry. + * This ensures that x-prefixed keys are inherited across staged deployments + * even though _ostree_deployment_set_bootconfig_from_kargs() creates a fresh + * bootconfig containing only the "options" key. + */ + { + GVariant *extra = NULL; + OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment); + if (bootconfig) + extra = _ostree_bootconfig_parser_get_extra_keys_variant (bootconfig); + if (!extra && merge_deployment) + { + OstreeBootconfigParser *merge_bootconfig + = ostree_deployment_get_bootconfig (merge_deployment); + if (merge_bootconfig) + extra = _ostree_bootconfig_parser_get_extra_keys_variant (merge_bootconfig); + } + if (extra) + g_variant_builder_add (builder, "{sv}", "bootconfig-extra", extra); + } + const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED)); if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error)) return FALSE; diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index 282df6d100..5b837bb2f7 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -1225,6 +1225,24 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error) _ostree_deployment_set_overlay_initrds (staged, overlay_initrds); + /* Restore any extension BLS keys (e.g. x-options-source-tuned) + * that were serialized during staging. This preserves custom keys + * set by consumers like bootc through the staging roundtrip. + */ + { + g_autoptr (GVariant) bootconfig_extra = NULL; + if (g_variant_dict_lookup (staged_deployment_dict, "bootconfig-extra", "@a{ss}", + &bootconfig_extra)) + { + OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (staged); + GVariantIter iter; + const char *key, *value; + g_variant_iter_init (&iter, bootconfig_extra); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &value)) + ostree_bootconfig_parser_set (bootconfig, key, value); + } + } + self->staged_deployment = g_steal_pointer (&staged); self->staged_deployment_data = g_steal_pointer (&staged_deployment_data); /* We set this flag for ostree_deployment_is_staged() because that API diff --git a/tests/test-bootconfig-parser-internals.c b/tests/test-bootconfig-parser-internals.c index 00b18d4a17..d84c4a3be0 100644 --- a/tests/test-bootconfig-parser-internals.c +++ b/tests/test-bootconfig-parser-internals.c @@ -49,6 +49,211 @@ test_parse_tries_invalid (void) g_assert_cmpuint (done, ==, 0); } +static void +test_extra_keys_variant_empty (void) +{ + /* A bootconfig with no x-prefixed keys should return NULL */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43"); + ostree_bootconfig_parser_set (parser, "version", "1"); + ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw quiet"); + ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0"); + ostree_bootconfig_parser_set (parser, "initrd", "/initramfs-6.8.0.img"); + + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_null (extra); + + g_object_unref (parser); +} + +static void +test_extra_keys_variant_with_extension_keys (void) +{ + /* Standard keys should be excluded, only extension keys returned */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43"); + ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw"); + ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0"); + ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full isolcpus=1-3"); + ostree_bootconfig_parser_set (parser, "x-options-source-dracut", "rd.driver.pre=vfio-pci"); + + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_nonnull (extra); + + GVariant *extra_owned = g_variant_ref_sink (extra); + + /* Should be a{ss} with exactly 2 entries */ + g_assert_true (g_variant_is_of_type (extra_owned, G_VARIANT_TYPE ("a{ss}"))); + g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2); + + /* Verify the contents */ + GVariantIter iter; + const char *key, *val; + gboolean found_tuned = FALSE, found_dracut = FALSE; + g_variant_iter_init (&iter, extra_owned); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &val)) + { + if (g_str_equal (key, "x-options-source-tuned")) + { + g_assert_cmpstr (val, ==, "nohz=full isolcpus=1-3"); + found_tuned = TRUE; + } + else if (g_str_equal (key, "x-options-source-dracut")) + { + g_assert_cmpstr (val, ==, "rd.driver.pre=vfio-pci"); + found_dracut = TRUE; + } + else + { + g_assert_not_reached (); + } + } + g_assert_true (found_tuned); + g_assert_true (found_dracut); + + g_variant_unref (extra_owned); + g_object_unref (parser); +} + +static void +test_extra_keys_variant_standard_excluded (void) +{ + /* Standard BLS keys (title, version, options, linux, initrd, devicetree) + * should be excluded. All other keys should be preserved. + */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (parser, "title", "Test"); + ostree_bootconfig_parser_set (parser, "version", "1.0"); + ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc"); + ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz"); + ostree_bootconfig_parser_set (parser, "initrd", "/initramfs.img"); + ostree_bootconfig_parser_set (parser, "devicetree", "/dtb"); + + /* Only standard keys -- should return NULL */ + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_null (extra); + + /* Add non-standard keys -- all should be preserved */ + ostree_bootconfig_parser_set (parser, "my-custom-key", "some-value"); + ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full"); + + extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_nonnull (extra); + GVariant *extra_owned = g_variant_ref_sink (extra); + + g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2); + + GVariantIter iter; + const char *key, *val; + gboolean found_custom = FALSE, found_tuned = FALSE; + g_variant_iter_init (&iter, extra_owned); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &val)) + { + if (g_str_equal (key, "my-custom-key")) + { + g_assert_cmpstr (val, ==, "some-value"); + found_custom = TRUE; + } + else if (g_str_equal (key, "x-options-source-tuned")) + { + g_assert_cmpstr (val, ==, "nohz=full"); + found_tuned = TRUE; + } + else + g_assert_not_reached (); + } + g_assert_true (found_custom); + g_assert_true (found_tuned); + + g_variant_unref (extra_owned); + g_object_unref (parser); +} + +static void +test_extra_keys_roundtrip (void) +{ + /* Test that extra keys can be serialized to a variant and restored */ + OstreeBootconfigParser *original = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (original, "options", "root=UUID=abc rw"); + ostree_bootconfig_parser_set (original, "linux", "/vmlinuz"); + ostree_bootconfig_parser_set (original, "x-options-source-tuned", "nohz=full isolcpus=1-3"); + + /* Serialize */ + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (original); + g_assert_nonnull (extra); + GVariant *extra_owned = g_variant_ref_sink (extra); + + /* Create a new parser (simulating deserialization) with only standard keys */ + OstreeBootconfigParser *restored = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (restored, "options", "root=UUID=abc rw nohz=full isolcpus=1-3"); + + /* Restore extra keys from variant */ + GVariantIter iter; + const char *key, *value; + g_variant_iter_init (&iter, extra_owned); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &value)) + ostree_bootconfig_parser_set (restored, key, value); + + /* Verify the extension key survived the roundtrip */ + g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "x-options-source-tuned"), ==, + "nohz=full isolcpus=1-3"); + /* Standard keys should also be present */ + g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "options"), ==, + "root=UUID=abc rw nohz=full isolcpus=1-3"); + + g_variant_unref (extra_owned); + g_object_unref (original); + g_object_unref (restored); +} + +static void +test_extra_keys_parse_write_roundtrip (void) +{ + /* Test that x-prefixed keys survive a parse -> write -> parse roundtrip + * via the BLS file format. + */ + const char *bls_content = "title Fedora Linux 43\n" + "version 6.8.0-300.fc40.x86_64\n" + "linux /vmlinuz-6.8.0\n" + "initrd /initramfs-6.8.0.img\n" + "options root=UUID=abc rw nohz=full\n" + "x-options-source-tuned nohz=full\n"; + + /* Write the BLS content to a temp file */ + g_autofree char *tmpdir = g_dir_make_tmp ("ostree-test-XXXXXX", NULL); + g_assert_nonnull (tmpdir); + g_autofree char *tmpfile = g_build_filename (tmpdir, "ostree-test.conf", NULL); + g_assert_true (g_file_set_contents (tmpfile, bls_content, -1, NULL)); + + /* Parse */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + g_assert_true (ostree_bootconfig_parser_parse_at (parser, AT_FDCWD, tmpfile, NULL, NULL)); + + /* The x-prefixed key should have been parsed */ + g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "x-options-source-tuned"), ==, + "nohz=full"); + g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "options"), ==, + "root=UUID=abc rw nohz=full"); + + /* Write it back out */ + g_autofree char *outfile = g_build_filename (tmpdir, "ostree-test-out.conf", NULL); + g_assert_true (ostree_bootconfig_parser_write_at (parser, AT_FDCWD, outfile, NULL, NULL)); + + /* Parse the output and verify the key survived */ + OstreeBootconfigParser *parser2 = ostree_bootconfig_parser_new (); + g_assert_true (ostree_bootconfig_parser_parse_at (parser2, AT_FDCWD, outfile, NULL, NULL)); + g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "x-options-source-tuned"), ==, + "nohz=full"); + g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "options"), ==, + "root=UUID=abc rw nohz=full"); + + g_object_unref (parser); + g_object_unref (parser2); + (void)unlink (tmpfile); + (void)unlink (outfile); + (void)rmdir (tmpdir); +} + int main (int argc, char *argv[]) { @@ -56,5 +261,13 @@ main (int argc, char *argv[]) g_test_add_func ("/bootconfig-parser/tries/valid", test_parse_tries_valid); g_test_add_func ("/bootconfig-parser/tries/invalid", test_parse_tries_invalid); + g_test_add_func ("/bootconfig-parser/extra-keys/empty", test_extra_keys_variant_empty); + g_test_add_func ("/bootconfig-parser/extra-keys/with-extension-keys", + test_extra_keys_variant_with_extension_keys); + g_test_add_func ("/bootconfig-parser/extra-keys/standard-excluded", + test_extra_keys_variant_standard_excluded); + g_test_add_func ("/bootconfig-parser/extra-keys/roundtrip", test_extra_keys_roundtrip); + g_test_add_func ("/bootconfig-parser/extra-keys/parse-write-roundtrip", + test_extra_keys_parse_write_roundtrip); return g_test_run (); }