# DF-4635 — radeon_audio.c enc_priv type confusion (eDP/LVDS) → cross-object heap OOB read/write + wild MMIO deref

**File:** `sys/dev/drm/radeon/radeon_audio.c`
**Bucket:** memcorrupt — Severity High — Confidence certain (static path proof + live allocator-primitive proof)
**Guest:** DragonFly 6.5-DEVELOPMENT x86_64 (`/usr/src/sys` byte-identical to the audited tree, MD5-verified)

## What the finding is

`radeon_encoder_is_digital()` (radeon_encoders.c:423) returns true for DVO1/DDI/UNIPHY-family
encoders. But for those encoders, `radeon_add_atom_encoder()`
(atombios_encoders.c:2860-2872) installs a **`struct radeon_encoder_lvds *`** (280 bytes on
amd64) as `enc_priv` whenever the encoder carries `ATOM_DEVICE_LCD_SUPPORT` (every eDP panel),
and registers the encoder as `DRM_MODE_ENCODER_LVDS`. `radeon_audio_detect()`
(radeon_audio.c:444-486) and `radeon_audio_enable()` (radeon_audio.c:251-278) fetch
`dig = radeon_encoder->enc_priv` and use it as **`struct radeon_encoder_atom_dig *`** (312
bytes), reading/writing `dig->pin` (**offset 296**) and `dig->afmt` (**offset 288**) — 8 and
16 bytes **past the end of the 280-byte lvds allocation**. On DragonFly's slab allocator a
280-byte request gets a **288-byte chunk with zero slack** (32-byte-granular zone classes,
`sys/kern/kern_slaballoc.c:638 zoneindex()`), so those two accesses land in the **first 16
bytes of the adjacent kernel object** in the shared 288-byte size class.

Reachability chain (all statically proven, path:line in VERDICT.md):
eDP connector (radeon_connectors.c:2264-2268, `.detect = radeon_dp_detect` at 1851)
→ `radeon_dp_detect` unguarded audio call (radeon_connectors.c:1765-1768)
→ `radeon_audio_detect` with the LCD-carrying UNIPHY encoder
→ `dig->pin` OOB read at +296 (line 475/479), OOB write at +296 (line 476/480/484),
   `dig->afmt` OOB read at +288 (lines 548, 638-642, 648-655, 663-672, 678-684, 690-696, 706-710)
→ garbage `pin` flows to `dce6_audio_enable` → `WREG32_ENDPOINT(pin->offset, …)` wild deref.
Trigger: any connector detect — boot, HPD/poll, or unprivileged `DRM_IOCTL_MODE_GETCONNECTOR`
(force=1). Requires radeon hardware with an eDP panel (laptops); `radeon_audio` defaults to
enabled (`radeon_audio = -1`, radeon_drv.c:180).

The identical upstream Linux v5.2 code is saved from *cross-object* corruption only by
kmalloc-512 slack (280→512 chunk); the uninit-read/wild-deref half is latent upstream too.
DragonFly's 32-byte-granular zones make it a real adjacent-object corruption here.

## What could / could not be run on this guest

The QEMU guest has **no radeon GPU and no /dev/dri at all** (QEMU std VGA), so the driver
path itself cannot execute. What WAS run and proven on the live guest kernel:

1. **Struct-offset proof** — `radeon_audio_offsets.c`, compiled from the tree's own struct
   definitions (guest `/usr/src/sys`, MD5-identical to the audited tree):
   `sizeof(radeon_encoder_lvds)=280`, `offsetof(atom_dig, afmt)=288`, `offsetof(atom_dig, pin)=296`.
2. **Allocator-primitive proof** — `aflvds.c` KLD: 8192 kmallocs of 280 bytes → usable chunk
   **288** (no slack), adjacent pair found at exactly 288 stride, and the exact driver
   accesses reproduced: read at +296 returns the *neighbor's* bytes; write at +296 (`dig->pin
   = NULL`) zeroes the neighbor's bytes [8..16); read at +288 returns the neighbor's first
   8 bytes (would be deref'd as `afmt->offset`).
3. **Fix compile-validation** — `fix.diff` (encoder-type gate mirroring the existing DVI-path
   gate at radeon_connectors.c:1424) applies clean to `sys/` and the full `radeon.ko` module
   rebuilds with `-Werror` on the guest toolchain (build.log = baseline, fix_build.log = patched).

## Build / run

On the guest (as maxx, root for kldload):

```
cc -O -o radeon_audio_offsets radeon_audio_offsets.c && ./radeon_audio_offsets
printf 'SYSDIR=/usr/src/sys\nKMOD=\taflvds\nSRCS=\taflvds.c\n\n.include "${SYSDIR}/conf/kmod.mk"\n' > Makefile
make SYSDIR=/usr/src/sys          # build aflvds.ko
sudo kldload ./aflvds.ko ; dmesg | tail -6
```

Expected decisive output (see run.log):

```
aflvds: 8192 objs of 280 bytes; usable chunk = 288 (want 288)
aflvds: adjacent pair A=... B=... (B-A=288 want 288)
aflvds: OOB READ  *(A+296) [dig->pin]  = 0xcc...   <- garbage 'pin'
aflvds: OOB WRITE *(A+296) = NULL -> victim[8..16) = 0x0  <- adjacent object zeroed
aflvds: OOB READ  *(A+288) [dig->afmt] = 0xcc...   <- neighbor bytes read as 'afmt'
aflvds: PRIMITIVE PROVEN: enc_priv+288/+296 alias the ADJACENT slab chunk on this kernel
```
