Skip to content

Commit 85265b9

Browse files
committed
bootconfig: Preserve specific comments for kargs source tracking
Add support for preserving specific lines in BLS config files that track kernel argument ownership by source. Comments matching the prefix '# x-ostree-options-source-<name>' are parsed, stored, and written back across parse/write roundtrips. This enables consumers like rpm-ostree to record which kargs belong to which source (e.g. TuneD) without adding non-standard key-value lines to BLS configs that could confuse other tools. Changes: - OstreeBootconfigParser: add GPtrArray *comments field to preserve allowlisted magic comment lines through parse_at()/write_at() cycles - Add public API: ostree_bootconfig_parser_{get,set}_comment() (Since: 2025.8) for reading and writing individual comment entries, exported in libostree-devel.sym under LIBOSTREE_2025.8. Private _get_comments_variant() for internal GVariant serialization. - Staged deployment roundtrip: serialize magic comments as 'bootconfig-comments' a{ss} in the staged GVariant and restore them via set_comment() during reload - Update clone() and finalize() to handle the comments array - Add 7 unit tests covering: empty state, get/set, variant serialization, BLS file parsing, write roundtrip, staging roundtrip, and empty-value (source disable) scenarios Resolves: RHEL-135363 See: bootc-dev/bootc#899 Assisted-by: OpenCode (Claude Opus 4.6) Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
1 parent c13d650 commit 85265b9

9 files changed

+550
-3
lines changed

Makefile-libostree.am

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ endif # USE_GPGME
177177
symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym
178178

179179
# Uncomment this include when adding new development symbols.
180-
#if BUILDOPT_IS_DEVEL_BUILD
181-
#symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
182-
#endif
180+
if BUILDOPT_IS_DEVEL_BUILD
181+
symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
182+
endif
183183

184184
# http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
185185
wl_versionscript_arg = -Wl,--version-script=

apidoc/ostree-sections.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ ostree_bootconfig_parser_write
3737
ostree_bootconfig_parser_write_at
3838
ostree_bootconfig_parser_set
3939
ostree_bootconfig_parser_get
40+
ostree_bootconfig_parser_set_comment
41+
ostree_bootconfig_parser_get_comment
4042
ostree_bootconfig_parser_set_overlay_initrds
4143
ostree_bootconfig_parser_get_overlay_initrds
4244
ostree_bootconfig_parser_get_tries_left

src/libostree/libostree-devel.sym

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
- uncomment the include in Makefile-libostree.am
2121
*/
2222

23+
LIBOSTREE_2025.8 {
24+
global:
25+
ostree_bootconfig_parser_get_comment;
26+
ostree_bootconfig_parser_set_comment;
27+
} LIBOSTREE_2025.3;
28+
2329
/* Stub section for the stable release *after* this development one; don't
2430
* edit this other than to update the year. This is just a copy/paste
2531
* source. Replace $LASTSTABLE with the last stable version, and $NEWVERSION

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_comments_variant (OstreeBootconfigParser *self);
12+
1113
G_END_DECLS

src/libostree/ostree-bootconfig-parser.c

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ struct _OstreeBootconfigParser
3434

3535
/* Additional initrds; the primary initrd is in options. */
3636
char **overlay_initrds;
37+
38+
/* Magic comments preserved across parse/write roundtrips.
39+
* Each entry is the comment body (without the leading "# "),
40+
* e.g. "x-ostree-options-source-tuned nohz=full isolcpus=1-3".
41+
* Only comments matching allowed_comment_prefixes are preserved.
42+
*/
43+
GPtrArray *comments;
3744
};
3845

3946
typedef GObjectClass OstreeBootconfigParserClass;
@@ -57,6 +64,13 @@ ostree_bootconfig_parser_clone (OstreeBootconfigParser *self)
5764
parser->filename = g_strdup (self->filename);
5865
parser->overlay_initrds = g_strdupv (self->overlay_initrds);
5966

67+
if (self->comments)
68+
{
69+
parser->comments = g_ptr_array_new_with_free_func (g_free);
70+
for (guint i = 0; i < self->comments->len; i++)
71+
g_ptr_array_add (parser->comments, g_strdup (g_ptr_array_index (self->comments, i)));
72+
}
73+
6074
return parser;
6175
}
6276

@@ -136,6 +150,25 @@ _ostree_bootconfig_parser_filename (OstreeBootconfigParser *self)
136150
return self->filename;
137151
}
138152

153+
/* Allowlist of comment prefixes that should be preserved across parse/write
154+
* roundtrips and the staged deployment serialization. Only BLS comment lines
155+
* (starting with '#') whose body (after stripping "# ") matches one of these
156+
* prefixes are stored in self->comments. To allow a new family of magic
157+
* comments, add its prefix here.
158+
*/
159+
static const char *const allowed_comment_prefixes[] = { "x-ostree-options-source-", NULL };
160+
161+
static gboolean
162+
is_allowed_comment (const char *comment_body)
163+
{
164+
for (const char *const *p = allowed_comment_prefixes; *p != NULL; p++)
165+
{
166+
if (g_str_has_prefix (comment_body, *p))
167+
return TRUE;
168+
}
169+
return FALSE;
170+
}
171+
139172
/**
140173
* ostree_bootconfig_parser_parse_at:
141174
* @self: Parser
@@ -188,6 +221,21 @@ ostree_bootconfig_parser_parse_at (OstreeBootconfigParser *self, int dfd, const
188221
g_strfreev (items);
189222
}
190223
}
224+
else if (*line == '#')
225+
{
226+
/* Check for magic comment lines that we preserve.
227+
* Strip the leading '#' and any whitespace to get the body.
228+
*/
229+
const char *body = line + 1;
230+
while (*body == ' ' || *body == '\t')
231+
body++;
232+
if (is_allowed_comment (body))
233+
{
234+
if (!self->comments)
235+
self->comments = g_ptr_array_new_with_free_func (g_free);
236+
g_ptr_array_add (self->comments, g_strdup (body));
237+
}
238+
}
191239
}
192240

193241
if (overlay_initrds)
@@ -324,6 +372,18 @@ ostree_bootconfig_parser_write_at (OstreeBootconfigParser *self, int dfd, const
324372
write_key (self, buf, k, v);
325373
}
326374

375+
/* Write preserved magic comments */
376+
if (self->comments)
377+
{
378+
for (guint i = 0; i < self->comments->len; i++)
379+
{
380+
const char *comment = g_ptr_array_index (self->comments, i);
381+
g_string_append (buf, "# ");
382+
g_string_append (buf, comment);
383+
g_string_append_c (buf, '\n');
384+
}
385+
}
386+
327387
if (!glnx_file_replace_contents_at (dfd, path, (guint8 *)buf->str, buf->len,
328388
GLNX_FILE_REPLACE_NODATASYNC, cancellable, error))
329389
return FALSE;
@@ -339,6 +399,136 @@ ostree_bootconfig_parser_write (OstreeBootconfigParser *self, GFile *output,
339399
cancellable, error);
340400
}
341401

402+
/**
403+
* ostree_bootconfig_parser_get_comment:
404+
* @self: Parser
405+
* @comment_key: The comment key to look up (e.g. "x-ostree-options-source-tuned")
406+
*
407+
* Searches the stored magic comments for one whose first space-delimited
408+
* token matches @comment_key. Returns the value portion (everything after
409+
* the first space), or NULL if not found.
410+
*
411+
* Returns: (nullable): The value string, or NULL if no matching comment exists.
412+
* The returned string is owned by the parser; do not free.
413+
*
414+
* Since: 2025.8
415+
*/
416+
const char *
417+
ostree_bootconfig_parser_get_comment (OstreeBootconfigParser *self, const char *comment_key)
418+
{
419+
if (!self->comments)
420+
return NULL;
421+
422+
const gsize key_len = strlen (comment_key);
423+
for (guint i = 0; i < self->comments->len; i++)
424+
{
425+
const char *entry = g_ptr_array_index (self->comments, i);
426+
if (g_str_has_prefix (entry, comment_key))
427+
{
428+
char after = entry[key_len];
429+
if (after == '\0')
430+
return ""; /* Key exists with no value */
431+
if (after == ' ' || after == '\t')
432+
return entry + key_len + 1; /* Skip the separator */
433+
}
434+
}
435+
return NULL;
436+
}
437+
438+
/**
439+
* ostree_bootconfig_parser_set_comment:
440+
* @self: Parser
441+
* @comment_key: The comment key (e.g. "x-ostree-options-source-tuned")
442+
* @value: (nullable): The value string (e.g. "nohz=full isolcpus=1-3"),
443+
* or NULL/empty to set a key-only comment
444+
*
445+
* Sets or replaces a magic comment entry. If a comment with the same key
446+
* already exists, it is replaced. Otherwise a new entry is appended.
447+
* The comment must match an allowed prefix (see allowed_comment_prefixes).
448+
*
449+
* Since: 2025.8
450+
*/
451+
void
452+
ostree_bootconfig_parser_set_comment (OstreeBootconfigParser *self, const char *comment_key,
453+
const char *value)
454+
{
455+
g_assert (is_allowed_comment (comment_key));
456+
457+
g_autofree char *new_entry = NULL;
458+
if (value && *value)
459+
new_entry = g_strdup_printf ("%s %s", comment_key, value);
460+
else
461+
new_entry = g_strdup (comment_key);
462+
463+
if (!self->comments)
464+
self->comments = g_ptr_array_new_with_free_func (g_free);
465+
466+
/* Replace existing entry if present */
467+
const gsize key_len = strlen (comment_key);
468+
for (guint i = 0; i < self->comments->len; i++)
469+
{
470+
const char *entry = g_ptr_array_index (self->comments, i);
471+
if (g_str_has_prefix (entry, comment_key))
472+
{
473+
char after = entry[key_len];
474+
if (after == '\0' || after == ' ' || after == '\t')
475+
{
476+
g_free (self->comments->pdata[i]);
477+
self->comments->pdata[i] = g_steal_pointer (&new_entry);
478+
return;
479+
}
480+
}
481+
}
482+
483+
/* Not found — append new entry */
484+
g_ptr_array_add (self->comments, g_steal_pointer (&new_entry));
485+
}
486+
487+
/**
488+
* _ostree_bootconfig_parser_get_comments_variant:
489+
* @self: Parser
490+
*
491+
* Returns a GVariant of type "a{ss}" containing magic comment entries
492+
* whose prefix is in the allowed_comment_prefixes allowlist (currently
493+
* "x-ostree-options-source-"). Each entry is a comment key mapped to
494+
* its value string. These are metadata set by consumers like rpm-ostree
495+
* (e.g. "x-ostree-options-source-tuned" -> "nohz=full isolcpus=1-3")
496+
* that need to survive the staged deployment serialization roundtrip.
497+
*
498+
* Returns: (nullable): A new floating #GVariant of type "a{ss}", or %NULL if
499+
* there are no matching comments. The returned variant is floating;
500+
* if the caller is the definite owner, use g_variant_ref_sink().
501+
*/
502+
GVariant *
503+
_ostree_bootconfig_parser_get_comments_variant (OstreeBootconfigParser *self)
504+
{
505+
if (!self->comments || self->comments->len == 0)
506+
return NULL;
507+
508+
g_auto (GVariantBuilder) builder = OT_VARIANT_BUILDER_INITIALIZER;
509+
g_variant_builder_init (&builder, (GVariantType *)"a{ss}");
510+
511+
for (guint i = 0; i < self->comments->len; i++)
512+
{
513+
const char *entry = g_ptr_array_index (self->comments, i);
514+
/* Split into key and value on the first space/tab */
515+
const char *sep = strpbrk (entry, " \t");
516+
if (sep)
517+
{
518+
g_autofree char *key = g_strndup (entry, sep - entry);
519+
const char *val = sep + 1;
520+
g_variant_builder_add (&builder, "{ss}", key, val);
521+
}
522+
else
523+
{
524+
/* Key only, no value */
525+
g_variant_builder_add (&builder, "{ss}", entry, "");
526+
}
527+
}
528+
529+
return g_variant_builder_end (&builder);
530+
}
531+
342532
static void
343533
ostree_bootconfig_parser_finalize (GObject *object)
344534
{
@@ -347,6 +537,7 @@ ostree_bootconfig_parser_finalize (GObject *object)
347537
g_free (self->filename);
348538
g_strfreev (self->overlay_initrds);
349539
g_hash_table_unref (self->options);
540+
g_clear_pointer (&self->comments, g_ptr_array_unref);
350541

351542
G_OBJECT_CLASS (ostree_bootconfig_parser_parent_class)->finalize (object);
352543
}

src/libostree/ostree-bootconfig-parser.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,12 @@ guint64 ostree_bootconfig_parser_get_tries_left (OstreeBootconfigParser *self);
7373
_OSTREE_PUBLIC
7474
guint64 ostree_bootconfig_parser_get_tries_done (OstreeBootconfigParser *self);
7575

76+
_OSTREE_PUBLIC
77+
const char *ostree_bootconfig_parser_get_comment (OstreeBootconfigParser *self,
78+
const char *comment_key);
79+
80+
_OSTREE_PUBLIC
81+
void ostree_bootconfig_parser_set_comment (OstreeBootconfigParser *self, const char *comment_key,
82+
const char *value);
83+
7684
G_END_DECLS

src/libostree/ostree-sysroot-deploy.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3884,6 +3884,21 @@ 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 magic comments (e.g. "# x-ostree-options-source-tuned ...").
3888+
* These are metadata comments set by consumers that need to survive the
3889+
* staging roundtrip so they are preserved during finalization at shutdown.
3890+
* See allowed_comment_prefixes in ostree-bootconfig-parser.c.
3891+
*/
3892+
{
3893+
OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment);
3894+
if (bootconfig)
3895+
{
3896+
GVariant *comments = _ostree_bootconfig_parser_get_comments_variant (bootconfig);
3897+
if (comments)
3898+
g_variant_builder_add (builder, "{sv}", "bootconfig-comments", comments);
3899+
}
3900+
}
3901+
38873902
const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED));
38883903
if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error))
38893904
return FALSE;

src/libostree/ostree-sysroot.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <sys/vfs.h>
2929
#include <sys/wait.h>
3030

31+
#include "ostree-bootconfig-parser-private.h"
3132
#include "ostree-bootloader-aboot.h"
3233
#include "ostree-bootloader-grub2.h"
3334
#include "ostree-bootloader-syslinux.h"
@@ -1225,6 +1226,25 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error)
12251226

12261227
_ostree_deployment_set_overlay_initrds (staged, overlay_initrds);
12271228

1229+
/* Restore magic comments (e.g. "# x-ostree-options-source-tuned ...")
1230+
* that were serialized during staging. These are metadata comments
1231+
* set by consumers like rpm-ostree that survive the staging roundtrip.
1232+
* See allowed_comment_prefixes in ostree-bootconfig-parser.c.
1233+
*/
1234+
{
1235+
g_autoptr (GVariant) bootconfig_comments = NULL;
1236+
if (g_variant_dict_lookup (staged_deployment_dict, "bootconfig-comments", "@a{ss}",
1237+
&bootconfig_comments))
1238+
{
1239+
OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (staged);
1240+
GVariantIter iter;
1241+
const char *key, *value;
1242+
g_variant_iter_init (&iter, bootconfig_comments);
1243+
while (g_variant_iter_next (&iter, "{&s&s}", &key, &value))
1244+
ostree_bootconfig_parser_set_comment (bootconfig, key, value);
1245+
}
1246+
}
1247+
12281248
self->staged_deployment = g_steal_pointer (&staged);
12291249
self->staged_deployment_data = g_steal_pointer (&staged_deployment_data);
12301250
/* We set this flag for ostree_deployment_is_staged() because that API

0 commit comments

Comments
 (0)