sys_extpread/sys_extpwrite/sys_extpreadv/sys_extpwritev accept negative offsets — unprivileged kernel panic on ext2 (INVARIANTS), POSIX EINVAL missing
| Field | Value |
|---|---|
| ID | DF-2731 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:N/I:N/A:C |
| CWE | CWE-20 / CWE-617 |
| File | sys/kern/sys_generic.c |
| Lines | 153-180, 219-246, 360-386, 423-450 (ext2 sinks :1842, :1980) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The positioned-I/O syscalls only special-case offset==(off_t)-1 ("use
file position"); every other negative offset is stored raw into
auio.uio_offset and reaches VOP_READ/VOP_WRITE unvalidated. POSIX
pread/pwrite require EINVAL for negative offsets. On ext2 the only
guard is KASSERT(uio->uio_offset >= 0) — a kernel panic on
INVARIANTS kernels (X86_64_GENERIC as shipped); on RELEASE builds the
negative offset enters ext2 block math. The documented O_FOFFSET flag
plus offset=-1 defeats the file-position convention the same way.
hammer2 has only incidental defenses (EINVAL/EFAULT/EFBIG from cluster
internals) proving the syscall layer performs no validation.
Threat model & preconditions
Any unprivileged local user who can open a file on a mounted ext2 filesystem (mount is an admin action; the trigger is one syscall on a world-readable file) panics an INVARIANTS kernel. Non-INVARIANTS builds take the negative offset into filesystem block math (ext2 lbn/bmap, hammer2 phantom lbase writes). Impact ceiling is local DoS (buffer-cache accesses stayed in-bounds in testing).
Proof of concept
VERIFIED twice as uid 1001 on stock kernel #0
(findings/poc/DF-2731/): panic: ext2_read: uio->uio_offset < 0 with
trace sys_extpread→kern_preadv→vn_read→vop_read→ext2_read; the second
trigger uses the documented API form extpread(fd,buf,n,O_FOFFSET,-1).
fix.diff validated by full nativekernel rebuild + ext2fs.ko rebuild
(kernel #1): both panic paths return EINVAL, guest stays up, O_FAPPEND
extpwrite unaffected.
Recommended fix
Validate offsets at the syscall layer (if (uap->offset < (off_t)-1)
return(EINVAL); at all four sites) and make ext2's guards runtime
errors (KASSERT → if (uio->uio_offset < 0) return (EINVAL); at both
sites). Validated diff in findings/poc/DF-2731/fix.diff.
Timeline
- 2026-08-30 Discovered during pass-2 audit of sys_generic.c (GLM 5.3); unpriv panic reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2731 · 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| negoffset.c | — | 3.4 KB | view raw | |
| ext2trig.c | — | 1.7 KB | view raw | |
| ext2trig2.c | — | 979 B | view raw | |
| fix.diff | — | 1.8 KB | view raw | |
| panic.txt | — | 897 B | view raw | |
| panic2.txt | — | 898 B | view raw | |
| run.baseline_hammer2.log | — | 692 B | view raw | |
| run.patched_kernel.log | — | 912 B | view raw | |
| run.patched_kernel.2.log | — | 1.1 KB | view raw | |
| build.log | — | 742 B | view raw | |
| env.txt | — | 150 B | view raw | |
| VERDICT.md | — | 2.7 KB | ↓ raw |
DF-2731 VERDICT
Status: reproduced (impact: panic — unprivileged kernel DoS, config ext2 mount). Fix: validated (fixed).
Chain (baseline, stock kernel #0, INVARIANTS X86_64_GENERIC)
pread(fd, buf, 8, (off_t)-5) from uid 1001 on a world-readable file on a
root-mounted ext2 filesystem:
sys_extpread() sys/kern/sys_generic.c:153 — no offset<0 validation auio.uio_offset = uap->offset (-5) :168 flags |= O_FOFFSET (offset != -1) :175-176 kern_preadv() sys/kern/sys_generic.c:252 — passes uio through vn_read() sys/kern/vfs_vnops.c:729 — O_FOFFSET set: keeps uio_offset=-1 vop_read -> ext2_read() sys/vfs/ext2fs/ext2_vnops.c:1842 KASSERT(uio->uio_offset >= 0) -> panic
Console proof: panic.txt (libc pread form) and panic2.txt
(documented-API form extpread(fd, buf, n, O_FOFFSET, -1) — the O_FOFFSET
flag defeats the -1 file-position convention at sys_generic.c:175 because
the != -1 test fails, but the flag is honored at :174/:238/:381/:442).
Both panics observed from an unprivileged account; guest wedged in DDB
(vm.sh status ⇒ down) — two independent boots.
Why the syscall layer is the defect locus
The offset reaches the VFS raw on every filesystem; only incidental filesystem defenses stopped it on hammer2 (EINVAL/EFAULT/EFBIG returned from cluster/chain internals — see run.baseline_hammer2.log), and ext2's only guard is a debug assert. POSIX (pread(2)): offset<0 ⇒ EINVAL.
Fix validation A/B
- Baseline stock kernel #0: panic (both forms). Guest down.
- Patched kernel #1 (
fix.diff: 4×offset-EINVAL in sys_generic.c + KASSERT→EINVAL in ext2_vnops.c read+write; ext2fs.ko rebuilt): - ext2: libc
pread(-5)→ EINVAL; raw(flags=0, offset=-5)→ EINVAL; raw(O_FOFFSET, offset=-1)→ EINVAL (ext2 hunk catches the residual documented-API path). Guest stayed up through all legs (run.patched_kernel.log, run.patched_kernel.2.log). - hammer2: every negative-offset leg now EINVAL at the syscall layer.
- No documented-API regression:
extpwrite(flags=O_FAPPEND, offset=-1)still appends at EOF (ret=7, size 20→27). - Guest reset to stock (
vm.sh reset with-src) after validation.
Exploitability ceiling
Panic/DoS, not memory corruption: on this INVARIANTS kernel the assert fires
before any block math. On RELEASE kernels the negative offset flows into
ext2's lblkno/bread path and hammer2's hammer2_write_file (negative
uio_offset at hammer2_vnops.c:1046, new_eof = offset + resid at :1033) —
all buffer-cache accesses remained in-bounds in testing; no OOB write
primitive was demonstrated, so impact is recorded as panic/dos.
Fix verification
fixedStock kernel #0: both trigger forms panic (panic.txt, panic2.txt). Patched kernel #1 + rebuilt ext2fs.ko: identical triggers return EINVAL, guest remains up; O_FAPPEND documented behavior unaffected. Guest reset to stock snapshot after validation.
['run.patched_kernel.log', 'run.patched_kernel.2.log', 'build.log']
Confirmed kernel references
Detail
Exploit chain
unpriv user -> open world-readable file on admin-mounted ext2 -> pread(fd,buf,8,-5) -> sys_extpread passes uio_offset=-5 -> vn_read (O_FOFFSET) -> ext2_read KASSERT -> kernel panic / DDB (local DoS). No memory-corruption primitive found beyond the panic (RELEASE ext2/hammer2 block math stayed in-bounds in testing).
Evidence (decisive lines)
['panic.txt (stock panic #1, full trace sys_extpread->ext2_read)', 'panic2.txt (documented-API O_FOFFSET+(-1) panic #2)', 'run.baseline_hammer2.log (syscall layer accepts -5/INT64_MIN pread/pwrite)', 'run.patched_kernel.2.log (all legs EINVAL after fix.diff, guest up)', 'fix.diff']
PoC changes
Seed rewritten entirely: corrected kernel arg order for extpread/extpwrite is (fd,buf,nbyte,FLAGS,OFFSET) per sysproto.h:488-494 (initial draft had them swapped); added ext2 image built on Linux host (DF has no newfs_ext2) + vnconfig/mount_ext2fs root setup; added second trigger for the documented-API O_FOFFSET+(-1) residual path discovered during verification.
Verified recommended fix
Reject offset < (off_t)-1 with EINVAL in sys_extpread/sys_extpwrite/sys_extpreadv/sys_extpwritev; convert ext2_read/ext2_write offset KASSERTs to EINVAL returns.
Verdict
Unprivileged pread/pwrite at negative offsets (and the documented O_FOFFSET+(-1) form) pass sys_extpread/sys_extpwrite/sys_extpreadv/sys_extpwritev unvalidated; on ext2 (INVARIANTS kernels) ext2_read's KASSERT panics the kernel - proven twice from uid 1001 on stock kernel #0. fix.diff (syscall-layer EINVAL for offset < -1 in all four handlers + ext2 KASSERT->EINVAL) validated on a rebuilt kernel: all panic paths return EINVAL, guest stays up, documented O_FAPPEND behavior preserved.
No comments yet.