Merge branch 'proa' into 'master'

bootctl: Initial experimentation

See merge request u-boot/u-boot!189
This commit is contained in:
Simon Glass
2025-09-28 23:31:16 +00:00
33 changed files with 3405 additions and 4 deletions

View File

@@ -185,6 +185,9 @@
version-offset = <0xdffe00>;
version-size = <0x100>;
};
boot_ctl: bootctl {
};
};
cedit: cedit {
@@ -2101,3 +2104,4 @@
#endif
#include "cedit.dtsi"
#include "bootctl.dtsi"

View File

@@ -42,6 +42,19 @@
};
};
boot_ctl: bootctl {
};
bootstd {
compatible = "u-boot,boot-std";
theme {
white-on-black;
font-size = <30>;
menu-inset = <3>;
menuitem-gap-y = <1>;
};
};
pci {
compatible = "pci-x86";
#address-cells = <3>;
@@ -73,3 +86,5 @@
};
};
#include "bootctl.dtsi"

View File

@@ -53,6 +53,19 @@
};
};
boot_ctl: bootctl {
};
bootstd {
compatible = "u-boot,boot-std";
theme {
white-on-black;
font-size = <30>;
menu-inset = <3>;
menuitem-gap-y = <1>;
};
};
pci {
compatible = "pci-x86";
#address-cells = <3>;
@@ -92,3 +105,5 @@
};
};
#include "bootctl.dtsi"

View File

@@ -505,6 +505,16 @@ config BOOT_DEFAULTS
of U-Boot to boot various images. Currently much functionality is
tied to enabling the command that exercises it.
config BOOTCTL
bool "Support for boot control"
depends on BOOTSTD
default y
help
This supports an experimental boot control, providing a way to
discover and boot Operating Systems.
For now, this is experimental.
menuconfig BOOTSTD
bool "Standard boot"
default y if BLK

14
boot/bootctl/Makefile Normal file
View File

@@ -0,0 +1,14 @@
# SPDX-License-Identifier: GPL-2.0+
#
# Copyright 2025 Canonical Ltd
# Written by Simon Glass <simon.glass@canonical.com>
obj-y += bootctl-uclass.o
obj-y += bootctl.o
obj-y += efi_os.o
obj-y += extlinux.o
obj-y += logic.o
obj-y += simple_meas.o
obj-y += simple_state.o
obj-y += simple_ui.o
obj-y += util.o

10
boot/bootctl/README.rst Normal file
View File

@@ -0,0 +1,10 @@
.. SPDX-License-Identifier: GPL-2.0+
Bootctl Library
===============
This library is a reference implementation of boot control, a way to control
the boot, used for experimentation.
.. Copyright 2025 Canonical Ltd
.. Written by Simon Glass <simon.glass@canonical.com>

View File

@@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Uclass implementation for boot schema
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <bootctl.h>
#include <dm.h>
UCLASS_DRIVER(bootctrl) = {
.id = UCLASS_BOOTCTL,
.name = "bootctrl",
#if CONFIG_IS_ENABLED(OF_REAL)
.post_bind = dm_scan_fdt_dev,
#endif
.per_device_plat_auto = sizeof(struct bootctl_uc_plat),
};
UCLASS_DRIVER(bootctrl_measure) = {
.id = UCLASS_BOOTCTL_MEASURE,
.name = "bootctrl_measure",
.per_device_plat_auto = sizeof(struct bootctl_uc_plat),
};
UCLASS_DRIVER(bootctrl_oslist) = {
.id = UCLASS_BOOTCTL_OSLIST,
.name = "bootctrl_oslist",
.per_device_plat_auto = sizeof(struct bootctl_uc_plat),
};
UCLASS_DRIVER(bootctrl_state) = {
.id = UCLASS_BOOTCTL_STATE,
.name = "bootctrl_state",
.per_device_plat_auto = sizeof(struct bootctl_uc_plat),
};
UCLASS_DRIVER(bootctrl_ui) = {
.id = UCLASS_BOOTCTL_UI,
.name = "bootctrl_ui",
.per_device_plat_auto = sizeof(struct bootctl_uc_plat),
};

66
boot/bootctl/bootctl.c Normal file
View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Implementation of the logic to perform a boot
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <bootctl.h>
#include <dm.h>
#include <hang.h>
#include <log.h>
#include <version.h>
#include <bootctl/logic.h>
#include <bootctl/oslist.h>
#include <bootctl/state.h>
#include <bootctl/ui.h>
#include <bootctl/util.h>
#include <dm/device-internal.h>
int bootctl_get_dev(enum uclass_id type, struct udevice **devp)
{
struct udevice *dev;
int ret;
ret = uclass_first_device_err(type, &dev);
if (ret)
return log_msg_ret("bfd", ret);
*devp = dev;
return 0;
}
int bootctl_run(void)
{
struct udevice *logic;
int ret;
printf("Boot Control (using U-Boot v%d.%02d)\n", U_BOOT_VERSION_NUM,
U_BOOT_VERSION_NUM_PATCH);
/* figure out the bootctl to use */
ret = bootctl_get_dev(UCLASS_BOOTCTL, &logic);
if (ret)
return log_msg_ret("bgl", ret);
ret = bc_logic_prepare(logic);
if (ret)
return log_msg_ret("bcl", ret);
ret = bc_logic_start(logic);
if (ret)
return log_msg_ret("bcL", ret);
do {
ret = bc_logic_poll(logic);
if (ret) {
printf("logic err %dE\n", ret);
hang();
}
} while (ret != -ESHUTDOWN);
return 0;
}

93
boot/bootctl/bootctl.rst Normal file
View File

@@ -0,0 +1,93 @@
bootctl - Boot Schema
=====================
Introduction
------------
This is a very basic prototype which aims to show some of the concepts behind
the 'boot schema' idea and how they can be implemented in practice.
Please see the FO215 document for details on the schema.
Features
--------
Very, very few features are supported:
- basic menu
- finding OSes for the menu (only extlinux.conf and EFI are supported)
- measurement of images using a TPM
Running on QEMU
---------------
To run this, first create an image with Ubuntu 2024.04. This script can help
but you will need to edit some variables at the top (imagedir and mnt) or pass
these vars into the script.
This runs the installer:
./scripts/build-qemu.sh -a x86 -r -k -d root.img -R 24.04
Go through the full install and then reboot.
Then run without the OS image:
./scripts/build-qemu.sh -a x86 -r -k -d root.img
Now you can install the u-boot-tools package so that an extlinux.conf file is
created on the disk.
When you reboot you should see a bootmenu with a few options.
If you know U-Boot well you can probably run on sandbox
Where is the schema?
--------------------
For now the schema is in `include/bootctl.dtsi` and is in devicetree format.
We will likely use YAML for this, although it may be useful to then compile the
YAML into devicetree in some cases.
Where is the boot logic?
------------------------
See `boot/bootctl/bootctl.c` for the top-level program. It really just gets the
logic driver and polls it until it either boots or gives up.
The real logic is in `boot/bootctl/logic.c`. The `logic_start()` function sets
things up, then `logic_poll()` actually manages finding things to boot and
sending them to the UI.
The data between `logic.c` and `ui.c` is a bit messy and can likely be tidied
up.
Source map
----------
boot/bootctl
Directory containing the source for use with U-Boot
cmd/
Provides a very simple 'bootctl' command to start things up
include/bootctl
Include files for bootctl
test/boot/bootctl
A few very simple tests to give a flavour of how tests might work
What about all the other patches related to expo? Please just ignore these. My
original prototype was terribly ugly so I spent some time trying to clean it
up.
Comments
--------
Please send any and all comments and suggestions to me.
--
Simon Glass
26-Mar-25

69
boot/bootctl/efi_os.c Normal file
View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Access to EFI files containing an 'opaque' OS
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <bootctl.h>
#include <bootmeth.h>
#include <bootstd.h>
#include <dm.h>
#include <log.h>
#include <bootctl/oslist.h>
static int efifile_bind(struct udevice *dev)
{
struct bootctl_uc_plat *ucp = dev_get_uclass_plat(dev);
ucp->desc = "Provides OSes to boot";
return 0;
}
static int efifile_next(struct udevice *dev, struct oslist_iter *iter,
struct osinfo *info)
{
int ret;
if (!iter->active) {
bootstd_clear_glob();
iter->active = true;
ret = bootmeth_set_order("efi");
if (ret)
return log_msg_ret("esf", ret);
ret = bootflow_scan_first(NULL, NULL, &iter->bf_iter,
BOOTFLOWIF_HUNT, &info->bflow);
if (ret)
return log_msg_ret("Esf", ret);
} else {
ret = bootflow_scan_next(&iter->bf_iter, &info->bflow);
if (ret) {
iter->active = false;
return log_msg_ret("Esn", ret);
}
}
return 0;
}
static struct bc_oslist_ops ops = {
.next = efifile_next,
};
static const struct udevice_id efifile_ids[] = {
{ .compatible = "bootctl,efifile-oslist" },
{ .compatible = "bootctl,os-list" },
{ }
};
U_BOOT_DRIVER(efifile) = {
.name = "efifile",
.id = UCLASS_BOOTCTL_OSLIST,
.of_match = efifile_ids,
.bind = efifile_bind,
.ops = &ops,
};

70
boot/bootctl/extlinux.c Normal file
View File

@@ -0,0 +1,70 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Access to extlinux.conf files containing OS information
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <bootctl.h>
#include <bootmeth.h>
#include <bootstd.h>
#include <dm.h>
#include <log.h>
#include <bootctl/oslist.h>
static int extlinux_bind(struct udevice *dev)
{
struct bootctl_uc_plat *ucp = dev_get_uclass_plat(dev);
ucp->desc = "Provides OSes to boot";
return 0;
}
static int extlinux_next(struct udevice *dev, struct oslist_iter *iter,
struct osinfo *info)
{
int ret;
if (!iter->active) {
bootstd_clear_glob();
iter->active = true;
ret = bootmeth_set_order("extlinux");
if (ret)
return log_msg_ret("eso", ret);
ret = bootflow_scan_first(NULL, NULL, &iter->bf_iter,
BOOTFLOWIF_HUNT, &info->bflow);
if (ret)
return log_msg_ret("esf", ret);
} else {
ret = bootflow_scan_next(&iter->bf_iter, &info->bflow);
if (ret) {
iter->active = false;
if (ret)
return log_msg_ret("esn", ret);
}
}
return 0;
}
static struct bc_oslist_ops ops = {
.next = extlinux_next,
};
static const struct udevice_id extlinux_ids[] = {
{ .compatible = "bootctl,extlinux-oslist" },
{ .compatible = "bootctl,os-list" },
{ }
};
U_BOOT_DRIVER(extlinux) = {
.name = "extlinux",
.id = UCLASS_BOOTCTL_OSLIST,
.of_match = extlinux_ids,
.bind = extlinux_bind,
.ops = &ops,
};

322
boot/bootctl/logic.c Normal file
View File

@@ -0,0 +1,322 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Implementation of the logic to perform a boot
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <bootctl.h>
#include <dm.h>
#include <log.h>
#include <time.h>
#include <version.h>
#include <bootctl/logic.h>
#include <bootctl/measure.h>
#include <bootctl/oslist.h>
#include <bootctl/state.h>
#include <bootctl/ui.h>
#include <bootctl/util.h>
#include <dm/device-internal.h>
enum {
COUNTDOWN_INTERVAL_MS = 1000, /* inteval between autoboot updates */
};
static int logic_prepare(struct udevice *dev)
{
struct logic_priv *priv = dev_get_priv(dev);
int ret;
/* figure out the UI to use */
ret = bootctl_get_dev(UCLASS_BOOTCTL_UI, &priv->ui);
if (ret) {
log_err("UI required but failed (err=%dE)\n", ret);
return log_msg_ret("bgd", ret);
}
/* figure out the measurement to use */
if (priv->opt_measure) {
ret = bootctl_get_dev(UCLASS_BOOTCTL_MEASURE,
&priv->meas);
if (ret) {
log_err("Measurement required but failed (err=%dE)\n",
ret);
return log_msg_ret("bgs", ret);
}
}
/* figure out at least one oslist driver to use */
ret = uclass_first_device_err(UCLASS_BOOTCTL_OSLIST, &priv->oslist);
if (ret)
return log_msg_ret("bgo", ret);
/* figure out the state to use */
ret = bootctl_get_dev(UCLASS_BOOTCTL_STATE, &priv->state);
if (ret)
return log_msg_ret("bgs", ret);
if (priv->opt_labels) {
ret = bootdev_set_order(priv->opt_labels);
if (ret)
return log_msg_ret("blo", ret);
}
return 0;
}
static int logic_start(struct udevice *dev)
{
struct logic_priv *priv = dev_get_priv(dev);
int ret;
if (priv->opt_persist_state) {
int ret;
/* read in our state */
ret = bc_state_load(priv->state);
if (ret)
log_warning("Cannot load state, starting fresh (err=%dE)\n", ret);
else
priv->state_loaded = true;
}
ret = bc_ui_show(priv->ui);
if (ret) {
log_err("Cannot show display (err=%dE)\n", ret);
return log_msg_ret("bds", ret);
}
priv->start_time = get_timer(0);
if (priv->opt_autoboot) {
priv->next_countdown = COUNTDOWN_INTERVAL_MS;
priv->autoboot_remain_s = priv->opt_timeout;
priv->autoboot_active = true;
}
if (priv->opt_default_os)
bc_state_read_str(priv->state, "default", &priv->default_os);
if (priv->opt_measure) {
ret = bc_measure_start(priv->meas);
if (ret)
return log_msg_ret("pme", ret);
}
/* start scanning for OSes */
bc_oslist_setup_iter(&priv->iter);
priv->scanning = true;
return 0;
}
/**
* prepare_for_boot() - Get ready to boot an OS
*
* Intended to include at least:
* - A/B/recovery logic
* - persist the state
* - devicetree fix-up
* - measure images
*
* @dev: Bootctrl logic device
* @osinfo: OS to boot
* Return: 0 if OK, -ve on error
*/
static int prepare_for_boot(struct udevice *dev, struct osinfo *osinfo)
{
struct logic_priv *priv = dev_get_priv(dev);
int ret;
if (priv->opt_track_success) {
ret = bc_state_write_bool(priv->state, "recordfail", true);
if (ret)
log_warning("Cannot set up recordfail (err=%dE)\n",
ret);
}
if (priv->opt_persist_state) {
ret = bc_state_save(priv->state);
if (ret)
log_warning("Cannot save state (err=%dE)\n", ret);
else
priv->state_saved = true;
}
/* devicetree fix-ups go here */
/* measure loaded images */
if (priv->opt_measure) {
struct alist result;
ret = bc_measure_process(priv->meas, osinfo, &result);
if (ret)
return log_msg_ret("pbp", ret);
show_measures(&result);
/* TODO: pass these measurements on to OS */
}
return 0;
}
/**
* read_images() - Read all the images needed to boot an OS
*
* @dev: Bootctrl logic device
* @osinfo: OS we intend to boot
* Return: 0 if OK, -ve on error
*/
static int read_images(struct udevice *dev, struct osinfo *osinfo)
{
struct bootflow *bflow = &osinfo->bflow;
int ret;
ret = bootflow_read_all(bflow);
if (ret)
return log_msg_ret("rea", ret);
log_debug("Images read: %d\n", bflow->images.count);
return 0;
}
static int logic_poll(struct udevice *dev)
{
struct logic_priv *priv = dev_get_priv(dev);
struct osinfo info;
bool selected;
int ret, seq;
/* scan for the next OS, if any */
if (priv->scanning) {
ret = bc_oslist_next(priv->oslist, &priv->iter, &info);
if (!ret) {
ret = bc_ui_add(priv->ui, &info);
if (ret)
return log_msg_ret("bda", ret);
priv->refresh = true;
} else {
/* No more OSes from this driver, try the next */
ret = uclass_next_device_err(&priv->oslist);
if (ret)
priv->scanning = false;
else
memset(&priv->iter, '\0',
sizeof(struct oslist_iter));
}
}
if (priv->autoboot_active &&
get_timer(priv->start_time) > priv->next_countdown) {
ulong secs = get_timer(priv->start_time) / 1000;
priv->autoboot_remain_s = secs >= priv->opt_timeout ? 0 :
max(priv->opt_timeout - secs, 0ul);
priv->next_countdown += COUNTDOWN_INTERVAL_MS;
priv->refresh = true;
}
if (priv->refresh) {
ret = bc_ui_render(priv->ui);
if (ret)
return log_msg_ret("bdr", ret);
priv->refresh = false;
}
ret = bc_ui_poll(priv->ui, &seq, &selected);
if (ret < 0)
return log_msg_ret("bdo", ret);
else if (ret)
priv->refresh = true;
if (!selected && priv->autoboot_active && !priv->autoboot_remain_s &&
seq >= 0) {
log_info("Selecting %d due to timeout\n", seq);
selected = true;
}
if (selected) {
struct osinfo *os;
os = alist_getw(&priv->osinfo, seq, struct osinfo);
log_info("Selected %d: %s\n", seq, os->bflow.os_name);
/*
* try to read the images first; some methods don't support
* this
*/
ret = read_images(dev, os);
if (ret && ret != -ENOSYS) {
if (ret)
return log_msg_ret("lri", ret);
}
ret = prepare_for_boot(dev, os);
if (ret)
return log_msg_ret("lpb", ret);
/* debugging */
// return -ESHUTDOWN;
/* boot OS */
ret = bootflow_boot(&os->bflow);
if (ret)
log_warning("Boot failed (err=%dE)\n", ret);
return -ESHUTDOWN;
}
return 0;
}
static int logic_of_to_plat(struct udevice *dev)
{
struct logic_priv *priv = dev_get_priv(dev);
ofnode node = ofnode_find_subnode(dev_ofnode(dev), "options");
priv->opt_persist_state = ofnode_read_bool(node, "persist-state");
priv->opt_default_os = ofnode_read_bool(node, "default-os");
ofnode_read_u32(node, "timeout", &priv->opt_timeout);
priv->opt_skip_timeout = ofnode_read_bool(node,
"skip-timeout-on-success");
priv->opt_track_success = ofnode_read_bool(node, "track-success");
priv->opt_labels = ofnode_read_string(node, "labels");
priv->opt_autoboot = ofnode_read_bool(node, "autoboot");
priv->opt_measure = ofnode_read_bool(node, "measure");
return 0;
}
static int logic_probe(struct udevice *dev)
{
struct logic_priv *priv = dev_get_priv(dev);
alist_init_struct(&priv->osinfo, struct osinfo);
return 0;
}
static struct bc_logic_ops ops = {
.prepare = logic_prepare,
.start = logic_start,
.poll = logic_poll,
};
static const struct udevice_id logic_ids[] = {
{ .compatible = "bootctl,ubuntu-desktop" },
{ .compatible = "bootctl,logic" },
{ }
};
U_BOOT_DRIVER(bc_logic) = {
.name = "bc_logic",
.id = UCLASS_BOOTCTL,
.of_match = logic_ids,
.ops = &ops,
.of_to_plat = logic_of_to_plat,
.probe = logic_probe,
.priv_auto = sizeof(struct logic_priv),
};

356
boot/bootctl/simple_meas.c Normal file
View File

@@ -0,0 +1,356 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Simple control of which display/output to use while booting
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <bloblist.h>
#include <dm.h>
#include <tpm_api.h>
#include <tpm_tcg2.h>
#include <version_string.h>
#include <bootctl/measure.h>
#include <bootctl/oslist.h>
#include <linux/sizes.h>
enum {
/* align TPM log to 4K boundary */
ALIGN_LOG2 = 12,
};
/**
* enum meas_algo_t - Available algorithms
*
* @ALGOT_SHA256: SHA256
* @ALGOT_COUNT: Number of algorithms
*/
enum meas_algo_t {
ALGOT_SHA256,
ALGOT_COUNT,
};
/**
* enum meas_payload_t - Types of things we can measure
*
* @PAYLOADT_OS: Operating system
* @PAYLOADT_INITRD: Initial ramdisk
* @PAYLOADT_FDT: Flattened devicetree
* @PAYLOADT_CMDLINE: OS command line
* @PAYLOADT_COUNT: Number of payload types
*/
enum meas_payload_t {
PAYLOADT_OS,
PAYLOADT_INITRD,
PAYLOADT_FDT,
PAYLOADT_CMDLINE,
PAYLOADT_COUNT,
};
/**
* struct meas_step - An individual measurement, e.g. for a single image
*
* For now only tpm-pcr is supported, so there is no field for the method - it
* is assumed to be tpm-pcr
*
* These parameters are read from the devicetree.
*
* @payload_type: Image type to measure
* @algos: Bitmap of algorithms to use to measure in this step
* @pcr: TPM Platform Configuration Register to use for this step
* @optional: true if it is OK if the image is missing and cannot be measured
*/
struct meas_step {
enum meas_payload_t type;
uint algos;
uint pcr;
bool optional;
};
/**
* struct measure_priv - Private information for the measure driver
*
* @tpm: TPM to use for measurement
* @tpm_log_size: Configured of TPM log
* @elog: Information about TPM log
* @steps: List of measurement steps, each struct measure_step
*/
struct measure_priv {
struct udevice *tpm;
u32 tpm_log_size;
struct tcg2_event_log elog;
struct alist steps;
};
static const char *const algo_name[ALGOT_COUNT] = {
[ALGOT_SHA256] = "sha256"
};
static struct payload {
const char *name;
enum bootflow_img_t type;
} payload_info[PAYLOADT_COUNT] = {
[PAYLOADT_OS] = {"os", (enum bootflow_img_t)IH_TYPE_KERNEL},
[PAYLOADT_INITRD] = {"initrd",
(enum bootflow_img_t)IH_TYPE_RAMDISK},
[PAYLOADT_FDT] = {"fdt", (enum bootflow_img_t)IH_TYPE_FLATDT},
[PAYLOADT_CMDLINE] = {"cmdline", BFI_CMDLINE},
};
static int simple_start(struct udevice *dev)
{
struct measure_priv *priv = dev_get_priv(dev);
struct tcg2_event_log *elog = &priv->elog;
struct udevice *tpm = priv->tpm;
int ret, size;
void *blob;
if (!IS_ENABLED(CONFIG_TPM_V2) || !IS_ENABLED(CONFIG_EFI_TCG2_PROTOCOL))
return log_msg_ret("spt", -ENOSYS);
blob = bloblist_get_blob(BLOBLISTT_TPM_EVLOG, &size);
if (blob) {
if (size < priv->tpm_log_size)
return log_msg_ret("spb", -ENOBUFS);
/* we don't support changing the alignment at present */
if ((ulong)blob != ALIGN((ulong)blob, 1 << ALIGN_LOG2))
return log_msg_ret("spf", -EBADF);
ret = bloblist_resize(BLOBLISTT_TPM_EVLOG, priv->tpm_log_size);
if (ret)
return log_msg_ret("msr", ret);
} else {
blob = bloblist_add(BLOBLISTT_TPM_EVLOG, priv->tpm_log_size,
ALIGN_LOG2);
if (!blob)
return log_msg_ret("sps", -ENOSPC);
}
ret = tpm_auto_start(tpm);
if (ret)
return log_msg_ret("spa", ret);
elog->log = blob;
elog->log_size = priv->tpm_log_size;
ret = tcg2_log_init(tpm, elog);
if (ret)
return log_msg_ret("spi", ret);
ret = tcg2_measure_event(tpm, elog, 0, EV_S_CRTM_VERSION,
strlen(version_string) + 1, version_string);
if (ret) {
tcg2_measurement_term(tpm, elog, true);
return log_msg_ret("spe", ret);
}
return 0;
}
static const char *get_typename(enum bootflow_img_t type)
{
switch ((int)type) {
case IH_TYPE_KERNEL:
return "linux";
case IH_TYPE_RAMDISK:
return "initrd";
case IH_TYPE_FLATDT:
return "dts";
default:
return NULL;
}
}
static int simple_process(struct udevice *dev, const struct osinfo *osinfo,
struct alist *result)
{
const struct bootflow *bflow = &osinfo->bflow;
struct measure_priv *priv = dev_get_priv(dev);
struct tcg2_event_log *elog = &priv->elog;
const struct meas_step *step;
if (!IS_ENABLED(CONFIG_TPM_V2) || !IS_ENABLED(CONFIG_EFI_TCG2_PROTOCOL))
return log_msg_ret("ptp", -ENOSYS);
alist_init_struct(result, struct measure_info);
alist_for_each(step, &priv->steps) {
enum bootflow_img_t type = payload_info[step->type].type;
const struct bootflow_img *img;
struct measure_info info;
const char *typename;
const void *ptr;
int ret;
log_debug("measuring %s\n", payload_info[step->type].name);
img = bootflow_img_find(bflow, type);
if (!img) {
if (step->optional)
continue;
log_err("Missing image '%s'\n",
bootflow_img_type_name(type));
return log_msg_ret("smi", -ENOENT);
}
ptr = map_sysmem(img->addr, img->size);
typename = get_typename(img->type);
if (!typename) {
log_err("Unknown image type %d (%s)\n", img->type,
bootflow_img_type_name(img->type));
return log_msg_ret("pim", -EINVAL);
}
/*
* TODO: Use the requested algos or at least check that the TPM
* supports them
*
* tcg2_measure_data() actually measures with the algorithms
* determined by the TPM. It also does this silently, so we
* don't really know what it did.
*
* See tcg2_get_pcr_info() for where this information is
* collected. We should add a function to the API that lets us
* see what is active
*
* Really the TPM code this should be integrated with the hash.h
* code too, rather than having parallel tables.
*/
ret = tcg2_measure_data(priv->tpm, elog, 8, img->size, ptr,
EV_COMPACT_HASH, strlen(typename) + 1
, typename);
if (ret)
return log_msg_ret("stc", ret);
log_debug("Measured '%s'\n", bootflow_img_type_name(type));
info.img = img;
if (!alist_add(result, img))
return log_msg_ret("sra", -ENOMEM);
}
return 0;
}
static int measure_probe(struct udevice *dev)
{
struct measure_priv *priv = dev_get_priv(dev);
struct udevice *tpm;
if (!IS_ENABLED(CONFIG_TPM_V2))
return log_msg_ret("spT", -ENOSYS);
/*
* TODO: We have to probe TPMs to find out what version they are. This
* could be updated to happen in the bind() method of the TPM
*/
uclass_foreach_dev_probe(UCLASS_TPM, tpm) {
if (tpm_get_version(tpm) == TPM_V2) {
priv->tpm = tpm;
break;
}
}
/* TODO: Add policy for what to do in this case */
if (!priv->tpm)
log_warning("TPM not present\n");
return 0;
}
static int measure_of_to_plat(struct udevice *dev)
{
struct measure_priv *priv = dev_get_priv(dev);
ofnode node;
alist_init_struct(&priv->steps, struct meas_step);
priv->tpm_log_size = SZ_64K;
dev_read_u32(dev, "tpm-log-size", &priv->tpm_log_size);
/* errors should not happen in production code, so use log_debug() */
for (node = ofnode_first_subnode(dev_ofnode(dev)); ofnode_valid(node);
node = ofnode_next_subnode(node)) {
const char *node_name = ofnode_get_name(node);
const char *method = ofnode_read_string(node, "method");
struct meas_step step = {0};
int count, i, j, ret, found;
/* for now we use the node name as the payload name */
for (j = 0, found = -1; j < ARRAY_SIZE(payload_info); j++) {
if (!strcmp(payload_info[j].name, node_name))
found = j;
}
if (found == -1) {
log_debug("Unknown payload '%s'\n", node_name);
return log_msg_ret("mta", -EINVAL);
}
step.type = found;
step.optional = ofnode_read_bool(node, "optional");
if (!method || strcmp("tpm-pcr", method)) {
log_debug("Unknown method in '%s': '%s'\n", node_name,
method);
return log_msg_ret("mtp", -EINVAL);
}
ret = ofnode_read_u32(node, "pcr-number", &step.pcr);
if (ret) {
log_debug("Missing pcr-number in '%s'", node_name);
return log_msg_ret("mtP", ret);
}
count = ofnode_read_string_count(node, "algos");
if (count < 1) {
log_debug("Missing algos in '%s'\n", node_name);
return log_msg_ret("mta", -EINVAL);
}
for (i = 0; i < count; i++) {
const char *name;
ret = ofnode_read_string_index(node, "algos", i, &name);
if (ret)
return log_msg_ret("mta", ret);
for (j = 0, found = -1;
j < ARRAY_SIZE(algo_name); j++) {
if (!strcmp(algo_name[j], name))
found = j;
}
if (found == -1) {
log_debug("Unknown algo in '%s': '%s'\n",
node_name, name);
return log_msg_ret("mta", -EINVAL);
}
step.algos |= BIT(found);
}
if (!alist_add(&priv->steps, step))
return log_msg_ret("mal", -ENOMEM);
}
return 0;
}
static struct bc_measure_ops measure_ops = {
.start = simple_start,
.process = simple_process,
};
static const struct udevice_id measure_ids[] = {
{ .compatible = "bootctl,simple-measure" },
{ .compatible = "bootctl,measure" },
{ }
};
U_BOOT_DRIVER(simple_measure) = {
.name = "simple_meas",
.id = UCLASS_BOOTCTL_MEASURE,
.of_match = measure_ids,
.ops = &measure_ops,
.priv_auto = sizeof(struct measure_priv),
.of_to_plat = measure_of_to_plat,
.probe = measure_probe,
};

447
boot/bootctl/simple_state.c Normal file
View File

@@ -0,0 +1,447 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Provides a simple name/value pair
*
* The file format a ordered series of lines of the form:
* key=value\n
*
* with a nul terminator at the end. Strings are stored without quoting. Ints
* are stored as decimal, perhaps with leading '-'. Bools are stored as 0 or 1
*
* keys consist only of characters a-z, _ and 0-9
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <alist.h>
#include <abuf.h>
#include <bootctl.h>
#include <bootmeth.h>
#include <bootstd.h>
#include <ctype.h>
#include <dm.h>
#include <fs_legacy.h>
#include <log.h>
#include <malloc.h>
#include <vsprintf.h>
#include <bootctl/state.h>
#include <linux/sizes.h>
enum {
/* maximum length of a key, excluding nul terminator */
MAX_KEY_LEN = 30,
/* maximum length of a value, excluding nul terminator */
MAX_VAL_LEN = SZ_4K,
MAX_LINE_LEN = MAX_KEY_LEN + MAX_VAL_LEN + 10,
MAX_FILE_SIZE = SZ_64K,
};
struct keyval {
char *key;
char *val;
};
/**
* struct sstate_priv - Private data for simple-state
*
* @ifname: Interface which stores the state
* @dev_part: Device and partition number which stores the state
* @filename: Filename which stores the state
* @items: List of struct keyval
*/
struct sstate_priv {
const char *ifname;
const char *dev_part;
const char *fname;
struct alist items;
};
static void clear_vals(struct sstate_priv *priv)
{
struct keyval *kv;
log_debug("clearing\n");
alist_for_each(kv, &priv->items) {
free(kv->key);
free(kv->val);
}
alist_empty(&priv->items);
}
static struct keyval *find_item(struct sstate_priv *priv, const char *key)
{
struct keyval *kv;
log_debug("find %s: ", key);
alist_for_each(kv, &priv->items) {
if (!strcmp(kv->key, key)) {
log_debug("found\n");
return kv;
}
}
log_debug("not found\n");
return NULL;
}
static int add_val(struct sstate_priv *priv, const char *key,
const char *val)
{
int keylen, vallen;
struct keyval kv;
const unsigned char *p;
log_content("add %s=%s\n", key, val);
for (keylen = 0, p = key; keylen <= MAX_KEY_LEN && *p; keylen++, p++) {
if (!(*p == '_' || isdigit(*p) || islower(*p)) || *p >= 127) {
log_content("- invalid character %02x\n", *p);
return log_msg_ret("wvk", -EKEYREJECTED);
}
}
if (!keylen) {
log_content("- empty key\n");
return log_msg_ret("wve", -EINVAL);
}
if (keylen > MAX_KEY_LEN) {
log_content("- key too long %d\n", keylen);
return log_msg_ret("wvl", -EKEYREJECTED);
}
vallen = strnlen(val, MAX_VAL_LEN + 1);
if (vallen > MAX_VAL_LEN) {
log_content("- val too long\n");
return log_msg_ret("wvv", -E2BIG);
}
kv.key = strndup(key, MAX_KEY_LEN);
if (!kv.key) {
return log_msg_ret("avk", -ENOMEM);
}
kv.val = strndup(val, MAX_VAL_LEN);
if (!kv.val) {
free(kv.key);
return log_msg_ret("avv", -ENOMEM);
}
if (!alist_add(&priv->items, kv)) {
free(kv.key);
free(kv.val);
return log_msg_ret("avl", -ENOMEM);
}
return 0;
}
static int write_val(struct sstate_priv *priv, const char *key,
const char *val)
{
struct keyval *kv;
int ret;
log_content("write %s=%s\n", key, val);
if (!key || !val)
return log_msg_ret("wkn", -EINVAL);
kv = find_item(priv, key);
if (kv) {
int len = strnlen(val, MAX_VAL_LEN + 1);
char *new;
if (len > MAX_VAL_LEN) {
log_content("- val too long\n");
return log_msg_ret("wvr", -E2BIG);
}
log_content("- update\n");
new = realloc(kv->val, len);
if (!new)
return log_msg_ret("wvr", -ENOMEM);
strlcpy(new, val, len + 1);
kv->val = new;
} else {
ret = add_val(priv, key, val);
if (ret)
return log_msg_ret("swB", ret);
}
log_content("done\n");
return 0;
}
static int sstate_clear(struct udevice *dev)
{
struct sstate_priv *priv = dev_get_priv(dev);
clear_vals(priv);
return 0;
}
static int sstate_load(struct udevice *dev)
{
struct sstate_priv *priv = dev_get_priv(dev);
struct membuf inf;
struct abuf buf;
char line[MAX_LINE_LEN];
bool ok;
int len;
int ret;
log_debug("loading\n");
clear_vals(priv);
log_debug("read file ifname '%s' dev_part '%s' fname '%s'\n",
priv->ifname, priv->dev_part, priv->fname);
ret = fs_load_alloc(priv->ifname, priv->dev_part, priv->fname,
MAX_FILE_SIZE, 0, &buf);
if (ret)
return log_msg_ret("ssa", ret);
log_debug("parsing\n");
membuf_init_with_data(&inf, buf.data, buf.size);
for (ok = true;
len = membuf_readline(&inf, line, sizeof(line), ' ', true),
len && ok;) {
char *key = strtok(line, "=");
char *val = strtok(NULL, "=");
if (key && val)
ok = !add_val(priv, key, val);
}
abuf_uninit(&buf);
if (!ok) {
clear_vals(priv);
return log_msg_ret("ssr", -ENOMEM);
}
return 0;
}
static int sstate_save_to_buf(struct udevice *dev, struct abuf *buf)
{
struct sstate_priv *priv = dev_get_priv(dev);
struct membuf inf;
struct keyval *kv;
char *data;
int size;
log_debug("saving\n");
abuf_init(buf);
if (!abuf_realloc(buf, MAX_FILE_SIZE))
return log_msg_ret("ssa", -ENOMEM);
membuf_init(&inf, buf->data, buf->size);
alist_for_each(kv, &priv->items) {
int keylen = strnlen(kv->key, MAX_KEY_LEN + 1);
int vallen = strnlen(kv->val, MAX_VAL_LEN + 1);
if (keylen > MAX_KEY_LEN || vallen > MAX_VAL_LEN)
return log_msg_ret("ssp", -E2BIG);
log_content("save %s=%s\n", kv->key, kv->val);
if (membuf_put(&inf, kv->key, keylen) != keylen ||
membuf_put(&inf, "=", 1) != 1 ||
membuf_put(&inf, kv->val, vallen) != vallen ||
membuf_put(&inf, "\n", 1) != 1)
return log_msg_ret("ssp", -ENOSPC);
}
if (membuf_put(&inf, "", 1) != 1)
return log_msg_ret("ssp", -ENOSPC);
size = membuf_getraw(&inf, MAX_FILE_SIZE, true, &data);
if (data != buf->data)
return log_msg_ret("ssp", -EFAULT);
buf->size = size;
return 0;
}
static int sstate_save(struct udevice *dev)
{
struct sstate_priv *priv = dev_get_priv(dev);
loff_t actwrite;
struct abuf buf;
int ret;
ret = sstate_save_to_buf(dev, &buf);
if (ret)
return log_msg_ret("sss", ret);
log_debug("set dest ifname '%s' dev_part '%s'\n", priv->ifname,
priv->dev_part);
ret = fs_set_blk_dev(priv->ifname, priv->dev_part, FS_TYPE_ANY);
if (ret) {
ret = log_msg_ret("sss", ret);
} else {
log_debug("write fname '%s' size %zx\n", priv->fname, buf.size);
ret = fs_write(priv->fname, abuf_addr(&buf), 0, buf.size,
&actwrite);
if (ret)
ret = log_msg_ret("ssw", ret);
}
abuf_uninit(&buf);
if (ret)
return ret;
return 0;
}
static int sstate_read_bool(struct udevice *dev, const char *prop, bool *valp)
{
struct sstate_priv *priv = dev_get_priv(dev);
const struct keyval *kv;
log_debug("read_bool\n");
kv = find_item(priv, prop);
if (!kv)
return log_msg_ret("srb", -ENOENT);
*valp = !strcmp(kv->val, "1") ? true : false;
log_debug("- val %s: %d\n", kv->val, *valp);
return 0;
}
static int sstate_write_bool(struct udevice *dev, const char *prop, bool val)
{
struct sstate_priv *priv = dev_get_priv(dev);
int ret;
ret = write_val(priv, prop, simple_itoa(val));
if (ret)
return log_msg_ret("swb", ret);
return 0;
}
static int sstate_read_int(struct udevice *dev, const char *prop, long *valp)
{
struct sstate_priv *priv = dev_get_priv(dev);
const struct keyval *kv;
log_debug("read_bool\n");
kv = find_item(priv, prop);
if (!kv)
return log_msg_ret("srb", -ENOENT);
*valp = simple_strtol(kv->val, NULL, 10);
log_debug("- val %s: %ld\n", kv->val, *valp);
return 0;
}
static int sstate_write_int(struct udevice *dev, const char *prop, long val)
{
struct sstate_priv *priv = dev_get_priv(dev);
int ret;
log_debug("write_int %ld\n", val);
ret = write_val(priv, prop, simple_itoa(val));
if (ret)
return log_msg_ret("swb", ret);
return 0;
}
static int sstate_read_str(struct udevice *dev, const char *prop,
const char **valp)
{
struct sstate_priv *priv = dev_get_priv(dev);
const struct keyval *kv;
log_debug("read_bool\n");
kv = find_item(priv, prop);
if (!kv)
return log_msg_ret("srb", -ENOENT);
*valp = kv->val;
log_debug("- val %s: %s\n", kv->val, *valp);
return 0;
}
static int sstate_write_str(struct udevice *dev, const char *prop,
const char *str)
{
struct sstate_priv *priv = dev_get_priv(dev);
int ret;
ret = write_val(priv, prop, str);
if (ret)
return log_msg_ret("swb", ret);
return 0;
}
static int sstate_probe(struct udevice *dev)
{
struct sstate_priv *priv = dev_get_priv(dev);
alist_init_struct(&priv->items, struct keyval);
return 0;
}
static int sstate_of_to_plat(struct udevice *dev)
{
struct sstate_priv *priv = dev_get_priv(dev);
int ret;
ret = dev_read_string_index(dev, "location", 0, &priv->ifname);
if (ret)
return log_msg_ret("ssi", ret);
ret = dev_read_string_index(dev, "location", 1, &priv->dev_part);
if (ret)
return log_msg_ret("ssd", ret);
priv->fname = dev_read_string(dev, "filename");
if (!priv->ifname || !priv->dev_part || !priv->fname)
return log_msg_ret("ssp", -EINVAL);
return 0;
}
static int sstate_bind(struct udevice *dev)
{
struct bootctl_uc_plat *ucp = dev_get_uclass_plat(dev);
ucp->desc = "Stores state information about booting";
return 0;
}
static struct bc_state_ops ops = {
.load = sstate_load,
.save = sstate_save,
.save_to_buf = sstate_save_to_buf,
.clear = sstate_clear,
.read_bool = sstate_read_bool,
.write_bool = sstate_write_bool,
.read_int = sstate_read_int,
.write_int = sstate_write_int,
.read_str = sstate_read_str,
.write_str = sstate_write_str,
};
static const struct udevice_id sstate_ids[] = {
{ .compatible = "bootctl,simple-state" },
{ .compatible = "bootctl,state" },
{ }
};
U_BOOT_DRIVER(simple_state) = {
.name = "simple_state",
.id = UCLASS_BOOTCTL_STATE,
.of_match = sstate_ids,
.ops = &ops,
.bind = sstate_bind,
.probe = sstate_probe,
.of_to_plat = sstate_of_to_plat,
.priv_auto = sizeof(struct sstate_priv),
};

274
boot/bootctl/simple_ui.c Normal file
View File

@@ -0,0 +1,274 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Provides a simple boot menu on a graphical display
*
* TODO: Support a text display / serial terminal
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <alist.h>
#include <bootctl.h>
#include <bootstd.h>
#include <dm.h>
#include <expo.h>
#include <video_console.h>
#include <bootctl/logic.h>
#include <bootctl/oslist.h>
#include <bootctl/ui.h>
#include <bootctl/util.h>
#include "../bootflow_internal.h"
/* TODO: Define to 1 to use text mode (for terminals), 0 for graphics */
#define TEXT_MODE 0
/**
* struct ui_priv - information about the display
*
* @expo: Expo containing the menu
* @scn: Current scene being shown
* @lpriv: Private data of logic device
* @console: vidconsole device in use
* @autoboot_template: template string to use for autoboot
* @autoboot_str: current string displayed for autoboot timeout
* @logo: logo in bitmap format, NULL to use default
* @logo_size: size of the logo in bytes
*/
struct ui_priv {
struct expo *expo;
struct scene *scn; /* consider dropping this */
struct logic_priv *lpriv;
struct udevice *console;
struct abuf autoboot_template;
struct abuf *autoboot_str;
const void *logo;
int logo_size;
};
static int simple_ui_probe(struct udevice *dev)
{
struct ui_priv *priv = dev_get_priv(dev);
struct udevice *ldev;
int ret;
ret = uclass_first_device_err(UCLASS_BOOTCTL, &ldev);
if (ret)
return log_msg_ret("sup", ret);
priv->lpriv = dev_get_priv(ldev);
return 0;
}
static int simple_ui_bind(struct udevice *dev)
{
struct bootctl_uc_plat *ucp = dev_get_uclass_plat(dev);
ucp->desc = "Graphical or textual display for user";
return 0;
}
static int simple_ui_print(struct udevice *dev, const char *msg)
{
printf("%s", msg);
return 0;
}
static int simple_ui_show(struct udevice *dev)
{
struct ui_priv *priv = dev_get_priv(dev);
struct bootstd_priv *std;
struct scene *scn;
struct abuf *buf;
uint scene_id;
int ret;
ret = bootstd_get_priv(&std);
if (ret)
return log_msg_ret("sdb", ret);
ret = bootflow_menu_setup(std, TEXT_MODE, &priv->expo);
if (ret)
return log_msg_ret("sds", ret);
ret = expo_first_scene_id(priv->expo);
if (ret < 0)
return log_msg_ret("ufs", ret);
scene_id = ret;
scn = expo_lookup_scene_id(priv->expo, scene_id);
scene_obj_set_hide(scn, OBJ_AUTOBOOT, false);
ret = expo_edit_str(priv->expo, STR_AUTOBOOT,
&priv->autoboot_template,
&priv->autoboot_str);
if (ret)
return log_msg_ret("ses", ret);
ret = expo_edit_str(priv->expo, STR_MENU_TITLE, NULL, &buf);
if (ret)
return log_msg_ret("set", ret);
abuf_printf(buf, "Boot control");
if (priv->logo) {
ret = scene_img_set_data(scn, OBJ_U_BOOT_LOGO,
priv->logo, priv->logo_size);
if (ret)
return log_msg_ret("log", ret);
ret = scene_obj_set_pos(scn, OBJ_U_BOOT_LOGO, 1135, 10);
if (ret)
return log_msg_ret("lop", ret);
}
log_debug("theme '%s'\n", ofnode_get_name(std->theme));
if (ofnode_valid(std->theme)) {
ret = expo_setup_theme(priv->expo, std->theme);
if (ret)
return log_msg_ret("thm", ret);
}
ret = scene_arrange(scn);
if (ret)
return log_msg_ret("usa", ret);
scene_set_highlight_id(scn, OBJ_MENU);
priv->scn = scn;
ret = device_find_first_child_by_uclass(priv->expo->display,
UCLASS_VIDEO_CONSOLE,
&priv->console);
if (ret)
return log_msg_ret("suq", ret);
vidconsole_set_quiet(priv->console, true);
return 0;
}
static int simple_ui_add(struct udevice *dev, struct osinfo *info)
{
struct ui_priv *priv = dev_get_priv(dev);
struct logic_priv *lpriv = priv->lpriv;
int seq = lpriv->osinfo.count;
struct bootstd_priv *std;
struct scene *scn;
int ret;
info = alist_add(&lpriv->osinfo, *info);
if (!info)
return -ENOMEM;
ret = bootflow_menu_add(priv->expo, &info->bflow, seq, &scn);
if (ret)
return log_msg_ret("sda", ret);
ret = bootstd_get_priv(&std);
if (ret)
return log_msg_ret("sup", ret);
if (ofnode_valid(std->theme)) {
ret = expo_setup_theme(priv->expo, std->theme);
if (ret)
return log_msg_ret("thm", ret);
}
ret = expo_calc_dims(priv->expo);
if (ret)
return log_msg_ret("ecd", ret);
if (lpriv->default_os &&
!strcmp(lpriv->default_os, info->bflow.os_name))
scene_menu_select_item(scn, OBJ_MENU, ITEM + seq);
ret = scene_arrange(scn);
if (ret)
return log_msg_ret("sua", ret);
return 0;
}
static int simple_ui_render(struct udevice *dev)
{
struct ui_priv *priv = dev_get_priv(dev);
int ret;
ret = abuf_printf(priv->autoboot_str,
priv->autoboot_template.data,
priv->lpriv->autoboot_remain_s);
if (ret < 0)
return log_msg_ret("uip", ret);
ret = expo_arrange(priv->expo);
if (ret)
return log_msg_ret("sda", ret);
ret = expo_render(priv->expo);
if (ret)
return log_msg_ret("sdr", ret);
return 0;
}
static int simple_ui_poll(struct udevice *dev, int *seqp, bool *selectedp)
{
struct ui_priv *priv = dev_get_priv(dev);
struct logic_priv *lpriv = priv->lpriv;
int seq, ret;
bool ok = true;
*seqp = -1;
*selectedp = false;
ret = bootflow_menu_poll(priv->expo, &seq);
ok = !ret;
if (ret == -ERESTART || ret == -EREMCHG) {
lpriv->autoboot_active = false;
scene_obj_set_hide(priv->scn, OBJ_AUTOBOOT, true);
ok = true;
} else if (ret == -EAGAIN) {
ok = true;
}
*seqp = seq;
if (ret) {
if (!ok)
return log_msg_ret("sdp", ret);
if (ret == -EAGAIN || ret == -ERESTART)
return 0;
return 1;
}
*selectedp = true;
return 0;
}
static int simple_ui_of_to_plat(struct udevice *dev)
{
struct ui_priv *priv = dev_get_priv(dev);
priv->logo = dev_read_prop(dev, "logo", &priv->logo_size);
return 0;
}
static struct bc_ui_ops ops = {
.print = simple_ui_print,
.show = simple_ui_show,
.add = simple_ui_add,
.render = simple_ui_render,
.poll = simple_ui_poll,
};
static const struct udevice_id simple_ui_ids[] = {
{ .compatible = "bootctl,simple-ui" },
{ .compatible = "bootctl,ui" },
{ }
};
U_BOOT_DRIVER(simple_ui) = {
.name = "simple_ui",
.id = UCLASS_BOOTCTL_UI,
.of_match = simple_ui_ids,
.of_to_plat = simple_ui_of_to_plat,
.bind = simple_ui_bind,
.probe = simple_ui_probe,
.ops = &ops,
.priv_auto = sizeof(struct ui_priv),
};

309
boot/bootctl/util.c Normal file
View File

@@ -0,0 +1,309 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Utility functions
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#define LOG_CATEGORY UCLASS_BOOTCTL
#include <alist.h>
#include <bootctl.h>
#include <dm.h>
#include <log.h>
#include <stdarg.h>
#include <vsprintf.h>
#include <bootctl/logic.h>
#include <bootctl/measure.h>
#include <bootctl/oslist.h>
#include <bootctl/state.h>
#include <bootctl/ui.h>
#include <bootctl/util.h>
/**
* bc_printf() - Print a string to the display
*
* @fmt: printf() format string for log record
* @...: Optional parameters, according to the format string @fmt
* Return: Number of characters emitted
*/
int bc_printf(struct udevice *disp, const char *fmt, ...)
{
struct bc_ui_ops *ops = bc_ui_get_ops(disp);
char buf[CONFIG_SYS_CBSIZE];
va_list args;
int count;
int ret;
va_start(args, fmt);
count = vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
ret = ops->print(disp, buf);
if (ret)
return log_msg_ret("bpp", ret);
return count;
}
int bc_ui_show(struct udevice *disp)
{
struct bc_ui_ops *ops = bc_ui_get_ops(disp);
int ret;
ret = ops->show(disp);
if (ret)
return log_msg_ret("bds", ret);
return 0;
}
int bc_ui_add(struct udevice *dev, struct osinfo *info)
{
struct bc_ui_ops *ops = bc_ui_get_ops(dev);
int ret;
ret = ops->add(dev, info);
if (ret)
return log_msg_ret("bda", ret);
return 0;
}
int bc_ui_render(struct udevice *disp)
{
struct bc_ui_ops *ops = bc_ui_get_ops(disp);
int ret;
ret = ops->render(disp);
if (ret)
return log_msg_ret("bds", ret);
return 0;
}
int bc_ui_poll(struct udevice *disp, int *seqp, bool *selectedp)
{
struct bc_ui_ops *ops = bc_ui_get_ops(disp);
int ret;
ret = ops->poll(disp, seqp, selectedp);
if (ret)
return log_msg_ret("bdp", ret);
return ret;
}
void bc_oslist_setup_iter(struct oslist_iter *iter)
{
memset(iter, '\0', sizeof(struct oslist_iter));
}
int bc_oslist_next(struct udevice *dev, struct oslist_iter *iter,
struct osinfo *info)
{
struct bc_oslist_ops *ops = bc_oslist_get_ops(dev);
int ret;
log_debug("oslist flags %x\n", iter->bf_iter.flags);
ret = ops->next(dev, iter, info);
if (ret)
return log_msg_ret("bon", ret);
return 0;
}
int bc_state_load(struct udevice *dev)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->load(dev);
if (ret)
return log_msg_ret("bsl", ret);
return 0;
}
int bc_state_save(struct udevice *dev)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->save(dev);
if (ret)
return log_msg_ret("bss", ret);
return 0;
}
int bc_state_save_to_buf(struct udevice *dev, struct abuf *buf)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->save_to_buf(dev, buf);
if (ret)
return log_msg_ret("bsb", ret);
return 0;
}
int bc_state_clear(struct udevice *dev)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->clear(dev);
if (ret)
return log_msg_ret("bsc", ret);
return 0;
}
int bc_state_read_bool(struct udevice *dev, const char *key, bool *valp)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->read_bool(dev, key, valp);
if (ret)
return log_msg_ret("srb", ret);
return 0;
}
int bc_state_write_bool(struct udevice *dev, const char *key, bool val)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->write_bool(dev, key, val);
if (ret)
return log_msg_ret("swb", ret);
return 0;
}
int bc_state_read_int(struct udevice *dev, const char *key, long *valp)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->read_int(dev, key, valp);
if (ret)
return log_msg_ret("sri", ret);
return 0;
}
int bc_state_write_int(struct udevice *dev, const char *key, long val)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->write_int(dev, key, val);
if (ret)
return log_msg_ret("swi", ret);
return 0;
}
int bc_state_read_str(struct udevice *dev, const char *key, const char **valp)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->read_str(dev, key, valp);
if (ret)
return log_msg_ret("srs", ret);
return 0;
}
int bc_state_write_str(struct udevice *dev, const char *key, const char *val)
{
struct bc_state_ops *ops = bc_state_get_ops(dev);
int ret;
ret = ops->write_str(dev, key, val);
if (ret)
return log_msg_ret("sws", ret);
return 0;
}
int bc_logic_prepare(struct udevice *dev)
{
struct bc_logic_ops *ops = bc_logic_get_ops(dev);
int ret;
ret = ops->prepare(dev);
if (ret)
return log_msg_ret("blp", ret);
return 0;
}
int bc_logic_start(struct udevice *dev)
{
struct bc_logic_ops *ops = bc_logic_get_ops(dev);
int ret;
ret = ops->start(dev);
if (ret)
return log_msg_ret("bls", ret);
return 0;
}
int bc_logic_poll(struct udevice *dev)
{
struct bc_logic_ops *ops = bc_logic_get_ops(dev);
int ret;
ret = ops->poll(dev);
if (ret)
return log_msg_ret("blP", ret);
return 0;
}
int bc_measure_start(struct udevice *dev)
{
struct bc_measure_ops *ops = bc_measure_get_ops(dev);
int ret;
ret = ops->start(dev);
if (ret)
return log_msg_ret("blM", ret);
return 0;
}
int bc_measure_process(struct udevice *dev, const struct osinfo *osinfo,
struct alist *result)
{
struct bc_measure_ops *ops = bc_measure_get_ops(dev);
int ret;
alist_init_struct(result, struct measure_info);
ret = ops->process(dev, osinfo, result);
if (ret)
return log_msg_ret("blm", ret);
return 0;
}
void show_measures(struct alist *result)
{
struct measure_info *res;
/* TODO: expand to show more info? */
printf("Measurement report:");
alist_for_each(res, result)
printf(" %s", bootflow_img_type_name(res->img->type));
printf("\n");
}

View File

@@ -317,6 +317,16 @@ config CMD_BOOTM_PRE_LOAD
This stage allow to check or modify the image provided
to the bootm command.
config CMD_BOOTCTL
bool "bootctl"
depends on BOOTCTL
default y
help
Provide information about the bootctl system, including access to
drivers and other features.
This command is not necessary for bootctl to work.
config CMD_BOOTDEV
bool "bootdev"
depends on BOOTSTD

58
cmd/bootctl.c Normal file
View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* 'bootctl' command
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#include <bootctl.h>
#include <command.h>
#include <dm.h>
static int do_bootctl_list(struct cmd_tbl *cmdtp, int flag, int argc,
char *const argv[])
{
struct udevice *dev;
struct uclass *uc;
int i;
printf("Seq Name Type Description\n");
printf("--- -------------- -------------- --------------------\n");
i = 0;
uclass_id_foreach_dev(UCLASS_BOOTCTL, dev, uc) {
struct bootctl_uc_plat *ucp = dev_get_uclass_plat(dev);
printf("%3d %-15.15s %-15.15s %s\n", i, dev->name,
dev_get_uclass_name(dev), ucp->desc);
i++;
}
printf("--- -------------- -------------- --------------------\n");
printf("(%d driver%s)\n", i, i != 1 ? "s" : "");
return 0;
}
static int do_bootctl_run(struct cmd_tbl *cmdtp, int flag, int argc,
char *const argv[])
{
int ret;
ret = bootctl_run();
if (ret) {
printf("Boot failed (err=%dE)\n", ret);
return CMD_RET_FAILURE;
}
return 0;
}
U_BOOT_LONGHELP(bootctl,
"list - list bootctl drivers\n"
"run - run a boot");
U_BOOT_CMD_WITH_SUBCMDS(bootctl, "Boot control", bootctl_help_text,
U_BOOT_SUBCMD_MKENT(list, 1, 1, do_bootctl_list),
U_BOOT_SUBCMD_MKENT(run, 1, 1, do_bootctl_run));

View File

@@ -1,6 +1,6 @@
CONFIG_TEXT_BASE=0
CONFIG_SYS_MALLOC_LEN=0x6000000
CONFIG_BLOBLIST_SIZE_RELOC=0x5000
CONFIG_BLOBLIST_SIZE_RELOC=0x20000
CONFIG_NR_DRAM_BANKS=1
CONFIG_ENV_SIZE=0x2000
CONFIG_DEFAULT_DEVICE_TREE="sandbox"
@@ -26,6 +26,8 @@ CONFIG_FIT=y
CONFIG_FIT_RSASSA_PSS=y
CONFIG_FIT_CIPHER=y
CONFIG_FIT_VERBOSE=y
CONFIG_BOOTM_OPENRTOS=y
CONFIG_BOOTM_OSE=y
CONFIG_BOOTMETH_ANDROID=y
CONFIG_UPL=y
CONFIG_LEGACY_IMAGE_FORMAT=y
@@ -62,8 +64,6 @@ CONFIG_CMD_LICENSE=y
CONFIG_CMD_SMBIOS=y
CONFIG_CMD_BOOTM_PRE_LOAD=y
CONFIG_CMD_BOOTZ=y
CONFIG_BOOTM_OPENRTOS=y
CONFIG_BOOTM_OSE=y
CONFIG_CMD_BOOTEFI_HELLO=y
CONFIG_CMD_BOOTMENU=y
CONFIG_CMD_ABOOTIMG=y
@@ -342,6 +342,7 @@ CONFIG_OSD=y
CONFIG_SANDBOX_OSD=y
CONFIG_BMP_16BPP=y
CONFIG_BMP_24BPP=y
CONFIG_VIRTIO_BLK=y
CONFIG_W1=y
CONFIG_W1_GPIO=y
CONFIG_W1_EEPROM=y
@@ -367,4 +368,3 @@ CONFIG_TEST_FDTDEC=y
CONFIG_UNIT_TEST=y
CONFIG_UT_TIME=y
CONFIG_UT_DM=y
CONFIG_VIRTIO_BLK=y

124
include/bootctl.dtsi Normal file
View File

@@ -0,0 +1,124 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* This contains the various components needed to boot Ubuntu 24.04 Desktop. It
* provides a description of the process, similar to a grub.cfg file.
*/
&boot_ctl {
compatible = "bootctl,ubuntu-desktop", "bootctl,logic";
options {
/* use persistent storage for variables */
persist-state;
/* store a default OS in the persistent storage */
default-os;
/* timeout in seconds before booting a default OS */
timeout = <5>;
/* skip timeout if boot was successful */
skip-timeout-on-success;
/* track boot success / failure in state */
track-success;
/* auto-boot the default OS after a timeout */
autoboot;
/* measure loaded images, etc. */
#if !defined(CONFIG_QEMU) && !defined(CONFIG_EFI_APP)
measure;
#endif
/* restrict labels to boot (separated by space) */
#ifdef CONFIG_QEMU
labels = "virtio";
#endif
#ifdef CONFIG_SANDBOX
labels = "mmc usb";
#endif
};
measure {
compatible = "bootctl,simple-measure";
tpm-log-size = <0x10000>;
/*
* should we use compatible strings for each subnode? That might
* provide more flexibility for expansion
*/
os {
method = "tpm-pcr";
pcr-number = <6>;
algos = "sha256";
};
initrd {
method = "tpm-pcr";
pcr-number = <9>;
algos = "sha256";
};
fdt {
method = "tpm-pcr";
pcr-number = <8>;
algos = "sha256";
};
cmdline {
method = "tpm-pcr";
pcr-number = <8>;
algos = "sha256";
optional;
};
};
oslist-extlinux {
compatible = "bootctl,extlinux-oslist";
/* indicates the filesystems needed to access extlinux */
filesystems {
ext4 {
/* add enabled options here? */
};
fat {
};
};
};
#ifndef CONFIG_SANDBOX
oslist-efi {
compatible = "bootctl,efifile-oslist";
/* indicates the filesystems needed to access EFI files */
filesystems {
fat {
};
};
};
#endif
state {
compatible = "bootctl,simple-state", "bootctl,state";
/* revisit, as we may want to reference the hardware device */
#ifdef CONFIG_SANDBOX
location = "hostfs", "0";
filename = "bootctl.ini";
#else
location = "efi", "1:5";
filename = "/boot/bootctl/bootctl.ini";
#endif
};
ui {
compatible = "bootctl,multiboot-ui", "bootctl,simple-ui",
"bootctl,ui";
graphical = "if-available";
textual = "if-available";
};
};

41
include/bootctl.h Normal file
View File

@@ -0,0 +1,41 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Uclass implementation for boot schema
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_h
#define __bootctl_h
#include <bootflow.h>
struct udevice;
/**
* struct bootctl_uc_plat - information the uclass keeps about each bootctl
*
* @desc: A long description of the bootctl
*/
struct bootctl_uc_plat {
const char *desc;
};
/**
* bootctl_get_dev() - Get a device of a given type
*
* @type: Type to search for
* @devp: Return the device found, on success
* Return: 0 on success, or -ve error
*/
int bootctl_get_dev(enum uclass_id type, struct udevice **devp);
/**
* bootctl_run() - Run a boot
*
* Return: 0 on success, or -ve error
*/
int bootctl_run(void);
#endif

158
include/bootctl/logic.h Normal file
View File

@@ -0,0 +1,158 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Implementation of the logic to perform a boot
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_logic_h
#define __bootctl_logic_h
#include <bootctl/oslist.h>
struct udevice;
/**
* struct logic_priv - Information maintained by the boot logic as it works
*
* @opt_persist_state: true if state can be preserved across reboots
* @opt_default_os: true if we record a default OS to boot
* @opt_timeout: boot timeout in seconds
* @opt_skip_timeout: true to skip any boot timeout if the last boot succeeded
* @opt_track_success: true to track whether the last boot succeeded (made it to
* user space)
* @opt_labels: if non-NULL, a space-separated list of bootstd labels which can
* be used to boot
* @opt_autoboot: true to autoboot the default OS after a timeout
* @opt_measure: true to measure loaded images, etc.
*
* @state_loaded: true if the state information has been loaded
* @scanning: true if scanning for new OSes
* @start_time: monotonic time when the boot started
* @next_countdown: next monotonic time to check the timeout
* @autoboot_remain_s: remaining autoboot time in seconds
* @autoboot_active: true if autoboot is active
* @default_os: name of the default OS to boot
* @osinfo: List of OSes to show
* @refresh: true if we need to refresh the UI because something has changed
*
* @iter: oslist iterator, used to find new OSes
* @selected: index of selected OS in osinfo alist, or -1 if none has been
* selected yet
* @meas: TPM-measurement device
* @oslist: provides OSes to boot; we iterate through each osinfo driver to find
* all OSes
* @state: provides persistent state
* @ui: provides a visual boot menu on a display / console device
*/
struct logic_priv {
bool opt_persist_state;
bool opt_default_os;
uint opt_timeout;
bool opt_skip_timeout;
bool opt_track_success;
const char *opt_labels;
bool opt_autoboot;
bool opt_measure;
bool state_loaded;
bool state_saved;
bool scanning;
ulong start_time;
uint next_countdown;
uint autoboot_remain_s;
bool autoboot_active;
const char *default_os;
struct alist osinfo;
bool refresh;
struct oslist_iter iter;
struct udevice *meas;
struct udevice *oslist;
struct udevice *state;
struct udevice *ui;
};
/**
* struct bc_logic_ops - Operations related to boot loader
*/
struct bc_logic_ops {
/**
* prepare() - Prepare the components needed for the boot
*
* This sets up the various device, like ui and oslist
*
* This must be called before start()
*
* @dev: Logic device
* Return: 0 if OK, or -ve error code
*/
int (*prepare)(struct udevice *dev);
/**
* start() - Start the boot process
*
* Gets things ready, shows the UI, etc.
*
* This pust be called before poll()
*
* @dev: Logic device
* Return: 0 if OK, or -ve error code
*/
int (*start)(struct udevice *dev);
/**
* poll() - Poll the boot process
*
* Try to progress the boot towards a result
*
* This should be called repeatedly until it either boots and OS (iwc
* it won't return) or returns an error code
*
* @dev: Logic device
* Return: does not return if OK, -ESHUTDOWN if something went wrong
*/
int (*poll)(struct udevice *dev);
};
#define bc_logic_get_ops(dev) ((struct bc_logic_ops *)(dev)->driver->ops)
/**
* bc_logic_prepare() - Prepare the components needed for the boot
*
* This sets up the various device, like ui and oslist
*
* This must be called before start()
*
* @dev: Logic device
* Return: 0 if OK, or -ve error code
*/
int bc_logic_prepare(struct udevice *dev);
/**
* bc_logic_start() - Start the boot process
*
* Gets things ready, shows the UI, etc.
*
* This pust be called before poll()
*
* @dev: Logic device
* Return: 0 if OK, or -ve error code
*/
int bc_logic_start(struct udevice *dev);
/**
* bc_logic_poll() - Poll the boot process
*
* Try to progress the boot towards a result
*
* This should be called repeatedly until it either boots and OS (iwc
* it won't return) or returns an error code
*
* @dev: Logic device
* Return: does not return if OK, -ESHUTDOWN if something went wrong
*/
int bc_logic_poll(struct udevice *dev);
#endif

95
include/bootctl/measure.h Normal file
View File

@@ -0,0 +1,95 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Implementation of measurement of loaded images and the like
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_measure_h
#define __bootctl_measure_h
#include <alist.h>
struct osinfo;
struct udevice;
/**
* enum measure_t - Types of measurement supported
*
* @BCMEAST_OS: OS image loaded from storage
* @BCMEAST_INITRD: Initial ramdisk loaded from storage
* @BCMEAST_CMDLINE: Command-line arguments constructed for the OS
* @BCMEAST_FDT: Flattened device tree for use by the OS
*/
enum measure_t {
BCMEAST_IMAGE,
BCMEAST_CMDLINE,
BCMEAST_FDT,
};
/**
* struct measure_info - Information about a particular measurement
*
* TODO: Add more details about the measurement
*
* @img: Image which was measured
*/
struct measure_info {
const struct bootflow_img *img;
};
/**
* struct bc_measure_ops - Operations for measurement, e.g. with a TPM
*/
struct bc_measure_ops {
/**
* start() - Start up ready for measurement
*
* Sets up the TPM log and starts the TPM
*
* @dev: Device to access
* Return: 0 if OK, or -ve error code
*/
int (*start)(struct udevice *dev);
/**
* process() - Measurement of images, etc.
*
* Process the required measurements to produce results
*
* @dev: Device to access
* @osinfo: OS being booting
* @result: Inits and returns a list of struct measure_info records
* Return: 0 if OK, or -ve error code
*/
int (*process)(struct udevice *dev, const struct osinfo *osinfo,
struct alist *result);
};
#define bc_measure_get_ops(dev) ((struct bc_measure_ops *)(dev)->driver->ops)
/**
* bc_measure_start() - Start up ready for measurement
*
* Sets up the TPM log and starts the TPM
*
* @dev: Device to access
* Return: 0 if OK, or -ve error code
*/
int bc_measure_start(struct udevice *dev);
/**
* bc_measure_process() - Measurement of images, etc.
*
* Processes the required measurements to produce results
*
* @dev: Device to access
* @osinfo: OS being booting
* @result: Inits and returns a list of struct measure_info records
* Return: 0 if OK, or -ve error code
*/
int bc_measure_process(struct udevice *dev, const struct osinfo *osinfo,
struct alist *result);
#endif

70
include/bootctl/oslist.h Normal file
View File

@@ -0,0 +1,70 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Uclass implementation for boot schema
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_oslist_h
#define __bootctl_oslist_h
#include <bootflow.h>
struct udevice;
/**
* struct osinfo - Information about an OS which is available for booting
*
* @bflow: Bootflow for this OS
*/
struct osinfo {
struct bootflow bflow;
};
/**
* struct oslist_iter - Iterator
*
* @active: true if the scan has started
* @bf_iter: bootstd iterator being used
*/
struct oslist_iter {
bool active;
struct bootflow_iter bf_iter;
};
/**
* struct bc_oslist_ops - Operations for displays
*/
struct bc_oslist_ops {
/**
* next() - Find the next available OS
*
* @info: Returns info on success
* Return 0 if OK, -ENOENT if there are no more
*/
int (*next)(struct udevice *dev, struct oslist_iter *iter,
struct osinfo *info);
};
#define bc_oslist_get_ops(dev) ((struct bc_oslist_ops *)(dev)->driver->ops)
/**
* bc_oslist_setup_iter() - Set up a new iterator ready for use
*
* This must be called before using the iterator with bc_oslist_next()
*
* @iter: Pointer to iterator to set up
*/
void bc_oslist_setup_iter(struct oslist_iter *iter);
/**
* bc_oslist_next() - Find the next available OS
*
* @info: Returns info on success
* Return 0 if OK, -ENOENT if there are no more
*/
int bc_oslist_next(struct udevice *dev, struct oslist_iter *iter,
struct osinfo *info);
#endif

228
include/bootctl/state.h Normal file
View File

@@ -0,0 +1,228 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Uclass implementation for boot schema
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_state_h
#define __bootctl_state_h
#include <abuf.h>
#include <alist.h>
struct udevice;
/**
* struct state - State information which can be read and written
*
* @bflow: Bootflow for this OS
*/
struct bc_state {
struct alist subnodes;
};
/**
* struct bc_state_ops - Operations for the UI
*/
struct bc_state_ops {
/**
* read_bool() - Read a boolean value
*
* @dev: Device to access
* @key: Key to access
* @valp: Returns boolean value on success
* Return: 0 if OK, or -ve error code
*/
int (*read_bool)(struct udevice *dev, const char *key, bool *valp);
/**
* write_bool() - Write a boolean value
*
* @dev: Device to access
* @key: Key to access
* @val: Value to write
* Return: 0 if OK, or -ve error code
*/
int (*write_bool)(struct udevice *dev, const char *key, bool val);
/**
* read_int() - Read an integer value
*
* This holds a 64-bit integer
*
* @dev: Device to access
* @key: Key to access
* @valp: Returns integer value on success
* Return: 0 if OK, or -ve error code
*/
int (*read_int)(struct udevice *dev, const char *key, long *valp);
/**
* write_str() - Write an integer value
*
* This holds a 64-bit integer
*
* @dev: Device to access
* @key: Key to access
* @val: Value to write
* Return: 0 if OK, or -ve error code
*/
int (*write_int)(struct udevice *dev, const char *key, long val);
/**
* read_str() - Read a string value
*
* @dev: Device to access
* @key: Key to access
* @valp: Returns string value on success
* Return: 0 if OK, or -ve error code
*/
int (*read_str)(struct udevice *dev, const char *key,
const char **valp);
/**
* write_str() - Write a string value
*
* @dev: Device to access
* @key: Key to access
* @val: Value to write
* Return: 0 if OK, or -ve error code
*/
int (*write_str)(struct udevice *dev, const char *key, const char *val);
/**
* load() - Read in the current state
*
* Return: 0 if OK, or -ve error code
*/
int (*load)(struct udevice *dev);
/**
* save() - Write out the current state
*
* Return: 0 if OK, or -ve error code
*/
int (*save)(struct udevice *dev);
/**
* save_to_buf() - Write the current state to a buffer
*
* The buffer is inited and filled with the contents of the state as it
* would be written to a file
*
* Return: 0 if OK, or -ve error code
*/
int (*save_to_buf)(struct udevice *dev, struct abuf *buf);
/**
* clear() - Clear all values
*
* Return: 0 if OK, or -ve error code
*/
int (*clear)(struct udevice *dev);
};
#define bc_state_get_ops(dev) ((struct bc_state_ops *)(dev)->driver->ops)
/**
* bc_state_read_bool() - Read a boolean value
*
* @dev: Device to access
* @key: Key to access
* @valp: Returns boolean value on success
* Return: 0 if OK, or -ve error code
*/
int bc_state_read_bool(struct udevice *dev, const char *key, bool *valp);
/**
* bc_state_write_bool() - Write a boolean value
*
* Sets the value for a key, overwriting any existing value
*
* @dev: Device to access
* @key: Key to access
* @val: Value to write
* Return: 0 if OK, or -ve error code
*/
int bc_state_write_bool(struct udevice *dev, const char *key, bool val);
/**
* bc_state_read_int() - Read an integer value
*
* This holds a 64-bit integer
*
* @dev: Device to access
* @key: Key to access
* @valp: Returns integer value on success
* Return: 0 if OK, or -ve error code
*/
int bc_state_read_int(struct udevice *dev, const char *key, long *valp);
/**
* bc_state_write_str() - Write an integer value
*
* This holds a 64-bit integer
*
* @dev: Device to access
* @key: Key to access
* @val: Value to write
* Return: 0 if OK, or -ve error code
*/
int bc_state_write_int(struct udevice *dev, const char *key, long val);
/**
* bc_state_read_str() - Read a string value
*
* @dev: Device to access
* @key: Key to access
* @valp: Returns string value on success
* Return: 0 if OK, or -ve error code
*/
int bc_state_read_str(struct udevice *dev, const char *key, const char **valp);
/**
* bc_state_write_str() - Write a string value
*
* @dev: Device to access
* @key: Key to access
* @val: Value to write
* Return: 0 if OK, or -ve error code
*/
int bc_state_write_str(struct udevice *dev, const char *key, const char *val);
/**
* bc_state_load() - Load state from a file
*
* @dev: Device to access
* Return: 0 if OK, or -ve error code
*/
int bc_state_load(struct udevice *dev);
/**
* bc_state_save() - Save state to a file
*
* @dev: Device to access
* Return: 0 if OK, or -ve error code
*/
int bc_state_save(struct udevice *dev);
/**
* bc_state_save_to_buf() - Write the current state to a buffer
*
* The buffer is inited and filled with the contents of the state as it
* would be written to a file
*
* Return: 0 if OK, or -ve error code
*/
int bc_state_save_to_buf(struct udevice *dev, struct abuf *buf);
/**
* bc_state_clear() - Clear all values
*
* Return: 0 if OK, or -ve error code
*/
int bc_state_clear(struct udevice *dev);
#endif

108
include/bootctl/ui.h Normal file
View File

@@ -0,0 +1,108 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Bootctl display
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_display_h
#define __bootctl_display_h
#include <stdbool.h>
struct osinfo;
struct oslist_iter;
struct udevice;
/**
* struct bc_ui_ops - Operations for displays
*/
struct bc_ui_ops {
/**
* print() - Show a string on the display
*
* @dev: Display device
* @msg: Message to show
* Return 0 if OK, -ve on error
*/
int (*print)(struct udevice *dev, const char *msg);
/**
* show() - Show the display, ready to accept boot options
*
* @dev: Display device
* Return 0 if OK, -ve on error
*/
int (*show)(struct udevice *dev);
/**
* add() - Add an OS to the display, so the user can select it
*
* @dev: Display device
* @info: Information about the OS to display
* Return 0 if OK, -ve on error
*/
int (*add)(struct udevice *dev, struct osinfo *info);
/**
* render() - Render any updates to the display
*
* @dev: Display device
* Return 0 if OK, -ve on error
*/
int (*render)(struct udevice *dev);
/**
* poll() - Check for user activity
*
* @dev: Display device
* @seqp: Returns the sequence number of the osinfo that is currently
* pointed to/highlighted, or -1 if nothing
* @selectedp: Returns true if the user selected an item, else false
* Return: 0 if OK, -EPIPE if the user tried to quit the menu, other
* -ve on error
*/
int (*poll)(struct udevice *dev, int *seqp, bool *selectedp);
};
#define bc_ui_get_ops(dev) ((struct bc_ui_ops *)(dev)->driver->ops)
/**
* bc_ui_show() - Show the display, ready to accept boot options
*
* @dev: Display device
* Return 0 if OK, -ve on error
*/
int bc_ui_show(struct udevice *dev);
/**
* bc_ui_add() - Add an OS to the display, so the user can select it
*
* @dev: Display device
* @info: Information about the OS to display
* Return 0 if OK, -ve on error
*/
int bc_ui_add(struct udevice *dev, struct osinfo *info);
/**
* bc_ui_render() - Render any updates to the display
*
* @dev: Display device
* Return 0 if OK, -ve on error
*/
int bc_ui_render(struct udevice *dev);
/**
* bc_ui_poll() - Check for user activity
*
* @dev: Display device
* @seqp: Returns the sequence number of the osinfo that is currently
* pointed to/highlighted, or -1 if nothing
* @selectedp: Returns true if the user selected an item, else false
* Return: 0 if OK, -EPIPE if the user tried to quit the menu, other
* -ve on error
*/
int bc_ui_poll(struct udevice *dev, int *seqp, bool *selectedp);
#endif

32
include/bootctl/util.h Normal file
View File

@@ -0,0 +1,32 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Utility functions
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_util_h
#define __bootctl_util_h
struct alist;
struct udevice;
/**
* bc_printf() - Print a string to the display
*
* @fmt: printf() format string for log record
* @...: Optional parameters, according to the format string @fmt
* Return: Number of characters emitted
*/
int bc_printf(struct udevice *disp, const char *fmt, ...)
__attribute__ ((format (__printf__, 2, 3)));
/**
* show_measures() - Show measurements that have been completed
*
* @result: List of struct measure_info records
*/
void show_measures(struct alist *result);
#endif

View File

@@ -46,6 +46,11 @@ enum uclass_id {
UCLASS_BLK, /* Block device */
UCLASS_BLKMAP, /* Composable virtual block device */
UCLASS_BOOTCOUNT, /* Bootcount backing store */
UCLASS_BOOTCTL, /* Provides the boot logic */
UCLASS_BOOTCTL_MEASURE, /* Measurement of images and the like */
UCLASS_BOOTCTL_OSLIST, /* Provides list of Operating Systems to boot */
UCLASS_BOOTCTL_STATE, /* Stores state information about booting */
UCLASS_BOOTCTL_UI, /* Display of information to the user */
UCLASS_BOOTDEV, /* Boot device for locating an OS to boot */
UCLASS_BOOTMETH, /* Bootmethod for booting an OS */
UCLASS_BOOTSTD, /* Standard boot driver */

View File

@@ -74,6 +74,11 @@ config UT_LIB_RSA
endif # UT_LIB
config UT_BOOTCTL
bool "Unit tests for boot schema"
depends on BOOTSTD && SANDBOX
default y
config UT_BOOTSTD
bool "Unit tests for standard boot"
depends on BOOTSTD && SANDBOX

View File

@@ -0,0 +1,5 @@
# SPDX-License-Identifier: GPL-2.0+
#
# Copyright 2025 Canonical Ltd
obj-y += bootctl.o

327
test/boot/bootctl/bootctl.c Normal file
View File

@@ -0,0 +1,327 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Tests for bootctl
*
* For now this is just samples, showing how the different functions can be
* tested
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#include <stdbool.h>
#include <bootctl.h>
#include <bootmeth.h>
#include <dm.h>
#include <os.h>
#include <test/ut.h>
#include "bootctl_common.h"
#include <bootctl/measure.h>
#include <bootctl/oslist.h>
#include <bootctl/state.h>
#include "../bootstd_common.h"
/* test that expected devices are available and can be probed */
static int bootctl_base(struct unit_test_state *uts)
{
struct udevice *dev;
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_UI, &dev));
ut_asserteq_str("ui", dev->name);
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_OSLIST, &dev));
ut_asserteq_str("oslist", dev->name);
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_STATE, &dev));
ut_asserteq_str("state", dev->name);
return 0;
}
BOOTCTL_TEST(bootctl_base, UTF_DM | UTF_SCAN_FDT);
/* test finding an OS */
static int bootctl_oslist(struct unit_test_state *uts)
{
struct oslist_iter iter;
struct osinfo info;
struct bootflow *bflow = &info.bflow;
struct udevice *dev;
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_OSLIST, &dev));
ut_asserteq_str("oslist", dev->name);
/* initially we should only see Fedora */
bc_oslist_setup_iter(&iter);
ut_assertok(bc_oslist_next(dev, &iter, &info));
ut_asserteq_str("mmc1.bootdev.part_1", bflow->name);
ut_asserteq_strn("Fedora-Workstation", bflow->os_name);
ut_asserteq(-ENODEV, bc_oslist_next(dev, &iter, &info));
return 0;
}
BOOTCTL_TEST(bootctl_oslist, UTF_DM | UTF_SCAN_FDT);
/* test finding OSes on mmc and usb */
static int bootctl_oslist_usb(struct unit_test_state *uts)
{
struct oslist_iter iter;
struct osinfo info;
struct bootflow *bflow = &info.bflow;
struct udevice *dev;
test_set_skip_delays(true);
bootstd_reset_usb();
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_OSLIST, &dev));
ut_asserteq_str("oslist", dev->name);
/* include usb in the bootdev order */
ut_assertok(bootdev_set_order("mmc usb"));
bc_oslist_setup_iter(&iter);
ut_assertok(bc_oslist_next(dev, &iter, &info));
ut_asserteq_str("mmc1.bootdev.part_1", bflow->name);
ut_assertok(bc_oslist_next(dev, &iter, &info));
ut_asserteq_str("hub1.p4.usb_mass_storage.lun0.bootdev.part_1", bflow->name);
ut_asserteq(-ENODEV, bc_oslist_next(dev, &iter, &info));
return 0;
}
BOOTCTL_TEST(bootctl_oslist_usb, UTF_DM | UTF_SCAN_FDT);
/* test basic use of state */
static int bootctl_simple_state_base(struct unit_test_state *uts)
{
struct udevice *dev;
const char *sval;
struct abuf buf;
bool bval;
long ival;
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_STATE, &dev));
ut_assertok(bc_state_write_bool(dev, "fred", false));
ut_assertok(bc_state_write_bool(dev, "mary", true));
ut_assertok(bc_state_write_int(dev, "alex", 123));
ut_assertok(bc_state_write_str(dev, "john", "abc"));
ut_assertok(bc_state_read_bool(dev, "fred", &bval));
ut_asserteq(false, bval);
ut_assertok(bc_state_read_bool(dev, "mary", &bval));
ut_asserteq(true, bval);
ut_assertok(bc_state_read_int(dev, "alex", &ival));
ut_asserteq(123, ival);
ut_assertok(bc_state_read_str(dev, "john", &sval));
ut_asserteq_str("abc", sval);
/* check the buffer contents, including the nul terminator */
ut_assertok(bc_state_save_to_buf(dev, &buf));
ut_asserteq_str("fred=0\nmary=1\nalex=123\njohn=abc\n", buf.data);
ut_asserteq(strlen("fred=0\nmary=1\nalex=123\njohn=abc\n") + 1,
buf.size);
ut_asserteq(0, *((char *)buf.data + buf.size - 1));
abuf_uninit(&buf);
/* overwrite */
ut_assertok(bc_state_write_str(dev, "fred", "def"));
ut_assertok(bc_state_read_str(dev, "fred", &sval));
ut_asserteq_str("def", sval);
ut_assertok(bc_state_clear(dev));
ut_asserteq(-ENOENT, bc_state_read_bool(dev, "fred", &bval));
ut_asserteq(-ENOENT, bc_state_read_bool(dev, "mary", &bval));
ut_asserteq(-ENOENT, bc_state_read_bool(dev, "john", &bval));
ut_asserteq(-ENOENT, bc_state_read_bool(dev, "alex", &bval));
return 0;
}
BOOTCTL_TEST(bootctl_simple_state_base, UTF_DM | UTF_SCAN_FDT);
/* test loading / saving state */
static int bootctl_simple_state_loadsave(struct unit_test_state *uts)
{
struct udevice *dev;
char *buf;
int size;
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_STATE, &dev));
ut_assertok(bc_state_write_bool(dev, "fred", false));
ut_assertok(bc_state_write_bool(dev, "mary", true));
ut_assertok(bc_state_save(dev));
/* check the file contents, including the nul terminator */
ut_assertok(os_read_file("bootctl.ini", (void **)&buf, &size));
ut_asserteq_str("fred=0\nmary=1\n", buf);
ut_asserteq(strlen("fred=0\nmary=1\n") + 1, size);
ut_asserteq(0, buf[size - 1]);
os_free(buf);
ut_assertok(bc_state_load(dev));
return 0;
}
BOOTCTL_TEST(bootctl_simple_state_loadsave, UTF_DM | UTF_SCAN_FDT);
/* test limits */
static int bootctl_simple_state_limits(struct unit_test_state *uts)
{
struct udevice *dev;
char long_key[32]; /* avoid using constants from impl */
struct abuf buf;
char *data;
int ch;
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_STATE, &dev));
/* cannot use NULL as a key or value */
ut_asserteq(-EINVAL, bc_state_write_bool(dev, NULL, false));
ut_asserteq(-EINVAL, bc_state_write_str(dev, "key", NULL));
/* empty key and value */
ut_asserteq(-EINVAL, bc_state_write_str(dev, "", "val"));
ut_assertok(bc_state_write_str(dev, "empty", ""));
/* no spaces allowed in a key */
ut_asserteq(-EKEYREJECTED, bc_state_write_str(dev, "my key", "val"));
/* check key characters */
for (ch = 1; ch < 256; ch++) {
char key[4] = "key";
bool ok;
ok = ch == '_' || (ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9');
key[1] = ch;
printf("checking ch %x\n", ch);
if (ok)
ut_assertok(bc_state_write_str(dev, key, "val"));
else
ut_asserteq(-EKEYREJECTED, bc_state_write_str(dev, key, "val"));
}
/* key too long */
strcpy(long_key, "1234567890123456789012345678901");
ut_asserteq(-EKEYREJECTED, bc_state_write_str(dev, long_key, "val"));
long_key[30] = '\0';
ut_assertok(bc_state_write_str(dev, long_key, "val"));
/* value too long */
abuf_init(&buf);
ut_asserteq(true, abuf_realloc(&buf, 0x1002));
data = buf.data;
memset(data, 'x', 0x1001);
data[0x1001] = '\0';
ut_asserteq(-E2BIG, bc_state_write_str(dev, "try", data));
data[0x1000] = '\0';
ut_assertok(bc_state_write_str(dev, "try", data));
abuf_uninit(&buf);
return 0;
}
BOOTCTL_TEST(bootctl_simple_state_limits, UTF_DM | UTF_SCAN_FDT);
/* test integers */
static int bootctl_simple_state_int(struct unit_test_state *uts)
{
struct udevice *dev;
long ival;
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_STATE, &dev));
/* basic integers */
ut_assertok(bc_state_write_int(dev, "val", 0));
ut_assertok(bc_state_read_int(dev, "val", &ival));
ut_asserteq(0, ival);
ut_assertok(bc_state_write_int(dev, "val", 1));
ut_assertok(bc_state_read_int(dev, "val", &ival));
ut_asserteq(1, ival);
ut_assertok(bc_state_write_int(dev, "val", -1));
ut_assertok(bc_state_read_int(dev, "val", &ival));
ut_asserteq(-1, ival);
/* large ints */
ut_assertok(bc_state_write_int(dev, "val", 0xffffffffl));
ut_assertok(bc_state_read_int(dev, "val", &ival));
ut_asserteq(0xffffffffl, ival);
ut_assertok(bc_state_write_int(dev, "val", -0xffffffffl));
ut_assertok(bc_state_read_int(dev, "val", &ival));
ut_asserteq_64(-0xffffffffl, ival);
ut_assertok(bc_state_write_int(dev, "val", 0x7fffffffffffffffll));
ut_assertok(bc_state_read_int(dev, "val", &ival));
ut_asserteq_64(0x7fffffffffffffffll, ival);
ut_assertok(bc_state_write_int(dev, "val", -0x7fffffffffffffffll));
ut_assertok(bc_state_read_int(dev, "val", &ival));
ut_asserteq_64(-0x7fffffffffffffffll, ival);
return 0;
}
BOOTCTL_TEST(bootctl_simple_state_int, UTF_DM | UTF_SCAN_FDT);
/* test measurement */
static int bootctl_simple_measure(struct unit_test_state *uts)
{
struct bootflow_img *img[3];
struct osinfo osinfo;
struct bootflow *bflow = &osinfo.bflow;
const struct measure_info *info;
struct udevice *dev;
struct alist result;
ut_assertok(bootctl_get_dev(UCLASS_BOOTCTL_MEASURE, &dev));
ut_assertok(bc_measure_start(dev));
/* set up some data */
memset(&osinfo, '\0', sizeof(struct osinfo));
alist_init_struct(&bflow->images, struct bootflow_img);
/* add a few images */
img[0] = bootflow_img_add(bflow, "kernel",
(enum bootflow_img_t)IH_TYPE_KERNEL, 0,
0x100);
ut_assertnonnull(img);
img[1] = bootflow_img_add(bflow, "initrd",
(enum bootflow_img_t)IH_TYPE_RAMDISK, 0x100,
0x200);
ut_assertnonnull(img);
/* the fdt is missing so this should fail */
ut_asserteq(-ENOENT, bc_measure_process(dev, &osinfo, &result));
if (IS_ENABLED(CONFIG_LOGF_FUNC))
ut_assert_nextline(" simple_process() Missing image 'flat_dt'");
else
ut_assert_nextline("Missing image 'flat_dt'");
ut_assert_console_end();
alist_uninit(&result);
img[2] = bootflow_img_add(bflow, "fdt",
(enum bootflow_img_t)IH_TYPE_FLATDT, 0x300,
0x30);
ut_assertok(bc_measure_process(dev, &osinfo, &result));
/* check the result */
ut_asserteq(3, result.count);
info = alist_get(&result, 0, struct measure_info);
ut_asserteq_ptr(img[0], info[0].img);
ut_asserteq_ptr(img[1], info[1].img);
ut_asserteq_ptr(img[2], info[2].img);
/* TODO: We should also a) read out the TPM log and b) check TPM PCRs */
ut_assertnonnull(img);
return 0;
}
BOOTCTL_TEST(bootctl_simple_measure, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);

View File

@@ -0,0 +1,14 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Common definitions for bootctl tests
*
* Copyright 2025 Canonical Ltd
* Written by Simon Glass <simon.glass@canonical.com>
*/
#ifndef __bootctl_common_h
#define __bootctl_common_h
#define BOOTCTL_TEST(_name, _flags) UNIT_TEST(_name, _flags, bootctl)
#endif

View File

@@ -50,6 +50,7 @@ static int do_ut_info(bool show_suites);
SUITE_DECL(addrmap);
SUITE_DECL(bdinfo);
SUITE_DECL(bloblist);
SUITE_DECL(bootctl);
SUITE_DECL(bootm);
SUITE_DECL(bootstd);
SUITE_DECL(cmd);
@@ -77,6 +78,7 @@ static struct suite suites[] = {
SUITE(addrmap, "very basic test of addrmap command"),
SUITE(bdinfo, "bdinfo (board info) command"),
SUITE(bloblist, "bloblist implementation"),
SUITE(bootctl, "bootctl (boot schema)"),
SUITE(bootm, "bootm command"),
SUITE(bootstd, "standard boot implementation"),
SUITE(cmd, "various commands"),