From 2ff26c1e37858ba0b2fd12c82e114a3cedcb317a Mon Sep 17 00:00:00 2001 From: Aristo Chen Date: Fri, 5 Jun 2026 15:42:49 +0000 Subject: bootm: fix overflow of the noload kernel decompression buffer For a compressed kernel_noload image, bootm_load_os() allocates a decompression buffer sized to ALIGN(image_len * 4, SZ_1M), assuming the kernel compresses by no more than a factor of four. It then passes CONFIG_SYS_BOOTM_LEN, rather than the size of that buffer, to image_decomp() as the output limit. The decompressors honour the limit they are given, so a kernel that decompresses to more than four times its compressed size is written past the end of the allocated buffer and corrupts adjacent memory. Pass the allocation size to image_decomp() and handle_decomp_error() so decompression stops at the buffer boundary and fails cleanly when the image is too large, instead of overflowing. The regular non-noload paths are unchanged and continue to use CONFIG_SYS_BOOTM_LEN. When the failure is triggered by the smaller per-image buffer, print a note so that handle_decomp_error()'s generic advice to increase CONFIG_SYS_BOOTM_LEN does not mislead the reader. Fixes: 69544c4fd8b1 ("bootm: Support kernel_noload with compression") Reviewed-by: Simon Glass Signed-off-by: Aristo Chen --- boot/bootm.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/boot/bootm.c b/boot/bootm.c index 4836d6b2d41..b55c41f30b7 100644 --- a/boot/bootm.c +++ b/boot/bootm.c @@ -610,6 +610,7 @@ static int bootm_load_os(struct bootm_headers *images, int boot_progress) ulong blob_end = os.end; ulong image_start = os.image_start; ulong image_len = os.image_len; + ulong decomp_len = CONFIG_SYS_BOOTM_LEN; ulong flush_start = ALIGN_DOWN(load, ARCH_DMA_MINALIGN); bool no_overlap; void *load_buf, *image_buf; @@ -623,11 +624,11 @@ static int bootm_load_os(struct bootm_headers *images, int boot_progress) * Use an alignment of 2MB since this might help arm64 */ if (os.type == IH_TYPE_KERNEL_NOLOAD && os.comp != IH_COMP_NONE) { - ulong req_size = ALIGN(image_len * 4, SZ_1M); phys_addr_t addr; + decomp_len = ALIGN(image_len * 4, SZ_1M); err = lmb_alloc_mem(LMB_MEM_ALLOC_ANY, SZ_2M, &addr, - req_size, LMB_NONE); + decomp_len, LMB_NONE); if (err) return 1; @@ -635,17 +636,20 @@ static int bootm_load_os(struct bootm_headers *images, int boot_progress) images->os.load = (ulong)addr; images->ep = (ulong)addr; debug("Allocated %lx bytes at %lx for kernel (size %lx) decompression\n", - req_size, load, image_len); + decomp_len, load, image_len); } load_buf = map_sysmem(load, 0); image_buf = map_sysmem(os.image_start, image_len); err = image_decomp(os.comp, load, os.image_start, os.type, load_buf, image_buf, image_len, - CONFIG_SYS_BOOTM_LEN, &load_end); + decomp_len, &load_end); if (err) { err = handle_decomp_error(os.comp, load_end - load, - CONFIG_SYS_BOOTM_LEN, err); + decomp_len, err); + if (os.type == IH_TYPE_KERNEL_NOLOAD && os.comp != IH_COMP_NONE) + printf("Note: noload decompression buffer is %#lx bytes (not CONFIG_SYS_BOOTM_LEN)\n", + decomp_len); bootstage_error(BOOTSTAGE_ID_DECOMP_IMAGE); return err; } -- cgit v1.3.1 From 4956f108539135afb1afdec2b509f62291087d16 Mon Sep 17 00:00:00 2001 From: Aristo Chen Date: Fri, 5 Jun 2026 15:42:50 +0000 Subject: bootm: increase kernel_noload decompression headroom from 4x to 8x For a compressed kernel_noload image, bootm_load_os() allocates a buffer of ALIGN(image_len * 4, SZ_1M). The 4x factor is at the edge of what modern compressors (zstd, xz) achieve on real kernels, so a well-compressed vendor kernel can fail to boot at runtime with no intervening warning. Bump the headroom to 8x. The buffer is still bounded by the compressed image size, and the SZ_1M alignment keeps the overhead below 1 MiB on small kernels. Suggested-by: Simon Glass Signed-off-by: Aristo Chen --- boot/bootm.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/boot/bootm.c b/boot/bootm.c index b55c41f30b7..4eee35d7e62 100644 --- a/boot/bootm.c +++ b/boot/bootm.c @@ -619,14 +619,14 @@ static int bootm_load_os(struct bootm_headers *images, int boot_progress) /* * For a "noload" compressed kernel we need to allocate a buffer large * enough to decompress in to and use that as the load address now. - * Assume that the kernel compression is at most a factor of 4 since - * zstd almost achieves that. + * Allow up to 8x compression: this comfortably covers what zstd and xz + * achieve on real kernels, with headroom for well-compressed payloads. * Use an alignment of 2MB since this might help arm64 */ if (os.type == IH_TYPE_KERNEL_NOLOAD && os.comp != IH_COMP_NONE) { phys_addr_t addr; - decomp_len = ALIGN(image_len * 4, SZ_1M); + decomp_len = ALIGN(image_len * 8, SZ_1M); err = lmb_alloc_mem(LMB_MEM_ALLOC_ANY, SZ_2M, &addr, decomp_len, LMB_NONE); if (err) -- cgit v1.3.1 From a7ea33e3a35860326aeb5792f337bd9082d40ecf Mon Sep 17 00:00:00 2001 From: Aristo Chen Date: Fri, 5 Jun 2026 15:42:51 +0000 Subject: test/py: test kernel_noload decompression buffer overflow Add sandbox tests that exercise the per-image decompression buffer that bootm_load_os() allocates for a compressed kernel_noload image (ALIGN(image_len * 8, SZ_1M)). The overflow test builds a FIT whose decompressed size far exceeds the per-image buffer and asserts that 'bootm loados' rejects it with a decompression error rather than overflowing. The boundary test builds a FIT whose decompressed size equals the per-image buffer exactly and asserts that 'bootm loados' succeeds, guarding against an off-by-one rejection at the buffer limit. Signed-off-by: Aristo Chen --- test/py/tests/test_fit.py | 125 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/test/py/tests/test_fit.py b/test/py/tests/test_fit.py index 4f56a1421e1..d63fdd80528 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') @@ -426,3 +456,98 @@ class TestFitImage: output = ubman.run_command_list(cmds) assert "can't get kernel image!" in '\n'.join(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) -- cgit v1.3.1