[PATCH v2 2/8] test: fit: cover the kernel_noload gzip header-size and lying-header paths
Aristo Chen <[email protected]>
| Newsgroups | org.u-boot-project.lists.u-boot |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]>
---
test/py/tests/test_fit.py | 84 ++++++++++++++++++++++++++++++---------
1 file 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
--
2.43.0