CVE-XXXX: Kernel Heap OOB Read in ext2 Extended Attribute Entry Validation
Summary
A heap out-of-bounds read vulnerability exists in fs/ext2/xattr.c in the
ext2_xattr_entry_valid() function. The bounds check on the computed next
entry pointer is off by up to 3 bytes, allowing IS_LAST_ENTRY() to read
past the end of the xattr block buffer. A local attacker who can mount (or
supply) a crafted ext2 filesystem can exploit this to leak kernel heap memory,
potentially bypassing KASLR and aiding a full privilege escalation chain.
Affected File
fs/ext2/xattr.c—ext2_xattr_entry_valid()(line 156)- Triggered via
fs/ext2/xattr_security.c— security xattr read/write paths
Root Cause
Macro definitions (xattr.h)
#define EXT2_XATTR_LEN(name_len) \
(((name_len) + EXT2_XATTR_ROUND + \
sizeof(struct ext2_xattr_entry)) & ~EXT2_XATTR_ROUND)
#define EXT2_XATTR_NEXT(entry) \
( (struct ext2_xattr_entry *)( \
(char *)(entry) + EXT2_XATTR_LEN((entry)->e_name_len)) )
#define IS_LAST_ENTRY(entry) (*(__u32 *)(entry) == 0)
IS_LAST_ENTRY dereferences the pointer as a 4-byte __u32. This requires
at least 4 bytes of valid, readable memory at entry.
The defective bounds check (xattr.c:155-157)
next = EXT2_XATTR_NEXT(entry);
if ((char *)next >= end) // ← off-by-up-to-3
return false;
The check rejects next only when it is at or past end
(bh->b_data + bh->b_size). It accepts next values of
end-1, end-2, and end-3.
After validation succeeds the caller loop advances and immediately calls
IS_LAST_ENTRY on the new pointer without further bounds checking:
// xattr.c:237-250
entry = FIRST_ENTRY(bh);
while (!IS_LAST_ENTRY(entry)) { // ← 4-byte dereference, no recheck
if (!ext2_xattr_entry_valid(entry, end, inode->i_sb->s_blocksize))
goto bad_block;
...
entry = EXT2_XATTR_NEXT(entry); // entry may now be end-1/2/3
}
The same pattern appears in ext2_xattr_list() (line 324-328) and
ext2_xattr_set() (line 468-483).
Out-of-bounds window
next value |
bytes read OOB by IS_LAST_ENTRY |
|---|---|
end - 1 |
3 bytes (end, end+1, end+2) |
end - 2 |
2 bytes (end, end+1) |
end - 3 |
1 byte (end) |
Trigger Conditions
- Mount (or loop-mount) a crafted ext2 filesystem image.
- The malicious xattr block must have
h_magic = EXT2_XATTR_MAGICandh_blocks = 1(passesext2_xattr_header_valid). - The last entry is positioned so that
EXT2_XATTR_NEXT(last_entry) = block_end - kfork ∈ {1,2,3}. - Trigger any xattr read (
getxattr,listxattr) or write (setxattr) on a file in the mounted filesystem, which calls the security handler throughext2_xattr_security_get/ext2_xattr_security_set.
No special privileges are needed on systems where unprivileged users can
mount filesystems (user namespaces with CLONE_NEWUSER+CLONE_NEWNS,
or fuse/udisks2 setups).
Exploitation Impact
Immediate: kernel heap disclosure
The 1–3 bytes read past the block buffer come from whatever the slab/page
allocator placed adjacent to bh->b_data. Depending on allocation layout
this can contain:
- Kernel text/data pointers (defeating KASLR / defeating pointer hardening)
struct buffer_headmetadata of adjacent blocks- Contents of other processes’ cached filesystem data
Chaining potential: local privilege escalation
A KASLR bypass is often the first step in chaining with a separate write primitive to achieve ring-0 code execution. The ext2 xattr code is reachable from unprivileged contexts, making this a reliable info-leak primitive.
Proof-of-Concept (Sketch)
import struct
BLOCK_SIZE = 4096
XATTR_MAGIC = 0xEA020000
HDR_SIZE = 16 # sizeof(ext2_xattr_header)
ENTRY_FIXED = 16 # sizeof(ext2_xattr_entry) without flexible name array
# Place entries whose cumulative EXT2_XATTR_NEXT() lands at end-1.
# EXT2_XATTR_LEN(name_len) = (name_len + 3 + 16) & ~3
# Adjust name_len of the last entry so that:
# HDR_SIZE + sum(EXT2_XATTR_LEN(nlen_i)) == BLOCK_SIZE - 1
# Since EXT2_XATTR_LEN is always 4-aligned, the closest achievable
# offset is BLOCK_SIZE - 4 + 3 = BLOCK_SIZE - 1 is impossible;
# the real closest is BLOCK_SIZE - 4 (k=4 byte read exactly at end -
# adjusted example):
# set name_len such that next == end - 1, end - 2, or end - 3.
# Use name_len values that produce non-4-aligned next pointers.
block = bytearray(BLOCK_SIZE)
# Write header: magic, refcount=1, blocks=1, hash=0, reserved=0
struct.pack_into('<IIII', block, 0, XATTR_MAGIC, 1, 1, 0)
# Craft one entry: name_index=6 (SECURITY), name_len chosen to make
# EXT2_XATTR_NEXT land at block_end - 1.
# EXT2_XATTR_LEN(N) = (N + 19) & ~3
# HDR_SIZE + EXT2_XATTR_LEN(N) = block_end - 1
# 16 + (N+19)&~3 = 4095 => (N+19)&~3 = 4079
# 4079 is not 4-aligned; use N=4060 => EXT2_XATTR_LEN(4060)=4080,
# next=16+4080=4096=end (rejected by >=end check).
# Use N=4057 => EXT2_XATTR_LEN(4057)=(4057+19)&~3=4076&~3=4076,
# next=16+4076=4092, then need additional entries to reach 4095.
# (Full image construction omitted for responsible disclosure.)
# --- snip ---
Fix
Change the bounds check in ext2_xattr_entry_valid from:
/* fs/ext2/xattr.c:156 — VULNERABLE */
if ((char *)next >= end)
return false;
to:
/* Ensure IS_LAST_ENTRY (4-byte read) at 'next' cannot go out of bounds */
if ((char *)next + sizeof(__u32) > end)
return false;
This ensures at least 4 readable bytes exist at next before any
IS_LAST_ENTRY dereference, closing the 1–3 byte OOB window entirely.
References
fs/ext2/xattr.c—ext2_xattr_entry_valid()lines 148–167fs/ext2/xattr.c— entry traversal loops lines 237–251, 323–329, 467–484fs/ext2/xattr_security.c—ext2_xattr_security_get/set,ext2_initxattrsfs/ext2/xattr.h—IS_LAST_ENTRY,EXT2_XATTR_NEXTmacros