[PATCH v4] image-fit: Validate external data offset and size
Anton Ivanov
anton at binarly.io
Tue Jun 2 22:51:24 CEST 2026
For this fix, offset/len could be made uint32_t, as was done in the patch
titled "image-fit-sig: Validate hashed-strings region size", but this
would require changing the prototypes of fit_image_get_data_position(),
fit_image_get_data_offset() and fit_image_get_data_size(). Please confirm
whether this is acceptable.
Thank you,
Anton
On Tue, Jun 2, 2026 at 7:30 PM Anton Ivanov <anton at binarly.io> wrote:
> 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 <anton at binarly.io>
> ---
> Changes in v4:
> - Add test for external data bounds validation
> - Clarify vulnerability reachability in the commit message
>
> Changes in v3:
> - Update From and Signed-off-by to personal email
>
> Changes in v2:
> - Rewrite commit message to be concise per maintainer feedback
>
> boot/image-fit.c | 16 ++++
> test/py/tests/test_vboot.py | 165 ++++++++++++++++++++++++++++++++++++
> 2 files changed, 181 insertions(+)
>
> diff --git a/boot/image-fit.c b/boot/image-fit.c
> index b0fcaf6e17f..b657142f626 100644
> --- a/boot/image-fit.c
> +++ b/boot/image-fit.c
> @@ -1084,8 +1084,24 @@ int fit_image_get_data(const void *fit, int
> noffset, const void **data,
>
> if (external_data) {
> debug("External Data\n");
> + if (offset < 0 || offset > UINTPTR_MAX - (uintptr_t)fit) {
> + printf("Invalid external data offset: %d\n",
> offset);
> + return -1;
> + }
> +
> ret = fit_image_get_data_size(fit, noffset, &len);
> if (!ret) {
> + if (len < 0) {
> + printf("Invalid external data size: %d\n",
> len);
> + return -1;
> + }
> +#if CONFIG_IS_ENABLED(FIT_SIGNATURE)
> + if (len > CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) -
> offset) {
> + printf("FIT external data is out of bounds
> (offset=%d, size=%d)\n",
> + offset, len);
> + return -1;
> + }
> +#endif
> *data = fit + offset;
> *size = len;
> }
> diff --git a/test/py/tests/test_vboot.py b/test/py/tests/test_vboot.py
> index 55518bed07e..85a1bd56af6 100644
> --- a/test/py/tests/test_vboot.py
> +++ b/test/py/tests/test_vboot.py
> @@ -563,6 +563,171 @@ def test_vboot(ubman, name, sha_algo, padding,
> sign_options, required,
> ubman.restart_uboot()
>
>
> + at pytest.mark.boardspec('sandbox')
> + at pytest.mark.buildconfigspec('fit_signature')
> + at pytest.mark.requiredtool('dtc')
> + at pytest.mark.requiredtool('fdtput')
> + at 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 offset'),
> + ('negative data-offset',
> + {'data-offset': 0x80000000}, '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],
> --
> 2.53.0
>
More information about the U-Boot
mailing list