β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2009

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:234 kmalloc(uio->uio_resid + 1, M_DEVBUF, M_WAITOK) β†’ kmalloc(0), which the DragonFly slab allocator special-cases at kern_slaballoc.c:888-890 to return ZERO_LENGTH_PTR, defined as ((void *)-8) at kern_slaballoc.c:193.
  • Then led.c:235 s[uio->uio_resid] = '\0' evaluates to s[-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:215 dev_dwrite() forwards the uio verbatim to d_write with no clamping,
  • dofilewrite (sys_generic.c:482-529) does no resid<0 check, and
  • sys_write (sys_generic.c:336-337) sets error = EINVAL for an oversized nbyte but omits the corresponding return, so the EINVAL is dead-coded and overwritten at sys_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.

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2009 Β· 5 files
FileTypeDescriptionSize
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
VERDICT.md verdict Source verification narrative
↓ download 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_testable
baseline reproduced→ patch + rebuild →patched clean

not_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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

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).