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)
PoC verification
Evidence pack
findings/poc/DF-4635 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- 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. - Allocator-primitive proof β
aflvds.cKLD: 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 asafmt->offset). - Fix compile-validation β
fix.diff(encoder-type gate mirroring the existing DVI-path gate at radeon_connectors.c:1424) applies clean tosys/and the fullradeon.komodule rebuilds with-Werroron 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
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/3encoders withdevices & ATOM_DEVICE_LCD_SUPPORTare registered asDRM_MODE_ENCODER_LVDSand getenc_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,pinat +296,afmtat +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;insideradeon_audio_detect(), reached for any encoder whereradeon_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 readsdig->pinβ same OOB read for every LCD-carrying digital encoder. dig->afmtOOB read at +288 and subsequentdig->afmt->offsetderef in radeon_audio.c:548-551, 632-643, 645-655, 657-673, 675-685, 687-697, 702-738.- The OOB-read
pinvalue flows todce6_audio_enable(dce6_afmt.c:270-280):WREG32_ENDPOINT(pin->offset, β¦)β wild kernel-pointer dereference + MMIO write at a pointer-derived offset (DCE6+).dig->afmtderefs 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
- eDP connector creation: radeon_connectors.c:2264-2268 β
radeon_edp_connector_funcs,.detect = radeon_dp_detect(radeon_connectors.c:1851-1858). - 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. radeon_dp_detecttail (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 onencoder->encoder_type == DRM_MODE_ENCODER_TMDSprecisely to avoid this).radeon_audiodefaults to-1(enabled; radeon_drv.c:180).- 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. - 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-writesdig->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 garbagestruct r600_audio_pin *the driver derefs.*(A+296) = NULLwrite zeroed victim bytes [8..16) β exactly the driver'sdig->pin = NULLon eDP detect.*(A+288)read returned victim bytes [0..8) β exactly the driver'sdig->afmt.- Fix compile-validation:
fix.diff(gate onencoder->encoder_type == DRM_MODE_ENCODER_TMDSinradeon_audio_enable's walk andradeon_audio_detect, mirroring the existing DVI-path gate; plusdigNULL guard) β applies clean tosys/(git apply --check) and the wholeradeon.komodule 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:
NULLor&rdev->audio.pin[i](a fixed kernel address insideradeon_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_testablefix.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)
Confirmed kernel references
- sys/dev/drm/radeon/radeon_audio.c:444-486
- sys/dev/drm/radeon/radeon_audio.c:459
- sys/dev/drm/radeon/radeon_audio.c:475-484
- sys/dev/drm/radeon/radeon_audio.c:263-268
- sys/dev/drm/radeon/radeon_audio.c:548-551
- sys/dev/drm/radeon/radeon_audio.c:632-643
- sys/dev/drm/radeon/atombios_encoders.c:2860-2872
- sys/dev/drm/radeon/radeon_encoders.c:423-445
- sys/dev/drm/radeon/radeon_connectors.c:1765-1768
- sys/dev/drm/radeon/radeon_connectors.c:1851-1858
- sys/dev/drm/radeon/radeon_connectors.c:2264-2268
- sys/dev/drm/radeon/radeon_mode.h:381-396
- sys/dev/drm/radeon/radeon_mode.h:427-442
- sys/kern/kern_slaballoc.c:638
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.
No comments yet.