diff options
| author | Aristo Chen <aristo.chen@canonical.com> | 2026-08-18 13:23:16 +0000 |
|---|---|---|
| committer | Tom Rini <trini@konsulko.com> | 2026-08-18 14:36:57 -0600 |
| commit | d097a56a12531c9e42a6dd2de9e469286c0d6e64 (patch) | |
| tree | 946f64a5405d5fe64e330b9f90b29ca5357151dc | |
| parent | 0a2e329b829d905889aa9bad0c9f4d28cc302ab1 (diff) | |
| download | u-boot-d097a56a12531c9e42a6dd2de9e469286c0d6e64.tar.gz u-boot-d097a56a12531c9e42a6dd2de9e469286c0d6e64.zip | |
test: fit: cover the kernel_noload gzip header-size and lying-header paths
Reshape and extend the kernel_noload decompression pytests to match
the new bootm behaviour that reads ISIZE from the gzip trailer:
- Rename test_fit_kernel_noload_decomp_overflow to
test_fit_kernel_noload_decomp_gzip_lying_hdr. Its setup (a 4 MiB
payload of zeros gzipped) used to force the failure via the 8x
heuristic starving the buffer; now that bootm reads ISIZE, the
honest trailer sizes the buffer correctly, so overwrite ISIZE
with a tiny value instead and verify the resulting decompression
is still stopped at the buffer boundary. This is the direct test
of the CONFIG_SYS_BOOTM_LEN cap on the attacker-controlled
header value.
- Add test_fit_kernel_noload_decomp_gzip_hdr_sized: a 6 MiB gzipped
payload whose compression ratio is past the 8x heuristic
decompresses cleanly because ISIZE is consulted.
- Rename the pre-existing test_fit_kernel_noload_decomp_boundary to
test_fit_kernel_noload_decomp_gzip_boundary so every
noload_decomp test carries the compressor in its name.
Signed-off-by: Aristo Chen <aristo.chen@canonical.com>
| -rwxr-xr-x | test/py/tests/test_fit.py | 84 |
1 files changed, 66 insertions, 18 deletions
diff --git a/test/py/tests/test_fit.py b/test/py/tests/test_fit.py index 76adb98e2c5..81df84f54c9 100755 --- a/test/py/tests/test_fit.py +++ b/test/py/tests/test_fit.py @@ -118,8 +118,9 @@ 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. +# per-image decompression buffer for this image type, sized either from the +# gzip ISIZE trailer or as a multiple of the compressed length; see the +# test_fit_kernel_noload_decomp_* tests. NOLOAD_ITS = ''' /dts-v1/; @@ -511,14 +512,13 @@ class TestFitImage: + 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 + def test_fit_kernel_noload_decomp_gzip_lying_hdr(self, ubman, fsetup): + """A tampered gzip ISIZE cannot shrink the buffer past the payload - 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. + bootm_load_os() sizes the kernel_noload decompression buffer from the + gzip ISIZE trailer. That value is attacker-controlled; rewriting + ISIZE to understate the real size must not let decompression overflow + the resulting buffer. """ sz_1m = 1 << 20 @@ -527,20 +527,21 @@ class TestFitImage: # 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 + assert decomp_size <= bootm_len, ( + 'Test setup error: uncompressed size (%#x) must be <= ' + 'CONFIG_SYS_BOOTM_LEN (%#x)' % (decomp_size, bootm_len)) 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)) + # Rewrite gzip ISIZE (the last 4 bytes) to claim a tiny image, so + # bootm allocates ALIGN(<lie>, SZ_1M) = 1 MiB and the real 4 MiB + # decompression has to overrun that buffer. + with open(kernel_gz, 'r+b') as fd: + fd.seek(-4, os.SEEK_END) + fd.write((256).to_bytes(4, 'little')) fit = fit_util.make_fit(ubman, fsetup['mkimage'], NOLOAD_ITS, {'kernel': kernel_gz}) @@ -563,7 +564,54 @@ class TestFitImage: ubman.restart_uboot() @pytest.mark.buildconfigspec('gzip') - def test_fit_kernel_noload_decomp_boundary(self, ubman, fsetup): + def test_fit_kernel_noload_decomp_gzip_hdr_sized(self, ubman, fsetup): + """A well-compressed kernel_noload image fits when ISIZE is honest + + bootm_load_os() reads gzip ISIZE to size the decompression buffer. + For a well-compressed image whose ratio exceeds the 8x fallback + heuristic (e.g. 6 MiB of zeros gzipping to a few KiB), an ISIZE-sized + buffer is the only way the decompression fits. + """ + sz_1m = 1 << 20 + bootm_len = int(ubman.config.buildconfig['config_sys_bootm_len'], 0) + + # Stay under CONFIG_SYS_BOOTM_LEN so the ISIZE hint isn't rejected as + # bogus; still large enough that image_len * 8 falls well short. + decomp_size = 6 * sz_1m + assert decomp_size <= bootm_len, ( + 'Test setup error: decomp_size (%#x) must be <= ' + 'CONFIG_SYS_BOOTM_LEN (%#x)' % (decomp_size, bootm_len)) + kernel = fit_util.make_fname(ubman, 'test-noload-kernel-hdrsized.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) + heuristic_bound = (image_len * 8 + sz_1m - 1) // sz_1m * sz_1m + assert heuristic_bound < decomp_size, ( + 'Test setup error: 8x heuristic bound (%#x) must be < uncompressed ' + 'size (%#x); if this fires, the compressor got less effective and ' + 'the test needs a bigger payload' % (heuristic_bound, decomp_size)) + + fit = fit_util.make_fit(ubman, fsetup['mkimage'], NOLOAD_ITS, + {'kernel': kernel_gz}, + basename='test-noload-hdrsized.fit') + fit_addr = fsetup['fit_addr'] + + # Decompression must succeed: bootm read ISIZE and allocated a big + # enough buffer despite the ratio being past the fallback heuristic. + 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 rejected a well-compressed kernel_noload image whose ' + 'ISIZE trailer records the real uncompressed size: %s' % text) + + @pytest.mark.buildconfigspec('gzip') + def test_fit_kernel_noload_decomp_gzip_boundary(self, ubman, fsetup): """Test that decompression succeeds exactly at the buffer limit For a compressed 'kernel_noload' kernel, bootm_load_os() allocates a |
