Hello,

I am reporting 5 vulnerabilities in erofs-utils across three versions.
All are triggered by crafted EROFS filesystem images.

Findings summary:

  - ZSTD decompression heap OOB read (erofs-utils 8a579d4, CVSS 5.5,
CWE-125)
  - u64-to-u32 truncation heap overflow (erofs-utils 1.8.5, CVSS 7.8,
CWE-190)
  - Off-by-one heap overflow in fsck path (erofs-utils 1.9.1, CVSS 6.2,
CWE-193)
  - Symlink extraction integer overflow (erofs-utils 1.9.1, CVSS 7.8,
CWE-190)
  - Uncontrolled recursion in dump.erofs (erofs-utils 1.9.1, CVSS 5.5,
CWE-674)

I would appreciate acknowledgement of receipt and CVE assignment.

Regards,
Tristan
Heap Overflow via u64-to-u32 Truncation in Fragment Decompression
===================================================================
CVSS 3.1: 7.8 (AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
CWE: CWE-190 (Integer Overflow)
Target: erofs-utils 1.8.5 (verified unfixed at HEAD, commit 8cd872d)

In erofs_verify_inode_data(), buffer_size is declared as unsigned int
(32-bit) but assigned from map.m_llen which is u64. The truncation
causes an undersized allocation; the subsequent read uses the full
64-bit length, overflowing the buffer.

Vulnerable code (fsck/main.c):

  unsigned int raw_size = 0, buffer_size = 0;  // line 526: u32

  if (map.m_llen > buffer_size) {     // line 593: comparison promotes
                                      // buffer_size to u64, passes
      buffer_size = map.m_llen;       // line 596: TRUNCATION u64->u32
      newbuffer = realloc(buffer, buffer_size);  // undersized alloc
  }

  ret = z_erofs_read_one_data(inode, &map, raw, buffer,
                              0, map.m_llen, false);  // writes full u64

Source type (include/erofs/internal.h:378):

  struct erofs_map_blocks {
      u64 m_plen, m_llen;   // 64-bit
  };

Overflow arithmetic:
  m_llen = 0x100000001 (4GB + 1)
  buffer_size = (uint32_t)0x100000001 = 0x1
  realloc(buffer, 1) -> z_erofs_read_one_data writes ~4GB

Impact: heap overflow with attacker-controlled size (up to ~4GB per
extent) and attacker-controlled content from the EROFS image data
blocks. fsck.erofs runs in Android build pipelines, initramfs
validation, and system image verification, often as root.

Fix: change buffer_size from unsigned int to u64:
  -  unsigned int raw_size = 0, buffer_size = 0;
  +  u64 raw_size = 0, buffer_size = 0;
Off-by-One Heap Overflow in fsck.erofs Path Construction
==========================================================
CVSS 3.1: 6.2 (AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H)
CWE: CWE-193 (Off-by-one Error)
Target: erofs-utils 1.9.1 (verified unfixed at HEAD, commit d3fbccb)

In erofsfsck_dirent_iter(), the bounds check for path construction
does not account for the prepended '/' separator, allowing a null
byte to be written one byte past the PATH_MAX-sized heap buffer.

Vulnerable code (fsck/main.c:901-915):

  if (prev_pos + ctx->de_namelen >= PATH_MAX) {  // line 904
      return -EOPNOTSUPP;                        // check misses '/'
  }

  fsckcfg.extract_path[curr_pos++] = '/';         // line 911: +1
  strncpy(fsckcfg.extract_path + curr_pos,
          ctx->dname, ctx->de_namelen);            // line 912-913
  curr_pos += ctx->de_namelen;                     // line 914
  fsckcfg.extract_path[curr_pos] = '\0';           // line 915: OOB

Buffer: malloc(PATH_MAX) -> valid indices 0 to 4095.

Concrete example (PATH_MAX = 4096):
  prev_pos = 4090, de_namelen = 5
  Check: 4090 + 5 = 4095 < 4096 -> PASSES
  '/' at index 4090, name at 4091-4095, '\0' at 4096 -> OOB

The check allows prev_pos + de_namelen up to PATH_MAX - 1, but
the actual write extends to prev_pos + 1 + de_namelen due to '/'.
Maximum OOB index: PATH_MAX (one byte past allocation).

Impact: single null byte written past a heap buffer (null byte
poisoning). On glibc, this corrupts the size metadata of the
adjacent heap chunk -- a known exploitation primitive (House of
Einherjar) that enables overlapping allocations. At minimum,
heap corruption leads to a crash.

Fix: account for the separator in the bounds check:
  -  if (prev_pos + ctx->de_namelen >= PATH_MAX) {
  +  if (prev_pos + ctx->de_namelen + 1 >= PATH_MAX) {
Heap OOB Read in ZSTD Decompression via Frame Size Mismatch
==============================================================
CVSS 3.1: 5.5 (AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N)
CWE: CWE-125 (Out-of-bounds Read)
Target: erofs-utils (commit 8a579d4, verified at HEAD)

z_erofs_decompress_zstd() allocates a decompression buffer based
on ZSTD_getFrameContentSize() (from the compressed frame header),
but copies rq->decodedlength bytes (from the filesystem extent
metadata) to the output. Both values are independently attacker-
controlled in a crafted EROFS image.

Vulnerable code (lib/decompress.c:52-76):

  total = ZSTD_getFrameContentSize(src + inputmargin, ...);
  // total = from ZSTD frame header (attacker-controlled)

  buff = malloc(total);  // allocates based on frame header

  ret = ZSTD_decompress(dest, total, ...);  // fills 'total' bytes

  memcpy(rq->out, dest + rq->decodedskip,
         rq->decodedlength - rq->decodedskip);
  // copies decodedlength bytes from total-sized buffer

Overflow arithmetic:
  Frame_Content_Size = 100 (ZSTD header)
  m_llen = 12MB (extent metadata, up to Z_EROFS_PCLUSTER_MAX_DSIZE)
  malloc(100) -> memcpy reads 12MB from 100-byte buffer -> ~12MB OOB

Sibling comparison: all other decompression backends (LZ4, LZMA,
DEFLATE, QPL) allocate based on rq->decodedlength, not the
compressed frame's self-declared size. Only ZSTD is vulnerable.

Impact: ~12MB heap OOB read. In erofsfuse, leaked heap data is
returned via FUSE to the reading process. In fsck.erofs --extract,
leaked data is written to extracted files.

Fix: allocate the intermediate buffer based on rq->decodedlength
(consistent with all other backends), or validate that
ZSTD_getFrameContentSize() >= rq->decodedlength.
Integer Overflow in Symlink Extraction Leading to Heap Overflow
=================================================================
CVSS 3.1: 7.8 (AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
CWE: CWE-190 (Integer Overflow)
Target: erofs-utils 1.9.1 (verified unfixed at HEAD, commit d3fbccb)

erofs_extract_symlink() allocates a buffer using malloc(i_size + 1)
where i_size is a 64-bit value read directly from the on-disk inode
with no upper bound validation. When i_size equals UINT64_MAX, the
addition wraps to zero, and malloc(0) returns a minimal allocation.
The subsequent erofs_pread() writes i_size bytes into this tiny
buffer.

Vulnerable code (fsck/main.c:797-807):

  buf = malloc(inode->i_size + 1);        // wraps to 0 at UINT64_MAX
  if (!buf) { ret = -ENOMEM; goto out; }  // malloc(0) returns non-NULL

  ret = erofs_pread(&vf, buf, inode->i_size, 0);  // writes i_size bytes

Source of i_size (lib/namei.c:100):

  vi->i_size = le64_to_cpu(die->i_size);  // raw 64-bit, no validation

Overflow arithmetic:
  i_size = 0xFFFFFFFFFFFFFFFF
  malloc(0xFFFFFFFFFFFFFFFF + 1) = malloc(0) -> ~16-32 byte allocation
  erofs_pread writes 0xFFFFFFFFFFFFFFFF bytes into 16-32 byte buffer

On 32-bit platforms, compact inodes (32-bit i_size = 0xFFFFFFFF)
also trigger the same pattern when size_t is 32 bits.

Impact: heap overflow with attacker-controlled write size and
attacker-controlled content (from EROFS image data blocks). This
provides a write-what-where primitive on the heap.

Fix: validate i_size before allocation:
  if (inode->i_size > PATH_MAX || inode->i_size > SIZE_MAX - 1) {
      erofs_err("symlink too large");
      return -EFSCORRUPTED;
  }
  buf = malloc(inode->i_size + 1);
Uncontrolled Recursion in dump.erofs Directory Traversal
==========================================================
CVSS 3.1: 5.5 (AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H)
CWE: CWE-674 (Uncontrolled Recursion)
Target: erofs-utils 1.9.1 (verified unfixed at HEAD, commit d3fbccb)

erofsdump_readdir() in dump.erofs recursively traverses directories
without depth limiting or cycle detection. A crafted EROFS image
with circular directory references causes unbounded recursion,
exhausting the process stack.

The developer acknowledged this with a comment at line 362:
  /* XXXX: the dir depth should be restricted in order to avoid loops */

Vulnerable code (dump/main.c:362-371):

  /* XXXX: the dir depth should be restricted ... */
  if (S_ISDIR(vi.i_mode)) {
      struct erofs_dir_context nctx = {
          .flags = ctx->dir ? EROFS_READDIR_VALID_PNID : 0,
          .pnid = ctx->dir ? ctx->dir->nid : 0,
          .dir = &vi,
          .cb = erofsdump_dirent_iter,
      };
      return erofs_iterate_dir(&nctx, false);  // no guard
  }

Sibling comparison -- fsck.erofs has both protections (fsck/main.c):

  if (fsckcfg.dirstack.top >= ARRAY_SIZE(fsckcfg.dirstack.dirs))
      return -ENAMETOOLONG;                    // depth limit
  for (i = 0; i < fsckcfg.dirstack.top; ++i)
      if (inode.nid == fsckcfg.dirstack.dirs[i])
          return -ELOOP;                       // cycle detection

Trigger: directory A contains entry pointing to directory B, B points
back to A. Each recursive call allocates struct erofs_inode + context
+ block buffer on the stack. Stack exhaustion produces SIGSEGV.

Impact: deterministic crash (denial of service) from crafted image.

Fix: add depth limiting and cycle detection matching the existing
fsck.erofs implementation, or convert to iterative traversal with
an explicit stack.

Reply via email to