DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2731

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2731 · 12 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Stock 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Aug 31 11:01:57 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.