diff options
| author | Tom Rini <[email protected]> | 2026-06-17 09:54:27 -0600 |
|---|---|---|
| committer | Tom Rini <[email protected]> | 2026-06-17 09:54:27 -0600 |
| commit | a57f2a31a7cd875dd96291394fb7c8c574b70a65 (patch) | |
| tree | 96bcb846f6a7ed1359a404981250966b4c50d1af | |
| parent | e79de74103d9d411aa6b4e63582c5d7075c6a7a8 (diff) | |
| parent | a7ea33e3a35860326aeb5792f337bd9082d40ecf (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]
| -rw-r--r-- | boot/bootm.c | 18 | ||||
| -rwxr-xr-x | test/py/tests/test_fit.py | 125 |
2 files changed, 136 insertions, 7 deletions
diff --git a/boot/bootm.c b/boot/bootm.c index ec74873b503..4c260a5f5ce 100644 --- a/boot/bootm.c +++ b/boot/bootm.c @@ -617,6 +617,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; @@ -625,16 +626,16 @@ 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) { - ulong req_size = ALIGN(image_len * 4, SZ_1M); phys_addr_t addr; + decomp_len = ALIGN(image_len * 8, 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; @@ -642,17 +643,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; } 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) |
