diff options
| author | Tom Rini <[email protected]> | 2026-09-04 12:22:49 -0600 |
|---|---|---|
| committer | Tom Rini <[email protected]> | 2026-09-04 16:55:10 -0600 |
| commit | 5534f2c742916efabff3e1888912dafc570f20f2 (patch) | |
| tree | 1ad0e57528769aef86a2cb7e3a4297495125d255 | |
| parent | 7bf077ec5733a230bbce34d8ce324f92d8c46cc1 (diff) | |
| parent | 3158b844335a4a84a5504a813f2daa6acff5985d (diff) | |
Merge patch series "fit: Harden handling of external-data properties"
Anton Ivanov <[email protected]> says:
The data-offset, data-position and data-size FIT properties are 32-bit
unsigned values, but were read through signed int. Also, they are
excluded from the configuration signature, so they are attacker
controlled.
Patch 1 switches the accessors and their callers to u32, removing
the ad-hoc handling of "negative" values in U-Boot proper. This
transition was discussed and agreed on in [1].
The SPL loader has its own copy of this logic with the same problems
and fewer checks. Patch 2 factors out a test helper, patch 3 makes the
SPL offset/size arithmetic overflow-safe, and patch 4 adds the
addressable-range and FIT_SIGNATURE_MAX_SIZE bounds check that U-Boot
proper already performs in fit_image_get_data().
Patches 3 and 4 build on each other and on patch 1, so they are not
intended to be cherry-picked individually.
[1] https://lore.kernel.org/u-boot/CAPWaX55XWFLcMGRuaUuXXn__MX_UG4J8QTd6rvXZmJ4WSOCy9w@mail.gmail.com/
Link: https://lore.kernel.org/r/[email protected]
| -rw-r--r-- | boot/image-fit.c | 29 | ||||
| -rw-r--r-- | common/spl/spl_fit.c | 122 | ||||
| -rw-r--r-- | common/splash_source.c | 2 | ||||
| -rw-r--r-- | drivers/fpga/socfpga_arria10.c | 3 | ||||
| -rw-r--r-- | include/image.h | 6 | ||||
| -rw-r--r-- | test/image/spl_load.c | 128 | ||||
| -rw-r--r-- | test/py/tests/test_vboot.py | 10 |
7 files changed, 210 insertions, 90 deletions
diff --git a/boot/image-fit.c b/boot/image-fit.c index ef90c5abd18..26e9323da06 100644 --- a/boot/image-fit.c +++ b/boot/image-fit.c @@ -963,7 +963,7 @@ int fit_image_get_emb_data(const void *fit, int noffset, const void **data, * 0, on success * -ENOENT if the property could not be found */ -int fit_image_get_data_offset(const void *fit, int noffset, int *data_offset) +int fit_image_get_data_offset(const void *fit, int noffset, u32 *data_offset) { const fdt32_t *val; @@ -988,7 +988,7 @@ int fit_image_get_data_offset(const void *fit, int noffset, int *data_offset) * -ENOENT if the property could not be found */ int fit_image_get_data_position(const void *fit, int noffset, - int *data_position) + u32 *data_position) { const fdt32_t *val; @@ -1012,7 +1012,7 @@ int fit_image_get_data_position(const void *fit, int noffset, * 0, on success * -ENOENT if the property could not be found */ -int fit_image_get_data_size(const void *fit, int noffset, int *data_size) +int fit_image_get_data_size(const void *fit, int noffset, u32 *data_size) { const fdt32_t *val; @@ -1070,18 +1070,13 @@ int fit_image_get_data(const void *fit, int noffset, const void **data, size_t *size) { bool external_data = false; - int offset; - int len; int ret; size_t fdt_total_size_aligned; + u32 offset; + u32 len; 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)) { /* @@ -1090,9 +1085,9 @@ int fit_image_get_data(const void *fit, int noffset, const void **data, * for the data-offset properties in each image. */ 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); + /* The resulting offset cannot exceed UINT32_MAX */ + if (fdt_total_size_aligned > UINT32_MAX - offset) { + printf("Invalid external data offset: %u\n", offset); return -EINVAL; } offset += fdt_total_size_aligned; @@ -1106,16 +1101,12 @@ int fit_image_get_data(const void *fit, int noffset, const void **data, 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); + printf("Invalid external data offset: %u\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. @@ -1135,7 +1126,7 @@ int fit_image_get_data(const void *fit, int noffset, const void **data, len > CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) - offset #endif ) { - printf("FIT external data is out of bounds (offset=%d, size=%d)\n", + printf("FIT external data is out of bounds (offset=%u, size=%u)\n", offset, len); return -EINVAL; } diff --git a/common/spl/spl_fit.c b/common/spl/spl_fit.c index 18bff7b8d4a..9da709fd79a 100644 --- a/common/spl/spl_fit.c +++ b/common/spl/spl_fit.c @@ -175,22 +175,32 @@ static int spl_fit_get_image_node(const struct spl_fit_info *ctx, return node; } -static int get_aligned_image_offset(struct spl_load_info *info, int offset) +static u32 get_aligned_image_offset(struct spl_load_info *info, u32 offset) { return ALIGN_DOWN(offset, spl_get_bl_len(info)); } -static int get_aligned_image_overhead(struct spl_load_info *info, int offset) +static u32 get_aligned_image_overhead(struct spl_load_info *info, u32 offset) { return offset & (spl_get_bl_len(info) - 1); } -static int get_aligned_image_size(struct spl_load_info *info, int data_size, - int offset) +static int get_aligned_image_size(struct spl_load_info *info, ulong data_size, + u32 offset, ulong *aligned_size) { - data_size = data_size + get_aligned_image_overhead(info, offset); + u32 overhead = get_aligned_image_overhead(info, offset); - return ALIGN(data_size, spl_get_bl_len(info)); + if (data_size > ULONG_MAX - overhead) + return -EOVERFLOW; + data_size += overhead; + + if (data_size > ULONG_MAX - (spl_get_bl_len(info) - 1)) + return -EOVERFLOW; + data_size = ALIGN(data_size, spl_get_bl_len(info)); + + *aligned_size = data_size; + + return 0; } /** @@ -216,24 +226,23 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, const struct spl_fit_info *ctx, int node, struct spl_image_info *image_info, ulong max_size) { - int offset; + u32 offset; + u32 len; size_t length; - int len; ulong size; ulong load_addr; void *load_ptr; void *src; - ulong overhead; uint8_t image_comp = -1, type = -1; const void *data; const void *fit = ctx->fit; bool external_data = false; + int ret; log_debug("starting\n"); if (CONFIG_IS_ENABLED(BOOTMETH_VBE) && xpl_get_phase(info) != IH_PHASE_NONE) { enum image_phase_t phase; - int ret; ret = fit_image_get_phase(fit, node, &phase); /* if the image is for any phase, let's use it */ @@ -273,13 +282,19 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, if (!fit_image_get_data_position(fit, node, &offset)) { external_data = true; } else if (!fit_image_get_data_offset(fit, node, &offset)) { - log_debug("read offset %x = offset from fit %lx\n", - offset, (ulong)offset + ctx->ext_data_offset); + /* The resulting offset cannot exceed UINT32_MAX */ + if (ctx->ext_data_offset > UINT32_MAX - offset) { + log_debug("Invalid external data offset: %u\n", offset); + return -EINVAL; + } + log_debug("read offset %x = offset from fit %x\n", offset, + (u32)(offset + ctx->ext_data_offset)); offset += ctx->ext_data_offset; external_data = true; } if (external_data) { + u32 aligned_offset; ulong read_offset; void *src_ptr; @@ -300,16 +315,26 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, * controlled even after fit_config_verify() succeeds. The * image hash is only verified after the device read below, so * an oversized value has to be rejected here. - * - * Bail out before get_aligned_image_size() runs on a hostile - * len: that helper does its arithmetic in int and would - * invoke signed-integer overflow on a value close to or above - * INT_MAX. The block-aligned check further down is the - * mathematically binding one, since size is len rounded up to - * the device block length. */ - if ((ulong)len > max_size) - goto too_big; + ret = get_aligned_image_size(info, len, offset, &size); + if (ret) { + log_debug("Invalid external data size: %u\n", len); + return ret; + } + + if (size > max_size) { + log_debug("Image too large: aligned size %lu, max %lu (data-size %u)\n", + size, max_size, len); + return -EFBIG; + } + + aligned_offset = get_aligned_image_offset(info, offset); + if (aligned_offset > ULONG_MAX - fit_offset) { + log_debug("Invalid aligned external data offset: %u\n", + aligned_offset); + return -EINVAL; + } + read_offset = fit_offset + aligned_offset; if (spl_decompression_enabled() && (image_comp == IH_COMP_GZIP || image_comp == IH_COMP_LZMA)) @@ -318,18 +343,31 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, src_ptr = map_sysmem(ALIGN(load_addr, ARCH_DMA_MINALIGN), len); length = len; - overhead = get_aligned_image_overhead(info, offset); - size = get_aligned_image_size(info, length, offset); - read_offset = fit_offset + get_aligned_image_offset(info, - offset); - /* - * info->read() transfers the block-aligned size into the - * destination, so this is the bound that actually matters; - * len was rejected above only to keep this computation safe. + * For non-signed FIT images, we can check that + * (read_offset + size) does not wrap and that + * (src_ptr + size) does not exceed the addressable range. + * For signed FITs, we can additionally check that + * (offset + len) doesn't exceed the allowed FIT image + * maximum size. + */ + if (size > ULONG_MAX - read_offset || + size > UINTPTR_MAX - (uintptr_t)src_ptr + /* + * #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 (size > max_size) - goto too_big; +#if CONFIG_IS_ENABLED(FIT_SIGNATURE) + || offset > CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) || + len > CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) - offset +#endif + ) { + log_debug("FIT external data is out of bounds (offset=%u, size=%u)\n", + offset, len); + return -EINVAL; + } log_debug("reading from offset %x / %lx size %lx to %p: ", offset, read_offset, size, src_ptr); @@ -339,7 +377,7 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, debug("External data: dst=%p, offset=%x, size=%lx\n", src_ptr, offset, (unsigned long)length); - src = src_ptr + overhead; + src = src_ptr + get_aligned_image_overhead(info, offset); } else { /* Embedded data */ if (fit_image_get_emb_data(fit, node, &data, &length)) { @@ -401,11 +439,6 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, upl_add_image(fit, node, load_addr, length); return 0; - -too_big: - printf("%s: FIT image too large (data-size %u, max %lu)\n", - __func__, (u32)len, max_size); - return -EFBIG; } static bool os_takes_devicetree(uint8_t os) @@ -737,8 +770,9 @@ static int spl_simple_fit_read(struct spl_fit_info *ctx, struct spl_load_info *info, ulong offset, const void *fit_header) { - unsigned long count, size; + unsigned long aligned_size, count, size; void *buf; + int ret; /* * For FIT with external data, figure out where the external images @@ -756,8 +790,12 @@ static int spl_simple_fit_read(struct spl_fit_info *ctx, * For FIT with data embedded, data is loaded as part of FIT image. * For FIT with external data, data is not loaded in this step. */ - size = get_aligned_image_size(info, size, 0); - buf = board_spl_fit_buffer_addr(size, size, 1); + ret = get_aligned_image_size(info, size, 0, &aligned_size); + if (ret) { + log_debug("Invalid FIT size: %lu\n", size); + return ret; + } + buf = board_spl_fit_buffer_addr(aligned_size, aligned_size, 1); if (!buf) { /* * We assume that none of the board will ever use 0x0 as a @@ -767,7 +805,7 @@ static int spl_simple_fit_read(struct spl_fit_info *ctx, return -EIO; } - count = info->read(info, offset, size, buf); + count = info->read(info, offset, aligned_size, buf); if (!count) { /* * FIT could not be read. This means we should free the @@ -800,7 +838,7 @@ static int spl_simple_fit_read(struct spl_fit_info *ctx, ctx->fit = buf; debug("fit read offset %lx, size=%lu, dst=%p, count=%lu\n", - offset, size, buf, count); + offset, aligned_size, buf, count); return 0; } diff --git a/common/splash_source.c b/common/splash_source.c index e02f9be05e4..a5ed7431b8e 100644 --- a/common/splash_source.c +++ b/common/splash_source.c @@ -351,7 +351,7 @@ static int splash_load_fit(struct splash_location *location, ulong bmp_load_addr const void *internal_splash_data; size_t internal_splash_size; int external_splash_addr; - int external_splash_size; + u32 external_splash_size; bool is_splash_external = false; struct legacy_img_hdr *img_header; const u32 *fit_header; diff --git a/drivers/fpga/socfpga_arria10.c b/drivers/fpga/socfpga_arria10.c index e9822b2bb0e..e397b37adf0 100644 --- a/drivers/fpga/socfpga_arria10.c +++ b/drivers/fpga/socfpga_arria10.c @@ -547,7 +547,8 @@ static int first_loading_rbf_to_buffer(struct udevice *dev, u32 *loadable = buffer_p; size_t buffer_size = *buffer_bsize; size_t fit_size; - int ret, i, count, confs_noffset, images_noffset, rbf_offset, rbf_size; + int ret, i, count, confs_noffset, images_noffset; + u32 rbf_offset, rbf_size; const char *fpga_node_name = NULL; const char *uname = NULL; diff --git a/include/image.h b/include/image.h index 6edcb1995bf..1de0a84007a 100644 --- a/include/image.h +++ b/include/image.h @@ -1263,10 +1263,10 @@ int fit_image_get_load(const void *fit, int noffset, ulong *load); int fit_image_get_entry(const void *fit, int noffset, ulong *entry); int fit_image_get_emb_data(const void *fit, int noffset, const void **data, size_t *size); -int fit_image_get_data_offset(const void *fit, int noffset, int *data_offset); +int fit_image_get_data_offset(const void *fit, int noffset, u32 *data_offset); int fit_image_get_data_position(const void *fit, int noffset, - int *data_position); -int fit_image_get_data_size(const void *fit, int noffset, int *data_size); + u32 *data_position); +int fit_image_get_data_size(const void *fit, int noffset, u32 *data_size); int fit_image_get_data_size_unciphered(const void *fit, int noffset, size_t *data_size); int fit_image_get_data(const void *fit, int noffset, const void **data, diff --git a/test/image/spl_load.c b/test/image/spl_load.c index c43c977f784..334ec77b18e 100644 --- a/test/image/spl_load.c +++ b/test/image/spl_load.c @@ -368,55 +368,145 @@ SPL_IMG_TEST(spl_test_image, FIT_INTERNAL, 0); SPL_IMG_TEST(spl_test_image, FIT_EXTERNAL, 0); /* - * A FIT image's data-size property is not covered by the configuration - * signature, so it is untrusted input. load_simple_fit() must reject a - * data-size larger than the destination rather than overrun it, because the - * device read happens before the image hash is verified. + * Build a FIT with external data, overwrite one property of the image node + * with a hostile value and check that loading fails with the expected error. + * The external-data properties are excluded from the configuration signature, + * so load_simple_fit() must reject values that would wrap its offset/size + * arithmetic rather than read from a bogus location. */ -static int spl_test_fit_external_oversize(struct unit_test_state *uts) +static int check_fit_ext_prop(struct unit_test_state *uts, const char *prop, + u32 value, uint bl_len, spl_load_reader h_read, + ulong fit_offset, int expected) { size_t img_size, img_data, data_size = SPL_TEST_DATA_SIZE; struct spl_image_info info_write = { - .name = "oversize", + .name = "ext-prop", .size = data_size, }, info_read = { }; struct spl_load_info load; void *img; int node; - if (!image_supported(FIT_EXTERNAL)) - return -EAGAIN; - img_size = create_image(NULL, FIT_EXTERNAL, &info_write, &img_data); ut_assert(img_size); img = calloc(img_size, 1); ut_assertnonnull(img); - generate_data(img + img_data, data_size, "oversize"); + generate_data(img + img_data, data_size, "ext-prop"); ut_asserteq(img_size, create_image(img, FIT_EXTERNAL, &info_write, NULL)); - /* - * Inflate data-size far beyond the image buffer and any plausible - * load region. Without a bounds check, load_simple_fit() reads this - * many bytes off the "device" before the hash is checked. - */ node = fdt_path_offset(img, FIT_IMAGES_PATH); ut_assert(node >= 0); node = fdt_first_subnode(img, node); ut_assert(node >= 0); - ut_assertok(fdt_setprop_inplace_u32(img, node, FIT_DATA_SIZE_PROP, - 0x40000000)); + ut_assertok(fdt_setprop_inplace_u32(img, node, prop, value)); - spl_load_init(&load, spl_test_read, img, 1); - ut_asserteq(-EFBIG, spl_load_simple_fit(&info_read, &load, 0, img)); + spl_load_init(&load, h_read, img, bl_len); + ut_asserteq(expected, + spl_load_simple_fit(&info_read, &load, fit_offset, img)); free(img); return 0; } + +/* + * A FIT image's data-size property is not covered by the configuration + * signature, so it is untrusted input. load_simple_fit() must reject a + * data-size larger than the destination rather than overrun it, because the + * device read happens before the image hash is verified. + */ +static int spl_test_fit_external_oversize(struct unit_test_state *uts) +{ + if (!image_supported(FIT_EXTERNAL)) + return -EAGAIN; + + return check_fit_ext_prop(uts, FIT_DATA_SIZE_PROP, 0x40000000, 1, + spl_test_read, 0, -EFBIG); +} SPL_TEST(spl_test_fit_external_oversize, 0); /* + * A data-offset which wraps past UINT32_MAX once the external-data base + * offset is added must be rejected before it is used as a read offset. + */ +static int spl_test_fit_data_offset_overflow(struct unit_test_state *uts) +{ + if (!image_supported(FIT_EXTERNAL)) + return -EAGAIN; + + return check_fit_ext_prop(uts, FIT_DATA_OFFSET_PROP, 0xffffffff, 1, + spl_test_read, 0, -EINVAL); +} +SPL_TEST(spl_test_fit_data_offset_overflow, 0); + +/* + * A data-size whose block-aligned read size wraps past ULONG_MAX must be + * rejected. Since data-size is a 32-bit property, the wrap is only reachable + * when ulong is 32 bits wide, so skip the test on other targets. + */ +static int spl_test_fit_data_size_overflow(struct unit_test_state *uts) +{ + if (!image_supported(FIT_EXTERNAL) || sizeof(ulong) != 4) + return -EAGAIN; + + return check_fit_ext_prop(uts, FIT_DATA_SIZE_PROP, 0xffffffff, 2, + spl_test_read, 0, -EOVERFLOW); +} +SPL_TEST(spl_test_fit_data_size_overflow, 0); + +/* Device offset the reader pretends the FIT was loaded from */ +static ulong spl_test_fit_offset; + +static ulong spl_test_read_fit_offset(struct spl_load_info *load, ulong sector, + ulong count, void *buf) +{ + return spl_test_read(load, sector - spl_test_fit_offset, count, buf); +} + +/* + * An aligned external-data offset which wraps past ULONG_MAX once the FIT's + * offset on the device is added must be rejected before it is used as a read + * offset. + */ +static int spl_test_fit_read_offset_overflow(struct unit_test_state *uts) +{ + if (!image_supported(FIT_EXTERNAL)) + return -EAGAIN; + + /* So that ULONG_MAX - fit_offset < the aligned external-data offset */ + spl_test_fit_offset = ULONG_MAX - 0xfff; + + return check_fit_ext_prop(uts, FIT_DATA_OFFSET_PROP, 0x1000, 1, + spl_test_read_fit_offset, spl_test_fit_offset, + -EINVAL); +} +SPL_TEST(spl_test_fit_read_offset_overflow, 0); + +/* + * A read whose end position (read_offset + size) wraps past the addressable + * range must be rejected. + */ +static int spl_test_fit_read_end_overflow(struct unit_test_state *uts) +{ + if (!image_supported(FIT_EXTERNAL)) + return -EAGAIN; + + /* + * The aligned external-data offset (0x2000 plus the size of the FIT + * itself) stays below the 0x2fff bytes remaining before ULONG_MAX, so + * read_offset passes the offset-wrap check above, but reading the + * SPL_TEST_DATA_SIZE bytes of data crosses past UINTPTR_MAX. + */ + spl_test_fit_offset = ULONG_MAX - 0x2fff; + + return check_fit_ext_prop(uts, FIT_DATA_OFFSET_PROP, 0x2000, 1, + spl_test_read_fit_offset, spl_test_fit_offset, + -EINVAL); +} +SPL_TEST(spl_test_fit_read_end_overflow, 0); + +/* * LZMA is too complex to generate on the fly, so let's use some data I put in * the oven^H^H^H^H compressed earlier */ diff --git a/test/py/tests/test_vboot.py b/test/py/tests/test_vboot.py index 4b6707caf70..fd91b3f10a2 100644 --- a/test/py/tests/test_vboot.py +++ b/test/py/tests/test_vboot.py @@ -700,12 +700,12 @@ def test_vboot_ext_data_bounds(ubman): fd.write(500 * b'\0') testcases = [ - ('negative data-position', - {'data-position': 0xffffffff}, 'Invalid external data position'), - ('negative data-offset', + ('invalid data-position', + {'data-position': 0xffffffff}, 'FIT external data is out of bounds'), + ('invalid data-offset', {'data-offset': 0xffffffff}, 'Invalid external data offset'), - ('negative data-size', - {'data-size': 0xffffffff}, 'Invalid external data size'), + ('invalid data-size', + {'data-size': 0xffffffff}, 'FIT external data is out of bounds'), ('off-bounds data-position', {'data-position': 0x7fffffff}, 'FIT external data is out of bounds'), ('off-bounds data-offset', |
