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.cext2_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

  1. Mount (or loop-mount) a crafted ext2 filesystem image.
  2. The malicious xattr block must have h_magic = EXT2_XATTR_MAGIC and h_blocks = 1 (passes ext2_xattr_header_valid).
  3. The last entry is positioned so that EXT2_XATTR_NEXT(last_entry) = block_end - k for k ∈ {1,2,3}.
  4. Trigger any xattr read (getxattr, listxattr) or write (setxattr) on a file in the mounted filesystem, which calls the security handler through ext2_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_head metadata 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.cext2_xattr_entry_valid() lines 148–167
  • fs/ext2/xattr.c — entry traversal loops lines 237–251, 323–329, 467–484
  • fs/ext2/xattr_security.cext2_xattr_security_get/set, ext2_initxattrs
  • fs/ext2/xattr.hIS_LAST_ENTRY, EXT2_XATTR_NEXT macros