@@ -690,6 +690,27 @@ config BOOTMETH_EXTLINUX_LOCALBOOT
This attempts to find a kernel and initrd on the disk and boot it,
in the case where there is no "localcmd" in the environment.
+config BOOTMETH_BLS
+ bool "Bootdev support for Boot Loader Specification (BLS) Type #1"
+ select PXE_UTILS
+ default y if SANDBOX
+ help
+ Enables support for Boot Loader Specification (BLS) Type #1 entries.
+ This makes bootdevs look for '*.conf' files in 'loader/entries/'
+ directories on each filesystem they scan.
+
+ BLS is a standard for boot entry configuration used by Fedora, RHEL,
+ and other distributions. Each .conf file describes a single boot
+ entry with fields like kernel path, initrd, and boot options.
+
+ The specification is here:
+
+ https://uapi-group.org/specifications/specs/boot_loader_specification/
+
+ This supports standard BLS fields (title, linux, initrd, options,
+ devicetree) and works with U-Boot FIT images using the path#config
+ syntax.
+
config BOOTMETH_EFI
bool "Bootdev support for EFI boot"
depends on EFI_BINARY_EXEC
@@ -29,6 +29,7 @@ obj-$(CONFIG_$(PHASE_)BOOTSTD_PROG) += prog_boot.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX) += ext_pxe_common.o bootmeth_extlinux.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX_PXE) += ext_pxe_common.o bootmeth_pxe.o
+obj-$(CONFIG_$(PHASE_)BOOTMETH_BLS) += bls_parse.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_EFI) += bootmeth_efi.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_CROS) += bootmeth_cros.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_FEL) += bootmeth_fel.o
new file mode 100644
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Boot Loader Specification (BLS) Type #1 parser
+ *
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <bls.h>
+#include <log.h>
+#include <malloc.h>
+#include <linux/ctype.h>
+#include <linux/string.h>
+
+/**
+ * enum bls_token_t - BLS Type #1 field tokens
+ *
+ * Token identifiers for BLS entry fields. Using an enum avoids repeated
+ * string comparisons during parsing.
+ *
+ * Note: The enum values correspond to indices in bls_token_names[]
+ */
+enum bls_token_t {
+ TOK_TITLE = 0,
+ TOK_VERSION,
+ TOK_LINUX,
+ TOK_OPTIONS,
+ TOK_INITRD,
+ TOK_DEVICETREE,
+ TOK_DEVICETREE_OVERLAY,
+ TOK_ARCHITECTURE,
+ TOK_MACHINE_ID,
+ TOK_SORT_KEY,
+
+ TOK_COUNT,
+};
+
+/* BLS field names indexed by enum bls_token_t */
+static const char *const bls_token_names[] = {
+ [TOK_TITLE] = "title",
+ [TOK_VERSION] = "version",
+ [TOK_LINUX] = "linux",
+ [TOK_OPTIONS] = "options",
+ [TOK_INITRD] = "initrd",
+ [TOK_DEVICETREE] = "devicetree",
+ [TOK_DEVICETREE_OVERLAY] = "devicetree-overlay",
+ [TOK_ARCHITECTURE] = "architecture",
+ [TOK_MACHINE_ID] = "machine-id",
+ [TOK_SORT_KEY] = "sort-key",
+};
+
+/**
+ * bls_lookup_token() - Look up a token by name
+ *
+ * @key: Field name to look up
+ * Return: Token enum value, or TOK_COUNT if not recognized
+ */
+static enum bls_token_t bls_lookup_token(const char *key)
+{
+ int i;
+
+ for (i = 0; i < TOK_COUNT; i++) {
+ if (!strcmp(key, bls_token_names[i]))
+ return i;
+ }
+
+ return TOK_COUNT;
+}
+
+/**
+ * bls_append_str() - Append a string to an existing field
+ *
+ * Used for fields that can appear multiple times (e.g., options).
+ * Concatenates with a space separator.
+ *
+ * @fieldp: Pointer to field to append to (may be NULL)
+ * @value: String to append
+ * Return: 0 on success, -ENOMEM if allocation fails
+ */
+static int bls_append_str(char **fieldp, const char *value)
+{
+ size_t old_len, val_len, new_len;
+ char *new_str;
+
+ if (!*fieldp) {
+ *fieldp = strdup(value);
+ return *fieldp ? 0 : -ENOMEM;
+ }
+
+ old_len = strlen(*fieldp);
+ val_len = strlen(value);
+ new_len = old_len + 1 + val_len + 1; /* +1 for space, +1 for nul */
+
+ new_str = realloc(*fieldp, new_len);
+ if (!new_str)
+ return -ENOMEM;
+
+ new_str[old_len] = ' ';
+ memcpy(new_str + old_len + 1, value, val_len + 1);
+
+ *fieldp = new_str;
+
+ return 0;
+}
+
+/**
+ * bls_skip_whitespace() - Skip leading whitespace
+ *
+ * @strp: Pointer to string pointer (updated to first non-whitespace char)
+ */
+static void bls_skip_whitespace(char **strp)
+{
+ char *p = *strp;
+
+ while (*p && isspace(*p))
+ p++;
+ *strp = p;
+}
+
+/**
+ * bls_parse_line() - Parse a single line from a BLS entry file
+ *
+ * Parses one line of a BLS entry. Lines are in "key value" format where
+ * the key and value are separated by whitespace. The value extends to
+ * the end of the line.
+ *
+ * @line: Line to parse (will be modified)
+ * @keyp: Returns pointer to key string
+ * @valuep: Returns pointer to value string
+ * Return: 0 on success, -EINVAL if line is invalid
+ */
+static int bls_parse_line(char *line, char **keyp, char **valuep)
+{
+ char *p = line;
+ char *key, *value;
+
+ /* Skip leading whitespace */
+ bls_skip_whitespace(&p);
+
+ /* Skip blank lines and comments */
+ if (!*p || *p == '#')
+ return -EINVAL;
+
+ /* Extract key */
+ key = p;
+ while (*p && !isspace(*p))
+ p++;
+ if (!*p)
+ return -EINVAL; /* No value */
+
+ *p++ = '\0';
+
+ /* Skip whitespace before value */
+ bls_skip_whitespace(&p);
+ if (!*p)
+ return -EINVAL; /* No value */
+
+ value = p;
+
+ /* Remove trailing whitespace from value */
+ p = value + strlen(value) - 1;
+ while (p >= value && isspace(*p))
+ *p-- = '\0';
+
+ *keyp = key;
+ *valuep = value;
+
+ return 0;
+}
+
+int bls_parse_entry(const char *buf, size_t size, struct bls_entry *entry)
+{
+ char *data = (char *)buf;
+ char *line, *next;
+ bool err = false;
+
+ /* Initialize entry to zero */
+ memset(entry, '\0', sizeof(*entry));
+
+ /* Initialize initrds list */
+ alist_init_struct(&entry->initrds, char *);
+
+ log_debug("parsing BLS entry, size %zx\n", size);
+
+ /* Parse buffer line by line, modifying it in place */
+ line = data;
+ while (line < data + size) {
+ enum bls_token_t token;
+ char *key, *value;
+ int ret;
+
+ /* Find end of line */
+ next = memchr(line, '\n', data + size - line);
+ if (next) {
+ *next = '\0';
+ next++;
+ } else {
+ next = data + size;
+ }
+
+ ret = bls_parse_line(line, &key, &value);
+ if (ret) {
+ line = next;
+ continue; /* Skip blank lines and comments */
+ }
+
+ log_debug("BLS field: '%s' = '%s'\n", key, value);
+ line = next;
+
+ /* Look up token and parse supported fields */
+ token = bls_lookup_token(key);
+ switch (token) {
+ case TOK_TITLE:
+ /* Point into buffer */
+ entry->title = value;
+ break;
+ case TOK_VERSION:
+ /* Point into buffer */
+ entry->version = value;
+ break;
+ case TOK_LINUX:
+ /* Point into buffer */
+ entry->kernel = value;
+ break;
+ case TOK_OPTIONS:
+ /* Multiple times - allocate and concatenate */
+ if (bls_append_str(&entry->options, value))
+ err = true;
+ break;
+ case TOK_INITRD:
+ /* Multiple times - add pointer to buffer */
+ if (!alist_add(&entry->initrds, value))
+ err = true;
+ break;
+ case TOK_DEVICETREE:
+ /* Point into buffer */
+ entry->devicetree = value;
+ break;
+ case TOK_DEVICETREE_OVERLAY:
+ /* Point into buffer */
+ entry->dt_overlays = value;
+ break;
+ case TOK_ARCHITECTURE:
+ /* Point into buffer */
+ entry->architecture = value;
+ break;
+ case TOK_MACHINE_ID:
+ /* Point into buffer */
+ entry->machine_id = value;
+ break;
+ case TOK_SORT_KEY:
+ /* Point into buffer */
+ entry->sort_key = value;
+ break;
+ default:
+ /* Ignore unknown fields for forward compatibility */
+ log_debug("Ignoring unknown BLS field: %s\n", key);
+ break;
+ }
+ }
+
+ /* Check for errors during parsing */
+ if (err)
+ return -ENOMEM;
+
+ /*
+ * Validate required fields: BLS spec requires at least one of
+ * 'linux' or 'efi'. We only support 'linux' for Type #1 entries.
+ */
+ if (!entry->kernel) {
+ log_err("BLS entry missing required 'linux' field\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+void bls_entry_uninit(struct bls_entry *entry)
+{
+ if (!entry)
+ return;
+
+ /*
+ * Most fields point into the parsed buffer and don't need freeing.
+ * Only options is allocated (for concatenation of multiple lines).
+ */
+ free(entry->options);
+
+ /*
+ * Uninit initrds list. The strings in the list point into the buffer,
+ * so we don't free them, just the list structure.
+ */
+ alist_uninit(&entry->initrds);
+}
+
new file mode 100644
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Boot Loader Specification (BLS) Type #1 support
+ *
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#ifndef __BLS_H
+#define __BLS_H
+
+#include <alist.h>
+#include <linux/types.h>
+
+/**
+ * struct bls_entry - represents a single BLS boot entry
+ *
+ * This structure holds the parsed fields from a BLS Type #1 entry file.
+ * BLS entries use a simple key-value format with one field per line.
+ *
+ * Most fields point directly into the parsed buffer and are only valid while
+ * the buffer remains valid. The exception is @options which is allocated
+ * because multiple option lines must be concatenated.
+ *
+ * @title: Human-readable name (points into buffer)
+ * @version: OS version string (points into buffer)
+ * @kernel: Kernel path or FIT image - required (points into buffer)
+ * Can include FIT config syntax: path#config
+ * @options: Kernel command line - ALLOCATED, must be freed
+ * Multiple options lines are concatenated with spaces
+ * @initrds: List of initrd paths (alist of char * pointing into buffer)
+ * Multiple initrd lines are supported and accumulated
+ * @devicetree: Device tree blob path (points into buffer)
+ * @dt_overlays: Device tree overlays (points into buffer)
+ * @architecture: Target architecture (points into buffer)
+ * @machine_id: OS identifier (points into buffer)
+ * @sort_key: Sorting identifier (points into buffer)
+ * @filename: Path to .conf file (points into buffer)
+ */
+struct bls_entry {
+ char *title;
+ char *version;
+ char *kernel;
+ char *options; /* Allocated */
+ struct alist initrds; /* list of char * into buffer */
+ char *devicetree;
+ char *dt_overlays;
+ char *architecture;
+ char *machine_id;
+ char *sort_key;
+ char *filename;
+};
+
+/**
+ * bls_parse_entry() - Parse a BLS entry file
+ *
+ * Parses the contents of a BLS Type #1 entry file into a pre-allocated
+ * entry structure. The format is simple key-value pairs with one field per
+ * line. Lines starting with '#' are comments and blank lines are ignored.
+ *
+ * The entry is initialized to zero before parsing. Most entry fields will
+ * point directly into the buffer (which is modified to add null terminators).
+ * The buffer must remain valid for the lifetime of the entry. Only the
+ * 'options' field is allocated separately because multiple option lines must
+ * be concatenated.
+ *
+ * The caller must call bls_entry_uninit() on the entry when done, regardless
+ * of whether this function succeeds or fails, to free any allocated memory.
+ *
+ * Supported fields:
+ * title - Human-readable name
+ * version - OS version string
+ * linux - Kernel path (required)
+ * options - Kernel command line (allocated, can appear multiple times)
+ * initrd - Initramfs path (can appear multiple times)
+ * devicetree - Device tree blob path
+ *
+ * Unknown fields are ignored for forward compatibility.
+ *
+ * @buf: Buffer containing the BLS entry file contents (will be modified)
+ * @size: Size of the buffer in bytes
+ * @entry: BLS entry structure to fill in (will be initialized)
+ * Return: 0 on success, -ENOMEM if out of memory, -EINVAL if required fields
+ * are missing
+ */
+int bls_parse_entry(const char *buf, size_t size, struct bls_entry *entry);
+
+
+/**
+ * bls_entry_uninit() - Clean up a BLS entry's fields
+ *
+ * Frees all allocated fields within the entry but does not free the entry
+ * structure itself. Use this for stack-allocated entries.
+ *
+ * @entry: Entry to clean up
+ */
+void bls_entry_uninit(struct bls_entry *entry);
+
+
+#endif /* __BLS_H */
@@ -4,6 +4,7 @@
ifdef CONFIG_UT_BOOTSTD
obj-$(CONFIG_BOOTSTD) += bootdev.o bootstd_common.o bootflow.o bootmeth.o
+obj-$(CONFIG_BOOTMETH_BLS) += bls_parse.o
obj-$(CONFIG_CMDLINE) += pxe.o
obj-$(CONFIG_FIT) += image.o
obj-$(CONFIG_$(PHASE_)FIT_PRINT) += fit_print.o
new file mode 100644
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Test for Boot Loader Specification (BLS) parser
+ *
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#include <bls.h>
+#include <test/ut.h>
+
+/* Test basic BLS entry parsing */
+static int bls_test_parse_basic(struct unit_test_state *uts)
+{
+ struct bls_entry entry;
+ char buf[] =
+ "title Test Entry\n"
+ "version 1.0\n"
+ "linux /vmlinuz\n"
+ "options root=/dev/sda\n"
+ "initrd /initrd.img\n"
+ "devicetree /test.dtb\n";
+
+ ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+ ut_asserteq_str("Test Entry", entry.title);
+ ut_asserteq_str("1.0", entry.version);
+ ut_asserteq_str("/vmlinuz", entry.kernel);
+ ut_asserteq_str("root=/dev/sda", entry.options);
+ ut_asserteq(1, entry.initrds.count);
+ ut_asserteq_str("/test.dtb", entry.devicetree);
+
+ bls_entry_uninit(&entry);
+
+ return 0;
+}
+UNIT_TEST(bls_test_parse_basic, 0, bootstd);
+
+/* Test multiple options lines are concatenated */
+static int bls_test_parse_multi_options(struct unit_test_state *uts)
+{
+ struct bls_entry entry;
+ char buf[] =
+ "title Test\n"
+ "linux /vmlinuz\n"
+ "options root=/dev/sda\n"
+ "options quiet splash\n";
+
+ ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+ ut_asserteq_str("root=/dev/sda quiet splash", entry.options);
+
+ bls_entry_uninit(&entry);
+
+ return 0;
+}
+UNIT_TEST(bls_test_parse_multi_options, 0, bootstd);
+
+/* Test multiple initrd lines */
+static int bls_test_parse_multi_initrd(struct unit_test_state *uts)
+{
+ struct bls_entry entry;
+ char buf[] =
+ "title Test\n"
+ "linux /vmlinuz\n"
+ "initrd /initrd1.img\n"
+ "initrd /initrd2.img\n";
+ const char **initrd;
+
+ ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+ ut_asserteq(2, entry.initrds.count);
+
+ initrd = alist_get(&entry.initrds, 0, char *);
+ ut_asserteq_str("/initrd1.img", *initrd);
+
+ initrd = alist_get(&entry.initrds, 1, char *);
+ ut_asserteq_str("/initrd2.img", *initrd);
+
+ bls_entry_uninit(&entry);
+
+ return 0;
+}
+UNIT_TEST(bls_test_parse_multi_initrd, 0, bootstd);
+
+/* Test comments and blank lines are ignored */
+static int bls_test_parse_comments(struct unit_test_state *uts)
+{
+ struct bls_entry entry;
+ char buf[] =
+ "# This is a comment\n"
+ "title Test\n"
+ "\n"
+ "# Another comment\n"
+ "linux /vmlinuz\n"
+ "\n";
+
+ ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+ ut_asserteq_str("Test", entry.title);
+ ut_asserteq_str("/vmlinuz", entry.kernel);
+
+ bls_entry_uninit(&entry);
+
+ return 0;
+}
+UNIT_TEST(bls_test_parse_comments, 0, bootstd);
+
+/* Test missing required field returns error */
+static int bls_test_parse_missing_kernel(struct unit_test_state *uts)
+{
+ struct bls_entry entry;
+ char buf[] =
+ "title Test\n"
+ "options root=/dev/sda\n";
+
+ ut_asserteq(-EINVAL, bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+
+ bls_entry_uninit(&entry);
+
+ return 0;
+}
+UNIT_TEST(bls_test_parse_missing_kernel, 0, bootstd);
+
+/* Test unknown fields are ignored */
+static int bls_test_parse_unknown_field(struct unit_test_state *uts)
+{
+ struct bls_entry entry;
+ char buf[] =
+ "title Test\n"
+ "linux /vmlinuz\n"
+ "unknown_field some_value\n"
+ "another_unknown 123\n";
+
+ ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+ ut_asserteq_str("Test", entry.title);
+
+ bls_entry_uninit(&entry);
+
+ return 0;
+}
+UNIT_TEST(bls_test_parse_unknown_field, 0, bootstd);
+
+/* Test all supported fields */
+static int bls_test_parse_all_fields(struct unit_test_state *uts)
+{
+ struct bls_entry entry;
+ char buf[] =
+ "title Full Test\n"
+ "version 2.0.1\n"
+ "linux /boot/vmlinuz-2.0.1\n"
+ "options root=/dev/sda1 ro\n"
+ "options quiet splash\n"
+ "initrd /boot/initrd-2.0.1.img\n"
+ "devicetree /boot/dtb-2.0.1.dtb\n"
+ "devicetree-overlay /boot/overlay.dtbo\n"
+ "architecture x86_64\n"
+ "machine-id abc123\n"
+ "sort-key 001\n";
+ const char **initrd;
+
+ ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+ ut_asserteq_str("Full Test", entry.title);
+ ut_asserteq_str("2.0.1", entry.version);
+ ut_asserteq_str("/boot/vmlinuz-2.0.1", entry.kernel);
+ ut_asserteq_str("root=/dev/sda1 ro quiet splash", entry.options);
+ ut_asserteq(1, entry.initrds.count);
+ initrd = alist_get(&entry.initrds, 0, char *);
+ ut_asserteq_str("/boot/initrd-2.0.1.img", *initrd);
+ ut_asserteq_str("/boot/dtb-2.0.1.dtb", entry.devicetree);
+ ut_asserteq_str("/boot/overlay.dtbo", entry.dt_overlays);
+ ut_asserteq_str("x86_64", entry.architecture);
+ ut_asserteq_str("abc123", entry.machine_id);
+ ut_asserteq_str("001", entry.sort_key);
+
+ bls_entry_uninit(&entry);
+
+ return 0;
+}
+UNIT_TEST(bls_test_parse_all_fields, 0, bootstd);