From e9848e30bd88bf889ef74799dab6c2a2a0628890 Mon Sep 17 00:00:00 2001 From: Aristo Chen Date: Mon, 11 May 2026 08:58:50 +0000 Subject: test: fs: Use shared generate_file from utils test_fs/test_erofs.py and test_fs/test_squashfs/sqfs_common.py both defined a generate_file() helper that writes a file of a given size filled with 'x'. The two functions were functionally identical and differed only in parameter names and docstrings. Move the helper into the existing test/py/utils.py module, which is the established home for generic test utilities (md5sum_file, PersistentRandomFile, attempt_to_open_file). Update both call sites to use it. Signed-off-by: Aristo Chen Reviewed-by: joaomarcos.costa@bootlin.com Reviewed-by: Simon Glass --- test/py/tests/test_fs/test_erofs.py | 16 ++++------------ test/py/tests/test_fs/test_squashfs/sqfs_common.py | 22 +++++----------------- 2 files changed, 9 insertions(+), 29 deletions(-) (limited to 'test/py/tests') diff --git a/test/py/tests/test_fs/test_erofs.py b/test/py/tests/test_fs/test_erofs.py index a2bb6b505f2..cec803256ac 100644 --- a/test/py/tests/test_fs/test_erofs.py +++ b/test/py/tests/test_fs/test_erofs.py @@ -6,19 +6,11 @@ import os import pytest import shutil import subprocess +import utils EROFS_SRC_DIR = 'erofs_src_dir' EROFS_IMAGE_NAME = 'erofs.img' -def generate_file(name, size): - """ - Generates a file filled with 'x'. - """ - content = 'x' * size - file = open(name, 'w') - file.write(content) - file.close() - def make_erofs_image(build_dir): """ Makes the EROFS images used for the test. @@ -36,15 +28,15 @@ def make_erofs_image(build_dir): os.makedirs(root) # 4096: uncompressed file - generate_file(os.path.join(root, 'f4096'), 4096) + utils.generate_file(os.path.join(root, 'f4096'), 4096) # 7812: Compressed file - generate_file(os.path.join(root, 'f7812'), 7812) + utils.generate_file(os.path.join(root, 'f7812'), 7812) # sub-directory with a single file inside subdir_path = os.path.join(root, 'subdir') os.makedirs(subdir_path) - generate_file(os.path.join(subdir_path, 'subdir-file'), 100) + utils.generate_file(os.path.join(subdir_path, 'subdir-file'), 100) # symlink os.symlink('subdir', os.path.join(root, 'symdir')) diff --git a/test/py/tests/test_fs/test_squashfs/sqfs_common.py b/test/py/tests/test_fs/test_squashfs/sqfs_common.py index d1621dcce3a..b366bde5f49 100644 --- a/test/py/tests/test_fs/test_squashfs/sqfs_common.py +++ b/test/py/tests/test_fs/test_squashfs/sqfs_common.py @@ -5,6 +5,7 @@ import os import shutil import subprocess +import utils """ standard test images table: Each table item is a key:value pair representing the output image name and its respective mksquashfs options. @@ -66,19 +67,6 @@ def init_standard_table(): for key, value in zip(STANDARD_TABLE.keys(), opts_list): STANDARD_TABLE[key] = value -def generate_file(file_name, file_size): - """ Generates a file filled with 'x'. - - Args: - file_name: the file's name. - file_size: the content's length and therefore the file size. - """ - content = 'x' * file_size - - file = open(file_name, 'w') - file.write(content) - file.close() - def generate_sqfs_src_dir(build_dir): """ Generates the source directory used to make the SquashFS images. @@ -107,20 +95,20 @@ def generate_sqfs_src_dir(build_dir): # 4096: minimum block size file_name = 'f4096' - generate_file(os.path.join(root, file_name), 4096) + utils.generate_file(os.path.join(root, file_name), 4096) # 5096: minimum block size + 1000 chars (fragment) file_name = 'f5096' - generate_file(os.path.join(root, file_name), 5096) + utils.generate_file(os.path.join(root, file_name), 5096) # 1000: less than minimum block size (fragment only) file_name = 'f1000' - generate_file(os.path.join(root, file_name), 1000) + utils.generate_file(os.path.join(root, file_name), 1000) # sub-directory with a single file inside subdir_path = os.path.join(root, 'subdir') os.makedirs(subdir_path) - generate_file(os.path.join(subdir_path, 'subdir-file'), 100) + utils.generate_file(os.path.join(subdir_path, 'subdir-file'), 100) # symlink (target: sub-directory) os.symlink('subdir', os.path.join(root, 'sym')) -- cgit v1.3.1 From b31f551bfcf05cec628fc79af5d12a601e0a1f48 Mon Sep 17 00:00:00 2001 From: Aristo Chen Date: Fri, 8 May 2026 21:32:00 +0000 Subject: test: fit: regression test for default-config print with reversed node order Add a test that builds a FIT whose /configurations node is defined before /images in the source, runs iminfo, and asserts that the "Default Configuration: ''" line appears in the output. Before the fix in the preceding commit ("boot/fit: read default-config property from the configurations node"), fit_print_contents() read the default-config property using the loop variable left over from iterating /images children. With /images defined first that variable accidentally pointed at /configurations and the line printed correctly; with /configurations defined first the read returned NULL and the line was silently omitted. The new test exercises the latter layout, which had no coverage. iminfo and the fit_print_contents() path had no test coverage at all before this commit. Signed-off-by: Aristo Chen Reviewed-by: Simon Glass --- test/py/tests/test_fit.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) (limited to 'test/py/tests') diff --git a/test/py/tests/test_fit.py b/test/py/tests/test_fit.py index bcaaa6a5fc4..a526307ea7a 100755 --- a/test/py/tests/test_fit.py +++ b/test/py/tests/test_fit.py @@ -438,3 +438,56 @@ class TestFitImage: output = ubman.run_command_list(cmds) assert "can't get kernel image!" in '\n'.join(output) + + def test_fit_iminfo_configs_first(self, ubman, fsetup): + """Regression: iminfo prints "Default Configuration" even when + /configurations is defined before /images in the source. + + fit_print_contents() in boot/image-fit.c used to read the default + configuration name from whatever offset libfdt happened to return + after iterating /images children. With /images defined first that + offset accidentally landed on /configurations; with /configurations + defined first the read returned NULL and the line silently went + missing. Fixed in commit "boot/fit: read default-config property + from the configurations node". + """ + configs_first_its = ''' +/dts-v1/; + +/ { + description = "FIT with /configurations before /images"; + #address-cells = <1>; + + configurations { + default = "conf-1"; + conf-1 { + description = "first config"; + kernel = "kernel-1"; + }; + }; + + images { + kernel-1 { + description = "first image"; + data = /incbin/("%(kernel)s"); + type = "kernel"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x40000>; + entry = <0x40000>; + }; + }; +}; +''' + fit = fit_util.make_fit(ubman, fsetup['mkimage'], configs_first_its, + fsetup, basename='configs-first.fit') + cmds = [ + 'host load hostfs 0 %#x %s' % (fsetup['fit_addr'], fit), + 'iminfo %#x' % fsetup['fit_addr'], + ] + output = '\n'.join(ubman.run_command_list(cmds)) + assert "Default Configuration: 'conf-1'" in output, ( + 'iminfo output is missing the "Default Configuration" line for a ' + 'FIT whose /configurations node precedes /images. Output was:\n' + + output) -- cgit v1.3.1 From e52b2c6e7fd0f99b8c3ccea92361db6896978222 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sat, 16 May 2026 00:38:39 +0100 Subject: test: py: add mkimage dm-verity round-trip test Add test/py/tests/test_fit_verity.py covering: - mkimage writes correct dm-verity properties for matched and mismatched block sizes (4096/4096 and 4096/1024); - veritysetup verify re-checks the digest against the .itb's external data section; - mkimage rejects dm-verity images built without -E. All tests are skipped if veritysetup is not installed on the host. Signed-off-by: Daniel Golle Reviewed-by: Simon Glass --- test/py/tests/test_fit_verity.py | 175 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 test/py/tests/test_fit_verity.py (limited to 'test/py/tests') diff --git a/test/py/tests/test_fit_verity.py b/test/py/tests/test_fit_verity.py new file mode 100644 index 00000000000..f1b6262ed0e --- /dev/null +++ b/test/py/tests/test_fit_verity.py @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright 2026 Daniel Golle + +""" +Test mkimage dm-verity Merkle-tree generation + +Build a minimal .its with a dm-verity subnode (user-provided properties only), +run mkimage -E, and verify that the computed properties (digest, salt, +num-data-blocks, hash-start-block) are written into the resulting FIT. +The computed digest is then re-verified by running ``veritysetup verify`` +against the external data section of the .itb. + +This test does not run the sandbox. It only exercises the host tool 'mkimage'. +Requires 'veritysetup' from the cryptsetup package on the build host. +""" + +import os +import struct +import pytest +import utils + +ITS_TEMPLATE = """\ +/dts-v1/; + +/ { + description = "dm-verity test"; + #address-cells = <1>; + + images { + rootfs { + description = "test filesystem"; + data = /incbin/("./rootfs.bin"); + type = "filesystem"; + arch = "sandbox"; + compression = "none"; + + dm-verity { + algo = "sha256"; + data-block-size = <%d>; + hash-block-size = <%d>; + }; + }; + }; + + configurations { + default = "conf-1"; + conf-1 { + description = "test config"; + loadables = "rootfs"; + }; + }; +}; +""" + +def _fdt_totalsize(path): + """Read the totalsize field from an FDT header (offset 4, big-endian u32).""" + with open(path, 'rb') as f: + magic, totalsize = struct.unpack('>II', f.read(8)) + assert magic == 0xd00dfeed, f'not an FDT: magic={magic:#x}' + return totalsize + + +def _run_round_trip(ubman, tempdir, data_block_size, hash_block_size): + """Build a FIT with dm-verity, verify written properties, re-verify with veritysetup.""" + mkimage = ubman.config.build_dir + '/tools/mkimage' + + rootfs_file = os.path.join(tempdir, 'rootfs.bin') + its_file = os.path.join(tempdir, 'image.its') + fit_file = os.path.join(tempdir, 'image.itb') + + # 64 data blocks of 0xa5 + num_blocks = 64 + data_size = data_block_size * num_blocks + with open(rootfs_file, 'wb') as f: + f.write(bytes([0xa5]) * data_size) + + with open(its_file, 'w') as f: + f.write(ITS_TEMPLATE % (data_block_size, hash_block_size)) + + dtc_args = f'-I dts -O dtb -i {tempdir}' + utils.run_and_log(ubman, + [mkimage, '-E', '-D', dtc_args, '-f', its_file, fit_file]) + + def fdt_get(node, prop): + val = utils.run_and_log(ubman, f'fdtget {fit_file} {node} {prop}') + return val.strip() + + def fdt_get_hex(node, prop): + val = utils.run_and_log(ubman, f'fdtget -tbx {fit_file} {node} {prop}') + return ''.join(b.zfill(2) for b in val.strip().split()) + + verity_path = '/images/rootfs/dm-verity' + + assert fdt_get(verity_path, 'algo') == 'sha256' + assert int(fdt_get(verity_path, 'data-block-size')) == data_block_size + assert int(fdt_get(verity_path, 'hash-block-size')) == hash_block_size + + nblk = int(fdt_get(verity_path, 'num-data-blocks')) + assert nblk == num_blocks, f'num-data-blocks {nblk} != {num_blocks}' + + hblk = int(fdt_get(verity_path, 'hash-start-block')) + # With --no-superblock, hash-start-block = data_size / hash-block-size + assert hblk == data_size // hash_block_size, \ + f'hash-start-block {hblk} != {data_size // hash_block_size}' + + digest = fdt_get_hex(verity_path, 'digest') + assert len(digest) == 64 and digest != '0' * 64 + salt = fdt_get_hex(verity_path, 'salt') + assert len(salt) == 64 + + # Re-verify the digest with veritysetup against the .itb's external data. + # With -E, image data sits after the FIT FDT at (fdt_totalsize + data-offset). + data_offset = int(fdt_get('/images/rootfs', 'data-offset')) + data_size_full = int(fdt_get('/images/rootfs', 'data-size')) + ext_pos = _fdt_totalsize(fit_file) + data_offset + expanded = os.path.join(tempdir, 'expanded.bin') + with open(fit_file, 'rb') as src, open(expanded, 'wb') as dst: + src.seek(ext_pos) + dst.write(src.read(data_size_full)) + + utils.run_and_log(ubman, [ + 'veritysetup', 'verify', expanded, expanded, digest, + '--no-superblock', + f'--data-block-size={data_block_size}', + f'--hash-block-size={hash_block_size}', + f'--data-blocks={nblk}', + '--hash=sha256', + f'--salt={salt}', + f'--hash-offset={data_size}', + ]) + + +@pytest.mark.requiredtool('dtc') +@pytest.mark.requiredtool('fdtget') +@pytest.mark.requiredtool('veritysetup') +@pytest.mark.parametrize('data_block_size,hash_block_size,subdir', [ + (4096, 4096, 'verity-equal'), + (4096, 1024, 'verity-unequal'), +]) +def test_mkimage_verity(ubman, data_block_size, hash_block_size, subdir): + """mkimage writes correct dm-verity properties and the digest verifies. + + Run with matching and mismatched block sizes so the + ``hash-start-block != num-data-blocks`` path is exercised. + """ + tempdir = os.path.join(ubman.config.result_dir, subdir) + os.makedirs(tempdir, exist_ok=True) + _run_round_trip(ubman, tempdir, data_block_size, hash_block_size) + + +@pytest.mark.requiredtool('dtc') +@pytest.mark.requiredtool('veritysetup') +def test_mkimage_verity_requires_external(ubman): + """mkimage rejects dm-verity without -E with the expected diagnostic.""" + + mkimage = ubman.config.build_dir + '/tools/mkimage' + tempdir = os.path.join(ubman.config.result_dir, 'verity_no_ext') + os.makedirs(tempdir, exist_ok=True) + + rootfs_file = os.path.join(tempdir, 'rootfs.bin') + its_file = os.path.join(tempdir, 'image.its') + fit_file = os.path.join(tempdir, 'image.itb') + + with open(rootfs_file, 'wb') as f: + f.write(bytes([0xa5]) * 4096 * 8) + + with open(its_file, 'w') as f: + f.write(ITS_TEMPLATE % (4096, 4096)) + + dtc_args = f'-I dts -O dtb -i {tempdir}' + utils.run_and_log_expect_exception( + ubman, + [mkimage, '-D', dtc_args, '-f', its_file, fit_file], + 1, 'dm-verity requires external data') -- cgit v1.3.1 From c8a636af67c640e1427e1085c8bada672e48f805 Mon Sep 17 00:00:00 2001 From: Rasmus Villemoes Date: Tue, 2 Jun 2026 23:30:13 +0200 Subject: test: hook up test of allowing control DTB to act as FIT image Add a test demonstrating how one can embed various scripts in the control DTB. Verify that the source command can be used with ${fdtcontroladdr} by itself (invoking the default script), and with : suffix. Check that the scripts themselves can invoke "sibling" scripts. Also verify that without CONTROL_DTB_AS_FIT set, the control DTB is not accepted by the source command. Reviewed-by: Simon Glass Signed-off-by: Rasmus Villemoes --- arch/sandbox/dts/sandbox-boot.sh | 2 ++ arch/sandbox/dts/sandbox-inner.sh | 4 ++++ arch/sandbox/dts/sandbox-outer.sh | 4 ++++ arch/sandbox/dts/sandbox_scripts.dtsi | 24 ++++++++++++++++++++++++ configs/sandbox_defconfig | 2 ++ test/py/tests/test_source.py | 31 +++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+) create mode 100644 arch/sandbox/dts/sandbox-boot.sh create mode 100644 arch/sandbox/dts/sandbox-inner.sh create mode 100644 arch/sandbox/dts/sandbox-outer.sh create mode 100644 arch/sandbox/dts/sandbox_scripts.dtsi (limited to 'test/py/tests') diff --git a/arch/sandbox/dts/sandbox-boot.sh b/arch/sandbox/dts/sandbox-boot.sh new file mode 100644 index 00000000000..4f7fa661151 --- /dev/null +++ b/arch/sandbox/dts/sandbox-boot.sh @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: (GPL-2.0+ OR MIT) +echo "* default script" diff --git a/arch/sandbox/dts/sandbox-inner.sh b/arch/sandbox/dts/sandbox-inner.sh new file mode 100644 index 00000000000..b8fc8f7484b --- /dev/null +++ b/arch/sandbox/dts/sandbox-inner.sh @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: (GPL-2.0+ OR MIT) + +# Some comment. +echo "* inner" diff --git a/arch/sandbox/dts/sandbox-outer.sh b/arch/sandbox/dts/sandbox-outer.sh new file mode 100644 index 00000000000..40294085433 --- /dev/null +++ b/arch/sandbox/dts/sandbox-outer.sh @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: (GPL-2.0+ OR MIT) +echo "* outer 1" +source ${fdtcontroladdr}:inner +echo "* outer 2" diff --git a/arch/sandbox/dts/sandbox_scripts.dtsi b/arch/sandbox/dts/sandbox_scripts.dtsi new file mode 100644 index 00000000000..c800ec39e87 --- /dev/null +++ b/arch/sandbox/dts/sandbox_scripts.dtsi @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/ { + images { + default = "boot"; + boot { + description = "Test boot script"; + data = /incbin/("sandbox-boot.sh"); + type = "script"; + compression = "none"; + }; + outer { + description = "Script testing recursion"; + data = /incbin/("sandbox-outer.sh"); + type = "script"; + compression = "none"; + }; + inner { + description = "Another test script"; + data = /incbin/("sandbox-inner.sh"); + type = "script"; + compression = "none"; + }; + }; +}; diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index ba800f7d19d..bfdc5ee6010 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -20,6 +20,7 @@ CONFIG_EFI_CAPSULE_AUTHENTICATE=y CONFIG_EFI_CAPSULE_CRT_FILE="board/sandbox/capsule_pub_key_good.crt" CONFIG_BUTTON_CMD=y CONFIG_FIT=y +CONFIG_CONTROL_DTB_AS_FIT=y CONFIG_FIT_SIGNATURE=y CONFIG_FIT_CIPHER=y CONFIG_FIT_VERBOSE=y @@ -152,6 +153,7 @@ CONFIG_CMD_STACKPROTECTOR_TEST=y CONFIG_CMD_SPAWN=y CONFIG_MAC_PARTITION=y CONFIG_OF_LIVE=y +CONFIG_DEVICE_TREE_INCLUDES="sandbox_scripts.dtsi" CONFIG_ENV_IS_NOWHERE=y CONFIG_ENV_IS_IN_FAT=y CONFIG_ENV_IS_IN_EXT4=y diff --git a/test/py/tests/test_source.py b/test/py/tests/test_source.py index 970d8c79869..29ab804f81b 100644 --- a/test/py/tests/test_source.py +++ b/test/py/tests/test_source.py @@ -34,3 +34,34 @@ def test_source(ubman): ubman.run_command('fdt rm /images default') assert 'Fail' in ubman.run_command('source || echo Fail') assert 'Fail' in ubman.run_command('source \\# || echo Fail') + +@pytest.mark.boardspec('sandbox') +@pytest.mark.buildconfigspec('cmd_echo') +@pytest.mark.buildconfigspec('cmd_source') +@pytest.mark.buildconfigspec('fit') +@pytest.mark.buildconfigspec('control_dtb_as_fit') +def test_source_control_dtb(ubman): + output = ubman.run_command('source ${fdtcontroladdr}') + assert '* default script' in output + + output = ubman.run_command('source ${fdtcontroladdr}:boot') + assert '* default script' in output + + output = ubman.run_command('source ${fdtcontroladdr}:outer') + assert '* outer 1' in output + assert '* inner' in output + assert '* outer 2' in output + + output = ubman.run_command('source ${fdtcontroladdr}:inner') + assert '* outer' not in output + assert '* inner' in output + + assert 'Fail' in ubman.run_command('source ${fdtcontroladdr}:no-such-script || echo Fail') + +@pytest.mark.buildconfigspec('cmd_echo') +@pytest.mark.buildconfigspec('cmd_source') +@pytest.mark.buildconfigspec('fit') +@pytest.mark.notbuildconfigspec('control_dtb_as_fit') +def test_source_reject_control_dtb(ubman): + assert 'Fail' in ubman.run_command('source ${fdtcontroladdr} || echo Fail') + assert 'Fail' in ubman.run_command('source ${fdtcontroladdr}:boot || echo Fail') -- cgit v1.3.1 From 4a4452a03916d8687442b1d0af5098be986e439e Mon Sep 17 00:00:00 2001 From: Aristo Chen Date: Tue, 26 May 2026 07:03:33 +0000 Subject: test/py: cover get_basename crash on paths with dotted directories Add a parametrized regression test for the fix in the previous commit. The test invokes mkimage in auto-FIT mode (-f auto) with a -b argument whose directory component contains a '.' and whose leaf either lacks an extension or is a plain identifier. Before the fix these inputs caused get_basename() to compute a negative length and segfault inside memcpy. The test asserts that mkimage exits successfully and that the fdt sub-image description matches the expected stripped basename, covering "./mydt", "./sub.d/leaf", and "./a.b/c". A control input of "./mydt.dtb" is also exercised to confirm normal extension stripping still works. Signed-off-by: Aristo Chen --- test/py/tests/test_fit_mkimage_validate.py | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) (limited to 'test/py/tests') diff --git a/test/py/tests/test_fit_mkimage_validate.py b/test/py/tests/test_fit_mkimage_validate.py index 170b2a8cbbb..5922f071dd8 100644 --- a/test/py/tests/test_fit_mkimage_validate.py +++ b/test/py/tests/test_fit_mkimage_validate.py @@ -7,6 +7,7 @@ import os import subprocess import pytest import fit_util +import utils import re @pytest.mark.boardspec('sandbox') @@ -103,3 +104,59 @@ def test_fit_invalid_default_config(ubman): assert result.returncode != 0, "mkimage should fail due to missing default config" assert re.search(r"Default configuration '.*' not found under /configurations", result.stderr) + +@pytest.mark.boardspec('sandbox') +@pytest.mark.requiredtool('dtc') +@pytest.mark.requiredtool('fdtget') +@pytest.mark.parametrize('dtb_relpath,expected_desc', [ + # Crash triggers: last '.' precedes last '/', or leaf has no extension. + ('./mydt', 'mydt'), + ('./sub.d/leaf', 'leaf'), + ('./a.b/c', 'c'), + # Control case: extension lives in the leaf, no dotted directory. + ('./mydt.dtb', 'mydt'), +]) +def test_fit_auto_basename_dotted_directory(ubman, dtb_relpath, expected_desc): + """Regression test: mkimage -f auto must not crash when a -b path has a + '.' in its directory portion. + + Before the fix, get_basename() in tools/fit_image.c searched the whole + path for both the last '/' and the last '.'. When the '.' fell before + the '/', the computed length went negative and was passed unchanged to + memcpy(), which segfaulted. This test exercises three crashing paths + plus one control input. + """ + build_dir = ubman.config.build_dir + kernel = fit_util.make_kernel(ubman, 'kernel.bin', 'kernel') + itb_fname = fit_util.make_fname(ubman, 'auto_basename.itb') + + # Materialize the dtb at the requested relative path inside build_dir. + dtb_abs = os.path.join(build_dir, dtb_relpath) + os.makedirs(os.path.dirname(dtb_abs), exist_ok=True) + with open(dtb_abs, 'wb') as f: + f.write(b'dummy') + + cmd = ['./tools/mkimage', '-f', 'auto', + '-A', 'arm', '-O', 'linux', '-T', 'kernel', '-C', 'none', + '-a', '0x80000000', '-e', '0x80000000', '-n', 'test', + '-d', kernel, + '-b', dtb_relpath, + itb_fname] + # Run with cwd=build_dir so both ./tools/mkimage and the relative -b + # path resolve the same way the bug originally reproduced. + result = subprocess.run(cmd, capture_output=True, text=True, + cwd=build_dir) + + assert result.returncode == 0, ( + f"mkimage crashed or failed on -b {dtb_relpath!r}: " + f"rc={result.returncode}\nstdout:\n{result.stdout}\n" + f"stderr:\n{result.stderr}" + ) + # The fdt sub-image description is set from get_basename(). Read it back + # from the produced FIT (a device tree) rather than parsing mkimage's + # console output. + desc = utils.run_and_log( + ubman, ['fdtget', itb_fname, '/images/fdt-1', 'description']).strip() + assert desc == expected_desc, ( + f"Expected /images/fdt-1 description {expected_desc!r}, got {desc!r}" + ) -- cgit v1.3.1 From e73328612534e81b41d0363fad9a7b4385cd3d39 Mon Sep 17 00:00:00 2001 From: Anton Ivanov Date: Tue, 2 Jun 2026 19:29:25 +0100 Subject: image-fit-sig: Validate hashed-strings region size fit_config_check_sig() reads the hashed-strings property and uses its size value without validation when building the region list for signature verification. A crafted FIT image can specify an arbitrary size, causing the hash calculation to read beyond the end of the FIT image. The property length is also not checked, so a truncated hashed-strings property causes strings[1] to be read past the end of the property. This may result in the out-of-bounds read during signature verification of an untrusted FIT. Validate both the property length and that the declared strings region fits within bounds before adding it to the region list. Signed-off-by: Anton Ivanov --- boot/image-fit-sig.c | 19 +++++++++++++++++-- test/py/tests/test_vboot.py | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) (limited to 'test/py/tests') diff --git a/boot/image-fit-sig.c b/boot/image-fit-sig.c index 433df20281f..9b5ab754561 100644 --- a/boot/image-fit-sig.c +++ b/boot/image-fit-sig.c @@ -452,6 +452,8 @@ static int fit_config_check_sig(const void *fit, int noffset, int conf_noffset, int max_regions; char path[200]; int count; + int len; + uint32_t size; debug("%s: fdt=%p, conf='%s', sig='%s'\n", __func__, key_blob, fit_get_name(fit, noffset, NULL), @@ -506,14 +508,27 @@ static int fit_config_check_sig(const void *fit, int noffset, int conf_noffset, } /* Add the strings */ - strings = fdt_getprop(fit, noffset, "hashed-strings", NULL); + strings = fdt_getprop(fit, noffset, "hashed-strings", &len); if (strings) { + if (len < (int)(2 * sizeof(fdt32_t))) { + *err_msgp = "Invalid hashed-strings property"; + return -1; + } + size = fdt32_to_cpu(strings[1]); + /* + * The offset should be already validated by fdt_check_header(); + * validate the size here. + */ + if (size > fdt_size_dt_strings(fit)) { + *err_msgp = "Strings region is out of bounds"; + return -1; + } /* * The strings region offset must be a static 0x0. * This is set in tool/image-host.c */ fdt_regions[count].offset = fdt_off_dt_strings(fit); - fdt_regions[count].size = fdt32_to_cpu(strings[1]); + fdt_regions[count].size = size; count++; } diff --git a/test/py/tests/test_vboot.py b/test/py/tests/test_vboot.py index 55518bed07e..8fa8f2d59cf 100644 --- a/test/py/tests/test_vboot.py +++ b/test/py/tests/test_vboot.py @@ -415,6 +415,32 @@ def test_vboot(ubman, name, sha_algo, padding, sign_options, required, ubman, [fit_check_sign, '-f', fit, '-k', dtb], 1, 'Failed to verify required signature') + # Create a new properly signed fit and replace hashed-strings + # size property + make_fit('sign-configs-%s%s.its' % (sha_algo, padding), ubman, mkimage, dtc_args, datadir, fit) + sign_fit(sha_algo, sign_options) + utils.run_and_log(ubman, 'fdtput -t x %s %s hashed-strings 0' % + (fit, sig_node)) + run_bootm(sha_algo, 'Signed config with truncated hashed-strings', + 'Invalid hashed-strings property', False) + ubman.log.action('%s: Check truncated hashed-strings property' % sha_algo) + + # size_dt_strings is at offset 32 in the FDT header + with open(fit, 'rb') as handle: + handle.seek(32) + size_dt_strings = struct.unpack(">I", handle.read(4))[0] + utils.run_and_log(ubman, 'fdtput -t x %s %s hashed-strings 0 %#x' % + (fit, sig_node, size_dt_strings + 1)) + run_bootm(sha_algo, 'Signed config with overflowed hashed-strings size', + 'Strings region is out of bounds', False) + ubman.log.action('%s: Check overflowed hashed-strings size' % sha_algo) + + utils.run_and_log(ubman, 'fdtput -t x %s %s hashed-strings 0 %#x' % + (fit, sig_node, size_dt_strings)) + run_bootm(sha_algo, 'Signed config with in-bounds hashed-strings size', + 'Bad Data Hash', False) + ubman.log.action('%s: Check in-bounds hashed-strings size' % sha_algo) + def test_required_key(sha_algo, padding, sign_options): """Test verified boot with the given hash algorithm. -- cgit v1.3.1 From 69f6272b24a26c17a5b6de3b041218ec57239943 Mon Sep 17 00:00:00 2001 From: Anton Ivanov Date: Thu, 4 Jun 2026 11:39:50 +0100 Subject: image-fit: Validate external data offset and size fit_image_get_data() uses the data-position, data-offset, and data-size FIT properties without bounds checking. A crafted FIT image can specify values that cause out-of-bounds read during signature verification of an untrusted FIT. Validate that the external data offset and size are non-negative, and that the data region fits within the FIT image bounds. Signed-off-by: Anton Ivanov Reviewed-by: Simon Glass --- boot/image-fit.c | 53 +++++++++++++- test/py/tests/test_vboot.py | 165 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 2 deletions(-) (limited to 'test/py/tests') diff --git a/boot/image-fit.c b/boot/image-fit.c index c7cf3c8f0bd..937654b6223 100644 --- a/boot/image-fit.c +++ b/boot/image-fit.c @@ -1073,23 +1073,72 @@ int fit_image_get_data(const void *fit, int noffset, const void **data, int offset; int len; int ret; + size_t fdt_total_size_aligned; + uintptr_t max_offset; if (!fit_image_get_data_position(fit, noffset, &offset)) { + if (offset < 0) { + printf("Invalid external data position: %d\n", offset); + return -EINVAL; + } + external_data = true; } else if (!fit_image_get_data_offset(fit, noffset, &offset)) { - external_data = true; /* * For FIT with external data, figure out where * the external images start. This is the base * for the data-offset properties in each image. */ - offset += ((fdt_totalsize(fit) + 3) & ~3); + fdt_total_size_aligned = ((fdt_totalsize(fit) + 3) & ~3); + /* The resulting offset cannot exceed INT_MAX */ + if (offset < 0 || fdt_total_size_aligned > INT_MAX - offset) { + printf("Invalid external data offset: %d\n", offset); + return -EINVAL; + } + offset += fdt_total_size_aligned; + + external_data = true; } if (external_data) { debug("External Data\n"); + + max_offset = UINTPTR_MAX - (uintptr_t)fit; + /* Check that external data offset is within the addressable range */ + if (offset > max_offset) { + printf("Invalid external data offset: %d\n", offset); + return -EINVAL; + } + ret = fit_image_get_data_size(fit, noffset, &len); if (!ret) { + if (len < 0) { + printf("Invalid external data size: %d\n", len); + return -EINVAL; + } + /* + * For non-signed FIT images, we can only check that + * (offset + len) doesn't exceed the addressable range. + * For signed FITs, we can additionally check that + * (offset + len) doesn't exceed the allowed FIT image + * maximum size. + */ + if (len > max_offset - offset + /* + * #if (not a runtime if) is required: FIT_SIGNATURE_MAX_SIZE + * depends on FIT_SIGNATURE, so CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) + * is undefined when signing is disabled and referencing it + * here would fail to compile. + */ +#if CONFIG_IS_ENABLED(FIT_SIGNATURE) + || offset > CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) || + len > CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) - offset +#endif + ) { + printf("FIT external data is out of bounds (offset=%d, size=%d)\n", + offset, len); + return -EINVAL; + } *data = fit + offset; *size = len; } diff --git a/test/py/tests/test_vboot.py b/test/py/tests/test_vboot.py index 8fa8f2d59cf..4b6707caf70 100644 --- a/test/py/tests/test_vboot.py +++ b/test/py/tests/test_vboot.py @@ -589,6 +589,171 @@ def test_vboot(ubman, name, sha_algo, padding, sign_options, required, ubman.restart_uboot() +@pytest.mark.boardspec('sandbox') +@pytest.mark.buildconfigspec('fit_signature') +@pytest.mark.requiredtool('dtc') +@pytest.mark.requiredtool('fdtput') +@pytest.mark.requiredtool('openssl') +def test_vboot_ext_data_bounds(ubman): + """Test that malformed external-data properties are rejected. + + A signed FIT with external data exposes 'data-position', 'data-offset' and + 'data-size' properties. U-Boot must validate these before hashing the image + components, otherwise a crafted FIT could trigger an out-of-bounds access + during signature verification. + + These checks are independent of the hashing algorithm, so a single signing + configuration is enough. + + This works using sandbox only as it needs to update the device tree used + by U-Boot to hold public keys from the signing process. + """ + sha_algo = 'sha256' + + def run_bootm(test_type, expect_string): + """Run a 'bootm' command in U-Boot and expect it to fail. + + This always starts a fresh U-Boot instance since the device tree may + contain a new public key. + + Args: + test_type: A string identifying the test type. + expect_string: A string which is expected in the output. + """ + ubman.restart_uboot() + with ubman.log.section('Verified boot %s %s' % (sha_algo, test_type)): + output = ubman.run_command_list( + ['host load hostfs - 100 %s' % fit, + 'fdt addr 100', + 'bootm 100']) + assert expect_string in ''.join(output) + assert 'sandbox: continuing, as we cannot run' not in ''.join(output) + + def sign_fit(options): + """Sign the FIT + + Signs the FIT and writes the signature into it. It also writes the + public key into the dtb. + + Args: + options: Options to provide to mkimage. + """ + args = [mkimage, '-F', '-k', tmpdir, '-K', dtb, '-r', fit] + if options: + args += options.split(' ') + ubman.log.action('%s: Sign images' % sha_algo) + utils.run_and_log(ubman, args) + + def create_rsa_pair(name): + """Generate a new RSA key pair and certificate. + + Args: + name: Name of the key (e.g. 'dev') + """ + public_exponent = 65537 + utils.run_and_log(ubman, 'openssl genpkey -algorithm RSA -out %s%s.key ' + '-pkeyopt rsa_keygen_bits:2048 ' + '-pkeyopt rsa_keygen_pubexp:%d' % + (tmpdir, name, public_exponent)) + + # Create a certificate containing the public key + utils.run_and_log(ubman, 'openssl req -batch -new -x509 -key %s%s.key ' + '-out %s%s.crt' % (tmpdir, name, tmpdir, name)) + + def set_external_data(prop, value): + """Set an external-data property of the kernel image. + + Args: + prop: Property name + value: The new value of the property + """ + utils.run_and_log( + ubman, 'fdtput -t x %s /images/kernel %s %#x' % (fit, prop, value) + ) + + def make_signed_fit(): + """Build a fresh signed FIT with external data. + + sign_fit() overwrites the FIT, so a new one is built before each test + case mutates its external-data properties. + """ + make_fit('sign-configs-%s.its' % sha_algo, ubman, mkimage, dtc_args, + datadir, fit) + sign_fit('-E') + + tmpdir = os.path.join(ubman.config.result_dir, 'ext-data-bounds') + '/' + if not os.path.exists(tmpdir): + os.mkdir(tmpdir) + datadir = ubman.config.source_dir + '/test/py/tests/vboot/' + fit = '%stest.fit' % tmpdir + mkimage = ubman.config.build_dir + '/tools/mkimage' + dtc_args = '-I dts -O dtb -i %s' % tmpdir + dtb = '%ssandbox-u-boot.dtb' % tmpdir + + bcfg = ubman.config.buildconfig + max_size = int(bcfg.get('config_fit_signature_max_size', 0x10000000), 0) + + create_rsa_pair('dev') + + # Create a kernel image filled with zeroes + with open('%stest-kernel.bin' % tmpdir, 'wb') as fd: + fd.write(500 * b'\0') + + testcases = [ + ('negative data-position', + {'data-position': 0xffffffff}, 'Invalid external data position'), + ('negative data-offset', + {'data-offset': 0xffffffff}, 'Invalid external data offset'), + ('negative data-size', + {'data-size': 0xffffffff}, 'Invalid external data size'), + ('off-bounds data-position', + {'data-position': 0x7fffffff}, 'FIT external data is out of bounds'), + ('off-bounds data-offset', + {'data-offset': 0x10000000}, 'FIT external data is out of bounds'), + ('oversized data-size', + {'data-size': 0x7fffffff}, 'FIT external data is out of bounds'), + ('off-bounds data-position', + {'data-position': max_size + 1, 'data-size': 0}, + 'FIT external data is out of bounds'), + ('off-bounds data-offset', + {'data-offset': max_size + 1, 'data-size': 0}, + 'FIT external data is out of bounds'), + ('oversized data-size', + {'data-position': 0x0, 'data-size': max_size + 1}, + 'FIT external data is out of bounds'), + ('in-bounds data-position', + {'data-position': max_size, 'data-size': 0}, 'Bad Data Hash'), + ('in-bounds data-offset', + {'data-offset': max_size, 'data-size': 0}, 'Bad Data Hash'), + ('in-bounds data-size', + {'data-position': 0x0, 'data-size': max_size}, 'Bad Data Hash'), + ] + + # We need to use our own device tree file. Remember to restore it + # afterwards. + old_dtb = ubman.config.dtb + try: + ubman.config.dtb = dtb + + # Compile our device tree files for kernel and U-Boot. These are + # regenerated here since mkimage will modify them (by adding a + # public key) below. + dtc('sandbox-kernel.dts', ubman, dtc_args, datadir, tmpdir, dtb) + dtc('sandbox-u-boot.dts', ubman, dtc_args, datadir, tmpdir, dtb) + + ubman.log.action( + '%s: Test signed FIT with malformed external-data properties' % sha_algo) + for desc, props, expect_string in testcases: + make_signed_fit() + for prop, value in props.items(): + set_external_data(prop, value) + run_bootm('Signed config with %s' % desc, expect_string) + finally: + # Go back to the original U-Boot with the correct dtb. + ubman.config.dtb = old_dtb + ubman.restart_uboot() + + TESTDATA_IN = [ ['sha1-basic', 'sha1', '', None, False], ['sha1-pad', 'sha1', '', '-E -p 0x10000', False], -- cgit v1.3.1 From 5a17d02223b942f97bd3df9d6acb835fded4ad28 Mon Sep 17 00:00:00 2001 From: Randolph Sapp Date: Thu, 4 Jun 2026 10:50:36 -0500 Subject: test_ut: add a ut_ubman fixture to clean up tests Add a ut_ubman fixture to clean up after certain problematic tests without negatively affecting the current assert based testing. Currently this catches "bootstd bootflow_cmd_boot" and "bootstd bootflow_scan_boot" ut_subtests, as these will change the sandbox state a little too much to be recoverable from. Signed-off-by: Randolph Sapp Reviewed-by: Simon Glass --- test/py/tests/test_ut.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) (limited to 'test/py/tests') diff --git a/test/py/tests/test_ut.py b/test/py/tests/test_ut.py index dce5a37dd35..fa50c8008a5 100644 --- a/test/py/tests/test_ut.py +++ b/test/py/tests/test_ut.py @@ -631,7 +631,23 @@ def test_ut_dm_init_bootstd(ubman): ubman.restart_uboot() -def test_ut(ubman, ut_subtest): +@pytest.fixture(name="ut_ubman") +def ut_ubman_fixture(ubman, ut_subtest): + """Fixture to restart the sandbox after known problematic tests. + + Args: + ubman (ConsoleBase): U-Boot console + ut_subtest (str): test to be executed via command ut, e.g 'foo bar' to + execute command 'ut foo bar' + """ + + yield ubman + + if ut_subtest in ("bootstd bootflow_cmd_boot", "bootstd bootflow_scan_boot"): + ubman.restart_uboot() + + +def test_ut(ut_ubman, ut_subtest): """Execute a "ut" subtest. The subtests are collected in function generate_ut_subtest() from linker @@ -644,18 +660,18 @@ def test_ut(ubman, ut_subtest): implemented in C function foo_test_bar(). Args: - ubman (ConsoleBase): U-Boot console + ut_ubman (ConsoleBase): U-Boot console ut_subtest (str): test to be executed via command ut, e.g 'foo bar' to execute command 'ut foo bar' """ if ut_subtest == 'hush hush_test_simple_dollar': # ut hush hush_test_simple_dollar prints "Unknown command" on purpose. - with ubman.disable_check('unknown_command'): - output = ubman.run_command('ut ' + ut_subtest) + with ut_ubman.disable_check('unknown_command'): + output = ut_ubman.run_command('ut ' + ut_subtest) assert 'Unknown command \'quux\' - try \'help\'' in output else: - output = ubman.run_command('ut ' + ut_subtest) + output = ut_ubman.run_command('ut ' + ut_subtest) assert output.endswith('failures: 0') lastline = output.splitlines()[-1] if "skipped: 0," not in lastline: -- cgit v1.3.1 From de12fa4df89277460aacaea7c9a43421190ee04d Mon Sep 17 00:00:00 2001 From: Randolph Sapp Date: Thu, 4 Jun 2026 10:50:37 -0500 Subject: test: boot: add a fdt reserved region check Add a image_fdt suite and a check for boot_fdt_add_mem_rsv_regions. This will ensure the user is properly informed of any reservation failures. It will also validate that reservations are cleaned up correctly when switching FDTs. Signed-off-by: Randolph Sapp Reviewed-by: Simon Glass Acked-by: Ilias Apalodimas --- test/boot/Makefile | 3 ++ test/boot/image_fdt.c | 83 +++++++++++++++++++++++++++++++++++++++++++++ test/cmd_ut.c | 2 ++ test/py/tests/test_suite.py | 2 +- 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 test/boot/image_fdt.c (limited to 'test/py/tests') diff --git a/test/boot/Makefile b/test/boot/Makefile index 89538d4f0a6..12904f7f508 100644 --- a/test/boot/Makefile +++ b/test/boot/Makefile @@ -14,6 +14,9 @@ endif ifdef CONFIG_SANDBOX obj-$(CONFIG_$(PHASE_)CMDLINE) += bootm.o +ifdef CONFIG_UT_DM +obj-$(CONFIG_$(PHASE_)OF_LIBFDT) += image_fdt.o +endif endif obj-$(CONFIG_MEASURED_BOOT) += measurement.o diff --git a/test/boot/image_fdt.c b/test/boot/image_fdt.c new file mode 100644 index 00000000000..5417689a683 --- /dev/null +++ b/test/boot/image_fdt.c @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2026 Texas Instruments Incorporated - https://www.ti.com/ + */ + +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#define IMAGE_FDT_TEST(_name, _flags) UNIT_TEST(_name, _flags, image_fdt) + +DECLARE_GLOBAL_DATA_PTR; + +/** + * test_boot_fdt_add_mem_rsv_regions - Make sure dt reservations are created and + * destroyed correctly + * @uts: Test state + * + * This test depends on the UT_DM device tree and ensures the following + * statements hold true: The default reservation in test.dtb exists. + * Re-reserving that region will result in an error. Loading a new device tree + * will remove old reservations. + */ +static int test_boot_fdt_add_mem_rsv_regions(struct unit_test_state *uts) +{ + phys_addr_t start = CFG_SYS_SDRAM_BASE + 0x100000; + const void *old_blob = gd->fdt_blob; + int ret = CMD_RET_FAILURE; + ulong fdt_sz; + int nodeoffset; + void *new_blob; + + /* Default reservation should exist */ + ut_asserteq(1, lmb_is_reserved_flags(start, LMB_NOMAP)); + + /* Attempting to re-reserve should warn the user */ + boot_fdt_add_mem_rsv_regions(gd->fdt_blob); + ut_assert_nextlinen("ERROR: reserving"); + ut_assert_console_end(); + + /* Loading a new_blob device tree should be allowed */ + fdt_sz = fdt_totalsize(gd->fdt_blob); + new_blob = malloc(fdt_sz); + ut_assertnonnull(new_blob); + memcpy(new_blob, gd->fdt_blob, fdt_sz); + + nodeoffset = fdt_path_offset(new_blob, "/reserved-memory"); + if (nodeoffset < 0) + goto free_blob; + + if (fdt_del_node(new_blob, nodeoffset)) + goto free_blob; + + boot_fdt_add_mem_rsv_regions(new_blob); + gd->fdt_blob = new_blob; + + if (ut_check_console_end(uts)) { + ut_failf(uts, __FILE__, __LINE__, __func__, "console", + "Expected no more output, got '%s'", uts->actual_str); + goto switch_fdt; + } + + /* Reservation should not exist now */ + if (!lmb_is_reserved_flags(start, LMB_NOMAP)) + ret = 0; + + /* Cleanup */ +switch_fdt: + boot_fdt_add_mem_rsv_regions(old_blob); + gd->fdt_blob = old_blob; +free_blob: + free(new_blob); + return ret; +} +IMAGE_FDT_TEST(test_boot_fdt_add_mem_rsv_regions, UTF_CONSOLE); diff --git a/test/cmd_ut.c b/test/cmd_ut.c index 44e5fdfdaa6..363ed4eab30 100644 --- a/test/cmd_ut.c +++ b/test/cmd_ut.c @@ -61,6 +61,7 @@ SUITE_DECL(fdt); SUITE_DECL(fdt_overlay); SUITE_DECL(font); SUITE_DECL(hush); +SUITE_DECL(image_fdt); SUITE_DECL(lib); SUITE_DECL(loadm); SUITE_DECL(log); @@ -88,6 +89,7 @@ static struct suite suites[] = { SUITE(fdt_overlay, "device tree overlays"), SUITE(font, "font command"), SUITE(hush, "hush behaviour"), + SUITE(image_fdt, "image fdt parsing"), SUITE(lib, "library functions"), SUITE(loadm, "loadm command parameters and loading memory blob"), SUITE(log, "logging functions"), diff --git a/test/py/tests/test_suite.py b/test/py/tests/test_suite.py index 7fe9a90dfd3..08285f12a5f 100644 --- a/test/py/tests/test_suite.py +++ b/test/py/tests/test_suite.py @@ -8,7 +8,7 @@ import re EXPECTED_SUITES = [ 'addrmap', 'bdinfo', 'bloblist', 'bootm', 'bootstd', 'cmd', 'common', 'dm', 'env', 'exit', 'fdt_overlay', - 'fdt', 'font', 'hush', 'lib', + 'fdt', 'font', 'hush', 'image_fdt', 'lib', 'loadm', 'log', 'mbr', 'measurement', 'mem', 'pci_mps', 'setexpr', 'upl', ] -- 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(+) (limited to 'test/py/tests') 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