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
[email protected]

<<attachment: poc.zip>>

Reply via email to