[PATCH 7/7] boot: add a minimal bootmeth for the Boot Loader Specification
Alexey Charkov
alchark at flipper.net
Thu Jun 4 17:31:12 CEST 2026
Add a bootmeth that finds and boots Boot Loader Specification (BLS)
type #1 entry files [1]. On each block-device partition it scans, the
bootmeth looks for files matching '<prefix>loader/entries/*.conf'
(where <prefix> comes from bootstd_get_prefixes(), typically '/' and
'/boot/'), picks the highest-sorting filename, parses it, and exposes
it as a bootflow.
Implementation reuses the existing pxelinux infrastructure.
For now the entry chosen on a partition is purely the lexicographic
maximum of *.conf filenames; sort-key / version field handling
(spec-mandated tiebreakers) and boot-counting (the '+TRIES_LEFT'
filename suffix) are left as TODOs. Likewise, only the top-sorted entry
is surfaced because the bootstd framework currently allows one bootflow
per (bootmeth, partition); exposing every discovered entry will require
a framework extension.
Type #2 BLS (drop-in directory of EFI binaries) is out of scope here;
existing EFI bootmeths cover that use case.
[1] https://uapi-group.org/specifications/specs/boot_loader_specification/
Signed-off-by: Alexey Charkov <alchark at flipper.net>
---
boot/Kconfig | 17 +++
boot/Makefile | 1 +
boot/bootmeth_bls.c | 333 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 351 insertions(+)
diff --git a/boot/Kconfig b/boot/Kconfig
index e1114aea843e..fa871da46640 100644
--- a/boot/Kconfig
+++ b/boot/Kconfig
@@ -636,6 +636,23 @@ config BOOTMETH_EXTLINUX_PXE
This provides a way to try out standard boot on an existing boot flow.
+config BOOTMETH_BLS
+ bool "Bootdev support for Boot Loader Specification entries"
+ select PXE_UTILS
+ default y
+ help
+ Enables support for booting via Boot Loader Specification type #1
+ entry files. The bootmeth scans each filesystem it finds for files
+ matching 'loader/entries/*.conf' (or '/boot/loader/entries/*.conf')
+ and boots the highest-sorting entry.
+
+ The specification is here:
+
+ https://uapi-group.org/specifications/specs/boot_loader_specification/
+
+ Type #2 BLS (drop-in directory of EFI binaries) is not handled here;
+ use BOOTMETH_EFI_BOOTMGR for that.
+
config BOOTMETH_EFILOADER
bool "Bootdev support for EFI boot"
depends on EFI_BINARY_EXEC
diff --git a/boot/Makefile b/boot/Makefile
index 7fb56e7ef379..0ce6fd1cd050 100644
--- a/boot/Makefile
+++ b/boot/Makefile
@@ -30,6 +30,7 @@ obj-$(CONFIG_$(PHASE_)BOOTSTD_PROG) += prog_boot.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX) += bootmeth_extlinux.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX_PXE) += bootmeth_pxe.o
+obj-$(CONFIG_$(PHASE_)BOOTMETH_BLS) += bootmeth_bls.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_EFILOADER) += bootmeth_efi.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_CROS) += bootm.o bootm_os.o bootmeth_cros.o
obj-$(CONFIG_$(PHASE_)BOOTMETH_QFW) += bootmeth_qfw.o
diff --git a/boot/bootmeth_bls.c b/boot/bootmeth_bls.c
new file mode 100644
index 000000000000..3df4f20a11dd
--- /dev/null
+++ b/boot/bootmeth_bls.c
@@ -0,0 +1,333 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Bootmethod for the Boot Loader Specification (type #1 entry files)
+ *
+ * Reuses the pxelinux parser/boot path: each on-disk entry is read into a
+ * struct pxe_label via parse_label_keys() and booted via label_boot().
+ *
+ * Spec: https://uapi-group.org/specifications/specs/boot_loader_specification/
+ *
+ * TODO: a partition typically holds several BLS entries, but the bootstd
+ * framework currently allows only one bootflow per (bootmeth, partition)
+ * pair, so this bootmeth surfaces only the highest-sorting entry. Once the
+ * framework grows a way for a bootmeth to emit multiple bootflows from a
+ * single partition, this should expose every discovered entry so the user
+ * can pick from the standard 'bootflow menu' UI rather than be limited to
+ * the default pick.
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <bootdev.h>
+#include <bootflow.h>
+#include <bootmeth.h>
+#include <bootstd.h>
+#include <command.h>
+#include <dm.h>
+#include <extlinux.h>
+#include <fs.h>
+#include <malloc.h>
+#include <mapmem.h>
+#include <pxe_utils.h>
+#include <linux/sizes.h>
+
+#define BLS_DIR "loader/entries"
+#define BLS_SUFFIX ".conf"
+
+static int bls_check(struct udevice *dev, struct bootflow_iter *iter)
+{
+ int ret;
+
+ /* This only works on block devices */
+ ret = bootflow_iter_check_blk(iter);
+ if (ret)
+ return log_msg_ret("blk", ret);
+
+ return 0;
+}
+
+static int bls_getfile(struct pxe_context *ctx, const char *file_path,
+ char *file_addr, enum bootflow_img_t type, ulong *sizep)
+{
+ struct extlinux_info *info = ctx->userdata;
+ ulong addr;
+ int ret;
+
+ addr = simple_strtoul(file_addr, NULL, 16);
+
+ /* Allow up to 1GB */
+ *sizep = 1 << 30;
+ ret = bootmeth_read_file(info->dev, info->bflow, file_path, addr,
+ type, sizep);
+ if (ret)
+ return log_msg_ret("read", ret);
+
+ return 0;
+}
+
+/**
+ * bls_pick_entry() - Find the highest-sorting *.conf across bootstd prefixes
+ *
+ * Walks ``<prefix>/loader/entries/`` for each prefix in @prefixes and
+ * returns the lexicographically maximum full path seen.
+ *
+ * The spec leaves ordering between prefixes unspecified; comparing full
+ * paths is a deterministic-and-cheap stand-in.
+ *
+ * The Boot Loader Specification says entries should be sorted by sort-key
+ * (descending), then version (descending), then filename (descending). For
+ * the time being only the filename criterion is implemented, which is
+ * sufficient for most distros that encode kernel version into the filename.
+ *
+ * TODO: implement proper spec-compliant ordering. That requires reading
+ * each candidate entry, parsing its 'sort-key' and 'version' fields (the
+ * latter compared with strverscmp()-style logic), and only falling back to
+ * filename order when those tie.
+ *
+ * @prefixes: NULL-terminated array of bootstd prefixes to search
+ * @desc: Block descriptor (used to re-mount per prefix)
+ * @bflow: Bootflow being populated (used to re-mount per prefix)
+ * @fullp: Returns the chosen full path (allocated), or NULL if none
+ * Return: 0 on success, -ENOENT if no entry was found, < 0 on other error
+ */
+static int bls_pick_entry(const char *const *prefixes, struct blk_desc *desc,
+ struct bootflow *bflow, char **fullp)
+{
+ char dirpath[256];
+ char *best = NULL;
+ int ret;
+ int i;
+
+ for (i = 0; prefixes && prefixes[i]; i++) {
+ struct fs_dir_stream *dirs;
+ struct fs_dirent *dent;
+
+ /* fs_closedir() below resets the global fs_type. */
+ ret = bootmeth_setup_fs(bflow, desc);
+ if (ret) {
+ free(best);
+ return log_msg_ret("fs", ret);
+ }
+
+ snprintf(dirpath, sizeof(dirpath), "%s%s",
+ prefixes[i], BLS_DIR);
+ dirs = fs_opendir(dirpath);
+ if (!dirs)
+ continue;
+
+ while ((dent = fs_readdir(dirs))) {
+ size_t len = strlen(dent->name);
+ char *full;
+
+ if (dent->type != FS_DT_REG)
+ continue;
+ if (len <= strlen(BLS_SUFFIX))
+ continue;
+ if (strcmp(dent->name + len - strlen(BLS_SUFFIX),
+ BLS_SUFFIX))
+ continue;
+
+ full = malloc(strlen(dirpath) + 1 + len + 1);
+ if (!full) {
+ free(best);
+ fs_closedir(dirs);
+ return -ENOMEM;
+ }
+ sprintf(full, "%s/%s", dirpath, dent->name);
+
+ if (!best || strcmp(full, best) > 0) {
+ free(best);
+ best = full;
+ } else {
+ free(full);
+ }
+ }
+ fs_closedir(dirs);
+ }
+
+ if (!best)
+ return -ENOENT;
+
+ *fullp = best;
+
+ return 0;
+}
+
+/*
+ * TODO: BLS entry filenames may carry a boot-counter suffix of the form
+ * '+TRIES_LEFT[-TRIES_DONE]' immediately before the .conf extension (see
+ * the spec section on "Boot counting"). When that is implemented, this
+ * bootmeth should:
+ * - parse and strip the suffix from the displayed entry name,
+ * - skip entries whose TRIES_LEFT has reached zero,
+ * - decrement TRIES_LEFT (renaming the file) on each boot attempt.
+ * For now the suffix is left intact in the entry name and ignored.
+ */
+static int bls_read_bootflow(struct udevice *dev, struct bootflow *bflow)
+{
+ struct blk_desc *desc;
+ const char *const *prefixes;
+ struct udevice *bootstd;
+ struct pxe_label *label = NULL;
+ struct pxe_menu scratch = {};
+ char *fpath = NULL;
+ const char *base;
+ char *body;
+ int ret;
+
+ ret = uclass_first_device_err(UCLASS_BOOTSTD, &bootstd);
+ if (ret)
+ return log_msg_ret("std", ret);
+
+ /* We require a partitioned block device */
+ if (!bflow->blk || !bflow->part)
+ return -ENOENT;
+
+ desc = dev_get_uclass_plat(bflow->blk);
+ prefixes = bootstd_get_prefixes(bootstd);
+
+ ret = bls_pick_entry(prefixes, desc, bflow, &fpath);
+ if (ret)
+ return log_msg_ret("scan", ret);
+
+ base = strrchr(fpath, '/');
+ base = base ? base + 1 : fpath;
+
+ /*
+ * bls_pick_entry() finished with fs_closedir(), which resets the
+ * global fs_type. Re-mount the partition so bootmeth_try_file()'s
+ * internal fs_size() call can find the right filesystem driver.
+ */
+ ret = bootmeth_setup_fs(bflow, desc);
+ if (ret) {
+ free(fpath);
+ return log_msg_ret("fs", ret);
+ }
+
+ ret = bootmeth_try_file(bflow, desc, NULL, fpath);
+ if (ret) {
+ free(fpath);
+ return log_msg_ret("try", ret);
+ }
+
+ ret = bootmeth_alloc_file(bflow, SZ_64K, 1, BFI_EXTLINUX_CFG);
+ if (ret) {
+ free(fpath);
+ return log_msg_ret("read", ret);
+ }
+
+ label = label_create();
+ if (!label) {
+ ret = -ENOMEM;
+ goto err;
+ }
+
+ /*
+ * BLS files have no 'label NAME' header — derive the label name from
+ * the basename (without the .conf suffix) so messages are useful.
+ */
+ label->name = strndup(base, strlen(base) - strlen(BLS_SUFFIX));
+ if (!label->name) {
+ ret = -ENOMEM;
+ goto err;
+ }
+
+ body = bflow->buf;
+ ret = parse_label_keys(&body, &scratch, label, true);
+ if (ret < 0)
+ goto err;
+
+ /*
+ * scratch is only used to give parse_label_keys() somewhere safe to
+ * stash menu-level state (e.g. a stray 'menu default' line). BLS
+ * entry files don't contain such lines but defensively free anything
+ * that did get allocated.
+ */
+ free(scratch.default_label);
+
+ /*
+ * label->menu was populated either from a BLS 'title' line (the
+ * spec-mandated human-readable name) or from a stray 'menu label'
+ * the parser may have picked up. Fall back to the filename-derived
+ * label name when neither is present.
+ */
+ bflow->os_name = strdup(label->menu ? label->menu : label->name);
+ if (!bflow->os_name) {
+ ret = -ENOMEM;
+ goto err;
+ }
+
+ bflow->bootmeth_priv = label;
+ free(fpath);
+
+ return 0;
+
+err:
+ if (label)
+ label_destroy(label);
+ free(fpath);
+ return log_msg_ret("bls", ret);
+}
+
+static int bls_boot(struct udevice *dev, struct bootflow *bflow)
+{
+ struct cmd_tbl cmdtp = {}; /* dummy */
+ struct pxe_context ctx;
+ struct extlinux_info info;
+ struct pxe_label *label = bflow->bootmeth_priv;
+ int ret;
+
+ if (!label)
+ return log_msg_ret("lbl", -ENOENT);
+
+ info.dev = dev;
+ info.bflow = bflow;
+
+ /*
+ * BLS paths are absolute relative to the filesystem root of the
+ * partition the entry lives on. allow_abs_path=true honours that;
+ * passing NULL as the bootfile keeps the prefix empty so absolute
+ * paths are not rebased.
+ */
+ ret = pxe_setup_ctx(&ctx, &cmdtp, bls_getfile, &info, true,
+ NULL, false, false);
+ if (ret)
+ return log_msg_ret("ctx", -EINVAL);
+
+ ret = label_boot(&ctx, label);
+ pxe_destroy_ctx(&ctx);
+ if (ret)
+ return log_msg_ret("boot", -EINVAL);
+
+ return 0;
+}
+
+static int bls_bootmeth_bind(struct udevice *dev)
+{
+ struct bootmeth_uc_plat *plat = dev_get_uclass_plat(dev);
+
+ plat->desc = IS_ENABLED(CONFIG_BOOTSTD_FULL) ?
+ "Boot Loader Specification" : "bls";
+
+ return 0;
+}
+
+static struct bootmeth_ops bls_bootmeth_ops = {
+ .check = bls_check,
+ .read_bootflow = bls_read_bootflow,
+ .read_file = bootmeth_common_read_file,
+ .boot = bls_boot,
+};
+
+static const struct udevice_id bls_bootmeth_ids[] = {
+ { .compatible = "u-boot,bls" },
+ { }
+};
+
+U_BOOT_DRIVER(bootmeth_2bls) = {
+ .name = "bootmeth_bls",
+ .id = UCLASS_BOOTMETH,
+ .of_match = bls_bootmeth_ids,
+ .ops = &bls_bootmeth_ops,
+ .bind = bls_bootmeth_bind,
+};
--
2.53.0
More information about the U-Boot
mailing list