# DF-5777 — splash() calls decoder->splash without the NULL guard init/term get

**File:** `sys/dev/video/fb/splash.c:201-206`
**Severity:** Info (hardening; trigger requires root-loaded decoder module —
no in-tree registrant leaves `.splash` NULL)
**Status:** reproduced live on the QEMU guest (kernel panic, `Stopped at 0:`).

## What the bug is

```c
int
splash(video_adapter_t *adp, int on)
{
	if (splash_decoder != NULL)
		return (*splash_decoder->splash)(adp, on);   /* <-- no NULL check */
	return ENODEV;
}
```

`splash_register()` accepts any `splash_decoder_t` (the `SPLASH_DECODER` /
`SAVER_MODULE` macros let any kld module register one). The two sibling
callbacks are both guarded — `init` at splash.c:83
(`if (*decoder->init && ...)`) and `term` at splash.c:193
(`if (error == 0 && splash_decoder->term)`) — but `splash()` invokes
`->splash` unchecked. A decoder registered with `.splash == NULL` becomes
the active decoder (via `splash_new()`), and the first screen-saver event
(or any caller of the exported `splash()`, e.g. syscons `scsplash_saver`
at syscons.c:2560) transfers control to address 0.

All 12 in-tree registrants (bmp, pcx, 10 syscons savers) populate `.splash`,
so this is defense-in-depth: a third-party or misdeclared root-loaded
decoder module panics the kernel instead of getting `ENODEV`.

## How to reproduce (on the audit QEMU rig)

The rig must have syscons as the kernel console so that `splash_init()`
runs at boot (`splash_adp != NULL` — otherwise `splash_register()` takes
the deferred path and the decoder is never activated):

1. `echo 'console="vidconsole"' > /boot/loader.conf.local && reboot`
   (serial console wins otherwise: sio CN_REMOTE > sc CN_INTERNAL —
   see siocnprobe, sio.c:~3141; sccninit sets sc_console_unit, which gates
   the SC_KERNEL_CONSOLE path through scinit → splash_init, syscons.c:3432).
2. Build: `sh build.sh` (kld module `nullsplash.ko`, Makefile included).
3. `kldload /tmp/nullsplash/nullsplash.ko` — dmesg shows
   `nullsplash: decoder init, adp=0x... (.splash left NULL)` proving the
   immediate-path registration succeeded.
4. `sysctl hw.nullsplash_fire=1` — the module calls `splash(real_adp, TRUE)`,
   the exact call `scsplash_saver()` makes.

## Expected result (baseline / unfixed kernel)

Kernel panics: `Fatal trap 12: page fault while in kernel mode`,
`fault virtual address = 0x0`, **`Stopped at 0:`** — control transferred
to NULL. The ssh session dies; with `debug.debugger_on_panic=1` (default)
the box sits in DDB on the VGA console. See `panic.txt` and the VGA
screendumps `vga_panic.png` / `vga_panic_2x.png` (captured via the QEMU
monitor).

## Fixed kernel

Apply `fix.diff` (one-line guard), rebuild, repeat steps 2–4:
the sysctl returns cleanly, dmesg shows
`nullsplash: returned -- guard present?`, guest stays up. See
`fix_run.log`, `VERDICT.md`.

## Contents

- `nullsplash.c` / `Makefile` — PoC kld module (deliberately NULL `.splash`)
- `build.sh` / `run.sh` — build + host-side driver
- `build.log` — module build (clean under -Werror)
- `run.log` — run transcript (serial console + ssh death)
- `panic.txt` — transcribed panic from the VGA screendump
- `vga_panic.png`, `vga_panic_2x.png`, `vga_trace_2x.png` — QEMU monitor
  screendumps of the DDB screen (the vidconsole kernel prints there)
- `env.txt` — guest uname, console rig notes
- `fix.diff` — the one-line fix (validated on a rebuilt guest kernel)
- `fix_build.log` / `fix_run.log` — patched-kernel build + PoC re-run
- `VERDICT.md`, `manifest.json`, `verdict.json`
