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

evergreen_hdmi_enable / evergreen_dp_enable consume lvds-typed enc_priv as radeon_encoder_atom_dig on the eDP dpms path: dig->afmt OOB read at +288 into the adjacent slab chunk β†’ guard falls through β†’ wild deref feeding six WREG32 MMIO sites + dig->afmt->enabled write-through (DF-4635/DF-4828 family, new sinks; corrects DF-4828's dpms-route dismissal)

Summary

The guards only check !dig/!dig->afmt but for LCD-carrying digital encoders enc_priv is the 280-byte lvds struct - dig->afmt at +288 reads the first 8 bytes of the ADJACENT 288-byte chunk; non-NULL garbage falls through, feeds wild MMIO offsets and a write-through on every dpms transition. NEW trigger vs the family: eDP panels - eDP yields ENCODER_MODE_DP so the dpms gate PASSES (disproving DF-4828's dismissal) and radeon_audio_detect assigns hdmi_funcs to the eDP encoder: every blank/unblank on a DCE4+ eDP laptop walks it, unpriv-triggered. KLD-PROVEN on the live allocator (OOB read returns the groomed pointer, guard falls through, offset collected, enabled flips 0->1->0 through it); fix compile-validated on full radeon.ko -Werror.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4853 Β· 13 files
FileTypeDescriptionSize
evg_dpms_probe.c β€” 8.1 KB view raw
evg_offsets.c β€” 4.0 KB view raw
Makefile β€” 63 B ↓ download
build.sh β€” 139 B view raw
build.log β€” 565 B view raw
run.log β€” 903 B view raw
fix.diff β€” 1.4 KB view raw
fix_build.log β€” 2.7 KB view raw
env.txt β€” 230 B view raw
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
verdict.json β€” 6.2 KB view raw
manifest.json β€” 1.2 KB view raw

DF-4853 β€” evergreen_hdmi_enable/evergreen_dp_enable consume lvds-typed enc_priv (DF-4635/DF-4828 family, new sinks, eDP-dpms trigger)

What

evergreen_hdmi_enable() (sys/dev/drm/radeon/evergreen_hdmi.c:429-461) and evergreen_dp_enable() (:470-514) load radeon_encoder->enc_priv as struct radeon_encoder_atom_dig * and guard only if (!dig || !dig->afmt) (:431, :473). For LCD-carrying digital encoders β€” every eDP panel β€” enc_priv is a 280-byte radeon_encoder_lvds (atombios_encoders.c:2862-2869, UNIPHY*/KLDSCP_LVTMA/DDI/DVO1 with ATOM_DEVICE_LCD_SUPPORT, and TMDS1/LVTM1 at :2824-2836), so:

  • dig->afmt at +288 reads 8 bytes past the 280-byte object (280 rounds to a 288-byte slab chunk, zero slack) = the first 8 bytes of the ADJACENT kernel object;
  • the NULL-guard consumes that garbage; if non-NULL it falls through and dig->afmt->offset (wild read) drives six WREG32/WREG32_AND/WREG32_OR sites (:438-455), and dig->afmt->enabled = enable (:458, :513) is a wild write through the OOB-read pointer on every dpms transition.

Trigger (why this is NOT covered by DF-4828's dismissal)

DF-4828's verdict dismissed the dpms path: "r600_hdmi_enable is only reachable via radeon_audio_dpms, which is gated on encoder_mode == HDMI || DP β€” the mis-typed LVDS-mode encoders never take it". That misses eDP:

  1. eDP panel encoder = INTERNAL_UNIPHY* + ATOM_DEVICE_LCD_SUPPORT β†’ registered DRM_MODE_ENCODER_LVDS, enc_priv = radeon_encoder_lvds* (atombios_encoders.c:2862-2869);
  2. eDP connector detect β†’ radeon_dp_detect (radeon_connectors.c:1764-68) β†’ radeon_audio_detect (radeon_audio.c:444): connector_type == eDP (β‰  DRM_MODE_CONNECTOR_DisplayPort) β†’ else branch β†’ radeon_encoder->audio = rdev->audio.hdmi_funcs (radeon_audio.c:471; DCE4+: .dpms = evergreen_hdmi_enable, radeon_audio.c:206);
  3. any dpms transition: radeon_atom_encoder_dpms (atombios_encoders.c:1836) β†’ atombios_get_encoder_mode β†’ eDP connector β‡’ ATOM_ENCODER_MODE_DP (atombios_encoders.c:824-829) β†’ the gate at :1848-1851 passes (ENCODER_MODE_IS_DP) β†’ radeon_audio_dpms (radeon_audio.c:777-783) β†’ evergreen_hdmi_enable with the mis-typed enc_priv.

So on any DCE4+ (evergreen/NI/SI) laptop with an eDP panel and audio enabled, every screen blank/unblank/mode-set walks the corrupted path. evergreen_dp_enable is the DP-sink twin (reachable if a DisplayPort-class connector is served by an LCD-carrying encoder).

Sibling in another file (noted for the queue): atombios_get_encoder_mode itself derefs enc_priv as atom_dig (dig_enc->active_mst_links, atombios_encoders.c:741-744, +304 OOB read).

Reproduce (live-allocator harness; no radeon GPU on this guest)

cd /tmp/df4853 && cc -O -o evg_offsets evg_offsets.c && ./evg_offsets
make && kldload -v /tmp/df4853/evg_probe.ko && kldunload evg_probe

Expected (run.log): dig->afmt (+288) reads the groomed adjacent-chunk pointer, the :431 guard FALLS THROUGH, dig->afmt->offset (0x5678) feeds the MMIO sites, and dig->afmt->enabled flips 0β†’1β†’0 through the OOB-read pointer; zeroed neighbor = silent return (field-hiding case).

Files

evg_offsets.c (geometry: lvds=280, atom_dig=312, afmt@+288), evg_dpms_probe.c + Makefile (KLD harness), build.sh, run.log, build.log, fix_build.log (patched radeon.ko, -Werror), fix.diff, VERDICT.md, env.txt.

VERDICT.md
↓ download raw

DF-4853 β€” VERDICT

Bottom line

Inconclusive end-to-end (the radeon driver path is hardware-gated on this guest: no radeon GPU, no /dev/dri β€” QEMU std VGA only), 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): a KLD replica of evergreen_hdmi_enable's deref sequence read dig->afmt (+288) from the adjacent kernel object, the !dig || !dig->afmt guard fell through on the groomed non-NULL neighbor, dig->afmt->offset (0x5678) was collected as the value the six WREG32 sites would program, and dig->afmt->enabled = enable wrote through the OOB-read pointer (observed 0β†’1β†’0 in a controlled fake afmt, both dpms directions). Confidence: certain (offsets machine-verified by evg_offsets.c: lvds=280, atom_dig=312, afmt@+288; family precedent DF-4635/DF-4828).

Why this is a NEW finding (not DF-4635, not DF-4828)

  • DF-4635 covers the sinks in radeon_audio.c:262-480 (radeon_audio_enable's walk deref, radeon_audio_detect's dig->pin). The sinks here are in evergreen_hdmi.c (:431/:438-455/:458 and :473/:483-510/:513) β€” dispatched into by radeon_audio_dpms.
  • DF-4828 covers the sinks in r600_hdmi.c (audio-work walk: buffer_status_changed / update_audio_settings), and its verdict explicitly dismissed the dpms route: "r600_hdmi_enable … only reachable via radeon_audio_dpms … gated on encoder_mode == HDMI || DP β€” the mis-typed LVDS-mode encoders never take it". That dismissal is wrong for eDP: an eDP connector yields ATOM_ENCODER_MODE_DP (atombios_encoders.c:824-829) while its encoder carries the lvds-typed enc_priv (atombios_encoders.c:2862-2869), so the gate at atombios_encoders.c:1848-1851 PASSES, and radeon_audio_detect (radeon_audio.c:461-472) assigns hdmi_funcs (= evergreen sinks on DCE4/5/6) to the eDP encoder because DRM_MODE_CONNECTOR_eDP β‰  DRM_MODE_CONNECTOR_DisplayPort. On a DCE4+ eDP laptop, every dpms transition (screen blank/unblank, any modeset, DRM_IOCTL_MODE_DPMS by an unprivileged DRM client) hits the mis-typed deref.
  • DF-1911 is the missing-mutex race on the shared walk β€” unrelated root.

Live primitive proof (run.log)

evg_probe: 8192 objs of 280 bytes; usable chunk = 288 (want 288)
evg_probe: adjacent pair A=0xfffff80117c95900 B=0xfffff80117c95a20 (B-A=288)
evg_probe: victim B[0..8) groomed to 0xffffffff82600380 (fake afmt)
evg_probe: OOB READ dig->afmt (+288) == 0xffffffff82600380 (non-NULL -> guard at evergreen_hdmi.c:431 FALLS THROUGH)
evg_probe: fake_afmt before: enabled=0 offset=0x5678
evg_probe: after evergreen_hdmi_enable(true): enabled=1  <- WILD WRITE via OOB-read pointer (evergreen_hdmi.c:458)
evg_probe: collected 2 distinct MMIO sites; dig->afmt->offset == 0x5678 would be added to HDMI_INFOFRAME_CONTROL0/AFMT_AUDIO_PACKET_CONTROL WREG32s (:438-455)
evg_probe: after evergreen_hdmi_enable(false): enabled=0 (write-through both directions)
evg_probe: victim B[0..8) zeroed -> OOB read NULL -> replica touched 0 MMIO site(s), no write (silent; hides the bug)

The harness replicates the exact deref sequence of evergreen_hdmi.c :429-:458 (verbatim comments map each line); WREG32 is factored out (the offset VALUE it would program is collected), since programming real MMIO on a GPU-less guest is impossible and meaningless.

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 β†’ eDP dpms transition β†’ evergreen_hdmi_enable β†’ OOB read dig->afmt = victim[0..8) β†’ if non-NULL: wild read (->offset drives HDMI_INFOFRAME_CONTROL0 / AFMT_AUDIO_PACKET_CONTROL MMIO offsets β€” attacker-influenced register programming), and wild write dig->afmt->enabled = enable (byte at victim[0..8)+0) on every dpms on AND off. NULL neighbor silently returns (hides the bug), garbage neighbor = panic, groomed victim = controlled write-where (through-pointer, address = victim[0..8)) β†’ crash-to-UAF escalation path, same analysis as DF-4635/DF-4828 (DF slab 32-byte granularity makes the cross-object effect DragonFly-specific in severity; upstream Linux code is byte-identical at the sinks but masked by kmalloc-512 slack).

Fix

fix.diff: type-gate both sinks β€” enc_priv is radeon_encoder_lvds* iff the encoder was registered DRM_MODE_ENCODER_LVDS (atombios_encoders.c:2824-2836, :2862-2869), so if (!dig || encoder->encoder_type == DRM_MODE_ENCODER_LVDS) return; in evergreen_hdmi_enable and evergreen_dp_enable. Compile-validated: patch applies clean (hunks at 428/485), full radeon.ko rebuild with -Werror succeeds including the patched evergreen_hdmi.o (fix_build.log, RC=0). Behavioral validation impossible on this guest (no radeon GPU β€” radeon_audio_dpms never executes); the KLD primitive harness is driver-path-independent by construction. Family precedent: the same gate shape compile-validated for DF-4828.

PoC fixes vs seed

No seed existed (new pass-2 finding). One -Werror fix during the KLD build (uninitialized mmio[0] in a kprintf).

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 (hunks at 428/485) and the whole radeon.ko module rebuilds with -Werror on the guest toolchain including the patched evergreen_hdmi.o (fix_build.log, RC=0). Behavioral validation impossible on this guest: no radeon GPU, so radeon_audio_dpms (the only route into both patched functions) never executes here; the KLD primitive harness is driver-path-independent by construction (it validates the geometry and the deref sequence, not the gate).

fix.diff; fix_build.log (patched evergreen_hdmi.c compiled -Werror, radeon.ko relinked RC=0)
↓ fix.diffper-fix-DF-4853

Confirmed kernel references

Detail

Exploit chain

(hardware-gated, not runnable here) groom the 288-byte slab size class so a chosen victim follows the lvds enc_priv allocation -> eDP dpms transition (screen blank/unblank, modeset, DRM_IOCTL_MODE_DPMS by an unprivileged DRM client) -> evergreen_hdmi_enable -> OOB read dig->afmt = victim[0..8) -> non-NULL: wild read (->offset programs HDMI_INFOFRAME_CONTROL0/AFMT_AUDIO_PACKET_CONTROL MMIO) + wild write dig->afmt->enabled at victim[0..8)+0 on every dpms on AND off -> NULL neighbor silently returns, garbage neighbor panics, groomed victim = controlled through-pointer write -> crash-to-UAF escalation path; deterministic per dpms transition 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, guard at evergreen_hdmi.c:431 falls through, afmt->offset 0x5678 collected for WREG32 sites, afmt->enabled written 0->1->0 through the OOB-read pointer (:458), NULL case silent); build.log + evg_offsets output (lvds=280 atom_dig=312 afmt@+288 afmt.enabled@+0 machine-verified); fix_build.log (patched radeon.ko rebuilds -Werror, RC=0); VERDICT.md full dispatch-chain citations incl. the DF-4828 dismissal correction

PoC changes

no seed; wrote evg_dpms_probe.c KLD (verbatim replica of evergreen_hdmi_enable's deref sequence on the live allocator with groomable adjacent victim) + evg_offsets.c (verbatim struct defs). One -Werror fix during build (uninitialized mmio[0]). fix.diff generated with real diff -u and compile-validated by a full radeon.ko rebuild (-Werror).

Verified recommended fix

Type-gate both sinks: early-return in evergreen_hdmi_enable/evergreen_dp_enable when (!dig || encoder->encoder_type == DRM_MODE_ENCODER_LVDS) -- enc_priv is radeon_encoder_lvds exactly for LVDS-registered encoders; see fix.diff

Verdict

Driver path hardware-gated on this guest (no radeon GPU, no /dev/dri), so evergreen_hdmi_enable/evergreen_dp_enable can never execute here; the type confusion is nonetheless proven statically with exact lines and machine-verified offsets, and the corruption PRIMITIVE is proven live on the guest kernel: evergreen_hdmi.c:429 loads radeon_encoder->enc_priv as a 312-byte radeon_encoder_atom_dig, but every LCD-carrying digital encoder (each eDP panel: UNIPHY*+ATOM_DEVICE_LCD_SUPPORT, atombios_encoders.c:2862-2869 and :2824-2836) carries a 280-byte radeon_encoder_lvds; the guard at evergreen_hdmi.c:431 reads dig->afmt (+288) OUT OF BOUNDS -- 8 bytes into the ADJACENT 288-byte slab chunk (zero slack) -- and consumes the garbage: non-NULL falls through, dig->afmt->offset feeds six WREG32 sites (:438-455) and dig->afmt->enabled = enable (:458; twin at :513) is a write through the OOB-read pointer. NEW vs DF-4635 (radeon_audio.c:262-480 sinks) and DF-4828 (r600_hdmi.c walk sinks): these are evergreen_hdmi.c's own dpms sinks, and DF-4828's dismissal of the dpms route is wrong for eDP -- an eDP connector yields ATOM_ENCODER_MODE_DP (atombios_encoders.c:824-829) so the radeon_audio_dpms gate at atombios_encoders.c:1848-1851 PASSES while the encoder carries the lvds-typed enc_priv, and radeon_audio_detect (radeon_audio.c:461-472) assigns hdmi_funcs (.dpms = evergreen_hdmi_enable, radeon_audio.c:206) to the eDP encoder; every screen blank/unblank on a DCE4+ eDP laptop walks the corrupted path. KLD replica on the live allocator: OOB read returned the groomed adjacent-chunk pointer, the :431 guard fell through, afmt->offset 0x5678 collected as the MMIO value, afmt->enabled flipped 0->1->0 through the garbage pointer (both dpms directions); zeroed neighbor = silent return (field-hiding case).