CVE-Candidate: FIONREAD loff_t-to-int Silent Truncation — Userspace Heap Buffer Overflow

File: fs/ioctl.c
Line: 553
Severity: High
Privileges required: None (unprivileged)


Vulnerable Code

// fs/ioctl.c:549-554
case FIONREAD:
    if (!S_ISREG(inode->i_mode) || IS_ANON_FILE(inode))
        return vfs_ioctl(filp, cmd, arg);

    return put_user(i_size_read(inode) - filp->f_pos,
            (int __user *)argp);

Root Cause

i_size_read(inode) returns loff_t (signed 64-bit). filp->f_pos is also loff_t. The subtraction result is therefore loff_t (64-bit).

put_user writes to int __user * — only 32 bits. The upper 32 bits of the 64-bit result are silently discarded with no range validation or error before writing to userspace.


Attack Scenarios

Scenario 1 — File ≥ 2 GiB (no special seek needed)

A regular file of size 3 GiB = 0xC000_0000 bytes, f_pos = 0:

i_size - f_pos = 0x0000_0000_C000_0000  (loff_t, positive)
put_user to int* → truncated to 0xC000_0000 as signed int = -1,073,741,824

FIONREAD returns −1 073 741 824 to the caller. Userspace programs that cast this to size_t (unsigned) get 0xFFFFFFFFC0000000 — a near-SIZE_MAX allocation request, leading to OOM or a near-zero-length heap allocation used as a read buffer.

Scenario 2 — Seek past EOF

Any unprivileged user can lseek(fd, large_offset, SEEK_SET) on any readable regular file without restriction. With f_pos > i_size:

i_size - f_pos  < 0  (loff_t, negative)

The negative 64-bit value is truncated to a 32-bit int. Depending on the magnitude:

f_pos − i_size FIONREAD result (int)
1 −1 (mimics EAGAIN/error)
0x1_0000_0001 −1 (lower 32 bits all-ones)
0x1_0000_0000 0 (false “no data” signal)

Programs that treat FIONREAD == −1 as “retry later” can be manipulated into an infinite busy-loop or skip a read entirely.

Scenario 3 — Heap buffer overflow via read-size confusion

A typical pattern in userspace:

int avail;
ioctl(fd, FIONREAD, &avail);
if (avail > 0) {
    char *buf = malloc(avail);   // avail is truncated/negative
    read(fd, buf, avail);        // reads actual data into undersized buffer
}

When avail is negative, malloc((size_t)avail) allocates SIZE_MAX - |avail| + 1 bytes (wraps unsigned), which fails or returns a tiny chunk. The subsequent read() with (size_t)avail as a count then writes gigabytes into that chunk — heap buffer overflow.


Secondary Vulnerability — ioctl_preallocate Signed Integer Overflow

Lines: 280, 283

case SEEK_CUR:
    sr.l_start += filp->f_pos;   // signed 64-bit overflow, UB in C
    break;
case SEEK_END:
    sr.l_start += i_size_read(inode);  // same
    break;

sr.l_start is fully user-controlled (__s64 from struct space_resv). There is no overflow check before adding filp->f_pos or i_size_read(). Setting sr.l_start = LLONG_MAX and using SEEK_CUR/SEEK_END causes signed 64-bit wraparound — undefined behavior in C, practically wrapping to a large negative on x86-64. While vfs_fallocate rejects negative offsets, this UB can be miscompiled or exploited via compiler optimizations.


Fix

FIONREAD

Add a bounds check before writing to userspace:

case FIONREAD: {
    loff_t avail = i_size_read(inode) - filp->f_pos;
    if (avail < 0)
        avail = 0;
    if (avail > INT_MAX)
        avail = INT_MAX;
    return put_user((int)avail, (int __user *)argp);
}

ioctl_preallocate

Use checked arithmetic:

case SEEK_CUR:
    if (check_add_overflow(sr.l_start, filp->f_pos, &sr.l_start))
        return -EOVERFLOW;
    break;
case SEEK_END:
    if (check_add_overflow(sr.l_start, i_size_read(inode), &sr.l_start))
        return -EOVERFLOW;
    break;

Summary

Property Value
Location fs/ioctl.c:553
Type CWE-197 Numeric Truncation / CWE-190 Integer Overflow
Trigger FIONREAD ioctl on any regular file ≥ 2 GiB, or after lseek past EOF
Privileges None — any process that can open a file
Impact Wrong byte-count returned to userspace → downstream heap buffer overflow