r600_hdmi_buffer_status_changed / r600_hdmi_update_audio_settings consume lvds-typed enc_priv as radeon_encoder_atom_dig on the audio-work walk: dig->afmt OOB read at +288 into the adjacent slab chunk β wild deref + last_buffer_filled_status write-through (DF-4635 family, new sinks, interrupt-driven trigger)
Summary
The audio IH work walks every radeon_encoder_is_digital() encoder - including LCD-carrying digital encoders whose enc_priv is a 280-byte radeon_encoder_lvds - but derefs it as 312-byte atom_dig: dig->afmt at +288 reads the first 8 bytes of the ADJACENT slab object (280 rounds to 288 with zero slack), then derefs/writes through the garbage pointer and feeds its ->offset to MMIO. Interrupt-driven (no ioctl, no modeset); hides in the field only when the adjacent bytes are NULL. Distinct from DF-1911 (lock) and DF-4635 (same root cause, different sinks). Guest harness: verbatim logic + live-allocator spray proves the OOB read returns the groomed pointer and last_buffer_filled_status flips through it, and the offset feeds WREG32. encoder_type==TMDS gate fix, compile-validated on full radeon.ko -Werror rebuild.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4828 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| afmt_walk.c | β | 7.6 KB | view raw | |
| r600_hdmi_offsets.c | β | 3.9 KB | view raw | |
| Makefile | β | 131 B | β download | |
| build.sh | β | 186 B | view raw | |
| fixbuild.sh | β | 654 B | view raw | |
| build.log | β | 5.5 KB | view raw | |
| fix_build.log | β | 139.5 KB | view raw | |
| run.log | β | 919 B | view raw | |
| env.txt | β | 188 B | view raw | |
| fix.diff | β | 830 B | view raw | |
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 7.0 KB | β raw | |
| verdict.json | β | 5.5 KB | view raw |
DF-4828 β r600_hdmi.c consumes lvds-typed enc_priv as atom_dig on the audio-work path (OOB read at dig->afmt +288 β wild deref + write-through)
File: sys/dev/drm/radeon/r600_hdmi.c
Lines: 283-291 (r600_hdmi_buffer_status_changed), 425-435
(r600_hdmi_update_audio_settings), 271-274 (r600_hdmi_is_audio_buffer_filled)
Severity: High Β· Class: CWE-843 type confusion β CWE-125/CWE-787
(Same root-cause family as DF-4635 (radeon_audio.c) β new sinks, new
trigger path; not the DF-1911 encoder_list race, no race required.)
The bug
r600_audio_update_hdmi() (the audio interrupt work, scheduled from the IH
handlers r600.c:4341 / evergreen.c:4900 / rs600.c:821) walks every encoder
where radeon_encoder_is_digital() is true β which includes every
LCD-carrying digital encoder (KLDSCP_LVTMA/UNIPHY/LVTM1/TMDS1 with
ATOM_DEVICE_LCD_SUPPORT; radeon_encoders.c:423-443). For exactly those
encoders, enc_priv is a 280-byte radeon_encoder_lvds
(atombios_encoders.c:2860-2872), but r600_hdmi.c consumes it as a
312-byte radeon_encoder_atom_dig*:
r600_hdmi.c:283 dig = radeon_encoder->enc_priv; /* lvds* ! */ r600_hdmi.c:286 if (!dig->afmt || !dig->afmt->enabled) /* dig->afmt = +288 = OOB READ */ r600_hdmi.c:291 dig->afmt->last_buffer_filled_status = status; /* WRITE through garbage ptr */ r600_hdmi.c:433 same OOB read + deref in update_audio_settings r600_hdmi.c:272 dig->afmt->offset -> feeds WREG32 MMIO offsets
DragonFly's slab rounds 280 β 288-byte chunk with zero slack
(kern_slaballoc.c zoneindex, 32-byte granularity in 256..512), so +288
reads the first 8 bytes of the adjacent kernel object β groomable heap
content becomes a wild radeon_afmt * that the driver dereferences
(->enabled, ->offset β MMIO) and writes through
(->last_buffer_filled_status).
Trigger: any audio interrupt status change on r600/evergreen-era hardware with an LCD-carrying digital encoder present (every LVDS/eDP laptop of the family) β no modeset, no race, no user interaction beyond audio activity (DF-4635's sinks needed a connector probe; these fire from the interrupt work directly).
Reproduce on the guest
./build.sh # builds r600_hdmi_offsets (userland) + afmt_walk.ko (KLD) ./fixbuild.sh # optional: full radeon.ko rebuild with fix.diff (-Werror)
Expected (KLD, live kernel allocator):
sizeof(radeon_encoder_lvds) = 280 sizeof(radeon_encoder_atom_dig) = 312 offsetof(dig, afmt) = 288 afmt_walk: 8192 objs of 280 bytes; usable chunk = 288 (want 288) afmt_walk: adjacent pair A=... B=... (B-A=288) afmt_walk: OOB READ dig->afmt (+288) == 0xffffffff82600380 (non-NULL -> guard ... falls through) afmt_walk: after call#1(status=1): last_buffer_filled_status=1 <- WILD WRITE via OOB-read pointer (r600_hdmi.c:291) afmt_walk: update_audio_settings replica (r600_hdmi.c:433->435): ... dig->afmt->offset == 0x1234 -> would feed WREG32(HDMI0_* + offset) MMIO
(The driver path itself is hardware-gated on this guest β no radeon GPU, no /dev/dri β so the KLD reproduces the exact heap geometry and the exact audited accesses; see VERDICT.md.)
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 whosedevices & ATOM_DEVICE_LCD_SUPPORTare registered asDRM_MODE_ENCODER_LVDSwithenc_priv = radeon_atombios_get_lvds_info()βstruct radeon_encoder_lvds *(280 bytes)*, notatom_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 β includingINTERNAL_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_entryoverencoder_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!digguard);!dig->afmtOOB read at +288; if the garbage is non-NULL,dig->afmt->enabledwild read anddig->afmt->last_buffer_filled_status = statuswild 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 toRREG32(HDMI0_STATUS + offset)(wild MMIO on real hw). - r600_hdmi.c:425-435 β
r600_hdmi_update_audio_settings(): samedig->afmtOOB + deref at :433, thendig->afmt->offsetdrives 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 loadsdig->afmtOOB but is only reachable viaradeon_audio_dpms(atombios_encoders.c:1847-1850), which is gated onencoder_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, onlyencoderunrolled toenc_priv) then:- OOB-read
dig->afmtreturned the groomed pointer (guard at :286 falls through), - dereferenced
->enabled(wild read via OOB pointer), - wrote
last_buffer_filled_status0β1β0 through the OOB-read pointer (r600_hdmi.c:291) β observed in the attacker-controlled fake object;
- OOB-read
B[0..8) = NULLβ silentreturn 0(the common case that hides the bug);- the
update_audio_settingsguarded deref (r600_hdmi.c:433) also survived anddig->afmt->offset == 0x1234β the value that feedsWREG32(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.
Fix verification
not_testablefix.diff applies clean (patch -p1, hunks at 283/439) and the ENTIRE radeon.ko module rebuilds with -Werror on the guest toolchain including the patched r600_hdmi.o (fix_build.log, exit 0). Behavioral validation impossible on this guest: no radeon GPU, so r600_audio_update_hdmi (the only caller of both patched functions) never executes here; the KLD primitive harness is driver-path-independent by construction (it validates the geometry, not the gate).
fix.diff; fix_build.log (patched r600_hdmi.c compiled -Werror, radeon.ko linked exit 0)
Confirmed kernel references
- sys/dev/drm/radeon/r600_hdmi.c:283-291
- sys/dev/drm/radeon/r600_hdmi.c:425-435
- sys/dev/drm/radeon/r600_hdmi.c:271-274
- sys/dev/drm/radeon/r600_hdmi.c:132-137
- sys/dev/drm/radeon/atombios_encoders.c:2860-2872
- sys/dev/drm/radeon/radeon_encoders.c:423-443
- sys/dev/drm/radeon/radeon_mode.h:427-445
- sys/dev/drm/radeon/r600.c:4341
- sys/dev/drm/radeon/evergreen.c:4900
- sys/kern/kern_slaballoc.c:638
Detail
Exploit chain
(hardware-gated, not runnable here) groom the shared 288-byte slab size class so a chosen victim object follows the lvds enc_priv allocation -> audio interrupt status change schedules r600_audio_update_hdmi -> walk hits the LCD-carrying digital encoder -> OOB read dig->afmt = victim[0..8) -> if non-NULL: wild read (->enabled), wild write (last_buffer_filled_status at +8 of whatever victim[0..8) points to), and offset-driven wild MMIO (RREG32/WREG32 HDMI0_*+offset) -> NULL neighbor silently returns (hides the bug), garbage neighbor pointer = panic, groomed victim = controlled deref+write primitive -> crash-to-UAF escalation path; deterministic per audio status flip on affected laptops.
Evidence (decisive lines)
run.log (KLD: usable chunk=288, adjacent pair B-A=288, OOB READ dig->afmt(+288)==groomed ptr non-NULL, last_buffer_filled_status written 0->1->0 through it (r600_hdmi.c:291), NULL case silent, update_audio_settings replica dig->afmt->offset==0x1234); build.log (offsets: lvds=280 atom_dig=312 afmt@+288 + KLD build); fix_build.log (whole radeon.ko rebuilds -Werror with fix.diff, r600_hdmi.o linked); VERDICT.md full path citations
PoC changes
wrote afmt_walk.c KLD (verbatim replica of r600_hdmi_buffer_status_changed on the live allocator with groomable adjacent victim) + r600_hdmi_offsets.c (verbatim struct defs); no seed PoC. fix.diff generated with real diff -u and compile-validated by a full radeon.ko rebuild (-Werror).
Verified recommended fix
In r600_hdmi_buffer_status_changed() and r600_hdmi_update_audio_settings(), early-return unless (dig && encoder->encoder_type == DRM_MODE_ENCODER_TMDS) - LCD-carrying digital encoders are LVDS-registered with a lvds-typed enc_priv; see fix.diff
Verdict
Driver path hardware-gated on this guest (no radeon GPU, no /dev/dri - pciconf shows only QEMU std VGA), so r600_audio_update_hdmi can never run here; the type confusion is nonetheless proven statically with exact lines (atombios_encoders.c:2860-2872 installs a 280-byte radeon_encoder_lvds as enc_priv for LCD-carrying KLDSCP_LVTMA/UNIPHY/TMDS1/LVTM1 digital encoders; radeon_encoders.c:423-443 is_digital returns true for them) and the corruption PRIMITIVE is proven live on the guest kernel: the audio IH work (r600.c:4341/evergreen.c:4900 -> r600_audio_update_hdmi walk at r600_hdmi.c:132) calls r600_hdmi_buffer_status_changed (r600_hdmi.c:283-291) and r600_hdmi_update_audio_settings (:425-435) on those encoders; dig->afmt at +288 reads the ADJACENT 288-byte slab chunk (kmalloc(280) has zero slack per kern_slaballoc.c zoneindex), a KLD reproduced the exact accesses - the OOB read returned a groomed non-NULL pointer, the guard at r600_hdmi.c:286 fell through, and last_buffer_filled_status was written THROUGH the OOB-read pointer (observed 0->1->0 in a controlled fake afmt), plus dig->afmt->offset (0x1234) would feed WREG32(HDMI0_*+offset) MMIO. New sinks + interrupt-driven trigger vs DF-4635 (which covered radeon_audio.c detect/enable via connector probe); not the DF-1911 walk race - no race, no modeset, no user gesture needed beyond audio activity on r600/evergreen-era LVDS/eDP hardware.
No comments yet.