[PATCH 4/4] test: bootstd: add firmware-FDT source tests

Carlo Caione ccaione at baylibre.com
Mon Jul 6 17:06:45 CEST 2026


Add sandbox coverage for firmware_fdt_load(): a python fixture builds
mmc11.img, a GPT disk with A/B 'firmware' partitions whose first
partition carries a FAT filesystem holding the FIT manifest (fdt.itb:
a base DTB plus one overlay, with a default configuration that applies
the overlay and a base-only 'conf-base' configuration).

The tests cover the happy path (default configuration, packed size,
both fixture properties visible), configuration selection via
'fw_fdt_config', and the fail-closed semantics: a missing pinned
partition and a selector naming a configuration the manifest does not
ship are fatal errors rather than -ENOENT, an unresolvable
'firmware-fdt-source' phandle is -EINVAL, and only a genuinely absent
source returns -ENOENT.

Signed-off-by: Carlo Caione <ccaione at baylibre.com>
---
 arch/sandbox/dts/test.dts |  24 ++++
 configs/sandbox_defconfig |   1 +
 test/boot/Makefile        |   1 +
 test/boot/firmware_fdt.c  | 292 ++++++++++++++++++++++++++++++++++++++++++++++
 test/py/tests/test_ut.py  | 110 +++++++++++++++++
 5 files changed, 428 insertions(+)

diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts
index 0887de4333b..97199d1812e 100644
--- a/arch/sandbox/dts/test.dts
+++ b/arch/sandbox/dts/test.dts
@@ -49,6 +49,7 @@
 		mmc8 = "/mmc8";
 		mmc9 = "/mmc9";
 		mmc10 = "/mmc10";
+		mmc11 = "/mmc11";
 		pci0 = &pci0;
 		pci1 = &pci1;
 		pci2 = &pci2;
@@ -122,6 +123,14 @@
 		filename-prefixes = "/", "/boot/";
 		bootdev-order = "mmc2", "mmc1";
 
+		/*
+		 * Not read by U-Boot: the firmware_fdt tests copy this into a
+		 * live 'firmware-fdt-source' property at runtime (and delete
+		 * it afterwards), so the source stays unconfigured for every
+		 * other test. It also keeps a phandle emitted for &fw_fdt.
+		 */
+		test-fw-fdt-source = <&fw_fdt>;
+
 		extlinux {
 			compatible = "u-boot,extlinux";
 		};
@@ -1348,6 +1357,21 @@
 		filename = "mmc10.img";
 	};
 
+	/* This is used for firmware-FDT (firmware_fdt) tests */
+	mmc11 {
+		status = "disabled";
+		compatible = "sandbox,mmc";
+		filename = "mmc11.img";
+
+		fw_fdt: firmware-fdt {
+			compatible = "u-boot,firmware-fdt-block";
+			partition-type-uuid =
+				"384e979b-eb76-435a-a3a6-1a071dbad91d";
+			partition-name = "firmware";
+			filename = "fdt.itb";
+		};
+	};
+
 	pch {
 		compatible = "sandbox,pch";
 	};
diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index ba800f7d19d..2d54de1c746 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -25,6 +25,7 @@ CONFIG_FIT_CIPHER=y
 CONFIG_FIT_VERBOSE=y
 CONFIG_BOOTMETH_ANDROID=y
 CONFIG_BOOTMETH_RAUC=y
+CONFIG_BOOTSTD_FIRMWARE_FDT=y
 CONFIG_UPL=y
 CONFIG_LEGACY_IMAGE_FORMAT=y
 CONFIG_MEASURED_BOOT=y
diff --git a/test/boot/Makefile b/test/boot/Makefile
index 89538d4f0a6..90704e6cbbc 100644
--- a/test/boot/Makefile
+++ b/test/boot/Makefile
@@ -4,6 +4,7 @@
 
 ifdef CONFIG_UT_BOOTSTD
 obj-$(CONFIG_BOOTSTD) += bootdev.o bootstd_common.o bootflow.o bootmeth.o
+obj-$(CONFIG_BOOTSTD_FIRMWARE_FDT) += firmware_fdt.o
 obj-$(CONFIG_FIT) += image.o
 
 ifdef CONFIG_VIDEO_SANDBOX_SDL
diff --git a/test/boot/firmware_fdt.c b/test/boot/firmware_fdt.c
new file mode 100644
index 00000000000..ef137f82648
--- /dev/null
+++ b/test/boot/firmware_fdt.c
@@ -0,0 +1,292 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Tests for the firmware-owned devicetree source (firmware_fdt_load()).
+ *
+ * Uses a sandbox mmc image (mmc11) carrying a GPT 'firmware' partition with
+ * a FAT filesystem holding the FIT manifest (fdt.itb: a base DTB and one
+ * overlay, with two configurations). The image is built by
+ * setup_firmware_fdt_image() in test/py/tests/test_ut.py.
+ *
+ * The shared test.dts deliberately does NOT configure a firmware-FDT source:
+ * a configured source turns the fail-closed semantics on for every EFI
+ * launch path, which would break the other sandbox EFI tests. Instead the
+ * bootstd node carries an inert 'test-fw-fdt-source' phandle, which these
+ * tests copy into a live 'firmware-fdt-source' property for their duration
+ * and delete again afterwards.
+ */
+
+#include <dm.h>
+#include <env.h>
+#include <firmware_fdt.h>
+#include <asm/global_data.h>
+#include <dm/lists.h>
+#include <dm/ofnode.h>
+#include <dm/root.h>
+#include <linux/libfdt.h>
+#include <test/test.h>
+#include <test/ut.h>
+#include "bootstd_common.h"
+
+DECLARE_GLOBAL_DATA_PTR;
+
+#define FWFDT_PROP	"firmware-fdt-source"
+#define FWFDT_TEST_PROP	"test-fw-fdt-source"
+
+static ofnode fwfdt_bootstd_node(void)
+{
+	return ofnode_by_compatible(ofnode_null(), "u-boot,boot-std");
+}
+
+/* Bind the (disabled) mmc node that owns the firmware-FDT image */
+static int fwfdt_bind_mmc(struct unit_test_state *uts)
+{
+	struct udevice *dev;
+	ofnode root, node;
+
+	root = oftree_root(oftree_default());
+	node = ofnode_find_subnode(root, "mmc11");
+	ut_assert(ofnode_valid(node));
+	ut_assertok(lists_bind_fdt(gd->dm_root, node, &dev, NULL, false));
+
+	return 0;
+}
+
+/*
+ * Configure the firmware-FDT source for this test: copy the inert
+ * 'test-fw-fdt-source' phandle into the property firmware_fdt_load() reads.
+ */
+static int fwfdt_configure(struct unit_test_state *uts)
+{
+	const void *prop;
+	ofnode bootstd;
+	fdt32_t val;
+	int len;
+
+	bootstd = fwfdt_bootstd_node();
+	ut_assert(ofnode_valid(bootstd));
+
+	prop = ofnode_get_property(bootstd, FWFDT_TEST_PROP, &len);
+	ut_assertnonnull(prop);
+	ut_asserteq(sizeof(val), len);
+
+	/*
+	 * Copy the value out first: on the flat tree, inserting the new
+	 * property shifts the blob region the old property value lives in,
+	 * so passing the in-blob pointer straight to ofnode_write_prop()
+	 * would copy from the stale location.
+	 */
+	memcpy(&val, prop, sizeof(val));
+	ut_assertok(ofnode_write_prop(bootstd, FWFDT_PROP, &val, sizeof(val),
+				      true));
+
+	return 0;
+}
+
+/* Undo fwfdt_configure() and clear any environment the tests use */
+static int fwfdt_deconfigure(struct unit_test_state *uts)
+{
+	ofnode bootstd;
+
+	bootstd = fwfdt_bootstd_node();
+	ut_assert(ofnode_valid(bootstd));
+	if (ofnode_get_property(bootstd, FWFDT_PROP, NULL))
+		ut_assertok(ofnode_delete_prop(bootstd, FWFDT_PROP));
+
+	env_set("boot_dtb", NULL);
+	env_set("fw_fdt_config", NULL);
+
+	return 0;
+}
+
+/* Happy path: the manifest's default configuration applies base + overlay */
+static int firmware_fdt_test_load(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+	void *fdt;
+
+	/*
+	 * Configure before binding: on the flat tree, inserting the property
+	 * shifts every later node offset, so a device bound beforehand would
+	 * no longer match its (shifted) devicetree node.
+	 */
+	ut_assertok(fwfdt_configure(uts));
+	ut_assertok(fwfdt_bind_mmc(uts));
+
+	ut_assertok(firmware_fdt_load(&fw));
+
+	ut_asserteq_str("fdt.itb", fw.name);
+	ut_assert(fw.size > 0);
+
+	fdt = fw.fdt;
+	ut_assertok(fdt_check_header(fdt));
+	/* the size reports the packed devicetree, not a padded buffer */
+	ut_asserteq(fw.size, fdt_totalsize(fdt));
+	/* the base property is present... */
+	ut_assertnonnull(fdt_getprop(fdt, 0, "fw-base-prop", NULL));
+	/* ...and the overlay was applied on top */
+	ut_assertnonnull(fdt_getprop(fdt, 0, "fw-overlay-prop", NULL));
+
+	firmware_fdt_free(&fw);
+	ut_assertnull(fw.fdt);
+
+	ut_assertok(fwfdt_deconfigure(uts));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_load, UTF_DM | UTF_SCAN_FDT);
+
+/* 'fw_fdt_config' selects another configuration the manifest ships */
+static int firmware_fdt_test_select(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+	void *fdt;
+
+	ut_assertok(fwfdt_configure(uts));
+	ut_assertok(fwfdt_bind_mmc(uts));
+
+	ut_assertok(env_set("fw_fdt_config", "conf-base"));
+	ut_assertok(firmware_fdt_load(&fw));
+
+	fdt = fw.fdt;
+	ut_assertnonnull(fdt_getprop(fdt, 0, "fw-base-prop", NULL));
+	/* the base-only configuration applies no overlay */
+	ut_assertnull(fdt_getprop(fdt, 0, "fw-overlay-prop", NULL));
+
+	firmware_fdt_free(&fw);
+
+	ut_assertok(fwfdt_deconfigure(uts));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_select, UTF_DM | UTF_SCAN_FDT);
+
+/*
+ * A selector naming a configuration the manifest does not ship must be
+ * fatal: -ENOENT strictly means "no source configured", so the inner miss
+ * must not leak out and let the caller fall back (fail closed).
+ */
+static int firmware_fdt_test_bad_config(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+
+	ut_assertok(fwfdt_configure(uts));
+	ut_assertok(fwfdt_bind_mmc(uts));
+
+	ut_assertok(env_set("fw_fdt_config", "conf-nonexistent"));
+	ut_asserteq(-ENODEV, firmware_fdt_load(&fw));
+
+	ut_assertok(fwfdt_deconfigure(uts));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_bad_config, UTF_DM | UTF_SCAN_FDT);
+
+/* Pinning a partition that does not exist is a hard error (fail closed) */
+static int firmware_fdt_test_no_part(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+
+	ut_assertok(fwfdt_configure(uts));
+	ut_assertok(fwfdt_bind_mmc(uts));
+
+	ut_assertok(env_set("boot_dtb", "9"));
+	ut_asserteq(-ENODEV, firmware_fdt_load(&fw));
+
+	ut_assertok(fwfdt_deconfigure(uts));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_no_part, UTF_DM | UTF_SCAN_FDT);
+
+/*
+ * Without the runtime injection no source is configured: -ENOENT, the only
+ * case where callers may fall back. This also proves the shared test.dts
+ * stays inert for every other sandbox test.
+ */
+static int firmware_fdt_test_no_source(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+
+	ut_assertok(fwfdt_bind_mmc(uts));
+
+	ut_asserteq(-ENOENT, firmware_fdt_load(&fw));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_no_source, UTF_DM | UTF_SCAN_FDT);
+
+/*
+ * A 'firmware-fdt-source' property whose phandle does not resolve is a
+ * broken configuration and must be fatal, not mistaken for "no source".
+ */
+static int firmware_fdt_test_bad_source(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+	ofnode bootstd;
+	fdt32_t bad;
+
+	bootstd = fwfdt_bootstd_node();
+	ut_assert(ofnode_valid(bootstd));
+
+	bad = cpu_to_fdt32(0x7fffffff);
+	ut_assertok(ofnode_write_prop(bootstd, FWFDT_PROP, &bad, sizeof(bad),
+				      true));
+
+	ut_asserteq(-EINVAL, firmware_fdt_load(&fw));
+
+	ut_assertok(fwfdt_deconfigure(uts));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_bad_source, UTF_DM | UTF_SCAN_FDT);
+
+/* A manifest using external data is refused: it must be self-contained */
+static int firmware_fdt_test_external(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+	ofnode node;
+
+	ut_assertok(fwfdt_configure(uts));
+
+	node = ofnode_path("/mmc11/firmware-fdt");
+	ut_assert(ofnode_valid(node));
+	ut_assertok(ofnode_write_string(node, "filename", "fdt-ext.itb"));
+
+	ut_assertok(fwfdt_bind_mmc(uts));
+
+	ut_asserteq(-EINVAL, firmware_fdt_load(&fw));
+
+	ut_assertok(ofnode_write_string(node, "filename", "fdt.itb"));
+	ut_assertok(fwfdt_deconfigure(uts));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_external, UTF_DM | UTF_SCAN_FDT);
+
+/*
+ * When both partition-type-uuid and partition-name are configured, both
+ * must match: a name matching nothing must not fall back to whichever
+ * same-type (A/B) partition comes first.
+ */
+static int firmware_fdt_test_part_mismatch(struct unit_test_state *uts)
+{
+	struct firmware_fdt fw;
+	ofnode node;
+
+	ut_assertok(fwfdt_configure(uts));
+
+	node = ofnode_path("/mmc11/firmware-fdt");
+	ut_assert(ofnode_valid(node));
+	/* the type UUID matches both A/B partitions; this name matches none */
+	ut_assertok(ofnode_write_string(node, "partition-name", "nomatch"));
+
+	ut_assertok(fwfdt_bind_mmc(uts));
+
+	ut_asserteq(-ENODEV, firmware_fdt_load(&fw));
+
+	ut_assertok(ofnode_write_string(node, "partition-name", "firmware"));
+	ut_assertok(fwfdt_deconfigure(uts));
+
+	return 0;
+}
+BOOTSTD_TEST(firmware_fdt_test_part_mismatch, UTF_DM | UTF_SCAN_FDT);
diff --git a/test/py/tests/test_ut.py b/test/py/tests/test_ut.py
index dce5a37dd35..15c36fecafb 100644
--- a/test/py/tests/test_ut.py
+++ b/test/py/tests/test_ut.py
@@ -614,6 +614,115 @@ def setup_rauc_image(ubman):
     boot.cleanup()
     root.cleanup()
 
+def setup_firmware_fdt_image(ubman):
+    """Create mmc11.img for the firmware_fdt tests
+
+    A GPT disk with two firmware partitions (A/B) sharing a firmware type
+    UUID; partition 1 (label 'firmware') holds a FAT filesystem with the
+    FIT manifest (fdt.itb) carrying a base DTB and an overlay, with two
+    configurations: the default applies the overlay, 'conf-base' does not.
+    """
+    Partition = collections.namedtuple('part', 'start,size,name')
+    parts = {}
+
+    mmc_dev = 11
+    fname = os.path.join(ubman.config.source_dir, f'mmc{mmc_dev}.img')
+    fw_type = '384e979b-eb76-435a-a3a6-1a071dbad91d'
+    sect_size = 512
+
+    # Compile a tiny base DTB and an overlay, then wrap them in the manifest
+    src = os.path.join(ubman.config.persistent_data_dir, 'fwfdt')
+    mkdir_cond(src)
+    base_dtb = os.path.join(src, 'base.dtb')
+    ovl_dtbo = os.path.join(src, 'overlay.dtbo')
+    utils.run_and_log(
+        ubman, f'dtc -O dtb -o {base_dtb}',
+        stdin=b'/dts-v1/; / { compatible = "test,fw-fdt-base"; '
+              b'fw-base-prop = "base"; };')
+    utils.run_and_log(
+        ubman, f'dtc -O dtb -o {ovl_dtbo}',
+        stdin=b'/dts-v1/; /plugin/; &{/} { fw-overlay-prop = "applied"; };')
+
+    its = os.path.join(src, 'fdt.its')
+    with open(its, 'w', encoding='ascii') as outf:
+        outf.write(f'''
+/dts-v1/;
+/ {{
+\tdescription = "Firmware-owned OS devicetree";
+\t#address-cells = <1>;
+
+\timages {{
+\t\tfdt-base {{
+\t\t\tdata = /incbin/("{base_dtb}");
+\t\t\ttype = "flat_dt";
+\t\t\tarch = "sandbox";
+\t\t\tcompression = "none";
+\t\t\thash-1 {{ algo = "sha256"; }};
+\t\t}};
+\t\tfdt-overlay {{
+\t\t\tdata = /incbin/("{ovl_dtbo}");
+\t\t\ttype = "flat_dt";
+\t\t\tarch = "sandbox";
+\t\t\tcompression = "none";
+\t\t\thash-1 {{ algo = "sha256"; }};
+\t\t}};
+\t}};
+
+\tconfigurations {{
+\t\tdefault = "conf-overlay";
+\t\tconf-overlay {{
+\t\t\tfdt = "fdt-base", "fdt-overlay";
+\t\t}};
+\t\tconf-base {{
+\t\t\tfdt = "fdt-base";
+\t\t}};
+\t}};
+}};
+''')
+
+    fs_dir = os.path.join(src, 'fs')
+    mkdir_cond(fs_dir)
+    mkimage = os.path.join(ubman.config.build_dir, 'tools/mkimage')
+    utils.run_and_log(
+        ubman, f'{mkimage} -f {its} {os.path.join(fs_dir, "fdt.itb")}')
+
+    # An external-data variant, which firmware_fdt_load() must refuse
+    utils.run_and_log(
+        ubman, f'{mkimage} -E -f {its} {os.path.join(fs_dir, "fdt-ext.itb")}')
+
+    fat_img = fs_helper.mk_fs(ubman.config, 'vfat', 1 << 20, 'fwfdt',
+                              src_dir=fs_dir)
+    with open(fat_img, 'rb') as inf:
+        fat_data = inf.read()
+
+    # GPT with two same-type firmware partitions; the FAT goes in partition 1
+    fat_sects = (len(fat_data) + sect_size - 1) // sect_size
+    utils.run_and_log(ubman, f'qemu-img create {fname} 8M')
+    utils.run_and_log(ubman, f'cgpt create {fname}')
+    ptr = 40
+    for num, label in ((1, 'firmware'), (2, 'firmware_b')):
+        utils.run_and_log(
+            ubman,
+            f'cgpt add -i {num} -b {ptr} -s {fat_sects} -t {fw_type} '
+            f'-l {label} {fname}')
+        ptr += fat_sects
+    utils.run_and_log(ubman, f'cgpt boot -p {fname}')
+    out = utils.run_and_log(ubman, f'cgpt show -q {fname}')
+    for line in out.splitlines():
+        start, size, num, name = line.split(maxsplit=3)
+        parts[int(num)] = Partition(int(start), int(size), name)
+
+    # Splice the FAT image into partition 1
+    with open(fname, 'rb') as inf:
+        disk_data = inf.read()
+    start = parts[1].start * sect_size
+    disk_data = disk_data[:start] + fat_data + disk_data[start + len(fat_data):]
+    with open(fname, 'wb') as outf:
+        outf.write(disk_data)
+
+    return fname
+
+
 @pytest.mark.buildconfigspec('cmd_bootflow')
 @pytest.mark.buildconfigspec('sandbox')
 def test_ut_dm_init_bootstd(ubman):
@@ -626,6 +735,7 @@ def test_ut_dm_init_bootstd(ubman):
     setup_android_image(ubman)
     setup_efi_image(ubman)
     setup_rauc_image(ubman)
+    setup_firmware_fdt_image(ubman)
 
     # Restart so that the new mmc1.img is picked up
     ubman.restart_uboot()

-- 
2.55.0



More information about the U-Boot mailing list