[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