Skip to content

libext: fix heap overflow in ext_readlink and ext_readdir from a crafted ext4 image - #1449

Open
gburd wants to merge 1 commit into
cloudius-systems:masterfrom
gburd:pr/sec-ext4-readlink
Open

libext: fix heap overflow in ext_readlink and ext_readdir from a crafted ext4 image#1449
gburd wants to merge 1 commit into
cloudius-systems:masterfrom
gburd:pr/sec-ext4-readlink

Conversation

@gburd

@gburd gburd commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Security fix — Critical: kernel heap overflow from mounting a crafted ext4 image (a realistic threat for attached volumes / multi-tenant hosts; in a unikernel this is kernel heap corruption → potential RCE).

1. ext_readlink() heap buffer overflow (Critical)

For a slow symlink, modules/libext/ext_vnops.cc did:

uint32_t block_size = ext4_sb_get_block_size(&fs->sb);
void *buf = malloc(block_size);                              // e.g. 4096 bytes
ext_internal_read(fs, &inode_ref._ref, uio->uio_offset, buf, fsize, &read_count);
//                                                          ^^^^^ up to 2^64-1

fsize = ext4_inode_get_size() is the raw 64-bit inode size read straight off disk — fully attacker-controlled and not clamped to block_size. ext_internal_read then writes ~fsize bytes into the block_size heap buffer.

PoC (verified with debugfs): craft an ext4 image with a symlink inode whose i_size = 0x100000 (1 MiB) and i_blocks != 0 (so the slow-symlink else branch is taken). readlink() — or any path traversal through the link — → malloc(4096) then a ~1 MiB write into it → heap corruption. Also uio_offset > fsize underflows fsize - offset inside ext_internal_read.

Fix: a symlink target is at most one block (bounded by PATH_MAX); reject fsize > block_size, guard uio_offset >= fsize, check malloc.

2. ext_readdir() d_name[256] overflow (High, old-rev image)

uint16_t name_length = ext4_dir_en_get_name_len(&fs->sb, it.curr);
memcpy(dir->d_name, it.curr->name, name_length);   // d_name is 256 bytes

On a rev-0 (minor<5) image, ext4_dir_en_get_name_len folds in name_length_high and can return up to block_size-8 (~4088) → ~4 KB memcpy into the 256-byte d_name. Fix: clamp to sizeof(d_name)-1.

Verification

With debugfs-crafted images: a normal symlink still reads back correctly (new tst-ext-readlink regression), and a symlink inode with i_size=1MiB now returns EINVAL instead of overflowing the heap.

Severity

#1 Critical (CVSS ~8.4, AV:L/needs mounted crafted image/C:H/I:H/A:H — RCE-class). #2 High.

Both bugs are in shipped master (the OSv ext/lwext4 glue), not new code.

@gburd

gburd commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Related upstream note: while auditing the ext/ext4 image-trust surface I also found an out-of-bounds read in the vendored lwext4 (osvunikernel/lwext4, cloned by modules/lwext4/Makefile): ext4_ext_check() in src/ext4_extent.c validates eh_entries <= eh_max but never bounds eh_max/eh_entries to the block capacity, so a crafted extent header (e.g. eh_max=eh_entries=0xFFFF) drives find_ext4_extent_tail() and the extent binary search hundreds of KiB past the one-block buffer on any read of that file.

Since osvunikernel/lwext4 has issues disabled, I reported it against the original upstream: gkostka/lwext4#100 (with a repro and a suggested fix porting Linux's ext4_valid_extent_entries() capacity bound). Flagging here so it's on the OSv radar for the osvunikernel/lwext4 fork — it's outside this PR's scope (this PR fixes the OSv-side ext_readlink/ext_readdir overflows), but it's the same 'crafted ext image' threat surface.

@gburd
gburd force-pushed the pr/sec-ext4-readlink branch from 13fe54a to 4d81010 Compare July 15, 2026 13:46
… from a crafted ext4 image

Two memory-safety bugs reachable by mounting an attacker-supplied ext4 image
(a realistic threat for attached volumes / multi-tenant hosts). In a unikernel
these are kernel heap corruption.

1. ext_readlink() heap buffer overflow (critical). For a slow symlink it did:
       void *buf = malloc(block_size);            // e.g. 4096 bytes
       ext_internal_read(fs, ref, offset, buf, fsize, &read_count);
   where fsize is ext4_inode_get_size() - the raw 64-bit inode size read
   straight off disk, fully attacker-controlled and NOT clamped. A crafted
   symlink inode with i_size = 1 MiB (and i_blocks != 0 so the slow-symlink
   branch is taken) makes ext_internal_read write ~1 MiB into the 4 KiB heap
   buffer -> heap corruption / potential RCE, reached by readlink() or any path
   traversal through the link. A symlink target is at most one block (bounded
   by PATH_MAX), so clamp: reject fsize > block_size, guard uio_offset >= fsize
   (which would otherwise underflow fsize-offset), and check malloc().

2. ext_readdir() d_name overflow. memcpy(dir->d_name, name, name_length) with
   name_length from ext4_dir_en_get_name_len(), which on an old-rev image
   (rev0, minor<5) folds in name_length_high and returns up to block_size-8
   (~4088) while d_name is 256 bytes. Clamp name_length to sizeof(d_name)-1.

Verified with debugfs-crafted images: a normal symlink still reads back
correctly (tst-ext-readlink), and a symlink inode with i_size=1MiB now returns
EINVAL instead of overflowing the heap. Added tst-ext-readlink as a regression.
@gburd
gburd force-pushed the pr/sec-ext4-readlink branch from 4d81010 to 63444df Compare July 17, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant