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 <[email protected]> --- 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

