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

Unchecked M_NOWAIT contigmalloc in dcons_drv_init: NULL buffer passed to dcons_init() -> NULL+0x14 store panic at kldload

Summary

The 16KB shared console buffer is allocated M_NOWAIT and passed to dcons_init unchecked - the first header store writes NULL+0x14 (dcons.c:147) -> Fatal trap 12 during kldload under memory pressure. dg.buf also dereferenced on MOD_UNLOAD. Sink REPRODUCED on guest via identical exported dcons_init(NULL,...) call (fatal trap va=0x14); negative control clean. NULL-check + skip-attach + unload-guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4781 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
dconsnull.c β€” 2.6 KB view raw
Makefile β€” 96 B ↓ download
build.sh β€” 123 B view raw
run.sh β€” 141 B view raw
build.log β€” 940 B view raw
run.log β€” 976 B view raw
panic.txt β€” 2.1 KB view raw
env.txt β€” 359 B view raw
fix.diff β€” 910 B view raw
verdict.json β€” 2.3 KB view raw

PoC DF-4781 β€” unchecked contigmalloc in dcons_drv_init β†’ NULL-deref panic at kldload (REPRODUCED)

What this proves

dcons_drv_init() (sys/dev/misc/dcons/dcons_os.c:462-464) does not check contigmalloc() for NULL before calling dcons_init(dg.buf, dg.size, sc). The very first thing dcons_init() does is write the shared-buffer header:

dcons_init_port():  buf->osize[port] = htonl(osize);    /* dcons.c:147 */

so a failed allocation is an immediate NULL+0x14 store β€” Fatal trap 12 at kldload dcons time.

The trigger is contigmalloc(16KB, M_DEVBUF, M_NOWAIT /* flags=0 */, ...) failing under kernel-memory pressure (it is called with M_NOWAIT, so it can legitimately return NULL).

Reproduce (guest, root)

cd /root/dconsrig && make
kldload dcons                       # baseline: module loads fine
kenv dconsnull.mode=1               # simulate the allocation failure
kldload ./dconsnull.ko              # -> panic (Fatal trap 12, va=0x14)

dconsnull.c calls the same exported dcons_init() entry that dcons_drv_init() calls on the allocation-failure path.

Expected output (serial console)

dconsnull: loaded, mode=1
dconsnull: DF-4781: calling dcons_init(NULL, 16384, sc) -- unchecked contigmalloc sink
Fatal user address access from kernel mode from kldload at ...
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x14
fault code             = supervisor write data, page not present
Stopped at      dcons_init_port+0x83:   movl    %edi,0x14(%rcx)

0x14 is offsetof(struct dcons_buf, osize[0]) β€” exactly the store that runs on the unchecked-NULL path.

Negative control

kenv dconsnull.mode=0 runs the identical dcons_init()+dcons_putc() calls on a valid 16KB buffer and prints dconsnull: NEGCTRL init+putc OK β€” the crash is specific to the NULL.

VERDICT.md
↓ download raw

DF-4781 verdict β€” REPRODUCED (panic primitive; natural trigger = allocation failure)

Reproduced?

Yes β€” the crash sink is reproduced directly on the DragonFly 6.5-DEVELOPMENT guest (kernel #0 Thu Jul 2 06:02:54 UTC 2026). Mode-1 of dconsnull.ko invokes the exact function dcons_drv_init() calls after an unchecked contigmalloc() failure β€” dcons_init(NULL, 16384, sc) β€” and the kernel takes Fatal trap 12 at dcons_init_port+0x83 (movl %edi,0x14(%rcx)), fault address 0x14 = &((struct dcons_buf *)0)->osize[0] (dcons.h:49-64, store at dcons.c:147).

The natural trigger β€” contigmalloc(..., M_DEVBUF, 0 /* M_NOWAIT */, ...) returning NULL during kldload dcons under kernel-memory pressure β€” was not forced on the guest (no fault-injection hook for contigmalloc); the sink itself is proven and the path from the unchecked allocation to the sink is three lines of straight-line code (dcons_os.c:462-464 β†’ dcons.c:147).

Root cause chain:

  1. dcons_os.c:462 dg.buf = contigmalloc(dg.size, M_DEVBUF, 0, 0x10000, 0xffffffff, PAGE_SIZE, 0ul) β€” M_NOWAIT, unchecked.
  2. dcons_os.c:464 dcons_init(dg.buf, dg.size, sc) β€” no NULL guard.
  3. dcons.c:147 buf->osize[port] = htonl(osize) β€” NULL+0x14 store β†’ fatal trap.

Also on the same failure path: if dcons_init were survivable, dcons_attach() still runs (dcons_os.c:559 ignores dcons_drv_init's return), and MOD_UNLOAD would later execute dg.buf->magic = 0 / contigfree(dg.buf, ...) (dcons_os.c:584-586) on the same NULL.

Trigger privilege: root (kldload). Impact: kernel panic (DoS), no corruption beyond the faulting store. Severity Low.

Evidence

  • panic.txt β€” full serial-console panic (Fatal trap 12, va 0x14, dcons_init_port+0x83).
  • dconsnull.c mode 0 vs mode 1 β€” negative control vs sink.

Fix

Check the allocation and refuse to attach (see fix.diff): bail out with drv_init = -1 and a dg.buf == NULL guard on the unload path (dg.buf->magic = 0 / contigfree), and make MOD_LOAD skip dcons_attach() when init failed.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not validated: baseline is a NULL-deref panic with no corruption primitive; fix is a straightforward guard and was not kernel-rebuilt on the guest

fix.diff (three hunks: NULL-check + attach guard + unload guard)
↓ fix.diffper-fix-DF-4781

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (Fatal trap 12, fault va=0x14, dcons_init_port+0x83 store); run.log (mode-0 NEGCTRL OK vs mode-1 panic); dconsnull.c (sink = exported dcons_init, same call dcons_os.c:464 makes)

PoC changes

Seed sketch did not exist; harness written from scratch: kenv-selectable modes (0=negative control, 1=dcons_init(NULL), 2=dcons_putc(NULL)); needed local copy of dcons.h and #include for DEV_MODULE.

Verified recommended fix

Check contigmalloc for NULL and fail dcons_drv_init; skip dcons_attach on failure; NULL-guard dg.buf on MOD_UNLOAD.

Verdict

Unchecked M_NOWAIT contigmalloc in dcons_drv_init (dcons_os.c:462-464) feeds NULL straight into dcons_init(), whose first header store (buf->osize[0], dcons.c:147) faults: Fatal trap 12, va=0x14, Stopped at dcons_init_port+0x83 (movl %edi,0x14(%rcx)) β€” reproduced via a KLD invoking the identical exported sink (mode 1) with a negative control (mode 0) proving the crash is NULL-specific. Natural trigger is kldload dcons under kernel-memory pressure (allocation failure not fault-injected on the guest); root-only, panic/DoS, no further corruption.