# DF-4635 — VERDICT

**Status: `inconclusive`** (driver path hardware-gated on this guest: no radeon GPU, no
`/dev/dri`). The memory-corruption **primitive is proven live** on the guest kernel and the
type confusion is **proven statically, end to end, with exact offsets and line numbers**.
Confidence in the finding itself: **certain**.

## 1. The bug, end to end

### 1.1 Root cause: two different structs behind `enc_priv`

`radeon_add_atom_encoder()` (sys/dev/drm/radeon/atombios_encoders.c) chooses `enc_priv`
per encoder id + supported device:

* atombios_encoders.c:2860-2872 — `DVO1/KLDSCP_DVO1/DDI/UNIPHY/KLDSCP_LVTMA/UNIPHY1/2/3`
  encoders with `devices & ATOM_DEVICE_LCD_SUPPORT` are registered as
  `DRM_MODE_ENCODER_LVDS` and get **`enc_priv = radeon_atombios_get_lvds_info()`** →
  `struct radeon_encoder_lvds *` (radeon_mode.h:381-396, **280 bytes** on amd64, `kzalloc`'d).
* Only the non-LCD branches (CRT/TMDS) get `radeon_atombios_set_dig_info()` →
  `struct radeon_encoder_atom_dig *` (radeon_mode.h:427-442, **312 bytes**, `pin` at
  **+296**, `afmt` at **+288**).

Offsets machine-verified on the guest (`radeon_audio_offsets.c`, struct definitions taken
verbatim from the tree; guest `/usr/src/sys` MD5-matched to the audited tree):

```
sizeof(drm_display_mode)       = 240
sizeof(radeon_encoder_lvds)    = 280
sizeof(radeon_encoder_atom_dig)= 312
offsetof(dig, afmt)            = 288
offsetof(dig, pin)             = 296
```

### 1.2 The unsafe derefs (this file, radeon_audio.c)

* radeon_audio.c:459 — `dig = radeon_encoder->enc_priv;` inside `radeon_audio_detect()`,
  reached for **any** encoder where `radeon_encoder_is_digital()` is true
  (radeon_encoders.c:423-445 — includes all UNIPHY/DVO/DDI ids, LCD-carrying ones included).
* radeon_audio.c:475-484 —
  `if (!dig->pin)` (OOB **read** +296), `dig->pin = radeon_audio_get_pin(encoder)`
  (OOB **write** +296, value `&rdev->audio.pin[i]` — a deterministic kernel pointer),
  `radeon_audio_enable(rdev, dig->pin, 0xf/0)` (OOB-read value used as a pointer),
  `dig->pin = NULL` (OOB **write** +296, value NULL).
* radeon_audio.c:263-268 — `radeon_audio_enable()` walks **every** digital encoder and
  reads `dig->pin` — same OOB read for every LCD-carrying digital encoder.
* `dig->afmt` OOB read at +288 and subsequent `dig->afmt->offset` deref in
  radeon_audio.c:548-551, 632-643, 645-655, 657-673, 675-685, 687-697, 702-738.
* The OOB-read `pin` value flows to `dce6_audio_enable` (dce6_afmt.c:270-280):
  `WREG32_ENDPOINT(pin->offset, …)` — **wild kernel-pointer dereference + MMIO write at a
  pointer-derived offset** (DCE6+). `dig->afmt` derefs happen on every mode set of the
  affected encoder regardless of EDID content (radeon_audio_hdmi_mode_set's else-branch,
  radeon_audio.c:732-737).

### 1.3 Reachability

1. eDP connector creation: radeon_connectors.c:2264-2268 → `radeon_edp_connector_funcs`,
   `.detect = radeon_dp_detect` (radeon_connectors.c:1851-1858).
2. Encoder binding: `radeon_link_encoder_connector()` (radeon_atombios.c) attaches the
   LCD-carrying UNIPHY encoder (devices & LCD) as the connector's first possible encoder;
   `radeon_best_single_encoder()` (radeon_connectors.c:379-387) returns exactly it.
3. `radeon_dp_detect` tail (radeon_connectors.c:1765-1768):
   `if ((radeon_audio != 0) && encoder) { …; radeon_audio_detect(connector, encoder, ret); }`
   — **no encoder_type gate** (contrast the DVI path at radeon_connectors.c:1421-1428,
   which gates on `encoder->encoder_type == DRM_MODE_ENCODER_TMDS` precisely to avoid this).
   `radeon_audio` defaults to `-1` (enabled; radeon_drv.c:180).
4. Detect runs at boot/HPD/poll and can be **forced by any local user** via
   `DRM_IOCTL_MODE_GETCONNECTOR` (force probe) — unprivileged trigger on affected hardware.
5. Preconditions: radeon ASIC DCE2+ non-NODCE (`radeon_audio_chipset_supported`,
   radeon_audio.c:301-304) with an eDP connector — i.e. most DCE4+ radeon laptops.
   Note the corruption fires on **every** eDP detect (connected *or* disconnected: the
   else-branch at radeon_audio.c:482-485 also reads then NULL-writes `dig->pin`).

### 1.4 Why it is *cross-object* on DragonFly (and masked on Linux)

`sys/kern/kern_slaballoc.c:638 zoneindex()`: requests 256..512 bytes round at **32-byte
granularity** — 280 → **288-byte chunk, no slack**. `dig->afmt` (+288) and `dig->pin` (+296)
therefore alias the **adjacent chunk's bytes 0..16** in the shared 288-byte size class.
Upstream Linux (v5.2 code is byte-identical here — verified against
torvalds/linux v5.2 atombios_encoders.c) rounds 280 into kmalloc-512; the accesses stay in
chunk slack, hiding the corruption (the uninit-read/wild-deref half remains latent upstream).

## 2. What ran on the guest (2026-09-22, DragonFly 6.5-DEVELOPMENT #0, gcc 8.3)

* **Offsets**: `./radeon_audio_offsets` → output above (see README).
* **Allocator primitive** (`aflvds.ko`, kldload'ed as root — run.log):
  * `kmalloc(280)` usable chunk = **288** → no slack, as predicted.
  * Adjacent pair `A=0xfffff8004f296c20 B=0xfffff8004f296d40` (`B-A == 288`).
  * `*(A+296)` read returned the *victim's* bytes (0xcc fill) — exactly the garbage
    `struct r600_audio_pin *` the driver derefs.
  * `*(A+296) = NULL` write **zeroed victim bytes [8..16)** — exactly the driver's
    `dig->pin = NULL` on eDP detect.
  * `*(A+288)` read returned victim bytes [0..8) — exactly the driver's `dig->afmt`.
* **Fix compile-validation**: `fix.diff` (gate on `encoder->encoder_type ==
  DRM_MODE_ENCODER_TMDS` in `radeon_audio_enable`'s walk and `radeon_audio_detect`,
  mirroring the existing DVI-path gate; plus `dig` NULL guard) — applies clean to `sys/`
  (`git apply --check`) and the whole `radeon.ko` module rebuilds with `-Werror`
  (fix_build.log; baseline build.log).

## 3. Why not `reproduced`

The guest has **no radeon hardware and no DRM device** (`ls /dev/dri` → No such file;
only QEMU std VGA `vgapci0 chip=0x11111234`). The vulnerable call chain
(`radeon_dp_detect` → `radeon_audio_detect`) requires a radeon GPU with an eDP panel to
bind. Status is therefore `inconclusive` with `impact=none` (nothing observable on this
guest); the corruption primitive itself is demonstrated live and the driver-path logic is
proven by citation.

## 4. Exploitability sketch (for triage)

* OOB **write** value is constrained: `NULL` or `&rdev->audio.pin[i]` (a fixed kernel
  address inside `radeon_device`) into bytes [8..16) of an adjacent 257..288-byte-class
  kernel object. NULLing a neighbor's list pointer / refcount-adjacent field is a
  realistic crash-to-UAF lever; heap grooming of the shared size class determines the victim.
* OOB **read** values (`afmt`, `pin`) are dereferenced (`afmt->offset`, `pin->offset`) —
  adjacent-chunk content (groomable) becomes a wild pointer for an MMIO write offset on
  DCE6+ (`WREG32_ENDPOINT(pin->offset, …)`).
* Trigger is unprivileged (force-probe ioctl) on affected laptops → High (memcorrupt),
  hardware-gated, no guest demo possible.

## 5. Fix

`fix.diff` — refuse audio handling for non-TMDS digital encoders (their `enc_priv` is not
an `atom_dig`), mirroring radeon_connectors.c:1424's existing gate; also guard `dig == NULL`.
Functional cost: eDP audio stays disabled (it was corrupting memory). Compile-validated;
behavioral validation impossible on this guest (`fix_status: not_testable`).

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