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 |