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)
PoC verification
Evidence pack
findings/poc/DF-0661 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 (
#0unpatched kernel) ng_deviceis NOT shipped as a prebuilt module and its source uses the obsoletecdevswAPI. A faithful port todev_opsis provided asng_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()entersngdwrite(), 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-1witherrno=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 |
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)whereN > kernel_stack_size(~16 KB) - Primitive: Stack-pointer decrement of
Nbytes 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
Nso 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/ngdNis created with mode 0600 (make_dev(..., 0600, "ngd%d", ...)atng_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
sys/netgraph/ng_device.c:509βchar buffer[uio->uio_resid+1];(ngdread VLA)sys/netgraph/ng_device.c:562βchar buffer[uio->uio_resid];(ngdwrite VLA)sys/netgraph/ng_device.c:563βint len = uio->uio_resid;(size_tβint truncation)sys/netgraph/ng_device.c:287βmake_dev(..., 0600, ...)(root-only device)sys/sys/_uio.h:69βsize_t uio_resid;(user-controlled size_t)
Fix verification
fixedVALIDATED: baseline double fault; patched EFBIG.
BEFORE: panic. AFTER: EFBIG.
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.
No comments yet.