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

Kernel stack overflow via unchecked user-controlled VLA in ngdread/ngdwrite

Summary

ngdread (:509): char buffer[uio->uio_resid+1] VLA on kernel stack sized by user-controlled size_t. ngdwrite (:562): char buffer[uio->uio_resid] same. Kernel stack 2-4 pages fixed. read(fd,buf,1<<20) or write(fd,buf,1<<20) reliably overflows stack -> panic or RIP control. No upper-bound check before VLA materialized. len=uio_resid :563 truncates size_t to int negative wraps to m_devget early-out NULL. Trigger: any process with open /dev/ngdN (root default 0600). write(fd,b,0) also triggers via DF-0666.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0661 Β· 18 files
FileTypeDescriptionSize
trigger.c trigger-source Minimal trigger: opens /dev/ngdN, write(fd, buf, LARGE) to blow ngdwrite VLA stack 2.7 KB view raw
ng_device_ported.c module-source Faithful port of ng_device.c to dev_ops API; VLA bug preserved verbatim in ngdread/ngdwrite 12.1 KB view raw
build.sh build-script Reproducible build script for ng_device.ko + trigger 1.1 KB view raw
run.sh run-script Reproducible run script: load modules, create hook, trigger 1.2 KB view raw
build_module.sh build-script Alternative module build script 605 B view raw
setup_and_trigger.sh run-script Combined setup + trigger script 1.4 KB view raw
build.log build-log Module build output (gcc 8.3, KERNBUILDDIR warm obj) 1.3 KB view raw
run.log run-log Baseline (unpatched) run documentation: DOUBLE FAULT panic 1.6 KB view raw
fix_build.log build-log Patched module build output 1.1 KB view raw
fix_run.log run-log Patched module test output: EFBIG, no panic, guest alive 1.1 KB view raw
panic.txt panic-signature DOUBLE FAULT panic signature from serial console (boot.log) 507 B view raw
panic_run2.txt panic-signature Second reproduction DOUBLE FAULT (same offset 0x5b8) 507 B view raw
env.txt environment Guest environment: uname, cc version, kldstat, /dev/ngd0 perms 471 B view raw
fix.diff suggested-fix Git-apply-able fix: bound uio_resid to NGD_QUEUE_SIZE before VLA in ngdread/ngdwrite 746 B view raw
VERDICT.md verdict Full analysis: mechanism, primitive characterization, fix validation 5.3 KB ↓ raw
README.md readme How to reproduce and file listing 2.9 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme How to reproduce and file listing
↓ download raw

DF-0661 β€” Kernel stack overflow via unchecked VLA in ngdread/ngdwrite

Finding

sys/netgraph/ng_device.c declares VLAs sized by user-controlled uio_resid (size_t) on the kernel stack with no upper-bound check:

  • ngdread (line 509): char buffer[uio->uio_resid+1];
  • ngdwrite (line 562): char buffer[uio->uio_resid];

A write(fd, buf, 1<<20) overflows the ~16 KB kernel stack β†’ DOUBLE FAULT panic. /dev/ngdN is mode 0600 (root-only).

How to reproduce

Prerequisites

  • DragonFlyBSD 6.5-DEVELOPMENT guest (#0 unpatched kernel)
  • ng_device is NOT shipped as a prebuilt module and its source uses the obsolete cdevsw API. A faithful port to dev_ops is provided as ng_device_ported.c (only the device-registration boilerplate changes; the VLA bug in ngdread/ngdwrite is preserved verbatim).

Build (as root in guest)

# Push ng_device_ported.c, trigger.c into /root/ng_device_build/
cd /root/ng_device_build
cp /usr/src/sys/netgraph/ng_device.h .
printf "KMOD=\tng_device\nSRCS=\tng_device.c\n.include <bsd.kmod.mk>\n" > Makefile
make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
cc -O2 -o trigger trigger.c

Run (as root in guest)

kldload netgraph
kldload ng_echo
kldload /root/ng_device_build/ng_device.ko
ngctl mkpeer device: echo lower downstream    # spawns /dev/ngd0
/root/ng_device_build/trigger /dev/ngd0 1048576   # 1 MiB write β†’ stack overflow

Expected behavior

  • Vulnerable kernel (#0): write() enters ngdwrite(), materializes a 1 MiB VLA on the ~16 KB kernel stack β†’ RSP lands in unmapped memory β†’ DOUBLE FAULT panic. The ssh session dies; the panic is captured in the serial console log.

  • Patched module (fix.diff applied): write() returns -1 with errno=EFBIG (27). No panic. Guest stays alive. Small writes (≀ 10240 bytes) still work normally.

Fix

Bound uio_resid to NGD_QUEUE_SIZE (10240) before the VLA declaration in both functions. See fix.diff.

Files in this evidence pack

File Description
trigger.c Minimal trigger: opens /dev/ngdN, write(fd, buf, N)
ng_device_ported.c Faithful port of ng_device.c to dev_ops API (VLA bug preserved)
build_module.sh Script to build ng_device.ko + trigger in the guest
setup_and_trigger.sh Script to load modules, create hook, trigger
build.sh Reproducible build script
run.sh Reproducible run script
build.log Module build output
run.log Baseline (unpatched) run documentation
fix_build.log Patched module build output
fix_run.log Patched module test output (EFBIG, no panic)
panic.txt DOUBLE FAULT panic signature from serial console
env.txt Guest environment (uname, cc version, module list)
fix.diff Git-apply-able fix for sys/netgraph/ng_device.c
VERDICT.md Full analysis and verdict
manifest.json Machine-readable catalog
VERDICT.md verdict Full analysis: mechanism, primitive characterization, fix validation
↓ download raw

DF-0661 β€” Kernel stack overflow via unchecked user-controlled VLA in ngdread/ngdwrite

Verdict: REPRODUCED (deterministic kernel panic β€” DOUBLE FAULT stack overflow)

Bug summary

sys/netgraph/ng_device.c declares Variable Length Arrays (VLAs) sized by the user-controlled uio->uio_resid (a size_t, sys/sys/_uio.h:69) directly on the kernel stack, with no upper-bound check:

  • ngdread (line 509): char buffer[uio->uio_resid+1];
  • ngdwrite (line 562): char buffer[uio->uio_resid];

The kernel stack is 2–4 fixed pages (~16 KB on this build). A write(fd, buf, 1<<20) (1 MiB) causes ngdwrite() to materialize a 1 MiB VLA on the stack, moving RSP far below the stack into unmapped memory. Any subsequent stack access triggers a double fault β†’ panic: double fault.

Reproduction

Module build (required: ng_device.ko is not shipped)

ng_device is not in the stock SUBDIR list of sys/netgraph/Makefile, and the original source uses the long-removed struct cdevsw API (modern DragonFly uses struct dev_ops). A faithful port was created (ng_device_ported.c) that adapts only the device-registration boilerplate to dev_ops, while preserving the VLA bug verbatim in ngdread/ngdwrite:

ngdread:  char buffer[uio->uio_resid+1];   (orig sys/netgraph/ng_device.c:509)
ngdwrite: char buffer[uio->uio_resid];      (orig sys/netgraph/ng_device.c:562)

Build (as root in guest):

cd /root/ng_device_build
make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC   # builds ng_device.ko
cc -O2 -o trigger trigger.c                               # builds trigger

Setup + trigger

kldload netgraph
kldload ng_echo                      # peer node for hook creation
kldload /root/ng_device_build/ng_device.ko
ngctl mkpeer device: echo lower downstream   # creates /dev/ngd0 via make_dev()
/root/ng_device_build/trigger /dev/ngd0 1048576   # write 1 MiB β†’ stack overflow

Observed result (unpatched, #0 kernel)

DOUBLE FAULT
Fatal double fault
rip = 0xffffffff8263e5b8       # inside ng_device.ko (base 0xffffffff8263e000)
rsp = 0xfffff801185d1758
panic: double fault
dblfault_handler() at dblfault_handler+0x10c

Reproduced twice (two independent vm.sh reset with-src cycles), both deterministic DOUBLE FAULT at module offset 0x5b8.

Why the write path is the reliable trigger

The read path (ngdread) also has the VLA, but GCC at -O2 can optimize it away when connection->loc == 0 (no data queued β†’ buffer never used β†’ VLA not materialized). The write path (ngdwrite) unconditionally uses buffer via uiomove((caddr_t)buffer, len, uio) when len > 0, so the VLA is always materialized β†’ deterministic overflow.

Characterized primitive

  • Class: CWE-787 Out-of-bounds write (kernel stack overflow via VLA)
  • Trigger: write(fd, buf, N) where N > kernel_stack_size (~16 KB)
  • Primitive: Stack-pointer decrement of N bytes below the current frame
  • Effect on this guest: DOUBLE FAULT (stack pointer lands in unmapped memory past the stack guard page β†’ unrecoverable). This is a deterministic DoS/panic.
  • RIP control feasibility: To overwrite the saved return address without hitting the guard page, the attacker would need to tune N so the VLA base lands exactly between the current stack frame's saved RIP and the guard page. This requires knowledge of the exact stack layout and the gap between the frame and the guard page β€” non-trivial but theoretically possible. On this guest (SMEP/SMAP/KASLR OFF), successful RIP control would jump straight to attacker shellcode. The deterministic panic is the honest, proven impact.
  • Privilege requirement: Root only β€” /dev/ngdN is created with mode 0600 (make_dev(..., 0600, "ngd%d", ...) at ng_device.c:287). This is a rootβ†’kernel vulnerability (relevant for jail escape, sandbox escape, or setuid-program-confused-deputy scenarios).

Fix

Bound uio->uio_resid to NGD_QUEUE_SIZE (10240, the existing queue size constant) before the VLA declaration in both ngdread and ngdwrite, returning EFBIG if exceeded. Reads/writes larger than the internal queue make no functional sense anyway.

See fix.diff for the git-apply-able unified diff against sys/netgraph/ng_device.c.

Fix validation

Validated by building the patched module (same fix logic applied to the ported source) and hot-swapping it:

Test Unpatched (#0) Patched module
write(fd, buf, 100) works (100) works (100)
write(fd, buf, 1<<20) DOUBLE FAULT panic returns -1/EFBIG
write(fd, buf, 1<<18) (would panic) returns -1/EFBIG
Guest alive after test? down (panic) up

The fix is deterministic: oversized writes are rejected with EFBIG on every attempt (3Γ— verified), normal-sized writes continue to work, and the guest never panics.

Kernel references

Fix verification

fixed

VALIDATED: baseline double fault; patched EFBIG.

BEFORE: panic. AFTER: EFBIG.
↓ fix.diff6.5-DEVELOPMENT #0 module swap

Confirmed kernel references

β€”

Detail

Exploit chain

none -- dead code + root-only

Evidence (decisive lines)

β€”

Verdict

REPRODUCED. ngdwrite VLA char buffer[uio_resid] -> 1MiB write -> stack overflow -> double fault panic. Ported module (ng_device dead code, cdevsw removed). Root-only 0600.