Skip to content

Commit 4affc1b

Browse files
committed
bootconfig: Preserve extension BLS keys across staged deployments
When a deployment is staged via ostree_sysroot_stage_tree_with_options(), the deployment metadata is serialized to /run/ostree/staged-deployment as a GVariant. During finalization at shutdown, _ostree_sysroot_reload_staged() creates a fresh OstreeBootconfigParser and only restores the "options" key from the serialized kargs. Any additional BLS keys that were set on the bootconfig are silently dropped. The parse/write/clone paths in OstreeBootconfigParser already handle unknown keys generically (the "Write unknown fields" loop in write_at()), so keys survive direct deployments and in-memory operations. The gap is exclusively in the staged deployment roundtrip, where a fresh bootconfig is rebuilt from just the kargs strv. This matters for the upcoming bootc `loader-entries set-options-for-source` feature, which stores kernel argument ownership as extension BLS keys (e.g. `x-options-source-tuned nohz=full isolcpus=1-3`). On bootc systems with transient /etc, tools like TuneD lose track of which kargs they own because their state files are wiped on reboot. Tracking ownership directly in the BLS config on /boot solves this, but only if the keys survive staging. systemd-boot, GRUB, and zipl all ignore unknown BLS keys, so extension keys are safe. Fix this by following the same pattern used for overlay-initrds: 1. Add _ostree_bootconfig_parser_get_extra_keys_variant() which returns all non-standard BLS keys as an a{ss} GVariant. Standard keys (title, version, options, linux, initrd, devicetree) are excluded since they are rebuilt from scratch during finalization. All other keys are preserved, trusting the caller. 2. In ostree_sysroot_stage_tree_with_options(), serialize any extra keys as "bootconfig-extra" in the staged GVariant dict. Since _ostree_deployment_set_bootconfig_from_kargs() creates a fresh bootconfig with only the "options" key, the code falls back to the merge deployment's bootconfig for extra keys. This ensures keys are inherited across staged deployments without the caller needing to re-set them. 3. In _ostree_sysroot_reload_staged(), restore extra keys from the "bootconfig-extra" dict onto the deployment's bootconfig via ostree_bootconfig_parser_set(). The function is private (_ostree_ prefix) since only ostree's own staging code uses it. No new public API, no changes to .sym files, no changes to GIR or Rust bindings. Backwards compatibility: - Old ostree ignores the unknown "bootconfig-extra" key in the a{sv} dict (extension keys silently lost, same as before this patch). - New ostree gracefully handles the absence of "bootconfig-extra" in staged data written by older versions (g_variant_dict_lookup returns FALSE, no restoration attempted). Assisted-by: OpenCode (Claude claude-opus-4-6) Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
1 parent c13d650 commit 4affc1b

File tree

5 files changed

+314
-0
lines changed

5 files changed

+314
-0
lines changed

src/libostree/ostree-bootconfig-parser-private.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ G_BEGIN_DECLS
88

99
const char *_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self);
1010

11+
GVariant *_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self);
12+
1113
G_END_DECLS

src/libostree/ostree-bootconfig-parser.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,60 @@ ostree_bootconfig_parser_write (OstreeBootconfigParser *self, GFile *output,
339339
cancellable, error);
340340
}
341341

342+
/* Standard BLS keys that are managed by ostree's own deployment code.
343+
* These are rebuilt from scratch during staged deployment finalization
344+
* (title, version, linux, initrd from the deployment; options from the
345+
* serialized kargs), so they must NOT be duplicated into bootconfig-extra.
346+
*/
347+
static const char *const standard_bls_keys[]
348+
= { "title", "version", "options", "linux", "initrd", "devicetree", NULL };
349+
350+
static gboolean
351+
is_standard_bls_key (const char *key)
352+
{
353+
for (const char *const *p = standard_bls_keys; *p != NULL; p++)
354+
{
355+
if (strcmp (key, *p) == 0)
356+
return TRUE;
357+
}
358+
return FALSE;
359+
}
360+
361+
/**
362+
* _ostree_bootconfig_parser_get_extra_keys_variant:
363+
* @self: Parser
364+
*
365+
* Returns a GVariant of type "a{ss}" containing all bootconfig keys
366+
* that are not part of the standard BLS set managed by ostree. These
367+
* are extension keys set by consumers like bootc (e.g.
368+
* "x-options-source-tuned") that need to survive the staged deployment
369+
* serialization roundtrip.
370+
*
371+
* Returns: (transfer full) (nullable): A new floating GVariant, or NULL if
372+
* there are no extra keys
373+
*/
374+
GVariant *
375+
_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self)
376+
{
377+
g_auto (GVariantBuilder) builder = OT_VARIANT_BUILDER_INITIALIZER;
378+
gboolean has_entries = FALSE;
379+
380+
g_variant_builder_init (&builder, (GVariantType *)"a{ss}");
381+
382+
GLNX_HASH_TABLE_FOREACH_KV (self->options, const char *, k, const char *, v)
383+
{
384+
if (is_standard_bls_key (k))
385+
continue;
386+
g_variant_builder_add (&builder, "{ss}", k, v);
387+
has_entries = TRUE;
388+
}
389+
390+
if (!has_entries)
391+
return NULL;
392+
393+
return g_variant_builder_end (&builder);
394+
}
395+
342396
static void
343397
ostree_bootconfig_parser_finalize (GObject *object)
344398
{

src/libostree/ostree-sysroot-deploy.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3884,6 +3884,33 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname,
38843884
g_variant_builder_add (builder, "{sv}", "overlay-initrds",
38853885
g_variant_new_strv ((const char *const *)opts->overlay_initrds, -1));
38863886

3887+
/* Serialize any extension BLS keys (e.g. x-options-source-tuned).
3888+
* These are custom keys set by consumers like bootc and need to survive
3889+
* the staging roundtrip so they are preserved during finalization at shutdown.
3890+
*
3891+
* First check the new deployment's bootconfig (in case the caller set keys
3892+
* on it directly). If none found, fall back to the merge deployment's
3893+
* bootconfig, which carries the keys from the currently deployed BLS entry.
3894+
* This ensures that x-prefixed keys are inherited across staged deployments
3895+
* even though _ostree_deployment_set_bootconfig_from_kargs() creates a fresh
3896+
* bootconfig containing only the "options" key.
3897+
*/
3898+
{
3899+
GVariant *extra = NULL;
3900+
OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment);
3901+
if (bootconfig)
3902+
extra = _ostree_bootconfig_parser_get_extra_keys_variant (bootconfig);
3903+
if (!extra && merge_deployment)
3904+
{
3905+
OstreeBootconfigParser *merge_bootconfig
3906+
= ostree_deployment_get_bootconfig (merge_deployment);
3907+
if (merge_bootconfig)
3908+
extra = _ostree_bootconfig_parser_get_extra_keys_variant (merge_bootconfig);
3909+
}
3910+
if (extra)
3911+
g_variant_builder_add (builder, "{sv}", "bootconfig-extra", extra);
3912+
}
3913+
38873914
const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED));
38883915
if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error))
38893916
return FALSE;

src/libostree/ostree-sysroot.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,24 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error)
12251225

12261226
_ostree_deployment_set_overlay_initrds (staged, overlay_initrds);
12271227

1228+
/* Restore any extension BLS keys (e.g. x-options-source-tuned)
1229+
* that were serialized during staging. This preserves custom keys
1230+
* set by consumers like bootc through the staging roundtrip.
1231+
*/
1232+
{
1233+
g_autoptr (GVariant) bootconfig_extra = NULL;
1234+
if (g_variant_dict_lookup (staged_deployment_dict, "bootconfig-extra", "@a{ss}",
1235+
&bootconfig_extra))
1236+
{
1237+
OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (staged);
1238+
GVariantIter iter;
1239+
const char *key, *value;
1240+
g_variant_iter_init (&iter, bootconfig_extra);
1241+
while (g_variant_iter_next (&iter, "{&s&s}", &key, &value))
1242+
ostree_bootconfig_parser_set (bootconfig, key, value);
1243+
}
1244+
}
1245+
12281246
self->staged_deployment = g_steal_pointer (&staged);
12291247
self->staged_deployment_data = g_steal_pointer (&staged_deployment_data);
12301248
/* We set this flag for ostree_deployment_is_staged() because that API

tests/test-bootconfig-parser-internals.c

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,225 @@ test_parse_tries_invalid (void)
4949
g_assert_cmpuint (done, ==, 0);
5050
}
5151

52+
static void
53+
test_extra_keys_variant_empty (void)
54+
{
55+
/* A bootconfig with no x-prefixed keys should return NULL */
56+
OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
57+
ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43");
58+
ostree_bootconfig_parser_set (parser, "version", "1");
59+
ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw quiet");
60+
ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0");
61+
ostree_bootconfig_parser_set (parser, "initrd", "/initramfs-6.8.0.img");
62+
63+
GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
64+
g_assert_null (extra);
65+
66+
g_object_unref (parser);
67+
}
68+
69+
static void
70+
test_extra_keys_variant_with_extension_keys (void)
71+
{
72+
/* Standard keys should be excluded, only extension keys returned */
73+
OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
74+
ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43");
75+
ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw");
76+
ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0");
77+
ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full isolcpus=1-3");
78+
ostree_bootconfig_parser_set (parser, "x-options-source-dracut", "rd.driver.pre=vfio-pci");
79+
80+
GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
81+
g_assert_nonnull (extra);
82+
83+
GVariant *extra_owned = g_variant_ref_sink (extra);
84+
85+
/* Should be a{ss} with exactly 2 entries */
86+
g_assert_true (g_variant_is_of_type (extra_owned, G_VARIANT_TYPE ("a{ss}")));
87+
g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2);
88+
89+
/* Verify the contents */
90+
GVariantIter iter;
91+
const char *key, *val;
92+
gboolean found_tuned = FALSE, found_dracut = FALSE;
93+
g_variant_iter_init (&iter, extra_owned);
94+
while (g_variant_iter_next (&iter, "{&s&s}", &key, &val))
95+
{
96+
if (g_str_equal (key, "x-options-source-tuned"))
97+
{
98+
g_assert_cmpstr (val, ==, "nohz=full isolcpus=1-3");
99+
found_tuned = TRUE;
100+
}
101+
else if (g_str_equal (key, "x-options-source-dracut"))
102+
{
103+
g_assert_cmpstr (val, ==, "rd.driver.pre=vfio-pci");
104+
found_dracut = TRUE;
105+
}
106+
else
107+
{
108+
g_assert_not_reached ();
109+
}
110+
}
111+
g_assert_true (found_tuned);
112+
g_assert_true (found_dracut);
113+
114+
g_variant_unref (extra_owned);
115+
g_object_unref (parser);
116+
}
117+
118+
static void
119+
test_extra_keys_variant_standard_excluded (void)
120+
{
121+
/* Standard BLS keys (title, version, options, linux, initrd, devicetree)
122+
* should be excluded. All other keys should be preserved.
123+
*/
124+
OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
125+
ostree_bootconfig_parser_set (parser, "title", "Test");
126+
ostree_bootconfig_parser_set (parser, "version", "1.0");
127+
ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc");
128+
ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz");
129+
ostree_bootconfig_parser_set (parser, "initrd", "/initramfs.img");
130+
ostree_bootconfig_parser_set (parser, "devicetree", "/dtb");
131+
132+
/* Only standard keys -- should return NULL */
133+
GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
134+
g_assert_null (extra);
135+
136+
/* Add non-standard keys -- all should be preserved */
137+
ostree_bootconfig_parser_set (parser, "my-custom-key", "some-value");
138+
ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full");
139+
140+
extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
141+
g_assert_nonnull (extra);
142+
GVariant *extra_owned = g_variant_ref_sink (extra);
143+
144+
g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2);
145+
146+
GVariantIter iter;
147+
const char *key, *val;
148+
gboolean found_custom = FALSE, found_tuned = FALSE;
149+
g_variant_iter_init (&iter, extra_owned);
150+
while (g_variant_iter_next (&iter, "{&s&s}", &key, &val))
151+
{
152+
if (g_str_equal (key, "my-custom-key"))
153+
{
154+
g_assert_cmpstr (val, ==, "some-value");
155+
found_custom = TRUE;
156+
}
157+
else if (g_str_equal (key, "x-options-source-tuned"))
158+
{
159+
g_assert_cmpstr (val, ==, "nohz=full");
160+
found_tuned = TRUE;
161+
}
162+
else
163+
g_assert_not_reached ();
164+
}
165+
g_assert_true (found_custom);
166+
g_assert_true (found_tuned);
167+
168+
g_variant_unref (extra_owned);
169+
g_object_unref (parser);
170+
}
171+
172+
static void
173+
test_extra_keys_roundtrip (void)
174+
{
175+
/* Test that extra keys can be serialized to a variant and restored */
176+
OstreeBootconfigParser *original = ostree_bootconfig_parser_new ();
177+
ostree_bootconfig_parser_set (original, "options", "root=UUID=abc rw");
178+
ostree_bootconfig_parser_set (original, "linux", "/vmlinuz");
179+
ostree_bootconfig_parser_set (original, "x-options-source-tuned", "nohz=full isolcpus=1-3");
180+
181+
/* Serialize */
182+
GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (original);
183+
g_assert_nonnull (extra);
184+
GVariant *extra_owned = g_variant_ref_sink (extra);
185+
186+
/* Create a new parser (simulating deserialization) with only standard keys */
187+
OstreeBootconfigParser *restored = ostree_bootconfig_parser_new ();
188+
ostree_bootconfig_parser_set (restored, "options", "root=UUID=abc rw nohz=full isolcpus=1-3");
189+
190+
/* Restore extra keys from variant */
191+
GVariantIter iter;
192+
const char *key, *value;
193+
g_variant_iter_init (&iter, extra_owned);
194+
while (g_variant_iter_next (&iter, "{&s&s}", &key, &value))
195+
ostree_bootconfig_parser_set (restored, key, value);
196+
197+
/* Verify the extension key survived the roundtrip */
198+
g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "x-options-source-tuned"), ==,
199+
"nohz=full isolcpus=1-3");
200+
/* Standard keys should also be present */
201+
g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "options"), ==,
202+
"root=UUID=abc rw nohz=full isolcpus=1-3");
203+
204+
g_variant_unref (extra_owned);
205+
g_object_unref (original);
206+
g_object_unref (restored);
207+
}
208+
209+
static void
210+
test_extra_keys_parse_write_roundtrip (void)
211+
{
212+
/* Test that x-prefixed keys survive a parse -> write -> parse roundtrip
213+
* via the BLS file format.
214+
*/
215+
const char *bls_content = "title Fedora Linux 43\n"
216+
"version 6.8.0-300.fc40.x86_64\n"
217+
"linux /vmlinuz-6.8.0\n"
218+
"initrd /initramfs-6.8.0.img\n"
219+
"options root=UUID=abc rw nohz=full\n"
220+
"x-options-source-tuned nohz=full\n";
221+
222+
/* Write the BLS content to a temp file */
223+
g_autofree char *tmpdir = g_dir_make_tmp ("ostree-test-XXXXXX", NULL);
224+
g_assert_nonnull (tmpdir);
225+
g_autofree char *tmpfile = g_build_filename (tmpdir, "ostree-test.conf", NULL);
226+
g_assert_true (g_file_set_contents (tmpfile, bls_content, -1, NULL));
227+
228+
/* Parse */
229+
OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
230+
g_assert_true (ostree_bootconfig_parser_parse_at (parser, AT_FDCWD, tmpfile, NULL, NULL));
231+
232+
/* The x-prefixed key should have been parsed */
233+
g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "x-options-source-tuned"), ==,
234+
"nohz=full");
235+
g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "options"), ==,
236+
"root=UUID=abc rw nohz=full");
237+
238+
/* Write it back out */
239+
g_autofree char *outfile = g_build_filename (tmpdir, "ostree-test-out.conf", NULL);
240+
g_assert_true (ostree_bootconfig_parser_write_at (parser, AT_FDCWD, outfile, NULL, NULL));
241+
242+
/* Parse the output and verify the key survived */
243+
OstreeBootconfigParser *parser2 = ostree_bootconfig_parser_new ();
244+
g_assert_true (ostree_bootconfig_parser_parse_at (parser2, AT_FDCWD, outfile, NULL, NULL));
245+
g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "x-options-source-tuned"), ==,
246+
"nohz=full");
247+
g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "options"), ==,
248+
"root=UUID=abc rw nohz=full");
249+
250+
g_object_unref (parser);
251+
g_object_unref (parser2);
252+
(void)unlink (tmpfile);
253+
(void)unlink (outfile);
254+
(void)rmdir (tmpdir);
255+
}
256+
52257
int
53258
main (int argc, char *argv[])
54259
{
55260
g_test_init (&argc, &argv, NULL);
56261

57262
g_test_add_func ("/bootconfig-parser/tries/valid", test_parse_tries_valid);
58263
g_test_add_func ("/bootconfig-parser/tries/invalid", test_parse_tries_invalid);
264+
g_test_add_func ("/bootconfig-parser/extra-keys/empty", test_extra_keys_variant_empty);
265+
g_test_add_func ("/bootconfig-parser/extra-keys/with-extension-keys",
266+
test_extra_keys_variant_with_extension_keys);
267+
g_test_add_func ("/bootconfig-parser/extra-keys/standard-excluded",
268+
test_extra_keys_variant_standard_excluded);
269+
g_test_add_func ("/bootconfig-parser/extra-keys/roundtrip", test_extra_keys_roundtrip);
270+
g_test_add_func ("/bootconfig-parser/extra-keys/parse-write-roundtrip",
271+
test_extra_keys_parse_write_roundtrip);
59272
return g_test_run ();
60273
}

0 commit comments

Comments
 (0)