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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4828 Β· 13 files
FileTypeDescriptionSize
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.)

VERDICT.md
↓ download raw

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 whose devices & ATOM_DEVICE_LCD_SUPPORT are registered as DRM_MODE_ENCODER_LVDS with enc_priv = radeon_atombios_get_lvds_info() β†’ struct radeon_encoder_lvds * (280 bytes)*, not atom_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 β€” including INTERNAL_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_entry over encoder_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 !dig guard); !dig->afmt OOB read at +288; if the garbage is non-NULL, dig->afmt->enabled wild read and dig->afmt->last_buffer_filled_status = status wild 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 to RREG32(HDMI0_STATUS + offset) (wild MMIO on real hw).
  • r600_hdmi.c:425-435 β€” r600_hdmi_update_audio_settings(): same dig->afmt OOB + deref at :433, then dig->afmt->offset drives 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 loads dig->afmt OOB but is only reachable via radeon_audio_dpms (atombios_encoders.c:1847-1850), which is gated on encoder_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, only encoder unrolled to enc_priv) then:
    • OOB-read dig->afmt returned the groomed pointer (guard at :286 falls through),
    • dereferenced ->enabled (wild read via OOB pointer),
    • wrote last_buffer_filled_status 0β†’1β†’0 through the OOB-read pointer (r600_hdmi.c:291) β€” observed in the attacker-controlled fake object;
  • B[0..8) = NULL β†’ silent return 0 (the common case that hides the bug);
  • the update_audio_settings guarded deref (r600_hdmi.c:433) also survived and dig->afmt->offset == 0x1234 β€” the value that feeds WREG32(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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4828

Confirmed kernel references

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.