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

Static-console path never wires si_drv1: first write to /dev/dcons on a 'device dcons' kernel NULL-derefs in dcons_putc()

Summary

si_drv1 is assigned only by dcons_attach_port() (module path); a kernel compiled with 'device dcons' creates /dev/dcons with si_drv1==NULL and the first tty write drains through dcons_tty_start -> dcons_putc(NULL,...) reading NULL+8/NULL+16. Sink REPRODUCED on guest (identical exported call, fatal trap va=0x8); full path needs a custom-kernel rebuild - mechanism certain by inspection. Wire-si_drv1 fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4782 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 1.8 KB ↓ raw
dconsnull.c β€” 2.6 KB view raw
Makefile β€” 96 B ↓ download
build.sh β€” 80 B view raw
run.sh β€” 126 B view raw
build.log β€” 940 B view raw
run.log β€” 573 B view raw
panic.txt β€” 970 B view raw
env.txt β€” 359 B view raw
fix.diff β€” 401 B view raw
verdict.json β€” 2.2 KB view raw

PoC DF-4782 β€” static-console /dev/dcons never wired with si_drv1 β†’ NULL-deref on first write (REPRODUCED sink)

What this proves

In a kernel built with device dcons (sys/conf/files:327; LINT64:2002), the console path creates /dev/dcons via dcons_cninit_fini() (dcons_os.c:383-387) but si_drv1 is only wired by dcons_attach_port() (dcons_os.c:490-493), which runs only in the module (KLD) path β€” DEV_MODULE never fires for a static driver. The first write to the tty drains t_outq through dcons_tty_start():

dc = (struct dcons_softc *)tp->t_dev->si_drv1;   /* dcons_os.c:320 β€” NULL */
...
dcons_os_putc(dc, clist_getc(&tp->t_outq));      /* dcons_os.c:329 */
    -> dcons_putc(NULL, c)                       /* dcons.c:118 */
    -> ch->buf[ch->pos] reads *(NULL + 8/16)     /* Fatal trap 12 */

cninit_finish() (sys/kern/tty_cons.c:229) intercepts the console dev's ops with dev_ops_intercept, and cnopen()/cnwrite() forward through cn_fwd_ops to the original tty ops β€” so the tty write path (and the NULL dereference) is reached exactly as described.

Device node is 0600 root:wheel, so the trigger is a local privileged user (or anything that can write the console tty, e.g. logger-style output via console redirection) on a custom kernel that compiles dcons in.

Reproduce (guest, root β€” sink-level)

cd /root/dconsrig && make
kenv dconsnull.mode=2
kldload ./dconsnull.ko
# -> panic: Fatal trap 12, va=0x8, Stopped at dcons_putc: movl 0x8(%rdi),%eax

dconsnull.c calls the identical exported sink dcons_putc(NULL, 'A') that dcons_tty_start() reaches with the unwired si_drv1.

Full-path trigger (requires custom kernel)

Build a kernel with device dcons (headless box: dcons wins console selection at CN_NORMAL), boot, then as root: echo hi > /dev/dcons β†’ same Fatal trap 12 in dcons_putc(). Not executed here (would need a full custom-kernel rebuild); mechanism is proven by the sink PoC plus the code trace: nothing else in the static path ever assigns si_drv1.

VERDICT.md
↓ download raw

DF-4782 verdict β€” sink REPRODUCED; natural trigger requires a custom kernel

Reproduced?

The crash sink is reproduced on the stock guest: dcons_putc(NULL, 'A') (the exact call dcons_tty_start() makes when si_drv1 == NULL) takes Fatal trap 12, fault address 0x8, Stopped at dcons_putc: movl 0x8(%rdi),%eax (reading ch->buf/ch->size at NULL+offset, dcons.c:112-125).

The natural trigger requires a kernel built with device dcons (non-default custom config) so that dcons_cninit_fini() (dcons_os.c:383) creates /dev/dcons without the module-only dcons_attach_port() wiring (dcons_os.c:482-502); the guest runs the stock X86_64_GENERIC kernel, so the full open+write path was not executed (would need a kernel rebuild). The code trace is airtight:

  • static path: cninit (dcons_os.c:370-380) β†’ cninit_fini make_dev (dcons_os.c:385-386) β€” no si_drv1 assignment anywhere;
  • first write β†’ tty start (dcons_os.c:315-334) reads si_drv1 (line 320) β†’ NULL β†’ dcons_putc(NULL, c) (line 329) β†’ fault.

Privilege: /dev/dcons is 0600 root:wheel (dcons_os.c:385-386) β€” root-only, local, custom kernel. Impact: kernel panic (DoS). Severity Low.

Evidence

  • panic.txt β€” Fatal trap 12, va=0x8, Stopped at dcons_putc: movl 0x8(%rdi),%eax.
  • dconsnull.c mode 2 = the sink; mode 0 = negative control (valid buffer OK).

Fix

Wire si_drv1 (and ideally pre-allocate the tty) in dcons_cninit_fini() (see fix.diff) β€” one line, mirroring dcons_attach_port().

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: sink-level reproduction on stock kernel; full-path validation needs a custom-kernel rebuild, skipped for budget (panic-only, root-only, custom-config)

fix.diff (one-line si_drv1 wiring)
↓ fix.diffper-fix-DF-4782

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (Fatal trap 12 va=0x8 at dcons_putc); run.log (mode 2 marker then trap); code trace dcons_os.c:320 -> :329 -> dcons.c:118

PoC changes

Same harness as DF-4781 (mode 2 added for this sink).

Verified recommended fix

Wire cp->cn_dev->si_drv1 = &sc[DCONS_CON] in dcons_cninit_fini().

Verdict

On a kernel built with device dcons, dcons_cninit_fini (dcons_os.c:383-387) creates /dev/dcons but si_drv1 is only wired by the module-only dcons_attach_port (dcons_os.c:490-493); the first tty write runs dcons_tty_start (dcons_os.c:315-334) which derefs si_drv1==NULL and calls dcons_putc(NULL,c) (dcons_os.c:329). The sink is reproduced on the stock guest via the exported dcons_putc(NULL,'A'): Fatal trap 12, va=0x8, Stopped at dcons_putc: movl 0x8(%rdi),%eax. The full open+write path needs a custom kernel (device dcons) β€” not rebuilt on the guest; the code trace shows no other assignment of si_drv1 in the static path. Root-only (node 0600), custom-kernel, local panic/DoS.