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)
PoC verification
Evidence pack
findings/poc/DF-4782 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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_testablenot 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)
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.
No comments yet.