Vulnerability Report - 5 findings in erofs-utils

Tristan <[email protected]>
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <CAA1XrhPMekMqAnRkC-jV9rTsO4LHjzh=kxn6zQKMgBrqfrnp8A@mail.gmail.com>
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
u64-to-u32-truncation-overflow.txt (text/plain, 1.7 KB)
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;
fsck-path-off-by-one.txt (text/plain, 1.8 KB)
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) {
zstd-decomp-oob-read.txt (text/plain, 1.7 KB)
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.
symlink-integer-overflow.txt (text/plain, 1.7 KB)
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);
dump-erofs-recursion.txt (text/plain, 1.8 KB)
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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.