summaryrefslogtreecommitdiff
path: root/test/py
diff options
context:
space:
mode:
authorTom Rini <[email protected]>2026-06-17 09:54:27 -0600
committerTom Rini <[email protected]>2026-06-17 09:54:27 -0600
commita57f2a31a7cd875dd96291394fb7c8c574b70a65 (patch)
tree96bcb846f6a7ed1359a404981250966b4c50d1af /test/py
parente79de74103d9d411aa6b4e63582c5d7075c6a7a8 (diff)
parenta7ea33e3a35860326aeb5792f337bd9082d40ecf (diff)
Merge patch series "bootm: bound noload kernel decompression to the allocated buffer"
Aristo Chen <[email protected]> says: For a compressed kernel_noload image, bootm_load_os() allocates a decompression buffer of ALIGN(image_len * 4, SZ_1M) and then passes CONFIG_SYS_BOOTM_LEN (typically 128 MiB on arm64) to image_decomp() as the output limit. The decompressors honour whatever limit they are given, so a kernel that decompresses to more than four times its compressed size runs past the end of the allocated buffer and silently corrupts adjacent memory. A 4x compression ratio is at the edge of what modern compressors (zstd, xz) achieve on real kernels, and is trivially exceeded by crafted, highly compressible payloads, so this is reachable both accidentally and intentionally. The overflow can land on already-loaded boot artefacts (FDT, ramdisk, loadables), U-Boot's own data, or memory-mapped device registers; the existing post-decompression overlap check in bootm_load_os() only catches overlap with the FIT itself. Patch 1 plumbs the actual allocation size through to image_decomp() and handle_decomp_error() via a single decomp_len variable, so decompression stops at the buffer boundary and fails cleanly when the image is too large. The non-noload code path is unchanged and continues to use CONFIG_SYS_BOOTM_LEN. A clarifying note is printed when the failure is gated by the per-image buffer, so the generic "increase CONFIG_SYS_BOOTM_LEN" advice does not mislead. Patch 2 raises the noload-decompression headroom from 4x to 8x. The 4x factor is at the edge of what zstd and xz achieve on real kernels, so well-compressed vendor kernels can fail to boot at runtime once the bound is enforced. 8x covers them comfortably while remaining bounded. Patch 3 adds two sandbox py-tests against the per-image buffer at the final 8x value: one that exceeds the buffer and must be rejected, and one that matches the buffer exactly and must succeed (guarding the boundary). Tested on sandbox: both new tests pass; the existing test_fit_compressed_images_load (which covers the load-address path) and the other tests in test/py/tests/test_fit.py continue to pass. Link: https://lore.kernel.org/r/[email protected]
Diffstat (limited to 'test/py')
-rwxr-xr-xtest/py/tests/test_fit.py125
1 files changed, 125 insertions, 0 deletions
diff --git a/test/py/tests/test_fit.py b/test/py/tests/test_fit.py
index 1c4bd667d6b..76adb98e2c5 100755
--- a/test/py/tests/test_fit.py
+++ b/test/py/tests/test_fit.py
@@ -117,6 +117,36 @@ host save hostfs 0 %(loadables1_addr)x %(loadables1_out)s %(loadables1_size)x
host save hostfs 0 %(loadables2_addr)x %(loadables2_out)s %(loadables2_size)x
'''
+# A minimal ITS for a compressed 'kernel_noload' kernel. bootm allocates a
+# per-image decompression buffer for this image type, sized as a multiple of
+# the compressed length; see the test_fit_kernel_noload_decomp_* tests.
+NOLOAD_ITS = '''
+/dts-v1/;
+
+/ {
+ description = "FIT with a compressed kernel_noload image";
+ #address-cells = <1>;
+
+ images {
+ kernel-1 {
+ data = /incbin/("%(kernel)s");
+ type = "kernel_noload";
+ arch = "sandbox";
+ os = "linux";
+ compression = "gzip";
+ load = <0>;
+ entry = <0>;
+ };
+ };
+ configurations {
+ default = "conf-1";
+ conf-1 {
+ kernel = "kernel-1";
+ };
+ };
+};
+'''
+
@pytest.mark.boardspec('sandbox')
@pytest.mark.buildconfigspec('fit')
@pytest.mark.requiredtool('dtc')
@@ -479,3 +509,98 @@ class TestFitImage:
'iminfo output is missing the "Default Configuration" line for a '
'FIT whose /configurations node precedes /images. Output was:\n'
+ output)
+
+ @pytest.mark.buildconfigspec('gzip')
+ def test_fit_kernel_noload_decomp_overflow(self, ubman, fsetup):
+ """Test that an over-large compressed kernel_noload image is rejected
+
+ For a compressed 'kernel_noload' kernel, bootm_load_os() allocates a
+ decompression buffer of ALIGN(image_len * 8, SZ_1M) and must bound the
+ decompressor by that buffer. A kernel that decompresses to far more
+ than eight times its compressed size must therefore fail with a
+ decompression error instead of overflowing the buffer.
+ """
+ sz_1m = 1 << 20
+
+ # CONFIG_SYS_BOOTM_LEN is the global decompression limit. Keep the
+ # uncompressed size below it, so the failure is forced by the smaller
+ # per-image kernel_noload buffer rather than by that global limit.
+ bootm_len = int(ubman.config.buildconfig['config_sys_bootm_len'], 0)
+
+ # 4MB of zeros compresses to a few KB, so the decompression buffer
+ # (ALIGN(image_len * 8, SZ_1M), i.e. 1MB here) ends up far smaller
+ # than the uncompressed image.
+ decomp_size = 4 * sz_1m
+ kernel = fit_util.make_fname(ubman, 'test-noload-kernel.bin')
+ with open(kernel, 'wb') as fd:
+ fd.write(b'\0' * decomp_size)
+ kernel_gz = self.make_compressed(ubman, kernel)
+
+ image_len = self.filesize(kernel_gz)
+ req_size = (image_len * 8 + sz_1m - 1) // sz_1m * sz_1m
+ assert req_size < decomp_size <= bootm_len, (
+ 'Test setup error: need decomp buffer (%#x) < image (%#x) <= '
+ 'CONFIG_SYS_BOOTM_LEN (%#x)' % (req_size, decomp_size, bootm_len))
+
+ fit = fit_util.make_fit(ubman, fsetup['mkimage'], NOLOAD_ITS,
+ {'kernel': kernel_gz})
+ fit_addr = fsetup['fit_addr']
+
+ ubman.run_command_list([
+ 'host load hostfs 0 %x %s' % (fit_addr, fit),
+ 'bootm start %x' % fit_addr,
+ ])
+
+ # 'bootm loados' decompresses the kernel. Decompression must stop at
+ # the buffer boundary and report 'Image too large'; it must not run
+ # past the buffer and return to the prompt.
+ ubman.run_command('bootm loados', wait_for_prompt=False)
+ try:
+ ubman.wait_for('Image too large')
+ finally:
+ # The decompression failure resets the board; bring up a fresh
+ # instance so later tests start from a clean console.
+ ubman.restart_uboot()
+
+ @pytest.mark.buildconfigspec('gzip')
+ def test_fit_kernel_noload_decomp_boundary(self, ubman, fsetup):
+ """Test that decompression succeeds exactly at the buffer limit
+
+ For a compressed 'kernel_noload' kernel, bootm_load_os() allocates a
+ decompression buffer of ALIGN(image_len * 8, SZ_1M). A kernel whose
+ decompressed size equals that buffer exactly must succeed, guarding
+ against an off-by-one rejection at the buffer limit.
+ """
+ sz_1m = 1 << 20
+
+ # 1MiB of zeros compresses to a few KB, so image_len * 8 rounds up to
+ # exactly 1MiB. Picking decomp_size = 1MiB makes the decompressed size
+ # match the buffer exactly.
+ decomp_size = sz_1m
+ kernel = fit_util.make_fname(ubman, 'test-noload-kernel-boundary.bin')
+ with open(kernel, 'wb') as fd:
+ fd.write(b'\0' * decomp_size)
+ kernel_gz = self.make_compressed(ubman, kernel)
+
+ image_len = self.filesize(kernel_gz)
+ req_size = (image_len * 8 + sz_1m - 1) // sz_1m * sz_1m
+ assert decomp_size == req_size, (
+ 'Test setup error: need decomp_size (%#x) == req_size (%#x)'
+ % (decomp_size, req_size))
+
+ fit = fit_util.make_fit(ubman, fsetup['mkimage'], NOLOAD_ITS,
+ {'kernel': kernel_gz},
+ basename='test-noload-boundary.fit')
+ fit_addr = fsetup['fit_addr']
+
+ # Decompression at the buffer limit must succeed, returning to the
+ # prompt cleanly and never printing 'Image too large'.
+ output = ubman.run_command_list([
+ 'host load hostfs 0 %x %s' % (fit_addr, fit),
+ 'bootm start %x' % fit_addr,
+ 'bootm loados',
+ ])
+ text = '\n'.join(output)
+ assert 'Image too large' not in text, (
+ "'bootm loados' rejected a kernel_noload image whose decompressed "
+ 'size matches its buffer exactly: %s' % text)