[SECURITY] Two out-of-bounds reads in U-Boot SquashFS parser (fs/squashfs)

Eun0us / Espilon eun0us at espilon.net
Wed Jul 1 15:28:02 CEST 2026


Hello,

I am a security researcher with Espilon (https://espilon.net), a French non-profit
focused on embedded, IoT and OT security research, with published CVEs in Espressif,
AWS/FreeRTOS and Arduino.

I am reporting a memory-safety vulnerability I found in your project, with a
self-contained reproducer attached. This is a coordinated disclosure; please
confirm receipt and your preferred timeline. I am happy to help validate a fix.

The attached archive (poc.zip) contains:
  - reproduce.sh : clones the upstream repository at a pinned commit, builds the
    real source under AddressSanitizer, and triggers the issue from a fresh
    checkout (one command: bash reproduce.sh)
  - report.md : full write-up (mechanism, file and line, honest impact, suggested fix)
  - the harness and stubs used by the reproducer

Summary is below; the full details are in report.md inside the archive.

----------------------------------------------------------------------
# U-Boot SquashFS parser: unchecked wire-size arithmetic - two out-of-bounds reads (plus a size-arithmetic hardening item)

**ID:** OSS-UBOOT-SQUASHFS-OOB-DOS-001
**Component:** U-Boot SquashFS filesystem driver (`fs/squashfs/sqfs.c`, `fs/squashfs/sqfs_inode.c`)
**Affected version:** `origin/master` at `f605dcee103c897b6f1a8873549a36949bd4e2a1` (verified on worktree `a3f393c0`, 2026-06-27). Unpatched at time of writing.
**Config gate:** `CONFIG_FS_SQUASHFS` (enabled in many distro/boot-scan defconfigs).
**Bug classes:** CWE-191 (integer underflow), CWE-125 (out-of-bounds read), CWE-789 / CWE-190 (uncontrolled / oversized allocation).
**Impact (honest):** Two confirmed out-of-bounds READs (RC-1, RC-3). A third related site (RC-2) is a size-arithmetic hardening item, reported as a robustness fix rather than claimed as a standalone vulnerability. There is **no out-of-bounds write and no control-flow hijack in any of these paths; RCE is NOT claimed.**
**Reachability tier:** boot-media / pre-OS. An attacker who can place a crafted SquashFS image on any media U-Boot enumerates and parses (`sqfs ls`, `load sqfs`, distro/boot-scan) reaches these paths during the pre-OS parse of raw image bytes.

---

## Summary

U-Boot's SquashFS driver reads several 16/64-bit size and offset fields directly from the
on-disk image and performs arithmetic on them (header subtraction, table-span subtraction,
per-inode walk) **without validating them against a sane lower bound or against the actual
size of the buffers/tables being walked**. Two of these are confirmed out-of-bounds reads of
the parser's own heap buffers (RC-1 and RC-3, both reproduced under AddressSanitizer on the
real parser). A third site (RC-2) is unguarded size-arithmetic on the table-span / fragment
paths; it is reported below as a hardening item (the primary allocation is null-checked, so
it is not claimed as a standalone vulnerability), because the fragment-path multiply has no
overflow guard at all and the pattern matches the accepted bug class.

These are the **sibling paths** of the sigma-star fixes (CVE-2024-57254..57259), which
hardened only the symlink-size and name-length paths. The directory-size, table-offset, and
inode-count paths described here were **not** covered by those fixes and remain unguarded on
`master`.

---

## Root cause 1 (RC-1): directory `file_size` unsigned underflow -> heap OOB read

**CWE-191 -> CWE-125. File:line: `fs/squashfs/sqfs.c:510`, `:648`, `:990` (also `:1048`).**

A directory inode's `file_size` is a 16-bit (basic dir) / 32-bit (long dir) wire field. The
driver subtracts the fixed 12-byte directory-header size (`SQFS_DIR_HEADER_SIZE == 12`) from
it and stores the result in the `size_t` field `dirs->size`, with no `file_size >= 12`
precondition:

- `sqfs.c:510` - `dirs->size = get_unaligned_le16(&dir->file_size) - SQFS_DIR_HEADER_SIZE;`
- `sqfs.c:646-648` - assigns `dirs->size = get_unaligned_le16(&dir->file_size);` then `dirs->size -= SQFS_DIR_HEADER_SIZE;`
- `sqfs.c:990` - `dirs->size -= SQFS_DIR_HEADER_SIZE;` (in `sqfs_opendir_nest`)

When `file_size < 12`, the subtraction underflows into a `size_t` near `1.8e19`. `dirs->size`
is the **only bound** on the `sqfs_readdir_nest` directory-walk loop, which advances
`dirs->table` through the parser's own metadata directory-table buffer and copies each entry
via `sqfs_read_entry` (`memcpy`, `sqfs.c:238`). A corrupted (near-`SIZE_MAX`) bound makes the
walk read far past the end of that buffer.

The directory-table buffer is the real `SQFS_METADATA_BLOCK_SIZE` (8192-byte) region the parser
allocates itself in `sqfs_read_directory_table` (`sqfs.c:855`). The existing empty-directory
guard (`sqfs_is_empty_dir`, ~`:637`) only catches `file_size == 3` on the `:648` path; there is
no guard at `:510` or `:990`, and `file_size` in `{0,1,2,4..11}` all underflow (`{0,1,11}`
confirmed reproducing).

**Observed (faithful host-ASan, real parser):** `heap-buffer-overflow READ of size 48839`, 0
bytes to the right of the 8192-byte region, in `sqfs_read_entry` (`sqfs.c:238`) <-
`sqfs_readdir_nest` (`sqfs.c:1060`) <- `sqfs_readdir` (`sqfs.c:1023`); the overflowed buffer is
the parser's own `dlmalloc` at `sqfs_read_directory_table` (`sqfs.c:855`) / `sqfs_opendir_nest`
(`sqfs.c:942`).

---

## Root cause 2 (RC-2): unguarded table-span size math (hardening item, not a standalone vulnerability claim)

**CWE-190 / CWE-789. File:line: `fs/squashfs/sqfs.c:733-734` (+`:744`), `:827` (+`:838`), and `sqfs.c:94` / `:136` (fragment path).**

Reported as a robustness / hardening fix. The primary oversized-allocation path is null-checked
(returns `-ENOMEM`), so it is not claimed as an independent memory-safety vulnerability; the
substantive gap is the fragment-path multiply at `sqfs.c:136`, which has no `__builtin_mul_overflow`
guard at all (unlike the inode/dir paths at `:741`/`:835`). It is included so the same guard is
applied consistently.

Table sizes are derived by subtracting one on-disk 64-bit table-start offset from another:

- inode table: `sqfs.c:733-734` - `table_size = get_unaligned_le64(&sblk->directory_table_start) - get_unaligned_le64(&sblk->inode_table_start);`
- directory table: `sqfs.c:827` - same pattern with `fragment_table_start - directory_table_start`.
- shared helper `sqfs_calc_n_blks` (`sqfs.c:94`) - `table_size = le64_to_cpu(end) - le64_to_cpu(start);` then returns `DIV_ROUND_UP(table_size + offset, blksz)`.

Neither the start ordering (`end >= start`) nor an upper sanity bound versus the actual image
length is checked. Two failure modes, both attacker-driven off-disk:

1. **Oversized allocation / disk read (primary, confirmed):** a large forward span (or a wrap
   that lands below the overflow threshold) yields a large block count and
   `malloc_cache_aligned(buf_size)` at `sqfs.c:744` (inode table) / `:838` (directory table).
   The confirmed reproducer drives a request on the order of tens of GB (host-ASan reports a
   ~15 TB reservation attempt at `sqfs.c:744`). Even when the allocation is null-checked
   (`:745`, `:839` return `-ENOMEM`), the subsequent `sqfs_disk_read(start, n_blks, ...)`
   (`:748`, `:842`) is asked to read the attacker-chosen huge block count. Result: memory
   exhaustion / OOM / long hang during the pre-OS parse.

2. **Unguarded multiply (fragment path):** `sqfs_frag_lookup` at `sqfs.c:136` calls
   `malloc_cache_aligned(n_blks * ctxt.cur_dev->blksz)` with **no `__builtin_mul_overflow`
   guard at all** (contrast the inode/dir paths at `:741`/`:835`, which do guard the multiply).
   With `n_blks` derived from the same underflow-prone `sqfs_calc_n_blks`, this multiply can
   wrap `size_t` and produce an undersized allocation (leading to a later overflow) or a huge
   one.

Note: the `__builtin_mul_overflow` guards at `sqfs.c:741` and `:835` only reject the case where
`n_blks * blksz` overflows `size_t`; they do **not** reject a merely-huge (multi-GB) but
in-range allocation, and they do not exist on the fragment path. So the DoS window is real and
unguarded.

**Observed (faithful host-ASan, real parser):** allocator failure attempting `0xdfff0001000`
(~15 TB) bytes at the `malloc_cache_aligned` call reached from `sqfs.c:744`.

---

## Root cause 3 (RC-3): unbounded inode-count walk -> OOB read

**CWE-125. File:line: `fs/squashfs/sqfs_inode.c:131` (loop at `:129`).**

`sqfs_find_inode` walks the decompressed inode table looking for a target inode number. The
loop bound `inode_count` is an on-disk 32-bit field, and `offset` advances by a per-inode
size that is itself parsed from on-disk fields. Nothing bounds `offset` (or `inode_table +
offset`) against the actual length of the allocated inode-table buffer. A large `inode_count`
(or per-inode sizes that outrun the buffer) walks `base` past the end of the table and
dereferences it via `get_unaligned_le32(&base->inode_number)` at `sqfs_inode.c:131`.

**Observed (faithful host-ASan, real parser):** wild read reaching an unmapped page - SEGV in
`get_unaligned_le32` (`include/asm-generic/unaligned.h:27`) inside `sqfs_find_inode`
(`sqfs_inode.c:131`) <- `sqfs_search_dir` (`sqfs.c:489`) <- `sqfs_opendir_nest` (`sqfs.c:977`).
This site faults even without ASan (see "Reproduction without ASan" below), i.e. it is a hard
crash / DoS on any target.

---

## Impact and severity

The two confirmed findings (RC-1, RC-3) are memory-safety defects in code that parses fully
attacker-controlled bytes off boot media before the OS is running.

- RC-1 (confirmed): heap out-of-bounds **read** of the parser's own metadata buffer -> corrupted
  directory listing, adjacent-heap information exposure, and (when the walk reaches unmapped
  memory) a crash. It is a read, not a write; **not** by itself a control-flow primitive.
- RC-3 (confirmed): unbounded walk -> out-of-bounds **read** that reliably reaches unmapped
  memory and faults (even without ASan) -> hard crash / DoS. Read only.
- RC-2 (hardening): unguarded size-arithmetic; primary allocation is null-checked, the fragment
  multiply lacks an overflow guard. Reported as a consistency/robustness fix, not a standalone
  vulnerability.

**No out-of-bounds write and no RCE are claimed for any of these sites.**

### CVSS 3.1 base vectors (honest - attacker-supplied boot media, read/DoS only)

The attacker must get U-Boot to parse a crafted SquashFS image. On most real deployments that
means local/physical access to the boot media or an enumerated partition (AV:L, occasionally
AV:P). None of these are network-reachable in the parser itself.

- **RC-1 (OOB read):** `CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L` = **4.4 (Medium)**
  (local attacker-supplied image; limited confidentiality via adjacent-heap read + limited
  availability via crash). If the deployment only accepts media via physical access, AV:P
  lowers it further.
- **RC-2 (hardening item):** no standalone CVSS claimed. The primary oversized allocation is
  null-checked; the reported gap is the missing overflow guard on the fragment-path multiply
  (`sqfs.c:136`). Treat as a robustness fix, not a scored vulnerability.
- **RC-3 (OOB read -> hard crash):** `CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H` =
  **6.1 (Medium)** (reliable crash / boot DoS, plus limited pre-fault adjacent-memory read).

Rationale for AV:L (not AV:N): the SquashFS parser consumes raw block-device bytes; it is not
itself exposed on a network interface. Do not upgrade these to AV:N/critical.

---

## Anti-duplication vs CVE-2024-57254..57259 (sigma-star)

The sigma-star series hardened the SquashFS driver against wire-size abuse, but only on the
**symlink-size and name-length** paths. On `master` (`f605dcee`) those fixes are present:

- `__builtin_add_overflow` guarding the name-length copy (~`sqfs.c:267`),
- `__builtin_add_overflow` guarding `symlink_size` (~`sqfs.c:439`),
- `MAX_SYMLINK_NEST == 8` bounding symlink recursion (~`sqfs.c:554`).

The three root causes here are the **directory-size**, **table-offset span**, and
**inode-count** siblings of that same wire-size-arithmetic bug class. No guard anywhere checks
`file_size >= SQFS_DIR_HEADER_SIZE`, checks `directory_table_start >= inode_table_start` (or the
fragment/id-table equivalents), or bounds the `sqfs_find_inode` walk against the actual table
length. The presence of the `__builtin_add_overflow` / `__builtin_mul_overflow` guards the
maintainers already added is direct evidence they accept this class of fix; these three sites
were simply missed.

Checked against public CVEs and our own pipeline - net-new candidate for U-Boot.

---

## Suggested fix

1. **RC-1:** before the `- SQFS_DIR_HEADER_SIZE` subtraction at `sqfs.c:510`, `:648`, `:990`
   (and `:1048`), reject `file_size < SQFS_DIR_HEADER_SIZE` (return `-EINVAL`), e.g. via
   `__builtin_sub_overflow` or an explicit `if (file_size < SQFS_DIR_HEADER_SIZE) return
   -EINVAL;`. Additionally, bound the `sqfs_readdir_nest` walk against the actual directory
   metadata buffer length, not solely against `dirs->size`.

2. **RC-2:** in `sqfs_calc_n_blks` (`sqfs.c:94`) and at the call sites (`:733`, `:827`,
   fragment path), reject `end < start` (table-start ordering) with `__builtin_sub_overflow`,
   and clamp the derived table size against the actual image / partition length before
   allocating. Add the missing `__builtin_mul_overflow` guard on the fragment-path multiply at
   `sqfs.c:136` to match the inode/dir paths.

3. **RC-3:** bound the `sqfs_find_inode` walk (`sqfs_inode.c:129-138`) so that
   `offset + sizeof(*base)` (and the per-inode `sz`) never exceeds the actual inode-table buffer
   length; return `NULL`/`-EINVAL` on overrun instead of trusting the on-disk `inode_count`.

---

## Reproduction (faithful host-ASan harness)

A self-contained harness is included in `poc/harness/` and driven by `reproduce.sh`, which
clones U-Boot at the pinned commit, compiles the **real** `fs/squashfs/*.c` with the exact
sandbox flags, links only leaf stubs (block read = memcpy from the backing image; allocators =
libc so ASan instruments the parser's real buffer sizes; decompressors return error for
uncompressed-metadata images), and builds the ASan harness.

`nm` confirms the parser is the real blob, not stubs: `sqfs_probe`, `sqfs_opendir`,
`sqfs_readdir`, `sqfs_find_inode`, `sqfs_dir_offset` are `T`; `sqfs_opendir_nest`,
`sqfs_readdir_nest`, `sqfs_search_dir` are real local (`t`) text (the enclosing functions of all
three root causes). `sqfs_read_entry` / `sqfs_read_directory_table` are inlined at `-O1` and
show in the ASan backtraces at their real `sqfs.c` line numbers.

**Trigger images are NOT shipped in this package** (public list). The crafting logic
(`craft.py` - set a directory inode's `file_size < 12` for RC-1; set table-start offsets out of
order / far apart for RC-2; inflate `inode_count` for RC-3) is included so a reviewer can
regenerate them from a stock `mksquashfs -noI` image. Prebuilt private reproducers are available
to the maintainers on request.

### Reproduction without ASan (honest)

- **RC-1 (read):** without ASan, on a host with a large heap the ~48 KB over-read lands in
  adjacent mapped memory and the run returns cleanly (no fault) - it is a genuine read
  primitive that ASan redzones catch but a flat/large heap masks. On a real bootloader the
  over-read silently exposes adjacent memory into the directory listing and can crash if the
  walk reaches an unmapped region. Faulting is not guaranteed without ASan.
- **RC-2 (alloc/disk-read DoS):** aborts/fails to allocate even without ASan (huge
  `malloc_cache_aligned`), and drives a huge `sqfs_disk_read` - resource-exhaustion DoS
  independent of ASan.
- **RC-3 (read):** **faults even without ASan** (observed SIGSEGV / exit 139 in a plain `-O0`
  build) - the unbounded walk reliably runs off into unmapped memory. Hard crash / DoS on any
  target.

---

## Routing (proposed, not sent)

Upstream OSS -> U-Boot security list / MITRE for CVE assignment; notify downstream integrators
(distro/embedded vendors shipping `CONFIG_FS_SQUASHFS`) per CISA-tier coordination. This is a
public-list disclosure: trigger images offered privately on request.

**Nothing has been sent.**

----------------------------------------------------------------------

Requirements to reproduce: clang++, git, internet access. The impact is stated
honestly in report.md (this one is a read-only out-of-bounds read / small info-leak,
no out-of-bounds write and no RCE claimed).

Best regards,
Eun0us / Espilon
eun0us at espilon.net
-------------- next part --------------
A non-text attachment was scrubbed...
Name: espilon.png
Type: image/png
Size: 9123 bytes
Desc: not available
URL: <https://lists.denx.de/pipermail/u-boot/attachments/20260701/144eaa70/attachment.png>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: poc.zip
Type: application/zip
Size: 16255 bytes
Desc: not available
URL: <https://lists.denx.de/pipermail/u-boot/attachments/20260701/144eaa70/attachment.zip>


More information about the U-Boot mailing list