# DF-5777 — VERDICT

**Finding:** `splash()` invokes `splash_decoder->splash` without a NULL
guard (`sys/dev/video/fb/splash.c:204-205`), although the sibling callbacks
are guarded (`init`: splash.c:83; `term`: splash.c:193).

**Verdict: REPRODUCED (panic), then fix validated (fixed).**
Severity stays **Info / hardening**: the only registrants of
`splash_decoder_t` are kernel modules (root kldload / loader preloaded),
and all 12 in-tree registrants populate `.splash`; there is no unprivileged
or remote reach into this registry. The bug turns a misdeclared third-party
decoder module into an instant kernel panic instead of an `ENODEV`.

## How it was reproduced (baseline, unpatched kernel)

1. Rig prep: the guest boots with `console="comconsole"` (serial console
   wins consdev selection: sio `CN_REMOTE` (sio.c siocnprobe:
   `cn_pri = (COM_FORCECONSOLE(flags) || (boothowto & RB_SERIAL)) ?
   CN_REMOTE : CN_NORMAL`) > syscons `CN_INTERNAL` (unconditional in
   sc_get_cons_priority, syscons_nexus.c:195). With serial as console,
   `sccninit` never runs, `sc_console_unit` stays -1, `scinit` runs without
   SC_KERNEL_CONSOLE, so `splash_init()` (gated `flags & SC_KERNEL_CONSOLE`
   at syscons.c:3431) never runs and `splash_adp == NULL` — first kldload
   attempt proved this: no init kprintf, deferred path taken.
2. `echo 'console="vidconsole"' > /boot/loader.conf.local && reboot` —
   syscons becomes the kernel console (sc0 attaches with flags 0x300 =
   SC_EFI_FB|SC_KERNEL_CONSOLE), `splash_init()` runs at boot.
3. Built the PoC kld (`nullsplash.ko`): a decoder with `.splash == NULL`
   and `.data_type == NULL` (so `splash_find_data()` returns 0 immediately,
   splash.c:59-60) and an `init` that returns 0 and stashes the real
   `video_adapter_t`. Clean build under `-Werror`.
4. `kldload` — dmesg: `nullsplash: decoder init, adp=0xffffffff81136880
   (.splash left NULL)` — the immediate path in `splash_register()`
   (splash.c:108-121) ran `splash_test()` and `splash_new()` installed the
   decoder as `splash_decoder`.
5. `sysctl hw.nullsplash_fire=1` — module calls `splash(real_adp, TRUE)`,
   the exact call the in-tree caller `scsplash_saver()` makes
   (syscons.c:2560). Result: **kernel panic** — `Fatal trap 12: page fault
   while in kernel mode`, `fault virtual address = 0x0`, **`Stopped at 0:`**
   (control transferred to address 0), process 988 = the sysctl caller.
   ssh died; with `debug.debugger_on_panic=1` the box sat in DDB on the VGA
   console until hypervisor reset. Evidence: panic.txt + vga_panic.png /
   vga_panic_2x.png (QEMU-monitor VGA screendumps; the kernel console was
   vidconsole for this run).

Why the module-driven `splash()` call rather than waiting for the screen
saver: the saver invocation also depends on syscons idle-blanking state
(scrn_timer / SC_SCRN_IDLE / current_saver, syscons.c:2193-2229) which is
orthogonal to splash.c; the finding under audit is the unguarded indirect
call at splash.c:205, reached through the same public `splash()` entry the
in-tree caller uses.

## Fix validation (patched kernel)

- Applied `fix.diff` (one line: `if (splash_decoder != NULL &&
  splash_decoder->splash != NULL)`) to the guest's /usr/src copy only;
  host `sys/` untouched. `make nativekernel KERNCONF=X86_64_GENERIC`
  (buildkernel refused: no built world) — BUILD_EXIT=0. Installed; uname
  shows `#1: Sun Oct 11 20:47:22 UTC 2026`.
- Re-ran the IDENTICAL PoC on the patched kernel: `nullsplash: calling
  splash(...)` then **`nullsplash: returned -- guard present?`**, sysctl
  exit 0, guest alive (uptime confirmed). See fix_run.log; applied-diff and
  kernel hashes in fix_build.log / fixsrc.txt.

Baseline (panic) vs patched (clean ENODEV-style return) — A/B complete.

## Why Info and not higher

- Registry writes (`splash_register`) happen only from module load events
  (boot-time preload or root `kldload`); `splash_init` only from the console
  driver's boot-time attach. No copyin/ioctl/sysctl from userland touches
  this file. An unprivileged user cannot influence which decoder is active.
- All in-tree decoders/savers set `.splash` (bmp/pcx + 10 syscons savers
  verified), so the NULL path needs a buggy third-party root-loaded module.
- Root can crash the kernel by design; the fix is defense-in-depth
  consistency with the two sibling NULL guards.

## Negative results recorded during pass 2 (nothing else found)

- Registry arithmetic beyond DF-2144: re-derived the insertion logic —
  `decoders` stuck at 1 after first registration means every later
  deferred registration writes `decoder_set[1]` (in-bounds, capacity 4);
  no integer overflow (`sizeof(*p)*(decoders+4)` with bounded module
  count); realloc path only reachable at decoders==0. No OOB is reachable;
  the functional miscount is exactly DF-2144.
- "kenv splash.image / namei" surface: does not exist in this file — no
  kenv/getenv/namei/copyin anywhere under sys/dev/video/fb/ (rg-verified).
  The image arrives exclusively as loader-preloaded module metadata read
  via preload_search_by_type/MODINFO_ADDR/MODINFO_SIZE (splash.c:61-71),
  i.e. boot-config trust level.
- offx/offy window-adapt math: not in this file (decoder-side; covered by
  the pass-1 findings DF-1857/1858/1859/2110/2111 in splash_bmp/splash_pcx).
- Unlocked registry races (kldload/kldunload MOD events vs saver-thread
  `splash()` reads of `splash_decoder`): trigger requires root module
  operations and is the same class already filed as DF-2143 (stale decoder
  after unload) — not re-reported.
- `splash_init` double-call / dropping an active decoder without term
  (splash.c:167): unreachable — guarded by SC_SPLASH_SCRN / SC_INIT_DONE
  in syscons (syscons.c:3430-3434) and `scterm` is #if 0 dead code.
- `splash_register` kmalloc/bcopy/kfree error path (splash.c:129-137):
  correct — ENOMEM leaves state untouched, old array freed after copy.
- kprintf of the image pointer at splash.c:73-74: bootverbose-only,
  console already root-trusted; not a finding.

## 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.
