# DF-4484 — `acpi_video_vo_bind()` leaves a dangling `vo->vo_levels` pointer when `_BCL` re-evaluation fails → UAF read (sysctl disclosure) + double-free panic

## Impact
**Medium.** Kernel use-after-free read AND a double-free (kernel panic) in the
ACPI video extension driver, triggered by a `0x81` (REPROBE) firmware notify
followed by a failing `_BCL` re-evaluation on rebind. Demonstrated on the audit
guest with a crafted SSDT (firmware/loader-level control of ACPI tables), which
is the attacker model for ACPI-table-driven bugs; on real hardware the same
sequence occurs with buggy firmware (dock/hotplug reprobes where `_BCL`
transiently fails).

## Where (sys/dev/acpica/acpi_video/acpi_video.c)
- `acpi_video_vo_bind()` :597-600 — frees `vo->vo_levels`, then assigns
  `vo->vo_numlevels = vo_get_brightness_levels(handle, &vo->vo_levels)`.
- `vo_get_brightness_levels()` :966-1009 — on EVERY failure path
  (`AcpiEvaluateObject` failure, `ACPI_PKG_VALID` failure, alloc failure,
  `n < 2`) it returns 0 **without writing `*levelp`** (:1005-1008).
- ⇒ after a failed rebind: `vo->vo_levels` points at the just-freed
  allocation, `vo->vo_numlevels == 0`, and the `vo_levels != NULL` guards in
  all consumers pass.
- Read primitive: `acpi_video_vo_presets_sysctl()` :834-837 — writing `-1` to
  `hw.acpi.video.<out>.fullpower` executes
  `level = vo->vo_levels[BCL_FULLPOWER]` — a read of freed kernel heap that is
  then stored in `vo_fullpower` and readable back from userspace.
- Double free: `acpi_video_vo_destroy()` :627-631 frees the dangling
  `vo->vo_levels` again when the module is detached or the output is destroyed
  by a later REPROBE.

## Reproduction (guest-verified)
SSDT `ssdt_dfa.asl` (in this directory) defines:
- `VGT0` with `_ADR 0x00020000` (binds vgapci0 via
  `acpi_pci_save_handle`, sys/dev/acpica/acpi_pci.c:246-268), `_DOD`
  `{0x110, 0x120}`, `_DOS`.
- `LCD0` (`_ADR 0x110`): `_BCL` succeeds only on the 1st evaluation (returns a
  6-entry package), fails afterwards (returns a 1-entry package so
  `ACPI_PKG_VALID(res, 2)` fails). `_DCS` fires `Notify(VGT0, 0x81)`
  (VID_NOTIFY_REPROBE) exactly on its 2nd evaluation — i.e. two sysctl reads
  of `hw.acpi.video.lcd0.active` choreograph the reprobe.
- `TVD0` (`_ADR 0x120`): control output whose `_BCL` always succeeds.

Run (`run_df4484.sh`, log in `run.log`):
```
QEMU_EXTRA="-acpitable file=ssdt_dfa.aml" vm.sh up    # boot guest with table
vm.sh run_root 'sh /tmp/run_df4484.sh'
```
1. `kldload acpi_video` → attach, `lcd0.levels: 100 55 90 75 55 25`,
   `fullpower: 100`.
2. read `lcd0.active` ×2 → 2nd `_DCS` fires `Notify(VGT0, 0x81)` → REPROBE →
   rebind → `_BCL` #2 fails (`dmesg`: "evaluation of ..._BCL makes no sense").
3. `lcd0.levels` now prints EMPTY (`vo_numlevels == 0`) while `crt0.levels`
   is intact — rebind-failure state reached.
4. `sysctl -w hw.acpi.video.lcd0.fullpower=-1` → kernel reads freed
   `vo_levels[0]`; readback: **`-1920985560`** (2nd boot:
   `-1925046824`) — heap freelist-pointer garbage, not any legal level.
5. `kldunload acpi_video` → `vo_destroy` frees the dangling pointer again →
   **kernel panic** (see `panic.txt`):
   ```
   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_detach() at acpi_video_detach+0x79
   ```

## Verdict summary
`reproduced` — both the UAF disclosure (steps 1-4, two independent boots) and
the double-free panic (step 5). Impact ceiling: kernel heap corruption class;
exploitation for code execution would additionally require controlling
`M_ACPICA` heap layout (the read value is the freed chunk's freelist link).
The trigger itself requires either crafted ACPI tables (boot privilege) or a
misbehaving firmware reprobe, plus root for the sysctl write/unload — hence
Medium, not High.

## Fix
`fix.diff` (validated on the guest — see VERDICT.md): NULL-out
`vo->vo_levels` (and `vo->vo_numlevels`) after freeing in `vo_bind`, and
remove the old notify handler before re-installing (matches FreeBSD's
`acpi_video_vo_bind`).
