summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-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/cmd/Makefile1
-rw-r--r--test/cmd/config.c28
-rw-r--r--test/cmd/unzip.c118
-rw-r--r--test/dm/Makefile1
-rw-r--r--test/dm/hash.c157
-rw-r--r--test/dm/net_defrag.c36
-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_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
21 files changed, 1221 insertions, 14 deletions
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/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 76aa1fff9ba..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
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/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_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() {