[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.