# DF-4828 — VERDICT

## Bottom line

**Inconclusive end-to-end** (the radeon driver path is hardware-gated on
this guest: no radeon GPU, no `/dev/dri`), **the type confusion is proven
statically with exact lines and machine-verified offsets, and its memory-
corruption primitive is proven LIVE on the guest kernel**: with the exact
DragonFly heap geometry (280-byte `radeon_encoder_lvds` in a 288-byte chunk,
zero slack), the verbatim r600_hdmi.c accesses read `dig->afmt` (+288) from
the **adjacent kernel object** and then dereference and **write through**
the garbage pointer. Confidence in the finding: **certain** (same
machine-verified offsets as DF-4635's KLD work; new sinks + new
interrupt-driven trigger proven by source trace).

## 1. Root cause and trigger path

* `atombios_encoders.c:2860-2872`: DVO1/KLDSCP_DVO1/DDI/UNIPHY*/KLDSCP_LVTMA
  encoders whose `devices & ATOM_DEVICE_LCD_SUPPORT` are registered as
  `DRM_MODE_ENCODER_LVDS` with `enc_priv = radeon_atombios_get_lvds_info()` →
  **`struct radeon_encoder_lvds *` (280 bytes)**, not `atom_dig` (312).
  (Same for TMDS1/KLDSCP_TMDS1/LVTM1 at :2834-2843.)
* `radeon_encoders.c:423-443`: `radeon_encoder_is_digital()` returns **true**
  for all of those ids — including `INTERNAL_LVDS`, `KLDSCP_LVTMA`,
  `UNIPHY*`.
* Trigger chain (no modeset, no race, no user gesture):
  audio IH event → `schedule_work(&rdev->audio_work)`
  (**r600.c:4341, evergreen.c:4900, rs600.c:821**) →
  `r600_audio_update_hdmi()` (r600_hdmi.c:114, work func registered at
  radeon_irq_kms.c:315) → `list_for_each_entry` over `encoder_list`
  (r600_hdmi.c:132) → `radeon_encoder_is_digital` → **true for the
  LCD-carrying encoder** → `r600_hdmi_buffer_status_changed(encoder)`
  (r600_hdmi.c:135) and, when the pin status changed,
  `r600_hdmi_update_audio_settings(encoder)` (r600_hdmi.c:136).
* The audited sinks:
  - r600_hdmi.c:283-291 — `dig = enc_priv` (no `!dig` guard);
    `!dig->afmt` **OOB read at +288**; if the garbage is non-NULL,
    `dig->afmt->enabled` **wild read** and
    `dig->afmt->last_buffer_filled_status = status` **wild write**
    (r600_hdmi.c:291) — the latter twice per audio status flip.
  - r600_hdmi.c:289 → 271-274 — `r600_hdmi_is_audio_buffer_filled()`:
    `offset = dig->afmt->offset` — OOB-derived value fed to
    `RREG32(HDMI0_STATUS + offset)` (wild MMIO on real hw).
  - r600_hdmi.c:425-435 — `r600_hdmi_update_audio_settings()`: same
    `dig->afmt` OOB + deref at :433, then `dig->afmt->offset` drives all
    the HDMI0_* MMIO writes in the function.
  - r600_hdmi.c:253-254 (`update_audio_infoframe`) and :304-305
    (`audio_workaround`, dead code — declared at radeon_asic.h:407, never
    called) carry the same unguarded pattern but are only reached behind
    the :433 guard.
  - `r600_hdmi_enable` (:484-488) also loads `dig->afmt` OOB but is only
    reachable via `radeon_audio_dpms` (atombios_encoders.c:1847-1850),
    which is gated on `encoder_mode == HDMI || DP` — LVDS-mode encoders
    (the mis-typed ones) never take it.

## 2. Why it is cross-object corruption on DragonFly

`kern_slaballoc.c:638 zoneindex()`: allocations of 256..512 bytes round at
32-byte granularity — 280 → **288-byte chunk, zero slack**. `dig->afmt`
(+288) therefore aliases bytes [0..8) of the **adjacent 288-byte-class
kernel object**. On upstream Linux 280 rounds into kmalloc-512 with slack,
masking the OOB (the wild-deref half remains latent upstream — code is
byte-identical).

## 3. What ran on the guest (2026-09-24, DragonFly 6.5-DEVELOPMENT, gcc 8.3)

* `r600_hdmi_offsets` (struct definitions verbatim from the tree):
  `drm_display_mode=240, lvds=280, atom_dig=312, offsetof(dig,afmt)=288,
  afmt.{enabled@0, offset@4, last_buffer_filled_status@8}` — matches
  DF-4635's machine-verified numbers.
* `afmt_walk.ko` (kldload'ed as root; unloaded cleanly):
  * sprayed 8192 × 280-byte objects; `kmalloc_usable_size` = **288**;
  * found adjacent pair `A=0xfffff80117ccff00 B=0xfffff80117cd0020`
    (`B-A == 288`);
  * groomed `B[0..8) = &fake_afmt`; the **verbatim replica of
    r600_hdmi_buffer_status_changed** (r600_hdmi.c:280-294, same field
    accesses/order, only `encoder` unrolled to `enc_priv`) then:
    - OOB-read `dig->afmt` returned the groomed pointer (guard at :286
      falls through),
    - dereferenced `->enabled` (wild read via OOB pointer),
    - **wrote `last_buffer_filled_status` 0→1→0 through the OOB-read
      pointer** (r600_hdmi.c:291) — observed in the attacker-controlled
      fake object;
  * `B[0..8) = NULL` → silent `return 0` (the common case that hides the
    bug);
  * the `update_audio_settings` guarded deref (r600_hdmi.c:433) also
    survived and `dig->afmt->offset == 0x1234` — the value that feeds
    `WREG32(HDMI0_* + offset)` MMIO on real hardware.
* **Fix compile-validation**: `fix.diff` (gate both entry functions on
  `!dig || encoder_type != DRM_MODE_ENCODER_TMDS`, the allow-list idiom of
  the existing DVI-path/DF-4635 fix) applies clean and the **whole
  radeon.ko module rebuilds with -Werror** (fix_build.log; r600_hdmi.o in
  the final link).

## 4. Why not `reproduced`

Same as DF-4635: this guest has no radeon GPU (`pciconf` shows only QEMU
std VGA; `/dev/dri` absent), so `r600_audio_update_hdmi` can never run
here. The corruption primitive itself ran live, and the driver-path logic
is proven by citation (§1). Status: `inconclusive`, impact `none`
(nothing observable via the driver on this guest), finding confidence
certain.

## 5. Exploitability sketch

Identical to DF-4635's analysis, with a *nicer* trigger (interrupt-driven,
no probe ioctl needed): groom the shared 288-byte size class so a chosen
victim object follows the lvds `enc_priv`; the audio work's OOB read makes
the victim's first 8 bytes a `radeon_afmt *` that the kernel dereferences
and **writes** (`last_buffer_filled_status`, i.e. an int write at +8 of
whatever the victim's first qword points to, plus `offset`-driven MMIO).
A NULL/zero neighbor silently returns (hides the bug); any non-NULL
neighbor pointer (list heads, function-pointer-adjacent fields) becomes a
wild-deref/panic; a groomed victim gives a controlled write-8-at-+8
primitive. Hardware-gated (r600-era radeon ASIC with an LVDS/eDP panel).

## 6. Fix

`fix.diff` — in `r600_hdmi_buffer_status_changed()` and
`r600_hdmi_update_audio_settings()`, return early unless
`dig && encoder->encoder_type == DRM_MODE_ENCODER_TMDS` (LCD-carrying
digital encoders are registered as `DRM_MODE_ENCODER_LVDS` and carry the
lvds-typed `enc_priv`; audio applies to TMDS(HDMI)/DP encoders only — the
DP-connector encoders are TMDS-registered, so they keep working). Same
idiom as DF-4635's proposed fix and the existing DVI-path gate
(radeon_connectors.c:1421-1428).

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