# VERDICT — DF-4484 (dangling `vo->vo_levels` → UAF read + double-free panic)

**Status: reproduced.** Two independent guest boots with SSDT-A
(`ssdt_dfa.asl`, injected via `QEMU_EXTRA="-acpitable file=ssdt_dfa.aml"`),
plus a third boot for fix validation (below).

## Chain (all on DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 2026, X86_64_GENERIC)

1. **Table**: `VGT0` (`_ADR 0x00020000`) binds to vgapci0 (0:2:0) at boot via
   `acpi_pci_save_handle` (sys/dev/acpica/acpi_pci.c:246-268). `_DOD` =
   `{0x110, 0x120}`, `_DOS` present ⇒ `acpi_video_probe()` succeeds
   (acpi_video.c:222-238) after `kldload acpi_video`.
2. **Attach**: namespace walk (vid_enum_outputs, acpi_video.c:955-956) finds
   `LCD0`/`TVD0`; `acpi_video_bind_outputs_subr` (:429-448) creates lcd0/crt0;
   `acpi_video_vo_bind` (:594-615) evaluates `_BCL` — call #1 returns a valid
   6-entry package ⇒ `vo_levels = {100,55,90,75,55,25}`, `numlevels = 6`,
   `fullpower = 100`, `economy = 55`.
   Observed: `hw.acpi.video.lcd0.levels: 100 55 90 75 55 25`.
3. **REPROBE**: `_DCS` fires `Notify(VGT0, 0x81)` on its 2nd evaluation
   (two sysctl reads of `lcd0.active`). Handler
   `acpi_video_notify_handler` (:384-397) NULLs all `vo->handle`, rebinds.
   `vo_bind` **frees `vo->vo_levels`** (:597-598) then calls
   `vo_get_brightness_levels` — `_BCL` #2 returns a 1-entry package ⇒
   `ACPI_PKG_VALID(res, 2)` fails (:984-988) ⇒ failure path returns 0 at
   :1005-1008 **without assigning `*levelp`** ⇒ `vo->vo_levels` dangles,
   `vo_numlevels = 0`.
   Observed: `lcd0.levels` prints empty; `crt0.levels` intact; dmesg:
   `evaluation of \_SB_.PCI0.VGT0.LCD0._BCL makes no sense`.
4. **UAF read**: `sysctl -w hw.acpi.video.lcd0.fullpower=-1` →
   `acpi_video_vo_presets_sysctl` (:808-848): `vo_levels != NULL` check passes
   on the dangling pointer; `level == -1` ⇒ `level = vo->vo_levels[0]`
   (:834-837) reads the freed 24-byte `M_ACPICA` chunk; `*preset = level`.
   Readback: `-1920985560` (boot 1), `-1925046824` (boot 2) — freed-chunk
   freelist-pointer garbage; not `-1` (written value), not `100` (old value),
   not any legal level (0..100). Values differ across boots (heap layout),
   stable within a boot (chunk not reused between samples) — consistent with
   a kfree-written freelist link, i.e. genuinely freed memory.
5. **Double free**: `kldunload acpi_video` → `acpi_video_detach` (:300-304) →
   `acpi_video_vo_destroy` (:627-631) frees the dangling `vo_levels` again:
   ```
   panic: memory chunk 0xfffff8008d801580 is already free!
   chunk_mark_free() at chunk_mark_free+0xae
   _kfree() at _kfree+0x262
   acpi_video_vo_destroy() at acpi_video_vo_destroy+0x6a  (acpi_video.c:630)
   acpi_video_detach() at acpi_video_detach+0x79
   device_detach() at device_detach+0x74
   ```
   Guest wedged in DDB (`vm.sh status` → down, serial log captured in
   `panic.txt`).

## Exploitability assessment
- The bug class is kernel heap UAF/double-free. Turning the read primitive
  into an info leak beyond root requires an unprivileged consumer of the
  dangling pointer; the `-1` write is root-only (sysctl write) and the
  notify path only reads `vo_levels[i]` under `vo_numlevels > 0` (which is 0
  here), so no unpriv→root chain exists through this specific defect on this
  guest — the demonstrated impact is a root-visible 4-byte freed-heap
  disclosure plus the double-free panic (kernel DoS / heap corruption on
  allocators without the already-free check).
- Preconditions: firmware `0x81` reprobe + failing `_BCL` on rebind. Crafted
  ACPI table (boot privilege) demonstrated; real-world equivalent is buggy
  firmware during dock/hotplug events.

## Fix validation
`fix.diff` (in this directory) NULLs `vo->vo_levels`/`vo->vo_numlevels` after
the free in `vo_bind`, removes the old notify handler before re-install, and
switches the REPROBE marker from `vo->handle = NULL` to a `vo_matched` flag
(also covering DF-4486). Built as a kernel+modules via `make nativekernel` on
the guest and re-tested: see `fix_run.log` — after the same REPROBE
choreography, `lcd0.levels` returns `ENODEV` (driver reports "device has no
levels" instead of an empty list), the `fullpower=-1` write returns `ENODEV`
(readback stays `100`), and `kldunload acpi_video` completes cleanly with NO
panic. Baseline (unpatched) vs patched behavior recorded in
`run.log` / `fix_run.log`.

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