[RFC PATCH 11/20] test: boot: add image_loader unit tests

Daniel Golle daniel at makrotopia.org
Mon Feb 16 22:22:59 CET 2026


Add unit tests for the image_loader framework covering its core
logic with a mock storage backend:

- map() allocates, reads and records a region
- map() returns cached pointer for already-mapped range
- map() returns correct offset within a larger region
- map() re-reads when extending a region to a larger size
- map_to() reads to a specified address and records it
- lookup() returns NULL for unmapped ranges
- alloc_ptr advances with correct alignment
- map() returns NULL when the translation table is full
- cleanup() calls backend and resets state
- map() with multiple disjoint regions
- read beyond image size returns error

Also fix IMAGE_LOADER_MAX_REGIONS Kconfig to depend on IMAGE_LOADER
and default to 16 unconditionally (the previous 'default 0' fallback
caused the regions array to be zero-sized when IMAGE_LOADER was
enabled after initial defconfig generation).

Register the new 'image_loader' test suite in test/cmd_ut.c so it
can be run via 'ut image_loader'.

Signed-off-by: Daniel Golle <daniel at makrotopia.org>
---
 boot/Kconfig             |   4 +-
 test/boot/Makefile       |   2 +
 test/boot/image_loader.c | 429 +++++++++++++++++++++++++++++++++++++++
 test/cmd_ut.c            |   2 +
 4 files changed, 435 insertions(+), 2 deletions(-)
 create mode 100644 test/boot/image_loader.c

diff --git a/boot/Kconfig b/boot/Kconfig
index 1f870c7d251..efc06f3cd1a 100644
--- a/boot/Kconfig
+++ b/boot/Kconfig
@@ -1179,8 +1179,8 @@ config IMAGE_LOADER
 
 config IMAGE_LOADER_MAX_REGIONS
 	int "Maximum number of mapped regions in image loader"
-	default 16 if IMAGE_LOADER
-	default 0
+	depends on IMAGE_LOADER
+	default 16
 	help
 	  Maximum number of distinct image regions that can be mapped
 	  into RAM simultaneously. 16 is sufficient for typical FIT
diff --git a/test/boot/Makefile b/test/boot/Makefile
index 89538d4f0a6..6fd349a65bc 100644
--- a/test/boot/Makefile
+++ b/test/boot/Makefile
@@ -23,3 +23,5 @@ endif
 obj-$(CONFIG_BOOTMETH_VBE) += vbe_fixup.o
 
 obj-$(CONFIG_UPL) += upl.o
+
+obj-$(CONFIG_IMAGE_LOADER) += image_loader.o
diff --git a/test/boot/image_loader.c b/test/boot/image_loader.c
new file mode 100644
index 00000000000..dc4b0b4173a
--- /dev/null
+++ b/test/boot/image_loader.c
@@ -0,0 +1,429 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Tests for image_loader framework
+ *
+ * Copyright (C) 2026 Daniel Golle <daniel at makrotopia.org>
+ */
+
+#include <image-loader.h>
+#include <mapmem.h>
+#include <malloc.h>
+#include <asm/cache.h>
+#include <test/test.h>
+#include <test/ut.h>
+
+#define IMG_LOADER_TEST(_name, _flags) \
+	UNIT_TEST(_name, _flags, image_loader)
+
+/* Synthetic image size used throughout the tests */
+#define IMAGE_SIZE	4096
+
+/**
+ * struct mock_priv - private data for the mock storage backend
+ *
+ * @image:	pointer to synthetic image data in RAM
+ * @image_size:	size of the synthetic image
+ * @read_count:	number of times .read() was called
+ * @last_off:	offset from the most recent .read() call
+ * @last_size:	size from the most recent .read() call
+ */
+struct mock_priv {
+	const void *image;
+	size_t image_size;
+	int read_count;
+	ulong last_off;
+	ulong last_size;
+};
+
+static int mock_read(struct image_loader *ldr, ulong src, ulong size,
+		     void *dst)
+{
+	struct mock_priv *p = ldr->priv;
+
+	if (src + size > p->image_size)
+		return -EINVAL;
+
+	memcpy(dst, (const char *)p->image + src, size);
+	p->read_count++;
+	p->last_off = src;
+	p->last_size = size;
+
+	return 0;
+}
+
+static void mock_cleanup(struct image_loader *ldr)
+{
+	/* Nothing dynamic to free — just verify it's called */
+}
+
+/**
+ * init_mock_loader() - set up a loader with the mock backend
+ *
+ * @ldr:	loader to initialise
+ * @priv:	mock private data (caller-allocated)
+ * @image:	synthetic image buffer
+ * @image_size:	size of @image
+ * @alloc_base:	RAM address to use as alloc_ptr base
+ */
+static void init_mock_loader(struct image_loader *ldr, struct mock_priv *priv,
+			     const void *image, size_t image_size,
+			     ulong alloc_base)
+{
+	memset(ldr, 0, sizeof(*ldr));
+	memset(priv, 0, sizeof(*priv));
+
+	priv->image = image;
+	priv->image_size = image_size;
+
+	ldr->read = mock_read;
+	ldr->cleanup = mock_cleanup;
+	ldr->priv = priv;
+	ldr->alloc_ptr = alloc_base;
+}
+
+/* Test: map() allocates, reads and records a region */
+static int image_loader_test_map_basic(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p;
+
+	/* Fill image with a recognisable pattern */
+	for (int i = 0; i < IMAGE_SIZE; i++)
+		image[i] = (u8)(i & 0xff);
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	/* Map a 64-byte region at offset 0 */
+	p = image_loader_map(&ldr, 0, 64);
+	ut_assertnonnull(p);
+	ut_asserteq_mem(image, p, 64);
+	ut_asserteq(1, mock.read_count);
+	ut_asserteq(1, ldr.nr_regions);
+	ut_asserteq(0, (int)ldr.regions[0].img_offset);
+	ut_asserteq(64, (int)ldr.regions[0].size);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_map_basic, 0);
+
+/* Test: map() returns cached pointer for already-mapped range */
+static int image_loader_test_map_cached(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p1, *p2;
+
+	memset(image, 0xaa, IMAGE_SIZE);
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	p1 = image_loader_map(&ldr, 0, 128);
+	ut_assertnonnull(p1);
+	ut_asserteq(1, mock.read_count);
+
+	/* Same range — should return same pointer, no new read */
+	p2 = image_loader_map(&ldr, 0, 128);
+	ut_asserteq_ptr(p1, p2);
+	ut_asserteq(1, mock.read_count);
+
+	/* Subset of the already-mapped range — still cached */
+	p2 = image_loader_map(&ldr, 0, 64);
+	ut_asserteq_ptr(p1, p2);
+	ut_asserteq(1, mock.read_count);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_map_cached, 0);
+
+/* Test: map() returns correct offset within a larger region */
+static int image_loader_test_map_offset(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p1, *p2;
+
+	for (int i = 0; i < IMAGE_SIZE; i++)
+		image[i] = (u8)(i & 0xff);
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	/* Map a 256-byte region starting at offset 0 */
+	p1 = image_loader_map(&ldr, 0, 256);
+	ut_assertnonnull(p1);
+
+	/* Request a sub-range within the previously mapped region */
+	p2 = image_loader_map(&ldr, 64, 64);
+	ut_assertnonnull(p2);
+	/* p2 should point 64 bytes into p1 */
+	ut_asserteq_ptr((char *)p1 + 64, p2);
+	ut_asserteq_mem(image + 64, p2, 64);
+	/* Only one read should have occurred */
+	ut_asserteq(1, mock.read_count);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_map_offset, 0);
+
+/* Test: map() re-reads when extending a region to a larger size */
+static int image_loader_test_map_extend(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p1, *p2;
+
+	for (int i = 0; i < IMAGE_SIZE; i++)
+		image[i] = (u8)(i & 0xff);
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	/* Initial small mapping */
+	p1 = image_loader_map(&ldr, 0, 64);
+	ut_assertnonnull(p1);
+	ut_asserteq(1, mock.read_count);
+
+	/* Request larger range at same base — should re-read (extend) */
+	p2 = image_loader_map(&ldr, 0, 256);
+	ut_assertnonnull(p2);
+	ut_asserteq(2, mock.read_count);
+	ut_asserteq_ptr(p1, p2);	/* same RAM base */
+	ut_asserteq_mem(image, p2, 256);
+
+	/* Region count should still be 1 (updated, not added) */
+	ut_asserteq(1, ldr.nr_regions);
+	ut_asserteq(256, (int)ldr.regions[0].size);
+
+	/* alloc_ptr must have advanced past the extended region */
+	ut_assert(ldr.alloc_ptr >= 0x1000000 + 256);
+
+	/*
+	 * Map a new region after the extend — it must not overlap the
+	 * extended first region. This is the exact pattern that bit us
+	 * on real hardware: FIT header at offset 0 extended from 64 to
+	 * 4096, then kernel payload at offset 4096 was allocated at
+	 * alloc_ptr that hadn't been advanced, clobbering the header.
+	 */
+	{
+		void *p3 = image_loader_map(&ldr, 256, 128);
+
+		ut_assertnonnull(p3);
+		ut_asserteq(2, ldr.nr_regions);
+		/* New region must start at or after the extended region end */
+		ut_assert((ulong)map_to_sysmem(p3) >= 0x1000000 + 256);
+		ut_asserteq_mem(image + 256, p3, 128);
+	}
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_map_extend, 0);
+
+/* Test: map_to() reads to a specified address and records it */
+static int image_loader_test_map_to(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	u8 dst[256];
+	void *p;
+
+	for (int i = 0; i < IMAGE_SIZE; i++)
+		image[i] = (u8)(i & 0xff);
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	p = image_loader_map_to(&ldr, 128, 256, dst);
+	ut_asserteq_ptr(dst, p);
+	ut_asserteq_mem(image + 128, dst, 256);
+	ut_asserteq(1, mock.read_count);
+	ut_asserteq(1, ldr.nr_regions);
+	ut_asserteq(128, (int)ldr.regions[0].img_offset);
+	ut_asserteq(256, (int)ldr.regions[0].size);
+	ut_asserteq_ptr(dst, ldr.regions[0].ram);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_map_to, 0);
+
+/* Test: lookup() returns NULL for unmapped ranges */
+static int image_loader_test_lookup_miss(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p;
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	/* Nothing mapped yet — should return NULL */
+	p = image_loader_lookup(&ldr, 0, 64);
+	ut_assertnull(p);
+
+	/* Map a region at offset 0 */
+	ut_assertnonnull(image_loader_map(&ldr, 0, 64));
+
+	/* Lookup within the mapped region — should succeed */
+	p = image_loader_lookup(&ldr, 0, 32);
+	ut_assertnonnull(p);
+
+	/* Lookup at a different offset — should miss */
+	p = image_loader_lookup(&ldr, 128, 32);
+	ut_assertnull(p);
+
+	/* Lookup extending beyond the mapped region — should miss */
+	p = image_loader_lookup(&ldr, 0, 128);
+	ut_assertnull(p);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_lookup_miss, 0);
+
+/* Test: alloc_ptr advances with correct alignment */
+static int image_loader_test_alloc_advance(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	ulong base = 0x1000000;
+	ulong expected;
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, base);
+
+	/* Map 100 bytes — alloc_ptr should advance to ALIGN(base + 100) */
+	ut_assertnonnull(image_loader_map(&ldr, 0, 100));
+	expected = ALIGN(base + 100, ARCH_DMA_MINALIGN);
+	ut_asserteq(expected, ldr.alloc_ptr);
+
+	/* Map another 200 bytes at a different offset */
+	ut_assertnonnull(image_loader_map(&ldr, 200, 200));
+	expected = ALIGN(expected + 200, ARCH_DMA_MINALIGN);
+	ut_asserteq(expected, ldr.alloc_ptr);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_alloc_advance, 0);
+
+/* Test: map() returns NULL when the translation table is full */
+static int image_loader_test_table_full(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p;
+	int i;
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	/* Fill all region slots with distinct offsets */
+	for (i = 0; i < CONFIG_IMAGE_LOADER_MAX_REGIONS; i++) {
+		p = image_loader_map(&ldr, i * 16, 8);
+		ut_assertnonnull(p);
+	}
+
+	ut_asserteq(CONFIG_IMAGE_LOADER_MAX_REGIONS, ldr.nr_regions);
+
+	/* Next map at a new offset should fail (table full) */
+	p = image_loader_map(&ldr, CONFIG_IMAGE_LOADER_MAX_REGIONS * 16, 8);
+	ut_assertnull(p);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_table_full, 0);
+
+/* Test: cleanup() calls the backend and resets state */
+static int image_loader_test_cleanup(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	/* Map something so nr_regions > 0 */
+	ut_assertnonnull(image_loader_map(&ldr, 0, 64));
+	ut_asserteq(1, ldr.nr_regions);
+
+	image_loader_cleanup(&ldr);
+
+	ut_assertnull(ldr.read);
+	ut_assertnull(ldr.cleanup);
+	ut_assertnull(ldr.priv);
+	ut_asserteq(0, ldr.nr_regions);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_cleanup, 0);
+
+/* Test: map() with multiple disjoint regions */
+static int image_loader_test_multi_region(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p1, *p2, *p3;
+
+	for (int i = 0; i < IMAGE_SIZE; i++)
+		image[i] = (u8)(i & 0xff);
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	p1 = image_loader_map(&ldr, 0, 64);
+	ut_assertnonnull(p1);
+	p2 = image_loader_map(&ldr, 512, 128);
+	ut_assertnonnull(p2);
+	p3 = image_loader_map(&ldr, 1024, 256);
+	ut_assertnonnull(p3);
+
+	ut_asserteq(3, ldr.nr_regions);
+	ut_asserteq(3, mock.read_count);
+
+	/* Verify data in each region */
+	ut_asserteq_mem(image, p1, 64);
+	ut_asserteq_mem(image + 512, p2, 128);
+	ut_asserteq_mem(image + 1024, p3, 256);
+
+	/* Lookup each region */
+	ut_asserteq_ptr(p1, image_loader_lookup(&ldr, 0, 64));
+	ut_asserteq_ptr(p2, image_loader_lookup(&ldr, 512, 128));
+	ut_asserteq_ptr(p3, image_loader_lookup(&ldr, 1024, 256));
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_multi_region, 0);
+
+/* Test: read beyond image size returns error */
+static int image_loader_test_read_oob(struct unit_test_state *uts)
+{
+	struct image_loader ldr;
+	struct mock_priv mock;
+	u8 image[IMAGE_SIZE];
+	void *p;
+
+	init_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);
+
+	/* Attempt to map beyond the end of the image */
+	p = image_loader_map(&ldr, IMAGE_SIZE - 32, 64);
+	ut_assertnull(p);
+
+	/* map_to should also fail */
+	u8 dst[64];
+
+	p = image_loader_map_to(&ldr, IMAGE_SIZE - 32, 64, dst);
+	ut_assertnull(p);
+
+	return 0;
+}
+
+IMG_LOADER_TEST(image_loader_test_read_oob, 0);
diff --git a/test/cmd_ut.c b/test/cmd_ut.c
index 44e5fdfdaa6..3b907a12e4e 100644
--- a/test/cmd_ut.c
+++ b/test/cmd_ut.c
@@ -71,6 +71,7 @@ SUITE_DECL(optee);
 SUITE_DECL(pci_mps);
 SUITE_DECL(seama);
 SUITE_DECL(setexpr);
+SUITE_DECL(image_loader);
 SUITE_DECL(upl);
 
 static struct suite suites[] = {
@@ -98,6 +99,7 @@ static struct suite suites[] = {
 	SUITE(pci_mps, "PCI Express Maximum Payload Size"),
 	SUITE(seama, "seama command parameters loading and decoding"),
 	SUITE(setexpr, "setexpr command"),
+	SUITE(image_loader, "image_loader on-demand storage loading"),
 	SUITE(upl, "Universal payload support"),
 };
 
-- 
2.53.0


More information about the U-Boot mailing list