summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/boot/Makefile2
-rw-r--r--test/boot/fit_verity.c200
-rw-r--r--test/boot/image.c68
-rw-r--r--test/boot/measurement.c2
-rw-r--r--test/boot/vbe_read_fit.c224
-rw-r--r--test/cmd/Makefile1
-rw-r--r--test/cmd/config.c28
-rw-r--r--test/cmd/unzip.c118
-rw-r--r--test/dm/Makefile2
-rw-r--r--test/dm/hash.c157
-rw-r--r--test/dm/net_defrag.c36
-rw-r--r--test/dm/pmbus.c242
-rw-r--r--test/dm/regulator.c38
-rw-r--r--test/image/spl_load.c49
-rw-r--r--test/lib/lmb.c12
-rw-r--r--test/lib/string.c24
-rw-r--r--test/py/tests/test_efi_secboot/conftest.py8
-rw-r--r--test/py/tests/test_efi_secboot/test_authvar.py47
-rw-r--r--test/py/tests/test_fit_import_data.py89
-rw-r--r--test/py/tests/test_fit_verity_sign.py203
-rw-r--r--test/py/tests/test_fs/test_squashfs/test_sqfs_overflow.py75
-rw-r--r--test/py/tests/test_load_sandbox.py51
-rw-r--r--test/py/tests/test_net.py94
-rw-r--r--test/py/tests/test_semihosting/conftest.py4
-rw-r--r--test/py/tests/test_semihosting/test_load_semihosting.py38
-rw-r--r--test/py/tests/test_trace.py5
26 files changed, 1802 insertions, 15 deletions
diff --git a/test/boot/Makefile b/test/boot/Makefile
index e6aa0ab7d3e..59a87028704 100644
--- a/test/boot/Makefile
+++ b/test/boot/Makefile
@@ -22,7 +22,7 @@ obj-$(CONFIG_$(PHASE_)FIT_VERITY) += fit_verity.o
obj-$(CONFIG_MEASURED_BOOT) += measurement.o
ifdef CONFIG_OF_LIVE
-obj-$(CONFIG_BOOTMETH_VBE_SIMPLE) += vbe_simple.o
+obj-$(CONFIG_BOOTMETH_VBE_SIMPLE) += vbe_simple.o vbe_read_fit.o
endif
obj-$(CONFIG_BOOTMETH_VBE) += vbe_fixup.o
diff --git a/test/boot/fit_verity.c b/test/boot/fit_verity.c
index 7459a9d6f81..4b5db839085 100644
--- a/test/boot/fit_verity.c
+++ b/test/boot/fit_verity.c
@@ -6,6 +6,11 @@
*/
#include <image.h>
+#include <fdt_region.h>
+#include <malloc.h>
+#include <linux/kernel.h>
+#include <linux/libfdt.h>
+#include <u-boot/hash-checksum.h>
#include <test/test.h>
#include <test/ut.h>
@@ -304,3 +309,198 @@ static int fit_verity_test_bad_blocksize(struct unit_test_state *uts)
return 0;
}
FIT_VERITY_TEST(fit_verity_test_bad_blocksize, 0);
+
+#if CONFIG_IS_ENABLED(FIT_SIGNATURE)
+/**
+ * build_signed_verity_fit() - build a FIT with a signable verity config
+ * @buf: output buffer (at least FIT_BUF_SIZE bytes)
+ *
+ * Like build_verity_fit(), but the filesystem image also carries a hash
+ * subnode (required for a configuration to be signable) so the config's
+ * signed-region node list can be built with fit_config_get_signed_nodes().
+ *
+ * Return: configuration node offset, or -ve on error
+ */
+static int build_signed_verity_fit(void *buf)
+{
+ int images_node, confs_node, conf_node, img_node, hash_node, verity_node;
+ fdt32_t val;
+ int ret;
+
+ ret = fdt_create_empty_tree(buf, FIT_BUF_SIZE);
+ if (ret)
+ return ret;
+
+ images_node = fdt_add_subnode(buf, 0, "images");
+ if (images_node < 0)
+ return images_node;
+
+ img_node = fdt_add_subnode(buf, images_node, "rootfs");
+ if (img_node < 0)
+ return img_node;
+ ret = fdt_setprop_string(buf, img_node, FIT_TYPE_PROP, "filesystem");
+ if (ret)
+ return ret;
+
+ hash_node = fdt_add_subnode(buf, img_node, "hash-1");
+ if (hash_node < 0)
+ return hash_node;
+ ret = fdt_setprop_string(buf, hash_node, FIT_ALGO_PROP, "sha256");
+ if (ret)
+ return ret;
+ ret = fdt_setprop(buf, hash_node, FIT_VALUE_PROP, test_digest,
+ sizeof(test_digest));
+ if (ret)
+ return ret;
+
+ verity_node = fdt_add_subnode(buf, img_node, FIT_VERITY_NODENAME);
+ if (verity_node < 0)
+ return verity_node;
+ ret = fdt_setprop_string(buf, verity_node, FIT_VERITY_ALGO_PROP,
+ "sha256");
+ if (ret)
+ return ret;
+ val = cpu_to_fdt32(4096);
+ ret = fdt_setprop(buf, verity_node, FIT_VERITY_DBS_PROP, &val,
+ sizeof(val));
+ if (ret)
+ return ret;
+ ret = fdt_setprop(buf, verity_node, FIT_VERITY_HBS_PROP, &val,
+ sizeof(val));
+ if (ret)
+ return ret;
+ val = cpu_to_fdt32(100);
+ ret = fdt_setprop(buf, verity_node, FIT_VERITY_NBLK_PROP, &val,
+ sizeof(val));
+ if (ret)
+ return ret;
+ ret = fdt_setprop(buf, verity_node, FIT_VERITY_HBLK_PROP, &val,
+ sizeof(val));
+ if (ret)
+ return ret;
+ ret = fdt_setprop(buf, verity_node, FIT_VERITY_DIGEST_PROP, test_digest,
+ sizeof(test_digest));
+ if (ret)
+ return ret;
+ ret = fdt_setprop(buf, verity_node, FIT_VERITY_SALT_PROP, test_salt,
+ sizeof(test_salt));
+ if (ret)
+ return ret;
+
+ confs_node = fdt_add_subnode(buf, 0, "configurations");
+ if (confs_node < 0)
+ return confs_node;
+ conf_node = fdt_add_subnode(buf, confs_node, "conf-1");
+ if (conf_node < 0)
+ return conf_node;
+ ret = fdt_setprop_string(buf, conf_node, FIT_LOADABLE_PROP, "rootfs");
+ if (ret)
+ return ret;
+
+ return conf_node;
+}
+
+/*
+ * Test: the dm-verity roothash and salt are inside the region covered by the
+ * configuration signature.
+ *
+ * A dm-verity filesystem image is not hashed by U-Boot; its integrity is
+ * delegated to the kernel, which trusts the roothash from the FIT dm-verity
+ * subnode. That roothash must therefore be part of the signed region, so that
+ * an attacker cannot swap both the filesystem and the roothash while keeping
+ * the configuration signature valid.
+ *
+ * This checks the property without a private key, so it also runs on real
+ * devices: it builds the exact node list the signature is computed over
+ * (fit_config_get_signed_nodes), turns it into hashed regions, and verifies both
+ * that the roothash bytes fall inside a region and that tampering them changes
+ * the hash. It uses the same hash path a device would (crypto accelerated where
+ * available).
+ */
+static int fit_verity_test_roothash_signed(struct unit_test_state *uts)
+{
+ char buf[FIT_BUF_SIZE];
+ char *node_inc[32];
+ char path_buf[256];
+ char region_path[256];
+ struct fdt_region fdt_regions[64];
+ struct image_region *region = NULL;
+ int conf_node, verity_node;
+ int count, i, digest_len;
+ const void *digest;
+ ulong digest_off, region_off;
+ bool covered = false;
+ u8 hash_clean[32], hash_tampered[32], hash_control[32];
+
+ conf_node = build_signed_verity_fit(buf);
+ ut_assert(conf_node >= 0);
+
+ verity_node = fdt_path_offset(buf, "/images/rootfs/dm-verity");
+ ut_assert(verity_node >= 0);
+
+ /* Build the node list the configuration signature is computed over. */
+ count = fit_config_get_signed_nodes(buf, conf_node, node_inc,
+ ARRAY_SIZE(node_inc), path_buf,
+ sizeof(path_buf));
+ ut_assert(count > 0);
+
+ /*
+ * Turn the node list into hashed regions. No exclude list is needed:
+ * the excluded properties (data, data-size, data-offset,
+ * data-position) never include the dm-verity digest or salt, so the
+ * coverage answer is the same with or without it.
+ */
+ count = fdt_find_regions(buf, node_inc, count, NULL, 0, fdt_regions,
+ ARRAY_SIZE(fdt_regions) - 1, region_path,
+ sizeof(region_path), 0);
+ ut_assert(count > 0);
+ /* Region array exhausted: mirror the bound fit_config_check_sig() enforces. */
+ ut_assert(count < ARRAY_SIZE(fdt_regions) - 1);
+
+ region = fit_region_make_list(buf, fdt_regions, count, NULL);
+ ut_assertnonnull(region);
+
+ digest = fdt_getprop(buf, verity_node, FIT_VERITY_DIGEST_PROP,
+ &digest_len);
+ ut_assertnonnull(digest);
+ ut_assert(digest_len > 0);
+ digest_off = (ulong)((const char *)digest - (const char *)buf);
+
+ /*
+ * Control: the hash covers a non-empty region and reacts to a change
+ * inside it. Flip a byte of the (signed) image hash value and confirm
+ * the computed hash differs, proving the region set and hash work.
+ */
+ ut_assertok(hash_calculate("sha256", region, count, hash_clean));
+ for (i = 0; i < count; i++) {
+ region_off = (ulong)((const char *)region[i].data -
+ (const char *)buf);
+ if (digest_off >= region_off &&
+ digest_off + digest_len <= region_off + region[i].size) {
+ covered = true;
+ break;
+ }
+ }
+
+ /* The roothash must be covered by the configuration signature. */
+ ut_assert(covered);
+
+ /*
+ * Tampering the roothash must change the signed hash. Only the digest
+ * is flipped here; salt sits in the same dm-verity node, so coverage
+ * of one implies coverage of the other.
+ */
+ buf[digest_off] ^= 0xff;
+ ut_assertok(hash_calculate("sha256", region, count, hash_tampered));
+ buf[digest_off] ^= 0xff;
+ ut_assert(memcmp(hash_clean, hash_tampered, sizeof(hash_clean)) != 0);
+
+ /* Sanity: with the byte restored the hash matches the clean value. */
+ ut_assertok(hash_calculate("sha256", region, count, hash_control));
+ ut_asserteq_mem(hash_clean, hash_control, sizeof(hash_clean));
+
+ free(region);
+ return 0;
+}
+FIT_VERITY_TEST(fit_verity_test_roothash_signed, 0);
+#endif /* FIT_SIGNATURE */
diff --git a/test/boot/image.c b/test/boot/image.c
index 4df7b17ce88..2c6d9dcbc22 100644
--- a/test/boot/image.c
+++ b/test/boot/image.c
@@ -8,8 +8,76 @@
#include <image.h>
#include <test/ut.h>
+#include <linux/libfdt.h>
#include "bootstd_common.h"
+/* Test that the default configuration breaks best-match ties */
+static int test_fit_conf_find_compat(struct unit_test_state *uts)
+{
+ char fdt[256], fit[1024];
+ int confs, images, node;
+ int ret;
+
+ /* control devicetree with a two-entry compatible list */
+ ut_assertok(fdt_create_empty_tree(fdt, sizeof(fdt)));
+ ut_assertok(fdt_appendprop_string(fdt, 0, "compatible",
+ "test,board-a"));
+ ut_assertok(fdt_appendprop_string(fdt, 0, "compatible",
+ "test,fallback"));
+
+ /* FIT with two configurations matching the same compatible */
+ ut_assertok(fdt_create_empty_tree(fit, sizeof(fit)));
+ images = fdt_add_subnode(fit, 0, "images");
+ ut_assert(images >= 0);
+ confs = fdt_add_subnode(fit, 0, "configurations");
+ ut_assert(confs >= 0);
+ ut_assertok(fdt_setprop_string(fit, confs, FIT_DEFAULT_PROP, "conf-2"));
+ /*
+ * fdt_add_subnode() inserts before existing subnodes: create conf-2
+ * first so that conf-1 ends up listed first, like an .its compiled
+ * with the configurations in that order
+ */
+ node = fdt_add_subnode(fit, confs, "conf-2");
+ ut_assert(node >= 0);
+ ut_assertok(fdt_setprop_string(fit, node, "compatible",
+ "test,board-a"));
+ node = fdt_add_subnode(fit, confs, "conf-1");
+ ut_assert(node >= 0);
+ ut_assertok(fdt_setprop_string(fit, node, "compatible",
+ "test,board-a"));
+ confs = fdt_path_offset(fit, "/configurations");
+ node = fdt_first_subnode(fit, confs);
+ ut_asserteq_str("conf-1", fdt_get_name(fit, node, NULL));
+
+ /* on a tie, the default configuration wins */
+ ret = fit_conf_find_compat(fit, fdt);
+ ut_assert(ret > 0);
+ ut_asserteq_str("conf-2", fdt_get_name(fit, ret, NULL));
+
+ /* without a default, the first listed configuration wins */
+ confs = fdt_path_offset(fit, "/configurations");
+ ut_assertok(fdt_delprop(fit, confs, FIT_DEFAULT_PROP));
+ confs = fdt_path_offset(fit, "/configurations");
+ ut_assertnull((void *)fdt_getprop(fit, confs, FIT_DEFAULT_PROP, NULL));
+ ret = fit_conf_find_compat(fit, fdt);
+ ut_assert(ret > 0);
+ ut_asserteq_str("conf-1", fdt_get_name(fit, ret, NULL));
+
+ /* a strictly better match still beats the default */
+ confs = fdt_path_offset(fit, "/configurations");
+ ut_assertok(fdt_setprop_string(fit, confs, FIT_DEFAULT_PROP, "conf-2"));
+ confs = fdt_path_offset(fit, "/configurations");
+ node = fdt_subnode_offset(fit, confs, "conf-2");
+ ut_assertok(fdt_setprop_string(fit, node, "compatible",
+ "test,fallback"));
+ ret = fit_conf_find_compat(fit, fdt);
+ ut_assert(ret > 0);
+ ut_asserteq_str("conf-1", fdt_get_name(fit, ret, NULL));
+
+ return 0;
+}
+BOOTSTD_TEST(test_fit_conf_find_compat, 0);
+
/* Test of image phase */
static int test_image_phase(struct unit_test_state *uts)
{
diff --git a/test/boot/measurement.c b/test/boot/measurement.c
index 71f503f1567..85a01f1fec8 100644
--- a/test/boot/measurement.c
+++ b/test/boot/measurement.c
@@ -9,9 +9,9 @@
#include <bootm.h>
#include <env.h>
#include <malloc.h>
+#include <mapmem.h>
#include <test/test.h>
#include <test/ut.h>
-#include <asm/io.h>
#define MEASUREMENT_TEST(_name, _flags) \
UNIT_TEST(_name, _flags, measurement)
diff --git a/test/boot/vbe_read_fit.c b/test/boot/vbe_read_fit.c
new file mode 100644
index 00000000000..f67de0e7165
--- /dev/null
+++ b/test/boot/vbe_read_fit.c
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Bounds-check tests for vbe_read_fit()
+ *
+ * vbe_read_fit() pulls a firmware-phase FIT from a trusted firmware area
+ * on a block device. The external-data location and size carried in the
+ * FIT image node are attacker-controllable when the firmware area is on
+ * mutable boot media, so vbe_read_fit() must reject FITs whose external
+ * data extends past @area_size before issuing the follow-up blk_read().
+ *
+ * These tests build small synthetic FITs with deliberately out-of-range
+ * values and confirm vbe_read_fit() returns -E2BIG for each.
+ *
+ * Copyright 2026 Canonical Ltd.
+ * Written by Aristo Chen <[email protected]>
+ */
+
+#include <blk.h>
+#include <dm.h>
+#include <image.h>
+#include <memalign.h>
+#include <mmc.h>
+#include <test/test.h>
+#include <test/ut.h>
+#include <linux/libfdt.h>
+#include "bootstd_common.h"
+#include "../../boot/vbe_common.h"
+
+/*
+ * The synthetic FIT is written to mmc1 starting at block TEST_FIT_BLK.
+ * bootstd_setup_for_tests() uses blocks 4 and 6 (see bootstd_common.h);
+ * block 16 leaves a comfortable gap.
+ */
+#define TEST_FIT_BLK 16
+#define TEST_FIT_OFF ((ulong)TEST_FIT_BLK * MMC_MAX_BLOCK_LEN)
+#define TEST_AREA_SIZE 0x1000
+
+/**
+ * build_fit() - Build a minimal external-data FIT for vbe_read_fit()
+ *
+ * The FIT advertises a single firmware image whose @data-position and
+ * @data-size are passed in directly. Both values are attacker-controlled
+ * in the real threat model.
+ *
+ * @buf: Destination buffer (must be at least 512 bytes)
+ * @buf_size: Size of @buf
+ * @data_position: Value written to the image's data-position property
+ * @data_size: Value written to the image's data-size property
+ * Returns: 0 on success, libfdt error otherwise
+ */
+static int build_fit(void *buf, size_t buf_size, u32 data_position,
+ u32 data_size)
+{
+ int ret;
+
+ ret = fdt_create(buf, buf_size);
+ if (ret)
+ return ret;
+ ret = fdt_finish_reservemap(buf);
+ if (ret)
+ return ret;
+
+ ret = fdt_begin_node(buf, "");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_DESC_PROP, "vbe-read-fit test");
+ if (ret)
+ return ret;
+ ret = fdt_property_u32(buf, FIT_TIMESTAMP_PROP, 0);
+ if (ret)
+ return ret;
+
+ ret = fdt_begin_node(buf, "images");
+ if (ret)
+ return ret;
+ ret = fdt_begin_node(buf, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_DESC_PROP, "U-Boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_TYPE_PROP, "firmware");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_ARCH_PROP, "sandbox");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_OS_PROP, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_PHASE_PROP, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_COMP_PROP, "none");
+ if (ret)
+ return ret;
+ ret = fdt_property_u32(buf, FIT_DATA_POSITION_PROP, data_position);
+ if (ret)
+ return ret;
+ ret = fdt_property_u32(buf, FIT_DATA_SIZE_PROP, data_size);
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* u-boot */
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* images */
+ if (ret)
+ return ret;
+
+ ret = fdt_begin_node(buf, "configurations");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_DEFAULT_PROP, "conf-1");
+ if (ret)
+ return ret;
+ ret = fdt_begin_node(buf, "conf-1");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, "compatible", "sandbox");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_FIRMWARE_PROP, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* conf-1 */
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* configurations */
+ if (ret)
+ return ret;
+
+ ret = fdt_end_node(buf); /* root */
+ if (ret)
+ return ret;
+
+ return fdt_finish(buf);
+}
+
+/**
+ * place_fit_on_mmc() - Write a synthetic FIT to mmc1 and return its blk dev
+ *
+ * @uts: Unit test state
+ * @fit: FIT image to write
+ * @blkp: On success, receives the block udevice for mmc1
+ * Returns: 0 on success, -ve on error
+ */
+static int place_fit_on_mmc(struct unit_test_state *uts, const void *fit,
+ struct udevice **blkp)
+{
+ ALLOC_CACHE_ALIGN_BUFFER(u8, blkbuf, MMC_MAX_BLOCK_LEN);
+ struct udevice *mmc;
+ struct blk_desc *desc;
+ size_t fit_size = fdt_totalsize(fit);
+ size_t pos;
+ int blknum = TEST_FIT_BLK;
+
+ ut_assertok(uclass_get_device(UCLASS_MMC, 1, &mmc));
+ desc = blk_get_by_device(mmc);
+ if (!desc)
+ return log_msg_ret("desc", -ENODEV);
+
+ for (pos = 0; pos < fit_size; pos += MMC_MAX_BLOCK_LEN, blknum++) {
+ size_t this_blk = min(fit_size - pos,
+ (size_t)MMC_MAX_BLOCK_LEN);
+
+ memset(blkbuf, '\0', MMC_MAX_BLOCK_LEN);
+ memcpy(blkbuf, (const u8 *)fit + pos, this_blk);
+ if (blk_dwrite(desc, blknum, 1, blkbuf) != 1)
+ return log_msg_ret("wr", -EIO);
+ }
+ *blkp = desc->bdev;
+
+ return 0;
+}
+
+/*
+ * data-position points past area_size: vbe_read_fit() must reject the
+ * FIT with -E2BIG before issuing the external-data blk_read().
+ */
+static int vbe_read_fit_oob_position(struct unit_test_state *uts)
+{
+ u8 fit[1024] __aligned(8);
+ struct udevice *blk;
+ ulong load_addr = 0, len = 0;
+ char *name = NULL;
+ int ret;
+
+ ut_assertok(build_fit(fit, sizeof(fit),
+ TEST_AREA_SIZE + 0x10, 0x40));
+ ut_assertok(place_fit_on_mmc(uts, fit, &blk));
+
+ ret = vbe_read_fit(blk, TEST_FIT_OFF, TEST_AREA_SIZE,
+ NULL, &load_addr, &len, &name);
+ ut_asserteq(-E2BIG, ret);
+
+ return 0;
+}
+
+BOOTSTD_TEST(vbe_read_fit_oob_position, UTF_DM | UTF_SCAN_FDT);
+
+/*
+ * data-position is inside the area but data-size pushes the end past
+ * area_size: vbe_read_fit() must reject the FIT with -E2BIG.
+ */
+static int vbe_read_fit_oversize_data(struct unit_test_state *uts)
+{
+ u8 fit[1024] __aligned(8);
+ struct udevice *blk;
+ ulong load_addr = 0, len = 0;
+ char *name = NULL;
+ int ret;
+
+ ut_assertok(build_fit(fit, sizeof(fit),
+ 0x400, TEST_AREA_SIZE));
+ ut_assertok(place_fit_on_mmc(uts, fit, &blk));
+
+ ret = vbe_read_fit(blk, TEST_FIT_OFF, TEST_AREA_SIZE,
+ NULL, &load_addr, &len, &name);
+ ut_asserteq(-E2BIG, ret);
+
+ return 0;
+}
+
+BOOTSTD_TEST(vbe_read_fit_oversize_data, UTF_DM | UTF_SCAN_FDT);
diff --git a/test/cmd/Makefile b/test/cmd/Makefile
index 8d6932f1176..8d36463879d 100644
--- a/test/cmd/Makefile
+++ b/test/cmd/Makefile
@@ -17,6 +17,7 @@ ifdef CONFIG_CONSOLE_RECORD
obj-$(CONFIG_CMD_ACPI) += acpi.o
endif
obj-$(CONFIG_CMD_BDI) += bdinfo.o
+obj-$(CONFIG_CMD_CONFIG) += config.o
obj-$(CONFIG_COREBOOT_SYSINFO) += coreboot.o
obj-$(CONFIG_CMD_FDT) += fdt.o
obj-$(CONFIG_CMD_HASH) += hash.o
diff --git a/test/cmd/config.c b/test/cmd/config.c
new file mode 100644
index 00000000000..5a48060801f
--- /dev/null
+++ b/test/cmd/config.c
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for config command
+ */
+
+#include <console.h>
+#include <test/cmd.h>
+#include <test/ut.h>
+
+static int cmd_test_config(struct unit_test_state *uts)
+{
+ ut_assertok(run_command("config", 0));
+ ut_assert_skip_to_line("# Automatically generated file; DO NOT EDIT.");
+ ut_assert_skip_to_linen("# Compiler:");
+ ut_assert_skip_to_line("CONFIG_CMD_CONFIG=y");
+
+ console_record_reset_enable();
+
+ ut_assertok(run_command("config cmd_config=y", 0));
+ ut_assert_nextline("CONFIG_CMD_CONFIG=y");
+ ut_assert_console_end();
+
+ ut_assertok(run_command("config 'this string never appears in .config'", 0));
+ ut_assert_console_end();
+
+ return 0;
+}
+CMD_TEST(cmd_test_config, UTF_CONSOLE);
diff --git a/test/cmd/unzip.c b/test/cmd/unzip.c
index 623a2785884..e33b6c3fb3a 100644
--- a/test/cmd/unzip.c
+++ b/test/cmd/unzip.c
@@ -101,11 +101,10 @@ static int dm_test_cmd_zip_unzip(struct unit_test_state *uts)
}
DM_TEST(dm_test_cmd_zip_unzip, UTF_CONSOLE);
-static int dm_test_cmd_zip_gzwrite(struct unit_test_state *uts)
+static int bind_mmc9(struct unit_test_state *uts)
{
struct udevice *dev;
ofnode root, node;
- int i, j, ret;
/* Enable the mmc9 node for this test */
root = oftree_root(oftree_default());
@@ -113,6 +112,15 @@ static int dm_test_cmd_zip_gzwrite(struct unit_test_state *uts)
ut_assert(ofnode_valid(node));
ut_assertok(lists_bind_fdt(gd->dm_root, node, &dev, NULL, false));
+ return 0;
+}
+
+static int dm_test_cmd_zip_gzwrite(struct unit_test_state *uts)
+{
+ int i, j, ret;
+
+ ut_assertok(bind_mmc9(uts));
+
for (i = 0; i < ARRAY_SIZE(sizes); i++) {
ret = do_test_cmd_zip_unzip(uts, sizes[i], true);
if (ret)
@@ -132,3 +140,109 @@ static int dm_test_cmd_zip_gzwrite(struct unit_test_state *uts)
return 0;
}
DM_TEST(dm_test_cmd_zip_gzwrite, UTF_CONSOLE);
+
+/*
+ * Regression test for the case where a decompression input chunk is
+ * exhausted at exactly the same time as the write buffer fills up, in
+ * which case gzwrite() used to call inflate() again with no input,
+ * receive Z_BUF_ERROR back and treat it as a fatal error.
+ *
+ * Craft a gzip file by hand from two stored (uncompressed) deflate
+ * blocks of 1 KiB each, and pick a chunk size that covers exactly the
+ * 5 byte header plus payload of the first stored block, so that with a
+ * 1 KiB write buffer the first chunk runs out precisely when the write
+ * buffer is full.
+ */
+#define STORED_BLK_HDR_LEN 5 /* deflate stored block header size */
+#define STORED_BLK_LEN SZ_1K /* payload bytes per stored block */
+
+static int gzwrite_chunk_boundary(struct unit_test_state *uts)
+{
+ static const u8 gzip_hdr[10] = {
+ 0x1f, 0x8b, /* magic */
+ 0x08, /* deflate */
+ 0x00, /* no flags */
+ 0x00, 0x00, 0x00, 0x00, /* mtime */
+ 0x00, /* extra flags */
+ 0x03, /* OS: unix */
+ };
+ unsigned long loadaddr = env_get_ulong("loadaddr", 16, 0);
+ unsigned long decaddr = loadaddr + SZ_1M;
+ u8 raw[2 * STORED_BLK_LEN];
+ const size_t rawsize = sizeof(raw);
+ unsigned char *gzmap = map_sysmem(loadaddr, sizeof(gzip_hdr) +
+ 2 * (STORED_BLK_HDR_LEN +
+ STORED_BLK_LEN) + 8);
+ unsigned char *decmap = map_sysmem(decaddr, rawsize);
+ struct blk_desc *mmc_dev_desc;
+ const u16 len = STORED_BLK_LEN;
+ const u16 nlen = ~STORED_BLK_LEN & 0xffff;
+ size_t gzlen, cnt;
+ u8 *p = gzmap;
+ u32 crc;
+ int i;
+
+ ut_assertok(bind_mmc9(uts));
+
+ for (i = 0; i < rawsize; i++)
+ raw[i] = (i * 251) & 0xff;
+ crc = crc32(0, raw, rawsize);
+
+ memcpy(p, gzip_hdr, sizeof(gzip_hdr));
+ p += sizeof(gzip_hdr);
+ for (i = 0; i < 2; i++) {
+ *p++ = (i == 1) ? 0x01 : 0x00; /* BFINAL on last block */
+ *p++ = len & 0xff; /* LEN */
+ *p++ = len >> 8;
+ *p++ = nlen & 0xff; /* NLEN */
+ *p++ = nlen >> 8;
+ memcpy(p, raw + i * STORED_BLK_LEN, STORED_BLK_LEN);
+ p += STORED_BLK_LEN;
+ }
+ *p++ = crc & 0xff; /* CRC32, little endian */
+ *p++ = (crc >> 8) & 0xff;
+ *p++ = (crc >> 16) & 0xff;
+ *p++ = (crc >> 24) & 0xff;
+ *p++ = rawsize & 0xff; /* ISIZE, little endian */
+ *p++ = (rawsize >> 8) & 0xff;
+ *p++ = (rawsize >> 16) & 0xff;
+ *p++ = (rawsize >> 24) & 0xff;
+ gzlen = p - gzmap;
+
+ ut_assertok(run_commandf("gzwrite mmc 9 %lx %zx %x", loadaddr,
+ gzlen, STORED_BLK_LEN));
+ ut_assert_skip_to_line("\t%zu bytes, crc 0x%08x", rawsize, crc);
+
+ ut_asserteq(9, blk_get_device_by_str("mmc", "9", &mmc_dev_desc));
+ cnt = rawsize / mmc_dev_desc->blksz;
+ ut_assertok(run_commandf("mmc dev 9"));
+ ut_assert_nextline("switch to partitions #0, OK");
+ ut_assert_nextline("mmc9 is current device");
+
+ ut_assertok(run_commandf("mmc read %lx 0 %zx", decaddr, cnt));
+ ut_assert_nextline("MMC read: dev # 9, block # 0, count %zu ... %zu blocks read: OK",
+ cnt, cnt);
+
+ ut_asserteq_mem(raw, decmap, rawsize);
+
+ ut_assert_console_end();
+
+ unmap_sysmem(gzmap);
+ unmap_sysmem(decmap);
+
+ return 0;
+}
+
+static int dm_test_cmd_gzwrite_chunk_boundary(struct unit_test_state *uts)
+{
+ int ret;
+
+ /* Input chunk: exactly one stored block header plus its payload */
+ ut_assertok(env_set_ulong("gzwrite_chunk",
+ STORED_BLK_HDR_LEN + STORED_BLK_LEN));
+ ret = gzwrite_chunk_boundary(uts);
+ ut_assertok(env_set("gzwrite_chunk", NULL));
+
+ return ret;
+}
+DM_TEST(dm_test_cmd_gzwrite_chunk_boundary, UTF_CONSOLE);
diff --git a/test/dm/Makefile b/test/dm/Makefile
index 0e3c63568dd..fb3e6a7008f 100644
--- a/test/dm/Makefile
+++ b/test/dm/Makefile
@@ -46,6 +46,7 @@ obj-$(CONFIG_DMA) += dma.o
obj-$(CONFIG_VIDEO_MIPI_DSI) += dsi_host.o
obj-$(CONFIG_DM_DSA) += dsa.o
obj-$(CONFIG_ECDSA_VERIFY) += ecdsa.o
+obj-$(CONFIG_DM_HASH) += hash.o
obj-$(CONFIG_EFI_MEDIA_SANDBOX) += efi_media.o
obj-$(CONFIG_DM_ETH) += eth.o
obj-$(CONFIG_EXTCON) += extcon.o
@@ -95,6 +96,7 @@ obj-$(CONFIG_PINCONF) += pinmux.o
endif
obj-$(CONFIG_POWER_DOMAIN) += power-domain.o
obj-$(CONFIG_ACPI_PMC) += pmc.o
+obj-$(CONFIG_CMD_PMBUS) += pmbus.o
obj-$(CONFIG_DM_PMIC) += pmic.o
obj-$(CONFIG_DM_PWM) += pwm.o
obj-$(CONFIG_ARM_FFA_TRANSPORT) += ffa.o
diff --git a/test/dm/hash.c b/test/dm/hash.c
new file mode 100644
index 00000000000..6adf916dc77
--- /dev/null
+++ b/test/dm/hash.c
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Tests for driver-model hash-provider selection
+ *
+ * Copyright (C) 2026 James Hilliard
+ */
+
+#include <dm.h>
+#include <dm/device-internal.h>
+#include <dm/root.h>
+#include <dm/test.h>
+#include <dm/uclass-internal.h>
+#include <u-boot/hash.h>
+#include <test/test.h>
+#include <test/ut.h>
+
+static int unsupported_calls;
+static int success_calls;
+static int hard_error_calls;
+
+static int hash_test_unsupported(struct udevice *dev, enum HASH_ALGO algo,
+ const void *ibuf, const uint32_t ilen,
+ void *obuf, uint32_t chunk_sz)
+{
+ unsupported_calls++;
+
+ return -EOPNOTSUPP;
+}
+
+static int hash_test_success(struct udevice *dev, enum HASH_ALGO algo,
+ const void *ibuf, const uint32_t ilen,
+ void *obuf, uint32_t chunk_sz)
+{
+ ssize_t digest_size;
+
+ success_calls++;
+ digest_size = hash_algo_digest_size(algo);
+ if (digest_size < 0)
+ return digest_size;
+
+ memset(obuf, 0x5a, digest_size);
+
+ return 0;
+}
+
+static int hash_test_hard_error(struct udevice *dev, enum HASH_ALGO algo,
+ const void *ibuf, const uint32_t ilen,
+ void *obuf, uint32_t chunk_sz)
+{
+ hard_error_calls++;
+
+ return -EINVAL;
+}
+
+static const struct hash_ops hash_test_unsupported_ops = {
+ .hash_digest_wd = hash_test_unsupported,
+};
+
+static const struct hash_ops hash_test_success_ops = {
+ .hash_digest_wd = hash_test_success,
+};
+
+static const struct hash_ops hash_test_hard_error_ops = {
+ .hash_digest_wd = hash_test_hard_error,
+};
+
+U_BOOT_DRIVER(hash_test_unsupported_drv) = {
+ .name = "hash_test_unsupported",
+ .id = UCLASS_HASH,
+ .ops = &hash_test_unsupported_ops,
+};
+
+U_BOOT_DRIVER(hash_test_success_drv) = {
+ .name = "hash_test_success",
+ .id = UCLASS_HASH,
+ .ops = &hash_test_success_ops,
+};
+
+U_BOOT_DRIVER(hash_test_hard_error_drv) = {
+ .name = "hash_test_hard_error",
+ .id = UCLASS_HASH,
+ .ops = &hash_test_hard_error_ops,
+};
+
+static int hash_test_unbind_all(void)
+{
+ struct udevice *dev;
+ int ret;
+
+ for (;;) {
+ ret = uclass_find_first_device(UCLASS_HASH, &dev);
+ if (ret || !dev)
+ return ret;
+ if (device_active(dev)) {
+ ret = device_remove(dev, DM_REMOVE_NORMAL);
+ if (ret)
+ return ret;
+ }
+ ret = device_unbind(dev);
+ if (ret)
+ return ret;
+ }
+}
+
+static int hash_test_bind(const struct driver *drv, const char *name)
+{
+ struct udevice *dev;
+
+ return device_bind(dm_root(), drv, name, 0, ofnode_null(), &dev);
+}
+
+static int dm_test_hash_provider_selection(struct unit_test_state *uts)
+{
+ u8 digest[32];
+ int ret;
+
+ ut_assertok(hash_test_unbind_all());
+ ut_assertok(hash_test_bind(DM_DRIVER_GET(hash_test_unsupported_drv),
+ "hash-unsupported"));
+ ut_assertok(hash_test_bind(DM_DRIVER_GET(hash_test_success_drv),
+ "hash-success"));
+
+ unsupported_calls = 0;
+ success_calls = 0;
+ memset(digest, 0, sizeof(digest));
+ ret = hash_digest_wd_lookup(HASH_ALGO_SHA256, "test", 4, digest, 4);
+ ut_assertok(ret);
+ ut_asserteq(1, unsupported_calls);
+ ut_asserteq(1, success_calls);
+ for (int i = 0; i < sizeof(digest); i++)
+ ut_asserteq(0x5a, digest[i]);
+
+ memset(digest, 0, sizeof(digest));
+ ret = hash_digest_wd_lookup(HASH_ALGO_INVALID, "test", 4, digest, 4);
+ ut_asserteq(-EINVAL, ret);
+ ut_asserteq(2, unsupported_calls);
+ ut_asserteq(2, success_calls);
+ for (int i = 0; i < sizeof(digest); i++)
+ ut_asserteq(0, digest[i]);
+
+ ut_assertok(hash_test_unbind_all());
+ ut_assertok(hash_test_bind(DM_DRIVER_GET(hash_test_hard_error_drv),
+ "hash-hard-error"));
+ ut_assertok(hash_test_bind(DM_DRIVER_GET(hash_test_success_drv),
+ "hash-success"));
+
+ hard_error_calls = 0;
+ success_calls = 0;
+ ret = hash_digest_wd_lookup(HASH_ALGO_SHA256, "test", 4, digest, 4);
+ ut_asserteq(-EINVAL, ret);
+ ut_asserteq(1, hard_error_calls);
+ ut_asserteq(0, success_calls);
+
+ return 0;
+}
+
+DM_TEST(dm_test_hash_provider_selection, UTF_SCAN_FDT);
diff --git a/test/dm/net_defrag.c b/test/dm/net_defrag.c
index 3fd40de90cd..7501b252db9 100644
--- a/test/dm/net_defrag.c
+++ b/test/dm/net_defrag.c
@@ -80,3 +80,39 @@ static int dm_test_net_ip_defrag_dup_last(struct unit_test_state *uts)
}
DM_TEST(dm_test_net_ip_defrag_dup_last, 0);
+
+/*
+ * A fragment placed at the very top of the reassembly buffer takes the
+ * split-hole branch, which writes an 8-byte "struct hole" at
+ * pkt_buff + IP_HDR_SIZE + (offset8 + len / 8) * 8. With start + len equal to
+ * IP_MAXUDP that write reaches the end of pkt_buff and spills past it. pkt_buff
+ * is a static array, so this is flagged under AddressSanitizer; the fix rejects
+ * such a fragment instead. The datagram is incomplete, so nothing is delivered
+ * either way.
+ */
+static int dm_test_net_ip_defrag_oob(struct unit_test_state *uts)
+{
+ rxhand_f *saved_handler = net_get_udp_handler();
+ uchar frame[FRAME_LEN];
+ struct ip_udp_hdr *ip = (struct ip_udp_hdr *)(frame + ETHER_HDR_SIZE);
+ u16 payload[4] = { 0, 0, 0, 0 };
+ /* Offset (8-byte units) so that start + FRAG_LEN == IP_MAXUDP. */
+ u16 off8 = (CONFIG_NET_MAXDEFRAG - IP_HDR_SIZE - FRAG_LEN) / 8;
+
+ udp_rx_count = 0;
+ net_set_udp_handler(defrag_udp_handler);
+
+ build_frag(frame, IP_FLAGS_MFRAG | off8, payload);
+ /* A distinct id forces a fresh reassembly independent of earlier tests. */
+ ip->ip_id = htons(0x7abc);
+ ip->ip_sum = 0;
+ ip->ip_sum = compute_ip_checksum(ip, IP_HDR_SIZE);
+ net_process_received_packet(frame, FRAME_LEN);
+
+ ut_asserteq(0, udp_rx_count);
+
+ net_set_udp_handler(saved_handler);
+
+ return 0;
+}
+DM_TEST(dm_test_net_ip_defrag_oob, 0);
diff --git a/test/dm/pmbus.c b/test/dm/pmbus.c
new file mode 100644
index 00000000000..0184b201829
--- /dev/null
+++ b/test/dm/pmbus.c
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2026 Free Mobile - Vincent Jardin
+ *
+ * Unit tests for the PMBus 1.x framework, the generic
+ * PMBus regulator and the pmbus CLI command.
+ */
+
+#include <dm.h>
+#include <i2c.h>
+#include <pmbus.h>
+#include <dm/test.h>
+#include <test/test.h>
+#include <test/ut.h>
+
+/* The line pmbus dev prints when a chip is selected */
+#define PMBUS_ACTIVE_LINE \
+ "pmbus: active i2c0:0x70 rail=\"sandbox-pmbus-vout\" " \
+ "MFR_ID=\"SANDBOX\" MODEL=\"PMBUS-EMUL\" vendor=(generic)"
+
+/* The line pmbus list prints for the bound chip */
+#define PMBUS_LIST_LINE \
+ " i2c0:0x70 rail=\"sandbox-pmbus-vout\" node=pmbus@70 " \
+ "driver=pmbus_generic_regulator"
+
+/* Select the emulated chip and check the resulting banner line */
+static int pmbus_select(struct unit_test_state *uts)
+{
+ ut_assertok(run_command("pmbus dev 0:70", 0));
+ ut_assert_nextline(PMBUS_ACTIVE_LINE);
+ return 0;
+}
+
+/* The chip is reachable via UCLASS_REGULATOR (compatible = "pmbus") */
+static int dm_test_pmbus_bind(struct unit_test_state *uts)
+{
+ struct udevice *dev;
+
+ ut_assertok(uclass_get_device_by_name(UCLASS_REGULATOR, "pmbus@70",
+ &dev));
+ ut_asserteq_str("pmbus_generic_regulator", dev->driver->name);
+ ut_asserteq(UCLASS_I2C, device_get_uclass_id(dev_get_parent(dev)));
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_bind, UTF_SCAN_FDT);
+
+/* pmbus dev by <bus>:<addr> and by regulator-name select the chip */
+static int dm_test_pmbus_dev(struct unit_test_state *uts)
+{
+ ut_assertok(run_command("pmbus dev 0:70", 0));
+ ut_assert_nextline(PMBUS_ACTIVE_LINE);
+ ut_assert_console_end();
+
+ /* Selecting by DT regulator-name resolves to the same chip */
+ ut_assertok(run_command("pmbus dev sandbox-pmbus-vout", 0));
+ ut_assert_nextline(PMBUS_ACTIVE_LINE);
+ ut_assert_console_end();
+
+ /* pmbus dev with no argument reprints the active chip */
+ ut_assertok(run_command("pmbus dev", 0));
+ ut_assert_nextline(PMBUS_ACTIVE_LINE);
+ ut_assert_console_end();
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_dev, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus list enumerates the bound chip among the UCLASS_REGULATOR devices */
+static int dm_test_pmbus_list(struct unit_test_state *uts)
+{
+ ut_assertok(run_command("pmbus list", 0));
+ ut_assert_skip_to_line(PMBUS_LIST_LINE);
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_list, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus info decodes identification + the detected driver_info */
+static int dm_test_pmbus_info(struct unit_test_state *uts)
+{
+ ut_assertok(pmbus_select(uts));
+
+ ut_assertok(run_command("pmbus info", 0));
+ ut_assert_nextline("pmbus device i2c0:0x70");
+ ut_assert_nextline(" regulator-name: \"sandbox-pmbus-vout\"");
+ ut_assert_nextline(" MFR_ID : \"SANDBOX\"");
+ ut_assert_nextline(" MFR_MODEL : \"PMBUS-EMUL\"");
+ ut_assert_nextline(" MFR_REVISION : \"1.0\" raw=0x312e30");
+ ut_assert_nextline(" PMBUS_REVISION: 0x33 (PMBus 1.3)");
+ ut_assert_nextline(" vendor : (none)");
+ ut_assert_nextline(" driver_info : pages=1");
+ ut_assert_nextline(" [VOLTAGE_IN ] format=LINEAR");
+ ut_assert_nextline(" [VOLTAGE_OUT ] format=LINEAR");
+ ut_assert_nextline(" [CURRENT_IN ] format=LINEAR");
+ ut_assert_nextline(" [CURRENT_OUT ] format=LINEAR");
+ ut_assert_nextline(" [POWER ] format=LINEAR");
+ ut_assert_nextline(" [TEMPERATURE ] format=LINEAR");
+ ut_assert_console_end();
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_info, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/*
+ * pmbus telemetry decodes the implemented sensors and prints
+ * "(not supported)" for the commands the emulator NAKs (READ_IIN,
+ * READ_POUT). LINEAR11 is used for VIN/IOUT/TEMP, LINEAR16 (with the
+ * VOUT_MODE 2^-8 exponent) for VOUT.
+ */
+static int dm_test_pmbus_telemetry(struct unit_test_state *uts)
+{
+ ut_assertok(pmbus_select(uts));
+
+ ut_assertok(run_command("pmbus telemetry", 0));
+ ut_assert_nextline("pmbus telemetry @ i2c0:0x70");
+ ut_assert_nextline(" VIN : raw=0x0abc 1400.000V");
+ ut_assert_nextline(" VOUT : raw=0x0200 2.000V");
+ ut_assert_nextline(" IIN : (not supported)");
+ ut_assert_nextline(" IOUT : raw=0x0123 291.000A");
+ ut_assert_nextline(" POUT : (not supported)");
+ ut_assert_nextline(" TEMP : raw=0x0019 25.000C");
+ ut_assert_console_end();
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_telemetry, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus status decodes every STATUS_* register; the emulator is clean */
+static int dm_test_pmbus_status(struct unit_test_state *uts)
+{
+ ut_assertok(pmbus_select(uts));
+
+ ut_assertok(run_command("pmbus status", 0));
+ ut_assert_nextline("pmbus status @ i2c0:0x70");
+ ut_assert_nextline(" STATUS_WORD (79h) = 0x0000 [clean]");
+ ut_assert_nextline(" STATUS_VOUT (7Ah) = 0x00 [clean]");
+ ut_assert_nextline(" STATUS_IOUT (7Bh) = 0x00 [clean]");
+ ut_assert_nextline(" STATUS_INPUT (7Ch) = 0x00 [clean]");
+ ut_assert_nextline(" STATUS_TEMP (7Dh) = 0x00 [clean]");
+ ut_assert_nextline(" STATUS_CML (7Eh) = 0x00 [clean]");
+ ut_assert_console_end();
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_status, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus read/pmbus write raw register access (byte, word, string) */
+static int dm_test_pmbus_read_write(struct unit_test_state *uts)
+{
+ ut_assertok(pmbus_select(uts));
+
+ /* Symbolic and numeric register names both resolve */
+ ut_assertok(run_command("pmbus read VOUT_MODE", 0));
+ ut_assert_nextline(" 20h VOUT_MODE b=0x18");
+ ut_assertok(run_command("pmbus read 8b w", 0));
+ ut_assert_nextline(" 8bh READ_VOUT w=0x0200");
+ ut_assertok(run_command("pmbus read MFR_ID s", 0));
+ ut_assert_nextline(" 99h MFR_ID s=\"SANDBOX\"");
+
+ /* A word write is observable on the next read-back */
+ ut_assertok(run_command("pmbus write VOUT_COMMAND 123 w", 0));
+ ut_assert_nextline("pmbus: wrote 0x123 to 21h (VOUT_COMMAND)");
+ ut_assertok(run_command("pmbus read VOUT_COMMAND w", 0));
+ ut_assert_nextline(" 21h VOUT_COMMAND w=0x0123");
+ ut_assert_console_end();
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_read_write, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus vout reads back VOUT via the active driver_info decoder */
+static int dm_test_pmbus_vout(struct unit_test_state *uts)
+{
+ ut_assertok(pmbus_select(uts));
+
+ ut_assertok(run_command("pmbus vout", 0));
+ ut_assert_nextline("pmbus VOUT @ i2c0:0x70 raw=0x0200 2.000V");
+ ut_assert_console_end();
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_vout, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus dump walks every standard register; spot-check one line */
+static int dm_test_pmbus_dump(struct unit_test_state *uts)
+{
+ ut_assertok(pmbus_select(uts));
+
+ ut_assertok(run_command("pmbus dump", 0));
+ ut_assert_nextline("pmbus dump @ i2c0:0x70 (registers known to <pmbus.h>)");
+ ut_assert_skip_to_line(" 20h VOUT_MODE b=0x18");
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_dump, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus clear [faults] issues CLEAR_FAULTS (03h) */
+static int dm_test_pmbus_clear(struct unit_test_state *uts)
+{
+ ut_assertok(pmbus_select(uts));
+
+ ut_assertok(run_command("pmbus clear faults", 0));
+ ut_assert_nextline("pmbus: CLEAR_FAULTS (03h) issued (RAM sticky STATUS_* cleared)");
+ ut_assert_console_end();
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_clear, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus scan finds the emulated chip by its MFR_ID block read */
+static int dm_test_pmbus_scan(struct unit_test_state *uts)
+{
+ ut_assertok(run_command("pmbus scan 0", 0));
+ ut_assert_skip_to_line(" i2c0:0x70 MFR_ID=\"SANDBOX\"");
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_scan, UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* pmbus help lists vendor extensions; none are registered here */
+static int dm_test_pmbus_help(struct unit_test_state *uts)
+{
+ ut_assertok(run_command("pmbus help", 0));
+ ut_assert_nextline("pmbus: no vendor extensions registered.");
+ ut_assert_skip_to_line(" board hook (boot snapshot) and re run 'pmbus help'.");
+
+ return 0;
+}
+
+DM_TEST(dm_test_pmbus_help, UTF_SCAN_FDT | UTF_CONSOLE);
diff --git a/test/dm/regulator.c b/test/dm/regulator.c
index 449748ad52f..51007d4079d 100644
--- a/test/dm/regulator.c
+++ b/test/dm/regulator.c
@@ -28,6 +28,7 @@ enum {
BUCK3,
LDO1,
LDO2,
+ LDO3,
OUTPUT_COUNT,
};
@@ -44,6 +45,7 @@ static const char *regulator_names[OUTPUT_COUNT][OUTPUT_NAME_COUNT] = {
{ SANDBOX_BUCK3_DEVNAME, SANDBOX_BUCK3_PLATNAME },
{ SANDBOX_LDO1_DEVNAME, SANDBOX_LDO1_PLATNAME},
{ SANDBOX_LDO2_DEVNAME, SANDBOX_LDO2_PLATNAME},
+ { SANDBOX_LDO3_DEVNAME, SANDBOX_LDO3_PLATNAME},
};
/* Test regulator get method */
@@ -118,6 +120,42 @@ static int dm_test_power_regulator_set_get_voltage(struct unit_test_state *uts)
}
DM_TEST(dm_test_power_regulator_set_get_voltage, UTF_SCAN_FDT);
+/* Test regulator set Voltage clamp method */
+static int dm_test_power_regulator_set_value_clamp(struct unit_test_state *uts)
+{
+ struct udevice *dev;
+ const char *platname;
+
+ /* LDO3 have 'min' 1.8V and 'max' 3.3V */
+ platname = regulator_names[LDO3][PLATNAME];
+ ut_assertok(regulator_get_by_platname(platname, &dev));
+
+ /* 'target' in 'min'/'max' range - should not clamp voltage */
+ ut_assertok(regulator_set_value_clamp(dev, 1700000, 1800000, 1950000));
+ ut_asserteq(1800000, regulator_get_value(dev));
+ ut_assertok(regulator_set_value_clamp(dev, 2700000, 3300000, 3600000));
+ ut_asserteq(3300000, regulator_get_value(dev));
+
+ /* 'target' out of 'min'/'max' range - should clamp voltage */
+ ut_assertok(regulator_set_value_clamp(dev, 1700000, 1700000, 1950000));
+ ut_asserteq(1800000, regulator_get_value(dev));
+ ut_assertok(regulator_set_value_clamp(dev, 2700000, 3400000, 3600000));
+ ut_asserteq(3300000, regulator_get_value(dev));
+
+ /* 'min'/'max' out of range - should return -EINVAL */
+ ut_asserteq(-EINVAL,
+ regulator_set_value_clamp(dev, 1200000, 1500000, 1700000));
+ ut_asserteq(-EINVAL,
+ regulator_set_value_clamp(dev, 3500000, 4000000, 5000000));
+
+ /* 'min' higher than 'max' - should return -EINVAL */
+ ut_asserteq(-EINVAL,
+ regulator_set_value_clamp(dev, 3100000, 3000000, 2900000));
+
+ return 0;
+}
+DM_TEST(dm_test_power_regulator_set_value_clamp, UTF_SCAN_FDT);
+
/* Test regulator set and get Current method */
static int dm_test_power_regulator_set_get_current(struct unit_test_state *uts)
{
diff --git a/test/image/spl_load.c b/test/image/spl_load.c
index 3b6206955d3..c43c977f784 100644
--- a/test/image/spl_load.c
+++ b/test/image/spl_load.c
@@ -368,6 +368,55 @@ SPL_IMG_TEST(spl_test_image, FIT_INTERNAL, 0);
SPL_IMG_TEST(spl_test_image, FIT_EXTERNAL, 0);
/*
+ * A FIT image's data-size property is not covered by the configuration
+ * signature, so it is untrusted input. load_simple_fit() must reject a
+ * data-size larger than the destination rather than overrun it, because the
+ * device read happens before the image hash is verified.
+ */
+static int spl_test_fit_external_oversize(struct unit_test_state *uts)
+{
+ size_t img_size, img_data, data_size = SPL_TEST_DATA_SIZE;
+ struct spl_image_info info_write = {
+ .name = "oversize",
+ .size = data_size,
+ }, info_read = { };
+ struct spl_load_info load;
+ void *img;
+ int node;
+
+ if (!image_supported(FIT_EXTERNAL))
+ return -EAGAIN;
+
+ img_size = create_image(NULL, FIT_EXTERNAL, &info_write, &img_data);
+ ut_assert(img_size);
+ img = calloc(img_size, 1);
+ ut_assertnonnull(img);
+
+ generate_data(img + img_data, data_size, "oversize");
+ ut_asserteq(img_size, create_image(img, FIT_EXTERNAL, &info_write,
+ NULL));
+
+ /*
+ * Inflate data-size far beyond the image buffer and any plausible
+ * load region. Without a bounds check, load_simple_fit() reads this
+ * many bytes off the "device" before the hash is checked.
+ */
+ node = fdt_path_offset(img, FIT_IMAGES_PATH);
+ ut_assert(node >= 0);
+ node = fdt_first_subnode(img, node);
+ ut_assert(node >= 0);
+ ut_assertok(fdt_setprop_inplace_u32(img, node, FIT_DATA_SIZE_PROP,
+ 0x40000000));
+
+ spl_load_init(&load, spl_test_read, img, 1);
+ ut_asserteq(-EFBIG, spl_load_simple_fit(&info_read, &load, 0, img));
+
+ free(img);
+ return 0;
+}
+SPL_TEST(spl_test_fit_external_oversize, 0);
+
+/*
* LZMA is too complex to generate on the fly, so let's use some data I put in
* the oven^H^H^H^H compressed earlier
*/
diff --git a/test/lib/lmb.c b/test/lib/lmb.c
index b6259bef442..168c66ae649 100644
--- a/test/lib/lmb.c
+++ b/test/lib/lmb.c
@@ -477,7 +477,7 @@ static int lib_test_lmb_at_0(struct unit_test_state *uts)
0, 0, 0, 0);
/* check that this was an error by freeing b */
ret = lmb_free(b, 4, LMB_NONE);
- ut_asserteq(ret, -1);
+ ut_asserteq(ret, -EFAULT);
ASSERT_LMB(mem_lst, used_lst, ram, ram_size, 1, a, ram_size - 4,
0, 0, 0, 0);
@@ -779,11 +779,19 @@ static int test_alloc_addr(struct unit_test_state *uts, const phys_addr_t ram)
/* check that allocating outside memory fails */
if (ram_end != 0) {
ret = lmb_alloc_addr(ram_end, 1, LMB_NONE);
+ ut_asserteq(ret, -EFAULT);
+ ret = lmb_alloc_addr(ram_end - 1, 2, LMB_NOMAP);
+ ut_asserteq(ret, -EINVAL);
+ ret = lmb_alloc_addr(ram_end - 1, 2, LMB_NOOVERWRITE);
ut_asserteq(ret, -EINVAL);
}
if (ram != 0) {
ret = lmb_alloc_addr(ram - 1, 1, LMB_NONE);
- ut_asserteq(ret, -EINVAL);
+ ut_asserteq(ret, -EFAULT);
+ ret = lmb_alloc_addr(ram - 1, 2, LMB_NOMAP);
+ ut_asserteq(ret, -EEXIST);
+ ret = lmb_alloc_addr(ram - 1, 2, LMB_NOOVERWRITE);
+ ut_asserteq(ret, -EEXIST);
}
lmb_pop(&store);
diff --git a/test/lib/string.c b/test/lib/string.c
index db6f28dbfdf..d418a40c4d4 100644
--- a/test/lib/string.c
+++ b/test/lib/string.c
@@ -284,18 +284,36 @@ static int lib_strstr(struct unit_test_state *uts)
{
const char *s1 = "Itsy Bitsy Teenie Weenie";
const char *s2 = "eenie";
- const char *s3 = "easy";
+ const char *s3 = "bits";
ut_asserteq_ptr(&s1[12], strstr(s1, s2));
ut_asserteq_ptr(&s1[13], strstr(&s1[3], &s2[1]));
ut_assertnull(strstr(s1, s3));
- ut_asserteq_ptr(&s1[2], strstr(s1, &s3[2]));
- ut_asserteq_ptr(&s1[8], strstr(&s1[5], &s3[2]));
+ ut_asserteq_ptr(&s1[1], strstr(s1, &s3[2]));
+ ut_asserteq_ptr(&s1[7], strstr(&s1[5], &s3[2]));
return 0;
}
LIB_TEST(lib_strstr, 0);
+/** lib_strcasestr() - unit test for strcasestr() */
+static int lib_strcasestr(struct unit_test_state *uts)
+{
+ const char *s1 = "Itsy Bitsy Teenie Weenie";
+ const char *s2 = "eenie";
+ const char *s3 = "bits";
+
+ ut_asserteq_ptr(&s1[12], strcasestr(s1, s2));
+ ut_asserteq_ptr(&s1[13], strcasestr(&s1[3], &s2[1]));
+ ut_asserteq_ptr(&s1[5], strcasestr(s1, s3));
+ ut_asserteq_ptr(&s1[1], strcasestr(s1, &s3[2]));
+ ut_asserteq_ptr(&s1[7], strcasestr(&s1[5], &s3[2]));
+ ut_assertnull(strcasestr(&s1[6], s3));
+
+ return 0;
+}
+LIB_TEST(lib_strcasestr, 0);
+
static int lib_strim(struct unit_test_state *uts)
{
char buf[BUFLEN], *p;
diff --git a/test/py/tests/test_efi_secboot/conftest.py b/test/py/tests/test_efi_secboot/conftest.py
index 76b8f9fa0a3..0755497d46d 100644
--- a/test/py/tests/test_efi_secboot/conftest.py
+++ b/test/py/tests/test_efi_secboot/conftest.py
@@ -96,6 +96,14 @@ def efi_boot_env(request, ubman):
check_call('cd %s; %ssign-efi-sig-list -t "2020-04-05" -c KEK.crt -k KEK.key dbx db.esl dbx_db.auth'
% (mnt_point, EFITOOLS_PATH),
shell=True)
+ # dbt (with TEST_db certificate)
+ check_call('cd %s; %ssign-efi-sig-list -t "2020-04-05" -c KEK.crt -k KEK.key dbt db.esl dbt.auth'
+ % (mnt_point, EFITOOLS_PATH),
+ shell=True)
+ # dbr (with TEST_db certificate)
+ check_call('cd %s; %ssign-efi-sig-list -t "2020-04-05" -c KEK.crt -k KEK.key dbr db.esl dbr.auth'
+ % (mnt_point, EFITOOLS_PATH),
+ shell=True)
# Copy image
check_call('cp %s/lib/efi_loader/helloworld.efi %s' %
diff --git a/test/py/tests/test_efi_secboot/test_authvar.py b/test/py/tests/test_efi_secboot/test_authvar.py
index 7b45f8fb814..a41e9eb9204 100644
--- a/test/py/tests/test_efi_secboot/test_authvar.py
+++ b/test/py/tests/test_efi_secboot/test_authvar.py
@@ -279,3 +279,50 @@ class TestEfiAuthVar(object):
output = ubman.run_command(
'printenv -e SetupMode')
assert '00000000: 01' in output
+
+ def test_efi_var_auth6(self, ubman, efi_boot_env):
+ """
+ Test Case 6 - Default GUID of signature database variables
+ """
+ ubman.restart_uboot()
+ disk_img = efi_boot_env
+ with ubman.log.section('Test Case 6a'):
+ # Test Case 6a, install signature database variables in setup
+ # mode without -guid
+ output = ubman.run_command_list([
+ 'host bind 0 %s' % disk_img,
+ 'printenv -e SetupMode'])
+ assert '00000000: 01' in ''.join(output)
+
+ for var in ('db', 'dbx', 'dbt', 'dbr'):
+ output = ubman.run_command_list([
+ 'fatload host 0:1 4000000 %s.auth' % var,
+ 'setenv -e -nv -bs -rt -at -i 4000000:$filesize %s' % var,
+ 'printenv -e -n -guid d719b2cb-3d3a-4596-a3bc-dad00e67656f %s' % var])
+ assert 'Failed to set EFI variable' not in ''.join(output)
+ assert '%s:' % var in ''.join(output)
+
+ with ubman.log.section('Test Case 6b'):
+ # Test Case 6b, variables must not exist under the global
+ # variable GUID
+ for var in ('db', 'dbx', 'dbt', 'dbr'):
+ output = ubman.run_command(
+ 'printenv -e -n -guid 8be4df61-93ca-11d2-aa0d-00e098032b8c %s' % var)
+ assert '\"%s\" not defined' % var in output
+
+ with ubman.log.section('Test Case 6c'):
+ # Test Case 6c, PK and KEK get the global variable GUID by
+ # default. Enrolling PK leaves setup mode, so this must come
+ # after the signature database enrollment above.
+ for var in ('PK', 'KEK'):
+ output = ubman.run_command_list([
+ 'fatload host 0:1 4000000 %s.auth' % var,
+ 'setenv -e -nv -bs -rt -at -i 4000000:$filesize %s' % var,
+ 'printenv -e -n -guid 8be4df61-93ca-11d2-aa0d-00e098032b8c %s' % var])
+ assert 'Failed to set EFI variable' not in ''.join(output)
+ assert '%s:' % var in ''.join(output)
+
+ for var in ('PK', 'KEK'):
+ output = ubman.run_command(
+ 'printenv -e -n -guid d719b2cb-3d3a-4596-a3bc-dad00e67656f %s' % var)
+ assert '\"%s\" not defined' % var in output
diff --git a/test/py/tests/test_fit_import_data.py b/test/py/tests/test_fit_import_data.py
new file mode 100644
index 00000000000..efbbad14262
--- /dev/null
+++ b/test/py/tests/test_fit_import_data.py
@@ -0,0 +1,89 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright 2026 Canonical Ltd.
+#
+# Test mkimage import of external data in fit_import_data()
+
+"""Regression test for stale per-image state in fit_import_data().
+
+The import loop used to keep the data pointer and external property name
+of the previous image, so an image node carrying data-size but neither
+data-offset nor data-position imported the previous image's data and
+then made mkimage abort without printing any diagnostic. Such a node
+must be skipped by the import and reported by the later processing
+stages instead.
+"""
+
+import os
+import subprocess
+
+import pytest
+
+import fit_util
+
+BASE_ITS = '''
+/dts-v1/;
+
+/ {
+ description = "import-data test";
+
+ images {
+ kernel-1 {
+ description = "first kernel";
+ data = /incbin/("%(kernel1)s");
+ type = "kernel";
+ arch = "sandbox";
+ os = "linux";
+ compression = "none";
+ load = <0x40000>;
+ entry = <0x40000>;
+ };
+ kernel-2 {
+ description = "second kernel";
+ data = /incbin/("%(kernel2)s");
+ type = "kernel";
+ arch = "sandbox";
+ os = "linux";
+ compression = "none";
+ load = <0x80000>;
+ entry = <0x80000>;
+ };
+ };
+
+ configurations {
+ default = "conf-1";
+ conf-1 {
+ kernel = "kernel-1";
+ };
+ };
+};
+'''
+
+
+def test_fit_import_data_missing_offset(ubman):
+ """An image with data-size but no data-offset must not inherit data"""
+ mkimage = os.path.join(ubman.config.build_dir, 'tools/mkimage')
+ params = {
+ 'kernel1': fit_util.make_kernel(ubman, 'imp-kernel1.bin', 'first'),
+ 'kernel2': fit_util.make_kernel(ubman, 'imp-kernel2.bin', 'second'),
+ }
+ its = fit_util.make_its(ubman, BASE_ITS, params, 'imp.its')
+ itb = fit_util.make_fname(ubman, 'imp.itb')
+
+ result = subprocess.run([mkimage, '-E', '-f', its, itb],
+ capture_output=True, text=True)
+ assert result.returncode == 0, result.stderr
+
+ # Remove the offset so that only data-size is left on kernel-2
+ subprocess.run(['fdtput', '-d', itb, '/images/kernel-2', 'data-offset'],
+ check=True)
+
+ # Re-processing must skip the malformed image in the import, so that
+ # the hashing stage reports it; previously the stale pointer made the
+ # import write kernel-1's data into kernel-2 and abort silently
+ result = subprocess.run([mkimage, '-F', itb],
+ capture_output=True, text=True)
+ assert result.returncode != 0
+ assert "Can't get image data/size" in result.stderr
diff --git a/test/py/tests/test_fit_verity_sign.py b/test/py/tests/test_fit_verity_sign.py
new file mode 100644
index 00000000000..3c75ef8558c
--- /dev/null
+++ b/test/py/tests/test_fit_verity_sign.py
@@ -0,0 +1,203 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2026 Daniel Golle <[email protected]>
+
+"""Verify that the dm-verity roothash is covered by the FIT configuration
+signature.
+
+A dm-verity protected filesystem image is not hashed by U-Boot; its integrity
+is delegated to the kernel, which trusts the roothash taken from the FIT
+``dm-verity`` subnode. That roothash must therefore be part of the signed
+region of the configuration, otherwise an attacker can replace both the
+filesystem and the roothash while keeping the configuration signature valid.
+
+This test signs a configuration referencing a filesystem image that carries a
+``dm-verity`` subnode, then flips one byte of the roothash and of the salt and
+checks that verification rejects the image. A control tampering a byte that is
+known to be signed confirms that the check is able to detect a broken region.
+
+The FIT pairs a signed configuration with a filesystem image carrying a
+``dm-verity`` subnode:
+
+.. code-block:: devicetree
+
+ images {
+ rootfs-1 {
+ data = /incbin/("rootfs.bin");
+ type = "filesystem";
+ compression = "none";
+ hash-1 {
+ algo = "sha256";
+ };
+ dm-verity {
+ algo = "sha256";
+ data-block-size = <4096>;
+ hash-block-size = <4096>;
+ num-data-blocks = <16>;
+ hash-start-block = <16>;
+ };
+ };
+ };
+
+ configurations {
+ conf-1 {
+ kernel = "kernel-1";
+ loadables = "rootfs-1";
+ signature-1 {
+ algo = "sha256,rsa2048";
+ key-name-hint = "dev";
+ sign-images = "kernel", "loadables";
+ };
+ };
+ };
+
+mkimage builds the dm-verity hash tree when assembling the image and records
+the resulting roothash and salt in the ``dm-verity`` subnode; fit_check_sign
+must reject an image where either was modified after signing.
+"""
+
+import os
+import pytest
+import utils
+
+# 16 blocks of 4096 bytes, matching num-data-blocks/data-block-size below.
+ROOTFS_SIZE = 16 * 4096
+
+ITS = '''
+/dts-v1/;
+/ {
+ description = "verity roothash signing coverage test";
+ #address-cells = <1>;
+
+ images {
+ kernel-1 {
+ description = "kernel";
+ data = /incbin/("kernel.bin");
+ type = "kernel";
+ arch = "arm64";
+ os = "linux";
+ compression = "none";
+ load = <0x40000000>;
+ entry = <0x40000000>;
+ hash-1 { algo = "sha256"; };
+ };
+ rootfs-1 {
+ description = "rootfs";
+ data = /incbin/("rootfs.bin");
+ type = "filesystem";
+ arch = "arm64";
+ compression = "none";
+ hash-1 { algo = "sha256"; };
+ dm-verity {
+ algo = "sha256";
+ data-block-size = <4096>;
+ hash-block-size = <4096>;
+ num-data-blocks = <16>;
+ hash-start-block = <16>;
+ };
+ };
+ };
+
+ configurations {
+ default = "conf-1";
+ conf-1 {
+ description = "signed config";
+ kernel = "kernel-1";
+ loadables = "rootfs-1";
+ signature-1 {
+ algo = "sha256,rsa2048";
+ key-name-hint = "dev";
+ sign-images = "kernel", "loadables";
+ };
+ };
+ };
+};
+'''
+
+VERITY_NODE = '/images/rootfs-1/dm-verity'
+ROOTFS_HASH_NODE = '/images/rootfs-1/hash-1'
+
+
+def flip_prop_byte(ubman, fit, node, prop):
+ """Flip the first byte of a byte-array property in a FIT, in place.
+
+ The property is rewritten with the same length so that no node is
+ relaid out and the signed regions keep their offsets.
+ """
+ val = utils.run_and_log(ubman, 'fdtget -t bx %s %s %s' % (fit, node, prop))
+ bytelist = val.split()
+ bytelist[0] = '%x' % (int(bytelist[0], 16) ^ 0xff)
+ utils.run_and_log(ubman, 'fdtput -t bx %s %s %s %s' %
+ (fit, node, prop, ' '.join(bytelist)))
+
+
[email protected]('fit_signature')
[email protected]('veritysetup')
+def test_fit_verity_roothash_signed(ubman):
+ """The dm-verity roothash must be inside the signed configuration region."""
+ tmpdir = os.path.join(ubman.config.result_dir, 'verity-sign') + '/'
+ if not os.path.exists(tmpdir):
+ os.makedirs(tmpdir)
+ mkimage = ubman.config.build_dir + '/tools/mkimage'
+ fit_check_sign = ubman.config.build_dir + '/tools/fit_check_sign'
+ dtc_args = '-I dts -O dtb -i %s' % tmpdir
+ its = tmpdir + 'verity.its'
+ fit = tmpdir + 'verity.itb'
+ dtb = tmpdir + 'control.dtb'
+
+ # Signing key and empty control dtb to receive the public key.
+ utils.run_and_log(ubman, 'openssl genpkey -algorithm RSA -out %sdev.key '
+ '-pkeyopt rsa_keygen_bits:2048 '
+ '-pkeyopt rsa_keygen_pubexp:65537' % tmpdir)
+ utils.run_and_log(ubman, 'openssl req -batch -new -x509 -key %sdev.key '
+ '-out %sdev.crt' % (tmpdir, tmpdir))
+ with open(tmpdir + 'control.dts', 'w') as f:
+ f.write('/dts-v1/; / { model = "verity-test"; };\n')
+ utils.run_and_log(ubman, 'dtc -O dtb -o %s %scontrol.dts' % (dtb, tmpdir))
+
+ # Payloads. The rootfs must be a whole number of data blocks so mkimage can
+ # build the dm-verity hash tree and compute the roothash.
+ with open(tmpdir + 'rootfs.bin', 'wb') as f:
+ f.write(b'R' * ROOTFS_SIZE)
+ with open(tmpdir + 'kernel.bin', 'wb') as f:
+ f.write(b'KERNEL')
+
+ with open(its, 'w') as f:
+ f.write(ITS)
+
+ # Build and sign. -E keeps the (large) rootfs external, as on a real device.
+ utils.run_and_log(ubman, [mkimage, '-D', dtc_args, '-E', '-f', its,
+ '-k', tmpdir, '-K', dtb, '-r', fit])
+
+ # Baseline: the freshly signed image must verify.
+ utils.run_and_log(ubman, [fit_check_sign, '-f', fit, '-k', dtb])
+
+ # Control: tampering a byte that is signed (the filesystem image hash value)
+ # must be detected. This proves the check can fail.
+ control = tmpdir + 'control.itb'
+ utils.run_and_log(ubman, 'cp %s %s' % (fit, control))
+ flip_prop_byte(ubman, control, ROOTFS_HASH_NODE, 'value')
+ utils.run_and_log_expect_exception(
+ ubman, [fit_check_sign, '-f', control, '-k', dtb],
+ 1, 'Failed to verify required signature')
+
+ # Roothash: tampering the dm-verity digest must be rejected. If the digest
+ # is outside the signed region this check passes and boot is compromised.
+ tampered = tmpdir + 'tamper-digest.itb'
+ utils.run_and_log(ubman, 'cp %s %s' % (fit, tampered))
+ flip_prop_byte(ubman, tampered, VERITY_NODE, 'digest')
+ utils.run_and_log_expect_exception(
+ ubman, [fit_check_sign, '-f', tampered, '-k', dtb],
+ 1, 'Failed to verify required signature')
+
+ # Salt: likewise, the salt feeds the dm-verity target and must be signed.
+ tampered = tmpdir + 'tamper-salt.itb'
+ utils.run_and_log(ubman, 'cp %s %s' % (fit, tampered))
+ flip_prop_byte(ubman, tampered, VERITY_NODE, 'salt')
+ utils.run_and_log_expect_exception(
+ ubman, [fit_check_sign, '-f', tampered, '-k', dtb],
+ 1, 'Failed to verify required signature')
diff --git a/test/py/tests/test_fs/test_squashfs/test_sqfs_overflow.py b/test/py/tests/test_fs/test_squashfs/test_sqfs_overflow.py
new file mode 100644
index 00000000000..df7f875a58f
--- /dev/null
+++ b/test/py/tests/test_fs/test_squashfs/test_sqfs_overflow.py
@@ -0,0 +1,75 @@
+# SPDX-License-Identifier: GPL-2.0
+# Regression test for the SquashFS directory-table integer overflow.
+
+import os
+import struct
+import pytest
+
+# metadata block size (SQFS_METADATA_BLOCK_SIZE) and metadata header size
+SQFS_METADATA_BLOCK_SIZE = 8192
+# metablks_count that makes metablks_count * SQFS_METADATA_BLOCK_SIZE wrap a
+# 32-bit int back down to a tiny value: (2^19 + 1) * 8192 == 2^32 + 8192.
+NR_METABLKS = (1 << 19) + 1
+
+def make_overflow_image(path):
+ """Build a SquashFS image whose directory table inflates metablks_count so
+ that metablks_count * SQFS_METADATA_BLOCK_SIZE wraps a 32-bit int, then
+ writes one block of data one metadata block past the resulting buffer."""
+ def metahdr(size):
+ # uncompressed metadata block header (bit 15 set)
+ return struct.pack('<H', 0x8000 | (size & 0x7fff))
+
+ # inode table: one small valid uncompressed metadata block
+ inode_region = metahdr(32) + b'\x00' * 32
+ inode_start = 96
+ dir_start = inode_start + len(inode_region)
+
+ # directory table: NR_METABLKS metadata blocks, only block 1 carries data
+ dir_region = bytearray()
+ dir_region += metahdr(0) # block 0: empty
+ dir_region += metahdr(200) + b'A' * 200 # block 1: data
+ dir_region += metahdr(0) * (NR_METABLKS - 2) # blocks 2..N-1: empty
+ frag_start = dir_start + len(dir_region)
+
+ sb = bytearray(96)
+ struct.pack_into('<I', sb, 0, 0x73717368) # s_magic
+ struct.pack_into('<I', sb, 4, 1) # inodes
+ struct.pack_into('<I', sb, 12, 131072) # block_size
+ struct.pack_into('<H', sb, 20, 1) # compression = gzip/zlib
+ struct.pack_into('<H', sb, 22, 17) # block_log
+ struct.pack_into('<H', sb, 26, 1) # no_ids
+ struct.pack_into('<H', sb, 28, 4) # s_major
+ struct.pack_into('<Q', sb, 48, frag_start) # id_table_start
+ struct.pack_into('<Q', sb, 56, 0xffffffffffffffff) # xattr_id_table_start
+ struct.pack_into('<Q', sb, 64, inode_start) # inode_table_start
+ struct.pack_into('<Q', sb, 72, dir_start) # directory_table_start
+ struct.pack_into('<Q', sb, 80, frag_start) # fragment_table_start
+ struct.pack_into('<Q', sb, 88, 0xffffffffffffffff) # export_table_start
+
+ img = bytearray(sb) + inode_region + dir_region
+ # pad so the directory-table block read stays within the file
+ need = ((len(img) + 511) // 512 + 1) * 512
+ img += b'\x00' * (need - len(img))
+ struct.pack_into('<Q', img, 40, len(img)) # bytes_used
+
+ with open(path, 'wb') as f:
+ f.write(img)
+
[email protected]('cmd_squashfs')
[email protected]('fs_squashfs')
+def test_sqfs_ls_dir_table_overflow(ubman):
+ """Listing a crafted image whose directory table declares an oversized
+ metadata-block count must be rejected without corrupting the heap.
+ """
+ ubman.restart_uboot()
+ image_path = os.path.join(ubman.config.build_dir, 'sqfs_dir_table_overflow')
+ make_overflow_image(image_path)
+ try:
+ ubman.run_command('host bind 0 {}'.format(image_path))
+ ubman.run_command('sqfsls host 0')
+ # The crafted image must not take the board down.
+ assert 'alive' in ubman.run_command('echo alive')
+ finally:
+ os.remove(image_path)
diff --git a/test/py/tests/test_load_sandbox.py b/test/py/tests/test_load_sandbox.py
new file mode 100644
index 00000000000..8d28a630e76
--- /dev/null
+++ b/test/py/tests/test_load_sandbox.py
@@ -0,0 +1,51 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright 2026 Free Mobile - Vincent Jardin
+
+"""Regression test for `load sandbox - <addr> <file>`.
+
+Exercises the null_dev_desc_ok dispatch added in
+"fs: dispatch null_dev_desc_ok filesystems before block lookup".
+
+It is the counterpart of test_load_semihosting.py
+"""
+
+import os
+import pytest
+
+
[email protected](scope='session')
+def sandbox_fixture(u_boot_config):
+ """Host-staged fixture file read by `load sandbox`."""
+ path = os.path.join(u_boot_config.persistent_data_dir,
+ 'sandbox-fstype.txt')
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write('Das U-Boot\n') # 11 bytes, same as test_hostfs.py / semihosting
+ yield path
+ os.remove(path)
+
+
+def test_sandbox_load(ubman, sandbox_fixture):
+ """Run `load sandbox - <addr> <file>` and check the bytes."""
+ response = ubman.run_command(
+ f'load sandbox - $loadaddr {sandbox_fixture}')
+
+ # Fixture is "Das U-Boot\n" (11 bytes).
+ assert '11 bytes read' in response
+
+ # crc32("Das U-Boot\n") -- identical to the semihosting / hostfs checks.
+ response = ubman.run_command('crc32 $loadaddr $filesize')
+ assert '==> 60cfccfc' in response
+
+
+def test_sandbox_load_offset(ubman, sandbox_fixture):
+ """Run the [bytes] [pos] variant through the same dispatch."""
+ response = ubman.run_command(
+ f'load sandbox - $loadaddr {sandbox_fixture} 4 6')
+ # bytes=4 pos=6 over "Das U-Boot\n" -> "Boot".
+ assert '4 bytes read' in response
+
+ # crc32("Boot")
+ response = ubman.run_command('crc32 $loadaddr $filesize')
+ assert '==> e6df01fa' in response
diff --git a/test/py/tests/test_net.py b/test/py/tests/test_net.py
index 27cdd73fd49..a2007c2fd3a 100644
--- a/test/py/tests/test_net.py
+++ b/test/py/tests/test_net.py
@@ -59,6 +59,19 @@ For example:
'fnu': 'ubtest-upload.bin',
}
+ # Details regarding a file that may be written to U-Boot using the tftpsrv
+ # command. This variable may be omitted or set to None if tftpsrv testing
+ # is not possible or desired. The test uses host-side curl TFTP support to
+ # upload a generated file to U-Boot. The optional tftpsrv_url entry may be
+ # used when the host must use a forwarded address instead of U-Boot's
+ # ipaddr value.
+ env__net_tftpsrv_file = {
+ 'fn': 'ubtest-tftpsrv.bin',
+ 'addr': 0x10000000,
+ 'size': 4096,
+ 'timeout': 50000,
+ }
+
# Details regarding a file that may be read from a NFS server. This variable
# may be omitted or set to None if NFS testing is not possible or desired.
env__net_nfs_readable_file = {
@@ -89,6 +102,8 @@ import utils
import uuid
import datetime
import re
+import tempfile
+import zlib
net_set_up = False
net6_set_up = False
@@ -460,3 +475,82 @@ def test_net_tftpput(ubman):
output = ubman.run_command("crc32 $fileaddr $filesize")
assert expected_tftpb_crc in output
+
+
[email protected]("cmd_crc32")
[email protected]("cmd_tftpsrv")
+def test_net_tftpsrv(ubman):
+ """Test the tftpsrv command.
+
+ A file is generated on the host, uploaded to U-Boot using TFTP and then
+ validated in U-Boot using its size and CRC32.
+
+ The details of the file to upload are provided by the boardenv_* file;
+ see the comment at the beginning of this file.
+ """
+
+ if not net_set_up:
+ pytest.skip("Network not initialized")
+
+ f = ubman.config.env.get("env__net_tftpsrv_file", None)
+ if not f:
+ pytest.skip("No tftpsrv file to write")
+
+ curl_version = utils.run_and_log(ubman, ["curl", "--version"])
+ if "tftp" not in curl_version.split():
+ pytest.skip("curl does not support TFTP")
+
+ addr = f.get("addr", None)
+ if not addr:
+ addr = utils.find_ram_base(ubman)
+
+ timeout = f.get("timeout", ubman.p.timeout)
+ timeout_secs = max(1, (timeout + 999) // 1000)
+ size = f.get("size", 4096)
+ fn = f.get("fn", "ubtest-tftpsrv.bin")
+ url = f.get("tftpsrv_url", None)
+ data = bytes([i % 251 for i in range(size)])
+ crc = "%08x" % (zlib.crc32(data) & 0xffffffff)
+
+ ip = ubman.run_command("echo $ipaddr").strip()
+ if not ip:
+ pytest.skip("No U-Boot IP address")
+ if not url:
+ url = "tftp://%s/%s" % (ip, fn)
+
+ with tempfile.NamedTemporaryFile() as tmp:
+ tmp.write(data)
+ tmp.flush()
+
+ done = False
+ with ubman.temporary_timeout(timeout):
+ try:
+ ubman.run_command("tftpsrv %x" % addr,
+ wait_for_prompt=False)
+ ubman.wait_for("Listening for TFTP transfer")
+ utils.run_and_log(
+ ubman,
+ [
+ "curl",
+ "--fail",
+ "--max-time",
+ str(timeout_secs),
+ "--upload-file",
+ tmp.name,
+ url,
+ ],
+ )
+ ubman.wait_for("Bytes transferred = %d" % size)
+ ubman.wait_for(ubman.prompt)
+ done = True
+ finally:
+ if not done:
+ ubman.ctrlc()
+ ubman.drain_console()
+
+ output = ubman.run_command("echo $filesize")
+ assert "%x" % size in output
+
+ output = ubman.run_command("crc32 $fileaddr $filesize")
+ assert crc in output
diff --git a/test/py/tests/test_semihosting/conftest.py b/test/py/tests/test_semihosting/conftest.py
index b00d8f4ea9c..6b7f3f3c2d9 100644
--- a/test/py/tests/test_semihosting/conftest.py
+++ b/test/py/tests/test_semihosting/conftest.py
@@ -6,9 +6,9 @@
import os
import pytest
[email protected](scope='session')
[email protected](scope='function')
def semihosting_data(u_boot_config):
- """Set up a file system to be used in semihosting tests
+ """Set up a new file for each semihosting test
Args:
u_boot_config -- U-Boot configuration.
diff --git a/test/py/tests/test_semihosting/test_load_semihosting.py b/test/py/tests/test_semihosting/test_load_semihosting.py
new file mode 100644
index 00000000000..7c2eb72c69a
--- /dev/null
+++ b/test/py/tests/test_semihosting/test_load_semihosting.py
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright 2026 Free Mobile - Vincent Jardin
+
+"""Regression test for `load semihosting - <addr> <file>`.
+
+Companion to test_hostfs.py: same fixture, same crc32, different
+fstype routing:
+see the doc/usage/cmd/load.rst "Null-block-device interfaces" section.
+"""
+
+import pytest
+
+
[email protected]('semihosting')
+def test_semihosting_load(ubman, semihosting_data):
+ """Run `load semihosting - <addr> <file>` and check the bytes."""
+ response = ubman.run_command(
+ f'load semihosting - $loadaddr {semihosting_data}')
+
+ # Fixture is "Das U-Boot\n" (11 bytes).
+ assert '11 bytes read' in response
+
+ # crc32("Das U-Boot\n")
+ response = ubman.run_command('crc32 $loadaddr $filesize')
+ assert '==> 60cfccfc' in response
+
+
[email protected]('semihosting')
+def test_semihosting_load_offset(ubman, semihosting_data):
+ """Run the [bytes] [pos] variant through the same dispatch."""
+ response = ubman.run_command(
+ f'load semihosting - $loadaddr {semihosting_data} 4 6')
+ # bytes=4 pos=6 over "Das U-Boot\n" -> "Boot".
+ assert '4 bytes read' in response
+
+ # crc32("Boot")
+ response = ubman.run_command('crc32 $loadaddr $filesize')
+ assert '==> e6df01fa' in response
diff --git a/test/py/tests/test_trace.py b/test/py/tests/test_trace.py
index 36a3c4e8fe9..a68851facc4 100644
--- a/test/py/tests/test_trace.py
+++ b/test/py/tests/test_trace.py
@@ -145,8 +145,6 @@ def check_function(ubman, fname, proftool, map_fname, trace_dat):
out = utils.run_and_log(ubman, ['sh', '-c', cmd])
# Format:
- # u-boot-1 0..... 60.805596: function: initf_malloc
- # u-boot-1 0..... 60.805597: function: initf_malloc
# u-boot-1 0..... 60.805601: function: initf_bootstage
# u-boot-1 0..... 60.805607: function: initf_bootstage
@@ -162,7 +160,7 @@ def check_function(ubman, fname, proftool, map_fname, trace_dat):
# Check for some expected functions
if ubman.config.buildconfig.get('config_trace_early'):
- assert 'initf_malloc' in vals.keys()
+ assert 'initf_upl' in vals.keys()
assert 'initr_watchdog' in vals.keys()
assert 'initr_dm' in vals.keys()
@@ -193,7 +191,6 @@ def check_funcgraph(ubman, fname, proftool, map_fname, trace_dat):
out = utils.run_and_log(ubman, ['sh', '-c', cmd])
# First look for this:
- # u-boot-1 0..... 282.101360: funcgraph_entry: 0.004 us | initf_malloc();
# ...
# u-boot-1 0..... 282.101369: funcgraph_entry: | initf_bootstage() {
# u-boot-1 0..... 282.101369: funcgraph_entry: | bootstage_init() {