diff options
Diffstat (limited to 'test/py')
| -rw-r--r-- | test/py/tests/fs_helper.py | 97 | ||||
| -rw-r--r-- | test/py/tests/test_cat.py | 27 | ||||
| -rw-r--r-- | test/py/tests/test_efi_bootmgr.py | 36 | ||||
| -rw-r--r-- | test/py/tests/test_efi_fit.py | 2 | ||||
| -rw-r--r-- | test/py/tests/test_efi_loader.py | 2 | ||||
| -rwxr-xr-x | test/py/tests/test_fit.py | 583 | ||||
| -rw-r--r-- | test/py/tests/test_fit_mkimage_validate.py | 57 | ||||
| -rw-r--r-- | test/py/tests/test_fit_verity.py | 175 | ||||
| -rw-r--r-- | test/py/tests/test_fpga.py | 4 | ||||
| -rw-r--r-- | test/py/tests/test_fs/conftest.py | 198 | ||||
| -rw-r--r-- | test/py/tests/test_fs/test_erofs.py | 16 | ||||
| -rw-r--r-- | test/py/tests/test_fs/test_squashfs/sqfs_common.py | 22 | ||||
| -rw-r--r-- | test/py/tests/test_net.py | 2 | ||||
| -rw-r--r-- | test/py/tests/test_source.py | 31 | ||||
| -rw-r--r-- | test/py/tests/test_suite.py | 2 | ||||
| -rw-r--r-- | test/py/tests/test_ut.py | 212 | ||||
| -rw-r--r-- | test/py/tests/test_vboot.py | 191 | ||||
| -rw-r--r-- | test/py/tests/test_xxd.py | 28 | ||||
| -rw-r--r-- | test/py/utils.py | 13 |
19 files changed, 1179 insertions, 519 deletions
diff --git a/test/py/tests/fs_helper.py b/test/py/tests/fs_helper.py index 800376b1e7d..e3824b2c1fd 100644 --- a/test/py/tests/fs_helper.py +++ b/test/py/tests/fs_helper.py @@ -66,8 +66,9 @@ class FsHelper: """Make a new filesystem and copy in the files""" self.setup() self._do_cleanup = True + src_dir = self.srcdir if os.listdir(self.srcdir) else None self.fs_img = mk_fs(self.config, self.fs_type, self.size_mb << 20, - self.prefix, self.srcdir, quiet=self.quiet) + self.prefix, src_dir, quiet=self.quiet) def setup(self): """Set up the srcdir ready to receive files""" @@ -86,7 +87,7 @@ class FsHelper: """Remove created image""" if self.tmpdir: self.tmpdir.cleanup() - if self._do_cleanup: + if self._do_cleanup and self.fs_img: os.remove(self.fs_img) def __enter__(self): @@ -97,6 +98,98 @@ class FsHelper: self.cleanup() +class DiskHelper: + """Helper class for creating disk images containing filesytems + + Usage: + with DiskHelper(ubman.config, 0, 'mmc') as img, \ + FsHelper(ubman.config, 'ext1', 1, 'mmc') as fsh: + # Write files to fsh.srcdir + ... + + # Create the filesystem + fsh.mk_fs() + + # Add this filesystem to the disk + img.add_fs(fsh, DiskHelper.VFAT) + + # Add more filesystems as needed (add another 'with' clause) + ... + + # Get the final disk image + data = img.create() + """ + + # Partition-type codes + VFAT = 0xc + EXT4 = 0x83 + + def __init__(self, config, devnum, prefix, cur_dir=False): + """Set up a new disk image + + Args: + config (u_boot_config): U-Boot configuration + devnum (int): Device number (for filename) + prefix (str): Prefix string of volume's file name + cur_dir (bool): True to put the file in the current directory, + instead of the persistent-data directory + """ + self.fs_list = [] + self.fname = os.path.join('' if cur_dir else config.persistent_data_dir, + f'{prefix}{devnum}.img') + + def add_fs(self, fs_img, part_type, bootable=False): + """Add a new filesystem + + Args: + fs_img (FsHelper): Filesystem to add + part_type (DiskHelper.FAT or DiskHelper.EXT4): Partition type + bootable (bool): True to set the 'bootable' flat + """ + self.fs_list.append([fs_img, part_type, bootable]) + + def create(self): + """Create the disk image + + Create an image with a partition table and the filesystems + """ + spec = '' + pos = 1 # Reserve 1MB for the partition table itself + for fsi, part_type, bootable in self.fs_list: + if spec: + spec += '\n' + spec += f'type={part_type:x}, size={fsi.size_mb}M, start={pos}M' + if bootable: + spec += ', bootable' + pos += fsi.size_mb + + img_size = pos + try: + check_call(f'qemu-img create {self.fname} {img_size}M', shell=True) + check_call(f'printf "{spec}" | sfdisk {self.fname}', shell=True) + except CalledProcessError: + os.remove(self.fname) + raise + + pos = 1 # Reserve 1MB for the partition table itself + for fsi, part_type, bootable in self.fs_list: + check_call( + f'dd if={fsi.fs_img} of={self.fname} bs=1M seek={pos} conv=notrunc', + shell=True) + pos += fsi.size_mb + return self.fname + + def cleanup(self, remove_full_img=False): + """Remove created file""" + os.remove(self.fname) + + def __enter__(self): + return self + + def __exit__(self, extype, value, traceback): + self.cleanup() + + def mk_fs(config, fs_type, size, prefix, src_dir=None, fs_img=None, quiet=False): """Create a file system volume diff --git a/test/py/tests/test_cat.py b/test/py/tests/test_cat.py index 252c3d50a02..f793b9fe0a1 100644 --- a/test/py/tests/test_cat.py +++ b/test/py/tests/test_cat.py @@ -4,8 +4,7 @@ """ import pytest -from subprocess import call, check_call, CalledProcessError -from tests import fs_helper +from tests.fs_helper import FsHelper @pytest.mark.boardspec('sandbox') @pytest.mark.buildconfigspec('cmd_cat') @@ -15,23 +14,11 @@ def test_cat(ubman): Args: ubman -- U-Boot console """ - try: - scratch_dir = ubman.config.persistent_data_dir + '/scratch' + with FsHelper(ubman.config, 'vfat', 1, 'test_cat') as fsh: + with open(f'{fsh.srcdir}/hello', 'w', encoding = 'ascii') as outf: + outf.write('hello world\n') + fsh.mk_fs() - check_call('mkdir -p %s' % scratch_dir, shell=True) - - with open(scratch_dir + '/hello', 'w', encoding = 'ascii') as file: - file.write('hello world\n') - - cat_data = fs_helper.mk_fs(ubman.config, 'vfat', 0x100000, - 'test_cat', scratch_dir) - response = ubman.run_command_list([ f'host bind 0 {cat_data}', - 'cat host 0 hello']) + response = ubman.run_command_list([f'host bind 0 {fsh.fs_img}', + 'cat host 0 hello']) assert 'hello world' in response - except CalledProcessError as err: - pytest.skip('Preparing test_cat image failed') - call('rm -f %s' % cat_data, shell=True) - return - finally: - call('rm -rf %s' % scratch_dir, shell=True) - call('rm -f %s' % cat_data, shell=True) diff --git a/test/py/tests/test_efi_bootmgr.py b/test/py/tests/test_efi_bootmgr.py index 4c10cbdf17d..7d1f3f16d00 100644 --- a/test/py/tests/test_efi_bootmgr.py +++ b/test/py/tests/test_efi_bootmgr.py @@ -5,7 +5,7 @@ import shutil import pytest from subprocess import call, check_call, CalledProcessError -from tests import fs_helper +from tests.fs_helper import DiskHelper, FsHelper @pytest.mark.boardspec('sandbox') @pytest.mark.buildconfigspec('cmd_efidebug') @@ -20,22 +20,19 @@ def test_efi_bootmgr(ubman): Args: ubman -- U-Boot console """ - try: - efi_bootmgr_data, mnt = fs_helper.setup_image(ubman, 0, 0xc, - basename='test_efi_bootmgr') + with DiskHelper(ubman.config, 0, 'test_efi_bootmgr') as img, \ + FsHelper(ubman.config, 'vfat', 1, 'test_efi_bootmgr') as fsh: + with open(f'{fsh.srcdir}/initrd-1.img', 'w', encoding = 'ascii') as outf: + outf.write("initrd 1") + with open(f'{fsh.srcdir}/initrd-2.img', 'w', encoding = 'ascii') as outf: + outf.write("initrd 2") + shutil.copyfile( + ubman.config.build_dir + '/lib/efi_loader/initrddump.efi', + f'{fsh.srcdir}/initrddump.efi') + fsh.mk_fs() - with open(mnt + '/initrd-1.img', 'w', encoding = 'ascii') as file: - file.write("initrd 1") - - with open(mnt + '/initrd-2.img', 'w', encoding = 'ascii') as file: - file.write("initrd 2") - - shutil.copyfile(ubman.config.build_dir + '/lib/efi_loader/initrddump.efi', - mnt + '/initrddump.efi') - - fsfile = fs_helper.mk_fs(ubman.config, 'vfat', 0x100000, - 'test_efi_bootmgr', mnt) - check_call(f'dd if={fsfile} of={efi_bootmgr_data} bs=1M seek=1', shell=True) + img.add_fs(fsh, DiskHelper.VFAT) + efi_bootmgr_data = img.create() ubman.run_command(cmd = f'host bind 0 {efi_bootmgr_data}') @@ -61,10 +58,3 @@ def test_efi_bootmgr(ubman): ubman.run_command(cmd = 'efidebug boot rm 0001') ubman.run_command(cmd = 'efidebug boot rm 0002') - except CalledProcessError as err: - pytest.skip('Preparing test_efi_bootmgr image failed') - call('rm -f %s' % efi_bootmgr_data, shell=True) - return - finally: - call('rm -rf %s' % mnt, shell=True) - call('rm -f %s' % efi_bootmgr_data, shell=True) diff --git a/test/py/tests/test_efi_fit.py b/test/py/tests/test_efi_fit.py index 63ee8e6cef2..409cfdfd56f 100644 --- a/test/py/tests/test_efi_fit.py +++ b/test/py/tests/test_efi_fit.py @@ -225,7 +225,7 @@ def test_efi_fit_launch(ubman): has_dhcp = ubman.config.buildconfig.get('config_cmd_dhcp', 'n') == 'y' if not has_dhcp: - ubman.log.warning('CONFIG_NET != y: Skipping static network setup') + ubman.log.warning('CONFIG_NET_LEGACY != y: Skipping static network setup') return False env_vars = ubman.config.env.get('env__net_static_env_vars', None) diff --git a/test/py/tests/test_efi_loader.py b/test/py/tests/test_efi_loader.py index dc58c0d4dbd..91f151d09cd 100644 --- a/test/py/tests/test_efi_loader.py +++ b/test/py/tests/test_efi_loader.py @@ -98,7 +98,7 @@ def test_efi_setup_dhcp(ubman): global net_set_up net_set_up = True [email protected]('net', 'net_lwip') [email protected]('net') def test_efi_setup_static(ubman): """Set up the network using a static IP configuration. diff --git a/test/py/tests/test_fit.py b/test/py/tests/test_fit.py index 619f73153a0..76adb98e2c5 100755 --- a/test/py/tests/test_fit.py +++ b/test/py/tests/test_fit.py @@ -1,16 +1,17 @@ # SPDX-License-Identifier: GPL-2.0+ # Copyright (c) 2013, Google Inc. -# -# Sanity check of the FIT handling in U-Boot + +"""Sanity check of the FIT handling in U-Boot""" import os -import pytest import struct -import utils + +import pytest import fit_util +import utils # Define a base ITS which we can adjust using % and a dictionary -base_its = ''' +BASE_ITS = ''' /dts-v1/; / { @@ -70,7 +71,7 @@ base_its = ''' configurations { default = "conf-1"; conf-1 { - kernel = "kernel-1"; + %(kernel_config)s fdt = "fdt-1"; %(ramdisk_config)s %(loadables_config)s @@ -80,7 +81,7 @@ base_its = ''' ''' # Define a base FDT - currently we don't use anything in this -base_fdt = ''' +BASE_FDT = ''' /dts-v1/; / { @@ -103,7 +104,8 @@ base_fdt = ''' # This is the U-Boot script that is run for each test. First load the FIT, # then run the 'bootm' command, then save out memory from the places where # we expect 'bootm' to write things. Then quit. -base_script = ''' +BASE_SCRIPT = ''' +mw.b 0 0 160000 host load hostfs 0 %(fit_addr)x %(fit)s fdt addr %(fit_addr)x bootm start %(fit_addr)x @@ -115,60 +117,102 @@ 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') [email protected]('fit_signature') [email protected]('fit') @pytest.mark.requiredtool('dtc') -def test_fit(ubman): - def make_fname(leaf): - """Make a temporary filename +class TestFitImage: + """Test class for FIT image handling in U-Boot - Args: - leaf: Leaf name of file to create (within temporary directory) - Return: - Temporary filename - """ - return os.path.join(ubman.config.build_dir, leaf) + TODO: Almost everything: + - hash algorithms - invalid hash/contents should be detected + - signature algorithms - invalid sig/contents should be detected + - compression + - checking that errors are detected like: + - image overwriting + - missing images + - invalid configurations + - incorrect os/arch/type fields + - empty data + - images too large/small + - invalid FDT (e.g. putting a random binary in instead) + - default configuration selection + - bootm command line parameters should have desired effect + - run code coverage to make sure we are testing all the code + """ - def filesize(fname): + def filesize(self, fname): """Get the size of a file Args: - fname: Filename to check + fname (str): Filename to check + Return: - Size of file in bytes + int: Size of file in bytes """ return os.stat(fname).st_size - def read_file(fname): + def read_file(self, fname): """Read the contents of a file Args: - fname: Filename to read - Returns: - Contents of file as a string + fname (str): Filename to read + + Return: + str: Contents of file """ with open(fname, 'rb') as fd: return fd.read() - def make_ramdisk(filename, text): + def make_ramdisk(self, ubman, filename, text): """Make a sample ramdisk with test data Returns: - Filename of ramdisk created + str: Filename of ramdisk created """ - fname = make_fname(filename) + fname = fit_util.make_fname(ubman, filename) data = '' for i in range(100): - data += '%s %d was seldom used in the middle ages\n' % (text, i) - with open(fname, 'w') as fd: + data += f'{text} {i} was seldom used in the middle ages\n' + with open(fname, 'w', encoding='ascii') as fd: print(data, file=fd) return fname - def make_compressed(filename): + def make_compressed(self, ubman, filename): + """Compress a file using gzip""" utils.run_and_log(ubman, ['gzip', '-f', '-k', filename]) return filename + '.gz' - def find_matching(text, match): + def find_matching(self, text, match): """Find a match in a line of text, and return the unmatched line portion This is used to extract a part of a line from some text. The match string @@ -182,25 +226,30 @@ def test_fit(ubman): to use regex and return groups. Args: - text: Text to check (list of strings, one for each command issued) - match: String to search for + text (list of str): Text to check, one for each command issued + match (str): String to search for + Return: - String containing unmatched portion of line - Exceptions: + str: unmatched portion of line + + Raises: ValueError: If match is not found - >>> find_matching(['first line:10', 'second_line:20'], 'first line:') - '10' - >>> find_matching(['first line:10', 'second_line:20'], 'second line') - Traceback (most recent call last): - ... - ValueError: Test aborted - >>> find_matching('first line:10\', 'second_line:20'], 'second_line:') - '20' - >>> find_matching('first line:10\', 'second_line:20\nthird_line:30'], - 'third_line:') - '30' + .. code-block:: python + + >>> find_matching(['first line:10', 'second_line:20'], 'first line:') + '10' + >>> find_matching(['first line:10', 'second_line:20'], 'second line') + Traceback (most recent call last): + ... + ValueError: Test aborted + >>> find_matching(['first line:10', 'second_line:20'], 'second_line:') + '20' + >>> find_matching(['first line:10', 'second_line:20\\nthird_line:30'], + ... 'third_line:') + '30' """ + # pylint: disable=W0612 __tracebackhide__ = True for line in '\n'.join(text).splitlines(): pos = line.find(match) @@ -208,202 +257,350 @@ def test_fit(ubman): return line[:pos] + line[pos + len(match):] pytest.fail("Expected '%s' but not found in output") + return '<no-match>' - def check_equal(expected_fname, actual_fname, failure_msg): + def check_equal(self, params, expected_key, actual_key, failure_msg): """Check that a file matches its expected contents This is always used on out-buffers whose size is decided by the test script anyway, which in some cases may be larger than what we're actually looking for. So it's safe to truncate it to the size of the expected data. - - Args: - expected_fname: Filename containing expected contents - actual_fname: Filename containing actual contents - failure_msg: Message to print on failure """ - expected_data = read_file(expected_fname) - actual_data = read_file(actual_fname) + expected_data = self.read_file(params[expected_key]) + actual_data = self.read_file(params[actual_key]) if len(expected_data) < len(actual_data): actual_data = actual_data[:len(expected_data)] assert expected_data == actual_data, failure_msg - def check_not_equal(expected_fname, actual_fname, failure_msg): - """Check that a file does not match its expected contents - - Args: - expected_fname: Filename containing expected contents - actual_fname: Filename containing actual contents - failure_msg: Message to print on failure - """ - expected_data = read_file(expected_fname) - actual_data = read_file(actual_fname) + def check_not_equal(self, params, expected_key, actual_key, failure_msg): + """Check that a file does not match its expected contents""" + expected_data = self.read_file(params[expected_key]) + actual_data = self.read_file(params[actual_key]) assert expected_data != actual_data, failure_msg - def run_fit_test(mkimage): - """Basic sanity check of FIT loading in U-Boot - - TODO: Almost everything: - - hash algorithms - invalid hash/contents should be detected - - signature algorithms - invalid sig/contents should be detected - - compression - - checking that errors are detected like: - - image overwriting - - missing images - - invalid configurations - - incorrect os/arch/type fields - - empty data - - images too large/small - - invalid FDT (e.g. putting a random binary in instead) - - default configuration selection - - bootm command line parameters should have desired effect - - run code coverage to make sure we are testing all the code - """ - # Set up invariant files - control_dtb = fit_util.make_dtb(ubman, base_fdt, 'u-boot') + @pytest.fixture() + def fsetup(self, ubman): + """Set up files and default parameters for FIT tests""" + mkimage = os.path.join(ubman.config.build_dir, 'tools/mkimage') + fdt_data = fit_util.make_dtb(ubman, BASE_FDT, 'u-boot') kernel = fit_util.make_kernel(ubman, 'test-kernel.bin', 'kernel') - ramdisk = make_ramdisk('test-ramdisk.bin', 'ramdisk') - loadables1 = fit_util.make_kernel(ubman, 'test-loadables1.bin', 'lenrek') - loadables2 = make_ramdisk('test-loadables2.bin', 'ksidmar') - kernel_out = make_fname('kernel-out.bin') - fdt = make_fname('u-boot.dtb') - fdt_out = make_fname('fdt-out.dtb') - ramdisk_out = make_fname('ramdisk-out.bin') - loadables1_out = make_fname('loadables1-out.bin') - loadables2_out = make_fname('loadables2-out.bin') + ramdisk = self.make_ramdisk(ubman, 'test-ramdisk.bin', 'ramdisk') + loadables1 = fit_util.make_kernel(ubman, 'test-loadables1.bin', + 'lenrek') + loadables2 = self.make_ramdisk(ubman, 'test-loadables2.bin', + 'ksidmar') - # Set up basic parameters with default values - params = { + yield { + 'mkimage' : mkimage, 'fit_addr' : 0x1000, 'kernel' : kernel, - 'kernel_out' : kernel_out, + 'kernel_out' : fit_util.make_fname(ubman, 'kernel-out.bin'), 'kernel_addr' : 0x40000, - 'kernel_size' : filesize(kernel), + 'kernel_size' : self.filesize(kernel), + 'kernel_config' : 'kernel = "kernel-1";', - 'fdt' : fdt, - 'fdt_out' : fdt_out, + 'fdt_data' : fdt_data, + 'fdt' : fit_util.make_fname(ubman, 'u-boot.dtb'), + 'fdt_out' : fit_util.make_fname(ubman, 'fdt-out.dtb'), 'fdt_addr' : 0x80000, - 'fdt_size' : filesize(control_dtb), + 'fdt_size' : self.filesize(fdt_data), 'fdt_load' : '', 'ramdisk' : ramdisk, - 'ramdisk_out' : ramdisk_out, + 'ramdisk_out' : fit_util.make_fname(ubman, 'ramdisk-out.bin'), 'ramdisk_addr' : 0xc0000, - 'ramdisk_size' : filesize(ramdisk), + 'ramdisk_size' : self.filesize(ramdisk), 'ramdisk_load' : '', 'ramdisk_config' : '', 'loadables1' : loadables1, - 'loadables1_out' : loadables1_out, + 'loadables1_out' : fit_util.make_fname(ubman, 'loadables1-out.bin'), 'loadables1_addr' : 0x100000, - 'loadables1_size' : filesize(loadables1), + 'loadables1_size' : self.filesize(loadables1), 'loadables1_load' : '', 'loadables2' : loadables2, - 'loadables2_out' : loadables2_out, + 'loadables2_out' : fit_util.make_fname(ubman, 'loadables2-out.bin'), 'loadables2_addr' : 0x140000, - 'loadables2_size' : filesize(loadables2), + 'loadables2_size' : self.filesize(loadables2), 'loadables2_load' : '', 'loadables_config' : '', 'compression' : 'none', } - # Make a basic FIT and a script to load it - fit = fit_util.make_fit(ubman, mkimage, base_its, params) + def prepare(self, ubman, fsetup, **kwargs): + """Build a FIT with given overrides + + Args: + ubman (ConsoleBase): U-Boot fixture + fsetup (dict): Default parameters from the fsetup fixture + kwargs: Parameter overrides for this particular test + + Return: + tuple: + list of str: Commands to run for the test + dict: Parameters used by the test + str: Filename of the FIT that was created + """ + params = {**fsetup, **kwargs} + fit = fit_util.make_fit(ubman, params['mkimage'], BASE_ITS, params) params['fit'] = fit - cmd = base_script % params + cmds = (BASE_SCRIPT % params).splitlines() + return cmds, params, fit - # First check that we can load a kernel - # We could perhaps reduce duplication with some loss of readability - ubman.config.dtb = control_dtb - ubman.restart_uboot() - with ubman.log.section('Kernel load'): - output = ubman.run_command_list(cmd.splitlines()) - check_equal(kernel, kernel_out, 'Kernel not loaded') - check_not_equal(control_dtb, fdt_out, - 'FDT loaded but should be ignored') - check_not_equal(ramdisk, ramdisk_out, - 'Ramdisk loaded but should not be') + def test_fit_kernel_load(self, ubman, fsetup): + """Test loading a FIT image with only a kernel""" + cmds, params, fit = self.prepare(ubman, fsetup) - # Find out the offset in the FIT where U-Boot has found the FDT - line = find_matching(output, 'Booting using the fdt blob at ') - fit_offset = int(line, 16) - params['fit_addr'] - fdt_magic = struct.pack('>L', 0xd00dfeed) - data = read_file(fit) + output = ubman.run_command_list(cmds) + self.check_equal(params, 'kernel', 'kernel_out', 'Kernel not loaded') + self.check_not_equal(params, 'fdt_data', 'fdt_out', + 'FDT loaded but should be ignored') + self.check_not_equal(params, 'ramdisk', 'ramdisk_out', + 'Ramdisk loaded but should not be') - # Now find where it actually is in the FIT (skip the first word) - real_fit_offset = data.find(fdt_magic, 4) - assert fit_offset == real_fit_offset, ( - 'U-Boot loaded FDT from offset %#x, FDT is actually at %#x' % - (fit_offset, real_fit_offset)) + # Find out the offset in the FIT where U-Boot has found the FDT + line = self.find_matching(output, 'Booting using the fdt blob at ') + fit_offset = int(line, 16) - params['fit_addr'] + fdt_magic = struct.pack('>L', 0xd00dfeed) + data = self.read_file(fit) - # Check if bootargs strings substitution works - output = ubman.run_command_list([ - 'env set bootargs \\\"\'my_boot_var=${foo}\'\\\"', - 'env set foo bar', - 'bootm prep', - 'env print bootargs']) - assert 'bootargs="my_boot_var=bar"' in output, "Bootargs strings not substituted" + # Now find where it actually is in the FIT (skip the first word) + real_fit_offset = data.find(fdt_magic, 4) + assert fit_offset == real_fit_offset, ( + 'U-Boot loaded FDT from offset %#x, FDT is actually at %#x' % + (fit_offset, real_fit_offset)) - # Now a kernel and an FDT - with ubman.log.section('Kernel + FDT load'): - params['fdt_load'] = 'load = <%#x>;' % params['fdt_addr'] - fit = fit_util.make_fit(ubman, mkimage, base_its, params) - ubman.restart_uboot() - output = ubman.run_command_list(cmd.splitlines()) - check_equal(kernel, kernel_out, 'Kernel not loaded') - check_equal(control_dtb, fdt_out, 'FDT not loaded') - check_not_equal(ramdisk, ramdisk_out, - 'Ramdisk loaded but should not be') + # Check bootargs string substitution + output = ubman.run_command_list([ + 'env set bootargs \\"\'my_boot_var=${foo}\'\\"', + 'env set foo bar', + 'bootm prep', + 'env print bootargs']) + assert 'bootargs="my_boot_var=bar"' in output, \ + "Bootargs strings not substituted" - # Try a ramdisk - with ubman.log.section('Kernel + FDT + Ramdisk load'): - params['ramdisk_config'] = 'ramdisk = "ramdisk-1";' - params['ramdisk_load'] = 'load = <%#x>;' % params['ramdisk_addr'] - fit = fit_util.make_fit(ubman, mkimage, base_its, params) - ubman.restart_uboot() - output = ubman.run_command_list(cmd.splitlines()) - check_equal(ramdisk, ramdisk_out, 'Ramdisk not loaded') + def test_fit_kernel_fdt_load(self, ubman, fsetup): + """Test loading a FIT image with a kernel and FDT""" + cmds, params, _ = self.prepare( + ubman, fsetup, + fdt_load='load = <%#x>;' % fsetup['fdt_addr']) - # Configuration with some Loadables - with ubman.log.section('Kernel + FDT + Ramdisk load + Loadables'): - params['loadables_config'] = 'loadables = "kernel-2", "ramdisk-2";' - params['loadables1_load'] = ('load = <%#x>;' % - params['loadables1_addr']) - params['loadables2_load'] = ('load = <%#x>;' % - params['loadables2_addr']) - fit = fit_util.make_fit(ubman, mkimage, base_its, params) - ubman.restart_uboot() - output = ubman.run_command_list(cmd.splitlines()) - check_equal(loadables1, loadables1_out, - 'Loadables1 (kernel) not loaded') - check_equal(loadables2, loadables2_out, - 'Loadables2 (ramdisk) not loaded') + ubman.run_command_list(cmds) + self.check_equal(params, 'kernel', 'kernel_out', 'Kernel not loaded') + self.check_equal(params, 'fdt_data', 'fdt_out', 'FDT not loaded') + self.check_not_equal(params, 'ramdisk', 'ramdisk_out', + 'Ramdisk loaded but should not be') + + def test_fit_kernel_fdt_ramdisk_load(self, ubman, fsetup): + """Test loading a FIT image with kernel, FDT, and ramdisk""" + cmds, params, _ = self.prepare( + ubman, fsetup, + fdt_load='load = <%#x>;' % fsetup['fdt_addr'], + ramdisk_config='ramdisk = "ramdisk-1";', + ramdisk_load='load = <%#x>;' % fsetup['ramdisk_addr']) + + ubman.run_command_list(cmds) + self.check_equal(params, 'ramdisk', 'ramdisk_out', + 'Ramdisk not loaded') + + def test_fit_loadables_load(self, ubman, fsetup): + """Test a configuration with loadables""" + cmds, params, _ = self.prepare( + ubman, fsetup, + fdt_load='load = <%#x>;' % fsetup['fdt_addr'], + ramdisk_config='ramdisk = "ramdisk-1";', + ramdisk_load='load = <%#x>;' % fsetup['ramdisk_addr'], + loadables_config='loadables = "kernel-2", "ramdisk-2";', + loadables1_load='load = <%#x>;' % fsetup['loadables1_addr'], + loadables2_load='load = <%#x>;' % fsetup['loadables2_addr']) + + ubman.run_command_list(cmds) + self.check_equal(params, 'loadables1', 'loadables1_out', + 'Loadables1 (kernel) not loaded') + self.check_equal(params, 'loadables2', 'loadables2_out', + 'Loadables2 (ramdisk) not loaded') + + def test_fit_compressed_images_load(self, ubman, fsetup): + """Test loading compressed kernel, FDT, and ramdisk images""" + cmds, params, _ = self.prepare( + ubman, fsetup, + fdt_load='load = <%#x>;' % fsetup['fdt_addr'], + ramdisk_config='ramdisk = "ramdisk-1";', + ramdisk_load='load = <%#x>;' % fsetup['ramdisk_addr'], + compression='gzip', + kernel=self.make_compressed(ubman, fsetup['kernel']), + fdt=self.make_compressed(ubman, fsetup['fdt']), + ramdisk=self.make_compressed(ubman, fsetup['ramdisk'])) + + ubman.run_command_list(cmds) + self.check_equal(fsetup, 'kernel', 'kernel_out', + 'Kernel not loaded') + self.check_equal(fsetup, 'fdt_data', 'fdt_out', 'FDT not loaded') + self.check_not_equal(fsetup, 'ramdisk', 'ramdisk_out', + 'Ramdisk got decompressed?') + self.check_equal(params, 'ramdisk', 'ramdisk_out', + 'Ramdisk not loaded') + + def test_fit_no_kernel_load(self, ubman, fsetup): + """Test that bootm fails when no kernel is specified""" + cmds = self.prepare( + ubman, fsetup, + fdt_load='load = <%#x>;' % fsetup['fdt_addr'], + kernel_config='', + ramdisk_config='', + ramdisk_load='')[0] + + 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) + + @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 - # Kernel, FDT and Ramdisk all compressed - with ubman.log.section('(Kernel + FDT + Ramdisk) compressed'): - params['compression'] = 'gzip' - params['kernel'] = make_compressed(kernel) - params['fdt'] = make_compressed(fdt) - params['ramdisk'] = make_compressed(ramdisk) - fit = fit_util.make_fit(ubman, mkimage, base_its, params) + 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() - output = ubman.run_command_list(cmd.splitlines()) - check_equal(kernel, kernel_out, 'Kernel not loaded') - check_equal(control_dtb, fdt_out, 'FDT not loaded') - check_not_equal(ramdisk, ramdisk_out, 'Ramdisk got decompressed?') - check_equal(ramdisk + '.gz', ramdisk_out, 'Ramdist not loaded') + @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'] - # We need to use our own device tree file. Remember to restore it - # afterwards. - old_dtb = ubman.config.dtb - try: - mkimage = ubman.config.build_dir + '/tools/mkimage' - run_fit_test(mkimage) - finally: - # Go back to the original U-Boot with the correct dtb. - ubman.config.dtb = old_dtb - ubman.restart_uboot() + # 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) 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) + [email protected]('sandbox') [email protected]('dtc') [email protected]('fdtget') [email protected]('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}" + ) 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 <[email protected]> + +""" +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}', + ]) + + [email protected]('dtc') [email protected]('fdtget') [email protected]('veritysetup') [email protected]('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) + + [email protected]('dtc') [email protected]('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') diff --git a/test/py/tests/test_fpga.py b/test/py/tests/test_fpga.py index 299a8653f74..74cd42b910e 100644 --- a/test/py/tests/test_fpga.py +++ b/test/py/tests/test_fpga.py @@ -506,7 +506,7 @@ def test_fpga_loadfs(ubman): @pytest.mark.buildconfigspec('cmd_fpga_load_secure') @pytest.mark.buildconfigspec('cmd_net') @pytest.mark.buildconfigspec('cmd_dhcp') [email protected]('net', 'net_lwip') [email protected]('net') def test_fpga_secure_bit_auth(ubman): test_net.test_net_dhcp(ubman) @@ -534,7 +534,7 @@ def test_fpga_secure_bit_auth(ubman): @pytest.mark.buildconfigspec('cmd_fpga_load_secure') @pytest.mark.buildconfigspec('cmd_net') @pytest.mark.buildconfigspec('cmd_dhcp') [email protected]('net', 'net_lwip') [email protected]('net') def test_fpga_secure_bit_img_auth_kup(ubman): test_net.test_net_dhcp(ubman) diff --git a/test/py/tests/test_fs/conftest.py b/test/py/tests/test_fs/conftest.py index 0205048e73a..ba125cc7073 100644 --- a/test/py/tests/test_fs/conftest.py +++ b/test/py/tests/test_fs/conftest.py @@ -9,7 +9,7 @@ import re from subprocess import call, check_call, check_output, CalledProcessError from fstest_defs import * # pylint: disable=E0611 -from tests import fs_helper +from tests.fs_helper import FsHelper supported_fs_basic = ['fat16', 'fat32', 'exfat', 'ext4', 'fs_generic'] supported_fs_ext = ['fat12', 'fat16', 'fat32', 'exfat', 'fs_generic'] @@ -200,33 +200,26 @@ def fs_obj_basic(request, u_boot_config): fs_type = request.param fs_cmd_prefix = fstype_to_prefix(fs_type) fs_cmd_write = 'save' if fs_type == 'fs_generic' or fs_type == 'exfat' else 'write' - fs_img = '' fs_ubtype = fstype_to_ubname(fs_type) check_ubconfig(u_boot_config, fs_ubtype) - scratch_dir = u_boot_config.persistent_data_dir + '/scratch' - - small_file = scratch_dir + '/' + SMALL_FILE - big_file = scratch_dir + '/' + BIG_FILE - + fsh = FsHelper(u_boot_config, fs_type, 3072, '3GB') try: - check_call('mkdir -p %s' % scratch_dir, shell=True) - except CalledProcessError as err: - pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err)) - call('rm -f %s' % fs_img, shell=True) - return + fsh.setup() + + small_file = fsh.srcdir + '/' + SMALL_FILE + big_file = fsh.srcdir + '/' + BIG_FILE - try: # Create a subdirectory. - check_call('mkdir %s/SUBDIR' % scratch_dir, shell=True) + check_call('mkdir %s/SUBDIR' % fsh.srcdir, shell=True) # Create big file in this image. # Note that we work only on the start 1MB, couple MBs in the 2GB range # and the last 1 MB of the huge 2.5GB file. # So, just put random values only in those areas. check_call('dd if=/dev/urandom of=%s bs=1M count=1' - % big_file, shell=True) + % big_file, shell=True) check_call('dd if=/dev/urandom of=%s bs=1M count=2 seek=2047' % big_file, shell=True) check_call('dd if=/dev/urandom of=%s bs=1M count=1 seek=2499' @@ -234,65 +227,54 @@ def fs_obj_basic(request, u_boot_config): # Create a small file in this image. check_call('dd if=/dev/urandom of=%s bs=1M count=1' - % small_file, shell=True) - - # Delete the small file copies which possibly are written as part of a - # previous test. - # check_call('rm -f "%s.w"' % MB1, shell=True) - # check_call('rm -f "%s.w2"' % MB1, shell=True) + % small_file, shell=True) # Generate the md5sums of reads that we will test against small file out = check_output( 'dd if=%s bs=1M skip=0 count=1 2> /dev/null | md5sum' - % small_file, shell=True).decode() + % small_file, shell=True).decode() md5val = [ out.split()[0] ] # Generate the md5sums of reads that we will test against big file # One from beginning of file. out = check_output( 'dd if=%s bs=1M skip=0 count=1 2> /dev/null | md5sum' - % big_file, shell=True).decode() + % big_file, shell=True).decode() md5val.append(out.split()[0]) # One from end of file. out = check_output( 'dd if=%s bs=1M skip=2499 count=1 2> /dev/null | md5sum' - % big_file, shell=True).decode() + % big_file, shell=True).decode() md5val.append(out.split()[0]) # One from the last 1MB chunk of 2GB out = check_output( 'dd if=%s bs=1M skip=2047 count=1 2> /dev/null | md5sum' - % big_file, shell=True).decode() + % big_file, shell=True).decode() md5val.append(out.split()[0]) # One from the start 1MB chunk from 2GB out = check_output( 'dd if=%s bs=1M skip=2048 count=1 2> /dev/null | md5sum' - % big_file, shell=True).decode() + % big_file, shell=True).decode() md5val.append(out.split()[0]) # One 1MB chunk crossing the 2GB boundary out = check_output( 'dd if=%s bs=512K skip=4095 count=2 2> /dev/null | md5sum' - % big_file, shell=True).decode() + % big_file, shell=True).decode() md5val.append(out.split()[0]) - try: - # 3GiB volume - fs_img = fs_helper.mk_fs(u_boot_config, fs_type, 0xc0000000, '3GB', scratch_dir) - except CalledProcessError as err: - pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err)) - return + fsh.mk_fs() except CalledProcessError as err: pytest.skip('Setup failed for filesystem: ' + fs_type + '. {}'.format(err)) return else: - yield [fs_ubtype, fs_cmd_prefix, fs_cmd_write, fs_img, md5val] + yield [fs_ubtype, fs_cmd_prefix, fs_cmd_write, fsh.fs_img, md5val] finally: - call('rm -rf %s' % scratch_dir, shell=True) - call('rm -f %s' % fs_img, shell=True) + fsh.cleanup() # # Fixture for extended fs test @@ -312,26 +294,19 @@ def fs_obj_ext(request, u_boot_config): fs_type = request.param fs_cmd_prefix = fstype_to_prefix(fs_type) fs_cmd_write = 'save' if fs_type == 'fs_generic' or fs_type == 'exfat' else 'write' - fs_img = '' fs_ubtype = fstype_to_ubname(fs_type) check_ubconfig(u_boot_config, fs_ubtype) - scratch_dir = u_boot_config.persistent_data_dir + '/scratch' - - min_file = scratch_dir + '/' + MIN_FILE - tmp_file = scratch_dir + '/tmpfile' - + fsh = FsHelper(u_boot_config, fs_type, 128, '128MB') try: - check_call('mkdir -p %s' % scratch_dir, shell=True) - except CalledProcessError as err: - pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err)) - call('rm -f %s' % fs_img, shell=True) - return + fsh.setup() + + min_file = fsh.srcdir + '/' + MIN_FILE + tmp_file = fsh.srcdir + '/tmpfile' - try: # Create a test directory - check_call('mkdir %s/dir1' % scratch_dir, shell=True) + check_call('mkdir %s/dir1' % fsh.srcdir, shell=True) # Create a small file and calculate md5 check_call('dd if=/dev/urandom of=%s bs=1K count=20' @@ -370,21 +345,15 @@ def fs_obj_ext(request, u_boot_config): check_call('rm %s' % tmp_file, shell=True) - try: - # 128MiB volume - fs_img = fs_helper.mk_fs(u_boot_config, fs_type, 0x8000000, '128MB', scratch_dir) - except CalledProcessError as err: - pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err)) - return + fsh.mk_fs() except CalledProcessError: pytest.skip('Setup failed for filesystem: ' + fs_type) return else: - yield [fs_ubtype, fs_cmd_prefix, fs_cmd_write, fs_img, md5val] + yield [fs_ubtype, fs_cmd_prefix, fs_cmd_write, fsh.fs_img, md5val] finally: - call('rm -rf %s' % scratch_dir, shell=True) - call('rm -f %s' % fs_img, shell=True) + fsh.cleanup() # # Fixture for mkdir test @@ -403,20 +372,19 @@ def fs_obj_mkdir(request, u_boot_config): """ fs_type = request.param fs_cmd_prefix = fstype_to_prefix(fs_type) - fs_img = '' fs_ubtype = fstype_to_ubname(fs_type) check_ubconfig(u_boot_config, fs_ubtype) + fsh = FsHelper(u_boot_config, fs_type, 128, '128MB') try: - # 128MiB volume - fs_img = fs_helper.mk_fs(u_boot_config, fs_type, 0x8000000, '128MB', None) + fsh.mk_fs() except: pytest.skip('Setup failed for filesystem: ' + fs_type) return else: - yield [fs_ubtype, fs_cmd_prefix, fs_img] - call('rm -f %s' % fs_img, shell=True) + yield [fs_ubtype, fs_cmd_prefix, fsh.fs_img] + fsh.cleanup() # # Fixture for unlink test @@ -435,57 +403,44 @@ def fs_obj_unlink(request, u_boot_config): """ fs_type = request.param fs_cmd_prefix = fstype_to_prefix(fs_type) - fs_img = '' fs_ubtype = fstype_to_ubname(fs_type) check_ubconfig(u_boot_config, fs_ubtype) - scratch_dir = u_boot_config.persistent_data_dir + '/scratch' - + fsh = FsHelper(u_boot_config, fs_type, 128, '128MB') try: - check_call('mkdir -p %s' % scratch_dir, shell=True) - except CalledProcessError as err: - pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err)) - call('rm -f %s' % fs_img, shell=True) - return + fsh.setup() - try: # Test Case 1 & 3 - check_call('mkdir %s/dir1' % scratch_dir, shell=True) + check_call('mkdir %s/dir1' % fsh.srcdir, shell=True) check_call('dd if=/dev/urandom of=%s/dir1/file1 bs=1K count=1' - % scratch_dir, shell=True) + % fsh.srcdir, shell=True) check_call('dd if=/dev/urandom of=%s/dir1/file2 bs=1K count=1' - % scratch_dir, shell=True) + % fsh.srcdir, shell=True) # Test Case 2 - check_call('mkdir %s/dir2' % scratch_dir, shell=True) + check_call('mkdir %s/dir2' % fsh.srcdir, shell=True) for i in range(0, 20): check_call('mkdir %s/dir2/0123456789abcdef%02x' - % (scratch_dir, i), shell=True) + % (fsh.srcdir, i), shell=True) # Test Case 4 - check_call('mkdir %s/dir4' % scratch_dir, shell=True) + check_call('mkdir %s/dir4' % fsh.srcdir, shell=True) # Test Case 5, 6 & 7 - check_call('mkdir %s/dir5' % scratch_dir, shell=True) + check_call('mkdir %s/dir5' % fsh.srcdir, shell=True) check_call('dd if=/dev/urandom of=%s/dir5/file1 bs=1K count=1' - % scratch_dir, shell=True) + % fsh.srcdir, shell=True) - try: - # 128MiB volume - fs_img = fs_helper.mk_fs(u_boot_config, fs_type, 0x8000000, '128MB', scratch_dir) - except CalledProcessError as err: - pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err)) - return + fsh.mk_fs() except CalledProcessError: pytest.skip('Setup failed for filesystem: ' + fs_type) return else: - yield [fs_ubtype, fs_cmd_prefix, fs_img] + yield [fs_ubtype, fs_cmd_prefix, fsh.fs_img] finally: - call('rm -rf %s' % scratch_dir, shell=True) - call('rm -f %s' % fs_img, shell=True) + fsh.cleanup() # # Fixture for symlink fs test @@ -503,26 +458,19 @@ def fs_obj_symlink(request, u_boot_config): volume file name and a list of MD5 hashes. """ fs_type = request.param - fs_img = '' fs_ubtype = fstype_to_ubname(fs_type) check_ubconfig(u_boot_config, fs_ubtype) - scratch_dir = u_boot_config.persistent_data_dir + '/scratch' - - small_file = scratch_dir + '/' + SMALL_FILE - medium_file = scratch_dir + '/' + MEDIUM_FILE - + fsh = FsHelper(u_boot_config, fs_type, 1024, '1GB') try: - check_call('mkdir -p %s' % scratch_dir, shell=True) - except CalledProcessError as err: - pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err)) - call('rm -f %s' % fs_img, shell=True) - return + fsh.setup() + + small_file = fsh.srcdir + '/' + SMALL_FILE + medium_file = fsh.srcdir + '/' + MEDIUM_FILE - try: # Create a subdirectory. - check_call('mkdir %s/SUBDIR' % scratch_dir, shell=True) + check_call('mkdir %s/SUBDIR' % fsh.srcdir, shell=True) # Create a small file in this image. check_call('dd if=/dev/urandom of=%s bs=1M count=1' @@ -542,21 +490,15 @@ def fs_obj_symlink(request, u_boot_config): % medium_file, shell=True).decode() md5val.extend([out.split()[0]]) - try: - # 1GiB volume - fs_img = fs_helper.mk_fs(u_boot_config, fs_type, 0x40000000, '1GB', scratch_dir) - except CalledProcessError as err: - pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err)) - return + fsh.mk_fs() except CalledProcessError: pytest.skip('Setup failed for filesystem: ' + fs_type) return else: - yield [fs_ubtype, fs_img, md5val] + yield [fs_ubtype, fsh.fs_img, md5val] finally: - call('rm -rf %s' % scratch_dir, shell=True) - call('rm -f %s' % fs_img, shell=True) + fsh.cleanup() # # Fixture for rename test @@ -584,21 +526,15 @@ def fs_obj_rename(request, u_boot_config): return out.decode().split()[0] fs_type = request.param - fs_img = '' fs_ubtype = fstype_to_ubname(fs_type) check_ubconfig(u_boot_config, fs_ubtype) - mount_dir = u_boot_config.persistent_data_dir + '/scratch' - + fsh = FsHelper(u_boot_config, fs_type, 128, '128MB') try: - check_call('mkdir -p %s' % mount_dir, shell=True) - except CalledProcessError as err: - pytest.skip('Preparing mount folder failed for filesystem: ' + fs_type + '. {}'.format(err)) - call('rm -f %s' % fs_img, shell=True) - return + fsh.setup() + mount_dir = fsh.srcdir - try: md5val = {} # Test Case 1 check_call('mkdir %s/test1' % mount_dir, shell=True) @@ -657,21 +593,15 @@ def fs_obj_rename(request, u_boot_config): new_rand_file('%s/test11/dir1/file1' % mount_dir) md5val['test11'] = file_hash('%s/test11/dir1/file1' % mount_dir) - try: - # 128MiB volume - fs_img = fs_helper.mk_fs(u_boot_config, fs_type, 0x8000000, '128MB', mount_dir) - except CalledProcessError as err: - pytest.skip('Creating failed for filesystem: ' + fs_type + '. {}'.format(err)) - return + fsh.mk_fs() except CalledProcessError: pytest.skip('Setup failed for filesystem: ' + fs_type) return else: - yield [fs_ubtype, fs_img, md5val] + yield [fs_ubtype, fsh.fs_img, md5val] finally: - call('rm -rf %s' % mount_dir, shell=True) - call('rm -f %s' % fs_img, shell=True) + fsh.cleanup() # # Fixture for fat test @@ -697,19 +627,19 @@ def fs_obj_fat(request, u_boot_config): MIN_FAT16_SIZE = 8208 * 1024 fs_type = request.param - fs_img = '' fs_ubtype = fstype_to_ubname(fs_type) check_ubconfig(u_boot_config, fs_ubtype) fs_size = MAX_FAT12_SIZE if fs_type == 'fat12' else MIN_FAT16_SIZE + size_mb = (fs_size + (1 << 20) - 1) >> 20 + fsh = FsHelper(u_boot_config, fs_type, size_mb, f'{fs_size}') try: - # the volume size depends on the filesystem - fs_img = fs_helper.mk_fs(u_boot_config, fs_type, fs_size, f'{fs_size}', None, 1024) + fsh.mk_fs() except: pytest.skip('Setup failed for filesystem: ' + fs_type) return else: - yield [fs_ubtype, fs_img] - call('rm -f %s' % fs_img, shell=True) + yield [fs_ubtype, fsh.fs_img] + fsh.cleanup() 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')) diff --git a/test/py/tests/test_net.py b/test/py/tests/test_net.py index 6ef02e53389..27cdd73fd49 100644 --- a/test/py/tests/test_net.py +++ b/test/py/tests/test_net.py @@ -201,7 +201,7 @@ def test_net_dhcp6(ubman): global net6_set_up net6_set_up = True [email protected]('net', 'net_lwip') [email protected]('net') def test_net_setup_static(ubman): """Set up a static IP configuration. 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') + [email protected]('sandbox') [email protected]('cmd_echo') [email protected]('cmd_source') [email protected]('fit') [email protected]('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') + [email protected]('cmd_echo') [email protected]('cmd_source') [email protected]('fit') [email protected]('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') 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', ] diff --git a/test/py/tests/test_ut.py b/test/py/tests/test_ut.py index 98641a46c1d..fa50c8008a5 100644 --- a/test/py/tests/test_ut.py +++ b/test/py/tests/test_ut.py @@ -17,6 +17,7 @@ import pytest import utils # pylint: disable=E0611 from tests import fs_helper +from fs_helper import DiskHelper, FsHelper from test_android import test_abootimg def mkdir_cond(dirname): @@ -45,7 +46,6 @@ def setup_bootmenu_image(ubman): This is modelled on Armbian 22.08 Jammy """ mmc_dev = 4 - fname, mnt = fs_helper.setup_image(ubman, mmc_dev, 0x83) script = '''# DO NOT EDIT THIS FILE # @@ -121,7 +121,9 @@ booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r} # Recompile with: # mkimage -C none -A arm -T script -d /boot/boot.cmd /boot/boot.scr ''' - bootdir = os.path.join(mnt, 'boot') + fsh = FsHelper(ubman.config, 'ext4', 18, 'mmc') + fsh.setup() + bootdir = os.path.join(fsh.srcdir, 'boot') mkdir_cond(bootdir) cmd_fname = os.path.join(bootdir, 'boot.cmd') scr_fname = os.path.join(bootdir, 'boot.scr') @@ -150,36 +152,30 @@ booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r} utils.run_and_log( ubman, f'echo here {kernel} {symlink}') os.symlink(kernel, symlink) + fsh.mk_fs() + img = DiskHelper(ubman.config, mmc_dev, 'mmc', True) + img.add_fs(fsh, DiskHelper.EXT4) + img.create() + fsh.cleanup() - fsfile = 'ext18M.img' - utils.run_and_log(ubman, f'fallocate -l 18M {fsfile}') - utils.run_and_log(ubman, f'mkfs.ext4 {fsfile} -d {mnt}') - copy_partition(ubman, fsfile, fname) - utils.run_and_log(ubman, f'rm -rf {mnt}') - utils.run_and_log(ubman, f'rm -f {fsfile}') -def setup_bootflow_image(ubman): - """Create a 20MB disk image with a single FAT partition""" - mmc_dev = 1 - fname, mnt = fs_helper.setup_image(ubman, mmc_dev, 0xc, second_part=True) +def setup_extlinux_image(ubman, devnum, basename, vmlinux, initrd, dtbdir, + script): + """Create a 20MB disk image with a single FAT partition - vmlinux = 'vmlinuz-5.3.7-301.fc31.armv7hl' - initrd = 'initramfs-5.3.7-301.fc31.armv7hl.img' - dtbdir = 'dtb-5.3.7-301.fc31.armv7hl' - script = '''# extlinux.conf generated by appliance-creator -ui menu.c32 -menu autoboot Welcome to Fedora-Workstation-armhfp-31-1.9. Automatic boot in # second{,s}. Press a key for options. -menu title Fedora-Workstation-armhfp-31-1.9 Boot Options. -menu hidden -timeout 20 -totaltimeout 600 + Args: + ubman (ConsoleBase): Console to use + devnum (int): Device number to use, e.g. 1 + basename (str): Base name to use in the filename, e.g. 'mmc' + vmlinux (str): Kernel filename + initrd (str): Ramdisk filename + dtbdir (str or None): Devicetree filename + script (str): Script to place in the extlinux.conf file + """ + fsh = FsHelper(ubman.config, 'vfat', 18, prefix=basename) + fsh.setup() -label Fedora-Workstation-armhfp-31-1.9 (5.3.7-301.fc31.armv7hl) - kernel /%s - append ro root=UUID=9732b35b-4cd5-458b-9b91-80f7047e0b8a rhgb quiet LANG=en_US.UTF-8 cma=192MB cma=256MB - fdtdir /%s/ - initrd /%s''' % (vmlinux, dtbdir, initrd) - ext = os.path.join(mnt, 'extlinux') + ext = os.path.join(fsh.srcdir, 'extlinux') mkdir_cond(ext) conf = os.path.join(ext, 'extlinux.conf') @@ -191,24 +187,57 @@ label Fedora-Workstation-armhfp-31-1.9 (5.3.7-301.fc31.armv7hl) fd.write(gzip.compress(b'vmlinux')) mkimage = ubman.config.build_dir + '/tools/mkimage' utils.run_and_log( - ubman, f'{mkimage} -f auto -d {inf} {os.path.join(mnt, vmlinux)}') + ubman, f'{mkimage} -f auto -d {inf} {os.path.join(fsh.srcdir, vmlinux)}') - with open(os.path.join(mnt, initrd), 'w', encoding='ascii') as fd: + with open(os.path.join(fsh.srcdir, initrd), 'w', encoding='ascii') as fd: print('initrd', file=fd) - mkdir_cond(os.path.join(mnt, dtbdir)) + if dtbdir: + mkdir_cond(os.path.join(fsh.srcdir, dtbdir)) - dtb_file = os.path.join(mnt, f'{dtbdir}/sandbox.dtb') - utils.run_and_log( - ubman, f'dtc -o {dtb_file}', stdin=b'/dts-v1/; / {};') + dtb_file = os.path.join(fsh.srcdir, f'{dtbdir}/sandbox.dtb') + utils.run_and_log( + ubman, f'dtc -o {dtb_file}', stdin=b'/dts-v1/; / {};') + + fsh.mk_fs() + + img = DiskHelper(ubman.config, devnum, basename, True) + img.add_fs(fsh, DiskHelper.VFAT, bootable=True) + + ext4 = FsHelper(ubman.config, 'ext4', 1, prefix=basename) + ext4.setup() + ext4.mk_fs() + + img.add_fs(ext4, DiskHelper.EXT4) + img.create() + fsh.cleanup() + +def setup_fedora_image(ubman, devnum, basename): + """Create a 20MB Fedora disk image with a single FAT partition + + Args: + ubman (ConsoleBase): Console to use + devnum (int): Device number to use, e.g. 1 + basename (str): Base name to use in the filename, e.g. 'mmc' + """ + vmlinux = 'vmlinuz-5.3.7-301.fc31.armv7hl' + initrd = 'initramfs-5.3.7-301.fc31.armv7hl.img' + dtbdir = 'dtb-5.3.7-301.fc31.armv7hl' + script = '''# extlinux.conf generated by appliance-creator +ui menu.c32 +menu autoboot Welcome to Fedora-Workstation-armhfp-31-1.9. Automatic boot in # second{,s}. Press a key for options. +menu title Fedora-Workstation-armhfp-31-1.9 Boot Options. +menu hidden +timeout 20 +totaltimeout 600 - fsfile = 'vfat18M.img' - utils.run_and_log(ubman, f'fallocate -l 18M {fsfile}') - utils.run_and_log(ubman, f'mkfs.vfat {fsfile}') - utils.run_and_log(ubman, ['sh', '-c', f'mcopy -i {fsfile} {mnt}/* ::/']) - copy_partition(ubman, fsfile, fname) - utils.run_and_log(ubman, f'rm -rf {mnt}') - utils.run_and_log(ubman, f'rm -f {fsfile}') +label Fedora-Workstation-armhfp-31-1.9 (5.3.7-301.fc31.armv7hl) + kernel /%s + append ro root=UUID=9732b35b-4cd5-458b-9b91-80f7047e0b8a rhgb quiet LANG=en_US.UTF-8 cma=192MB cma=256MB + fdtdir /%s/ + initrd /%s''' % (vmlinux, dtbdir, initrd) + setup_extlinux_image(ubman, devnum, basename, vmlinux, initrd, dtbdir, + script) def setup_cros_image(ubman): """Create a 20MB disk image with ChromiumOS partitions""" @@ -513,8 +542,8 @@ def test_ut_dm_init(ubman): utils.run_and_log( ubman, f'sfdisk {fn}', stdin=b'type=83') - fs_helper.mk_fs(ubman.config, 'ext2', 0x200000, '2MB', None) - fs_helper.mk_fs(ubman.config, 'fat32', 0x100000, '1MB', None) + FsHelper(ubman.config, 'ext2', 2, '2MB').mk_fs() + FsHelper(ubman.config, 'fat32', 1, '1MB').mk_fs() mmc_dev = 6 fn = os.path.join(ubman.config.source_dir, f'mmc{mmc_dev}.img') @@ -532,11 +561,9 @@ def test_ut_dm_init(ubman): def setup_efi_image(ubman): """Create a 20MB disk image with an EFI app on it""" devnum = 1 - basename = 'flash' - fname, mnt = fs_helper.setup_image(ubman, devnum, 0xc, second_part=True, - basename=basename) - - efi_dir = os.path.join(mnt, 'EFI') + fsh = FsHelper(ubman.config, 'vfat', 18, 'flash') + fsh.setup() + efi_dir = os.path.join(fsh.srcdir, 'EFI') mkdir_cond(efi_dir) bootdir = os.path.join(efi_dir, 'BOOT') mkdir_cond(bootdir) @@ -546,66 +573,53 @@ def setup_efi_image(ubman): with open(efi_src, 'rb') as inf: with open(efi_dst, 'wb') as outf: outf.write(inf.read()) - fsfile = 'vfat18M.img' - utils.run_and_log(ubman, f'fallocate -l 18M {fsfile}') - utils.run_and_log(ubman, f'mkfs.vfat {fsfile}') - utils.run_and_log(ubman, ['sh', '-c', f'mcopy -vs -i {fsfile} {mnt}/* ::/']) - copy_partition(ubman, fsfile, fname) - utils.run_and_log(ubman, f'rm -rf {mnt}') - utils.run_and_log(ubman, f'rm -f {fsfile}') + + fsh.mk_fs() + + img = DiskHelper(ubman.config, devnum, 'flash', True) + img.add_fs(fsh, DiskHelper.VFAT) + img.create() + fsh.cleanup() + def setup_rauc_image(ubman): """Create a 40MB disk image with an A/B RAUC system on it""" mmc_dev = 10 - fname = os.path.join(ubman.config.source_dir, f'mmc{mmc_dev}.img') - mnt = ubman.config.persistent_data_dir - spec = 'type=c, size=8M, start=1M, bootable\n' \ - 'type=83, size=10M\n' \ - 'type=c, size=8M, bootable\n' \ - 'type=83, size=10M' - - utils.run_and_log(ubman, f'qemu-img create {fname} 40M') - utils.run_and_log(ubman, ['sh', '-c', f'printf "{spec}" | sfdisk {fname}']) + boot = FsHelper(ubman.config, 'fat32', 8, 'rauc_boot') + boot.setup() # Generate boot script script = '# dummy boot script' - bootdir = os.path.join(mnt, 'boot') - utils.run_and_log(ubman, f'mkdir -p {bootdir}') - cmd_fname = os.path.join(bootdir, 'boot.cmd') - scr_fname = os.path.join(bootdir, 'boot.scr') + cmd_fname = os.path.join(boot.srcdir, 'boot.cmd') + scr_fname = os.path.join(boot.srcdir, 'boot.scr') with open(cmd_fname, 'w', encoding='ascii') as outf: print(script, file=outf) mkimage = os.path.join(ubman.config.build_dir, 'tools/mkimage') utils.run_and_log( ubman, f'{mkimage} -C none -A arm -T script -d {cmd_fname} {scr_fname}') - utils.run_and_log(ubman, f'rm -f {cmd_fname}') - - # Generate empty rootfs - rootdir = os.path.join(mnt, 'root') - utils.run_and_log(ubman, f'mkdir -p {rootdir}') + os.remove(cmd_fname) + boot.mk_fs() - # Create boot filesystem image with boot script in it and copy to disk image - fsfile = f'rauc_boot.fat32.img' - fs_helper.mk_fs(ubman.config, 'fat32', 0x800000, fsfile.split('.')[0], bootdir) - utils.run_and_log(ubman, f'dd if={mnt}/{fsfile} of=mmc{mmc_dev}.img bs=1M seek=1 conv=notrunc') - utils.run_and_log(ubman, f'dd if={mnt}/{fsfile} of=mmc{mmc_dev}.img bs=1M seek=19 conv=notrunc') - utils.run_and_log(ubman, f'rm -f {scr_fname}') + root = FsHelper(ubman.config, 'ext4', 10, 'rauc_root') + root.mk_fs() - # Create empty root filesystem image and copy to disk image - fsfile = f'rauc_root.ext4.img' - fs_helper.mk_fs(ubman.config, 'ext4', 0xa00000, fsfile.split('.')[0], rootdir) - utils.run_and_log(ubman, f'dd if={mnt}/{fsfile} of=mmc{mmc_dev}.img bs=1M seek=9 conv=notrunc') - utils.run_and_log(ubman, f'dd if={mnt}/{fsfile} of=mmc{mmc_dev}.img bs=1M seek=27 conv=notrunc') - utils.run_and_log(ubman, f'rm -f {fsfile}') + img = DiskHelper(ubman.config, mmc_dev, 'mmc', True) + img.add_fs(boot, DiskHelper.VFAT, bootable=True) + img.add_fs(root, DiskHelper.EXT4) + img.add_fs(boot, DiskHelper.VFAT, bootable=True) + img.add_fs(root, DiskHelper.EXT4) + img.create() + boot.cleanup() + root.cleanup() @pytest.mark.buildconfigspec('cmd_bootflow') @pytest.mark.buildconfigspec('sandbox') def test_ut_dm_init_bootstd(ubman): """Initialise data for bootflow tests""" - setup_bootflow_image(ubman) + setup_fedora_image(ubman, 1, 'mmc') setup_bootmenu_image(ubman) setup_cedit_file(ubman) setup_cros_image(ubman) @@ -617,7 +631,23 @@ def test_ut_dm_init_bootstd(ubman): ubman.restart_uboot() -def test_ut(ubman, ut_subtest): [email protected](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 @@ -630,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: diff --git a/test/py/tests/test_vboot.py b/test/py/tests/test_vboot.py index 55518bed07e..4b6707caf70 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. @@ -563,6 +589,171 @@ def test_vboot(ubman, name, sha_algo, padding, sign_options, required, ubman.restart_uboot() [email protected]('sandbox') [email protected]('fit_signature') [email protected]('dtc') [email protected]('fdtput') [email protected]('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], diff --git a/test/py/tests/test_xxd.py b/test/py/tests/test_xxd.py index c457c54146c..a5f63f006fe 100644 --- a/test/py/tests/test_xxd.py +++ b/test/py/tests/test_xxd.py @@ -4,8 +4,7 @@ """ import pytest -from subprocess import call, check_call, CalledProcessError -from tests import fs_helper +from tests.fs_helper import FsHelper @pytest.mark.boardspec('sandbox') @pytest.mark.buildconfigspec('cmd_xxd') @@ -15,26 +14,13 @@ def test_xxd(ubman): Args: ubman -- U-Boot console """ - try: - scratch_dir = ubman.config.persistent_data_dir + '/scratch' - - check_call('mkdir -p %s' % scratch_dir, shell=True) - - with open(scratch_dir + '/hello', 'w', encoding = 'ascii') as file: - file.write('hello world\n\x00\x01\x02\x03\x04\x05') - - xxd_data = fs_helper.mk_fs(ubman.config, 'vfat', 0x100000, - 'test_xxd', scratch_dir) - response = ubman.run_command_list([ f'host bind 0 {xxd_data}', - 'xxd host 0 hello']) + with FsHelper(ubman.config, 'vfat', 1, 'test_xxd') as fsh: + with open(f'{fsh.srcdir}/hello', 'w', encoding = 'ascii') as outf: + outf.write('hello world\n\x00\x01\x02\x03\x04\x05') + fsh.mk_fs() + response = ubman.run_command_list([f'host bind 0 {fsh.fs_img}', + 'xxd host 0 hello']) assert '00000000: 68 65 6c 6c 6f 20 77 6f 72 6c 64 0a 00 01 02 03 hello world.....\r\r\n' + \ '00000010: 04 05 ..' \ in response - except CalledProcessError as err: - pytest.skip('Preparing test_xxd image failed') - call('rm -f %s' % xxd_data, shell=True) - return - finally: - call('rm -rf %s' % scratch_dir, shell=True) - call('rm -f %s' % xxd_data, shell=True) diff --git a/test/py/utils.py b/test/py/utils.py index ca80e4b0b0a..e8971502509 100644 --- a/test/py/utils.py +++ b/test/py/utils.py @@ -51,6 +51,19 @@ def md5sum_file(fn, max_length=None): data = fh.read(*params) return md5sum_data(data) +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() + class PersistentRandomFile: """Generate and store information about a persistent file containing random data.""" |
