β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4635

radeon_audio: enc_priv type confusion β€” radeon_encoder_lvds* consumed as radeon_encoder_atom_dig* on LCD-carrying digital encoders (eDP) β†’ dig->pin/dig->afmt OOB read/write into the ADJACENT 288-byte slab chunk + wild MMIO deref

Summary

radeon_encoder_is_digital returns true for DVO/DDI/UNIPHY encoders, but add_atom_encoder installs a 280-byte radeon_encoder_lvds* as enc_priv whenever the encoder carries LCD support (every eDP panel). audio_detect and audio_enable treat enc_priv as the 312-byte atom_dig: pin@+296 and afmt@+288 alias the first 16 bytes of the ADJACENT 288-byte slab object (DF slab rounds 280->288 with zero slack - the corruption is DF-specific in severity; upstream kmalloc-512 slack masks it). Each forced eDP probe reads 8 adjacent-chunk bytes as a pointer feeding a WREG32_ENDPOINT wild deref, writes NULL-or-kernel-pointer over neighbor bytes [8..16), and derefs the garbage 'afmt' in every mode_set - deterministic cross-object corruption with grooming, unpriv probe ioctl. Offsets machine-verified from the tree's structs; the exact accesses PROVEN live on the guest via a KLD against a real adjacent 288-byte pair (OOB read/write/deref all reproduced); user->root route analyzed (victim+8 deterministic write), hardware-gated. TMDS-gate fix compile-validated (-Werror).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4635 Β· 13 files
FileTypeDescriptionSize
README.md β€” 4.5 KB ↓ raw
VERDICT.md β€” 7.6 KB ↓ raw
radeon_audio_offsets.c β€” 3.1 KB view raw
aflvds.c β€” 4.1 KB view raw
build.sh β€” 172 B view raw
run.sh β€” 168 B view raw
build.log β€” 109.0 KB view raw
run.log β€” 545 B view raw
fix_build.log β€” 116.9 KB view raw
env.txt β€” 362 B view raw
fix.diff β€” 1.4 KB view raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 4.5 KB view raw

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
VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff applies clean (git apply --check) and the full radeon.ko module rebuilds with -Werror on the guest toolchain (fix_build.log); behavioral validation impossible on this guest - no radeon GPU to run the detect path against.

fix.diff; fix_build.log (patched radeon_audio.o + radeon.ko link, exit 0)
↓ fix.diffper-fix-DF-4635

Confirmed kernel references

Detail

Exploit chain

(hardware-gated, not run) spray/groom the shared 288-byte kmalloc size class so a chosen victim object sits after the lvds enc_priv allocation -> force eDP detect (unprivileged GETCONNECTOR) -> driver reads victim[8..16) as r600_audio_pin* (wild deref in dce6_audio_enable on DCE6+) and writes NULL or &rdev->audio.pin[i] over victim[8..16) -> corrupt adjacent object (list ptr/refcount-class fields) -> crash-to-UAF escalation path; deterministic 8-byte NULL write per eDP detect.

Evidence (decisive lines)

run.log (KLD: usable chunk=288, adjacent pair B-A=288, OOB read *(A+296)=0xcc.., OOB write *(A+296)=NULL zeroes victim[8..16), OOB read *(A+288)); README.md offsets (lvds=280/afmt=288/pin=296); VERDICT.md full path citations; fix_build.log (patched radeon.ko builds clean -Werror); build.log (baseline module build)

PoC changes

seed had no runnable PoC (hardware-gated finding); wrote two new artifacts: radeon_audio_offsets.c (struct offsets from tree definitions) and aflvds.c KLD (allocator-primitive demo), plus fix.diff compile-validated by rebuilding the whole radeon.ko module on the guest

Verified recommended fix

Gate radeon audio handling on encoder->encoder_type == DRM_MODE_ENCODER_TMDS in radeon_audio_detect() and radeon_audio_enable()'s walk (mirroring the existing DVI-path gate), plus a dig NULL guard; see fix.diff

Verdict

Driver path is hardware-gated on this guest (no radeon GPU, no /dev/dri) so the finding could not be exercised end-to-end; however the type confusion is proven statically with exact lines (atombios_encoders.c:2860-2872 installs a 280-byte radeon_encoder_lvds as enc_priv for LCD-carrying UNIPHY encoders; radeon_audio.c:459/475-484/263-268 use it as 312-byte radeon_encoder_atom_dig, touching pin@+296/afmt@+288), and the corruption PRIMITIVE is proven live on the guest kernel: kmalloc(280) gets a 288-byte chunk with zero slack (kern_slaballoc.c zoneindex), so +288/+296 alias the ADJACENT chunk - a KLD reproduced the exact accesses, showing the OOB read returning neighbor bytes and the dig->pin=NULL write zeroing the neighbor's bytes [8..16). Trigger on real hardware is unprivileged (force connector probe via DRM_IOCTL_MODE_GETCONNECTOR -> radeon_dp_detect:1765 -> radeon_audio_detect) on DCE4+ radeon laptops with eDP panels; OOB-read garbage also feeds dce6_audio_enable's WREG32_ENDPOINT(pin->offset,...) wild deref.