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

