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

NULL-pointer dereference of radeon_encoder->enc_priv in dce6_audio_get_pin() encoder walk (dce6_afmt.c:104-105)

Summary

The pin walk derefs dig->pin for every digital encoder without checking enc_priv - which is NULL-able for those exact ids (missing/unparsable LVDS_Info or kzalloc failure). One NULL system-wide panics the next audio-capable display detect; audio enabled by default on DCE6+ (SI/CIK). Sibling unguarded derefs at radeon_audio.c:483/266-267; same code verbatim upstream. NULL-page read only - panic, not exploitable. Hardware-gated on guest. NULL-guard fix applies cleanly (dce6_afmt + radeon_audio siblings).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5028 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.7 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
env.txt β€” 1023 B view raw
fix.diff β€” 612 B view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 3.7 KB view raw

DF-5028 β€” NULL-pointer dereference of radeon_encoder->enc_priv in dce6_audio_get_pin() (dce6_afmt.c)

What this is

sys/dev/drm/radeon/dce6_afmt.c:104-105 dereferences dig->pin for every digital encoder on the device's encoder list without first checking that dig = radeon_encoder->enc_priv is non-NULL:

list_for_each_entry(encoder, &rdev->ddev->mode_config.encoder_list, head) {
        if (radeon_encoder_is_digital(encoder)) {          /* id-only test   */
                radeon_encoder = to_radeon_encoder(encoder);
                dig = radeon_encoder->enc_priv;
                if (dig->pin == pin)                       /* NULL deref     */
                        pin_count++;
        }
}

Why enc_priv can be NULL for a digital encoder

radeon_encoder_is_digital() (sys/dev/drm/radeon/radeon_encoders.c:423-445) filters only by encoder_id. But for several of those very ids, radeon_add_atom_encoder() assigns enc_priv from functions that can return NULL:

  • atombios_encoders.c:2838 and :2872 β€” for INTERNAL_LVDS, INTERNAL_TMDS1, INTERNAL_KLDSCP_TMDS1, INTERNAL_LVTM1, INTERNAL_UNIPHY* with ATOM_DEVICE_LCD_SUPPORT, enc_priv = radeon_atombios_get_lvds_info(). radeon_atombios.c:1630-1640: when the VBIOS has no parsable LVDS_Info table (atom_parse_data_header() fails) the function returns its still-NULL lvds; it also returns NULL when kzalloc fails (radeon_atombios.c:1639).
  • atombios_encoders.c:2842/2876/2880 β€” enc_priv = radeon_atombios_set_dig_info() returns NULL on kzalloc failure (atombios_encoders.c:2760-2763).

So an encoder whose id satisfies radeon_encoder_is_digital() can carry a NULL enc_priv either from a missing/malformed VBIOS LVDS table or from a failed allocation during encoder creation.

Reachability

Audio is enabled by default (radeon_audio = -1, sys/dev/drm/radeon/radeon_drv.c:180; gate is radeon_audio != 0, radeon_connectors.c:1766). On every connector (hot-)detect:

radeon_dp_detect / radeon_dvi_detect (radeon_connectors.c:1767, :1439) -> radeon_audio_detect() (radeon_audio.c:444) dig = radeon_encoder->enc_priv; ... dig->pin <- same unguarded deref at :483 -> radeon_audio_get_pin() (radeon_audio.c:425) -> dce6_audio_get_pin() (dce6_afmt.c:86) walks all encoders -> dig->pin (dce6_afmt.c:105)

The walk at dce6_afmt.c:101 touches every digital encoder on the system, not just the one being detected, so a NULL enc_priv on any DCE6-era digital encoder panics the machine during an audio-capable display detect. The sibling unprotected derefs live in radeon_audio.c:483 (dig->pin in radeon_audio_detect) and radeon_audio.c:267 (dig->pin in radeon_audio_enable's identical walk) β€” same bug family, other file.

Impact

Kernel NULL-page read at a small fixed struct offset -> kernel panic. Not exploitable beyond DoS (no controlled data at the dereferenced address; page 0 is unmapped). Trigger requires SI/CIK (DCE6+) radeon hardware with audio (the default) and a VBIOS/an allocation state that leaves a digital encoder's enc_priv NULL β€” hence Low severity. The same code exists verbatim in upstream Linux drivers/gpu/drm/radeon/dce6_afmt.c.

Why no runtime PoC on this guest

The audit guest is QEMU/KVM with a Bochs std-VGA device (vgapci0: chip=0x11111234, see env.txt). There is no AMD GPU, so dce6_afmt.c never executes: radeon_audio_init is only reached on radeon DCE6+ ASICs. Reproduction would need SI/CIK hardware (or GPU passthrough) plus a doctored VBIOS (or forced allocation failure) β€” not possible here. The finding is a source-level proof; see VERDICT.md.

VERDICT.md
↓ download raw

DF-5028 β€” verdict narrative

Mode: pass-2 audit of sys/dev/drm/radeon/dce6_afmt.c (GLM 5.3)

Result: not reproduced at runtime (hardware-gated); source-level proof

The bug is a missing NULL check on radeon_encoder->enc_priv before dig->pin in the encoder-list walk inside dce6_audio_get_pin() (sys/dev/drm/radeon/dce6_afmt.c:104-105).

Proof chain (all path:line)

  1. dce6_afmt.c:101-107 β€” the walk dereferences dig->pin for every encoder for which radeon_encoder_is_digital() is true. No NULL guard.
  2. radeon_encoders.c:423-445 β€” radeon_encoder_is_digital() tests only radeon_encoder->encoder_id; nothing guarantees enc_priv != NULL.
  3. atombios_encoders.c:2838, 2872 β€” for digital encoder ids with ATOM_DEVICE_LCD_SUPPORT, enc_priv = radeon_atombios_get_lvds_info().
  4. radeon_atombios.c:1626-1640 β€” get_lvds_info() returns NULL when atom_parse_data_header(..., LVDS_Info, ...) fails (no/invalid VBIOS table) and when kzalloc fails; caller does not check.
  5. atombios_encoders.c:2757-2766 β€” set_dig_info() (the other digital enc_priv source, lines 2842/2876/2880) returns NULL on kzalloc failure.
  6. Reachable by default: radeon_audio = -1 (radeon_drv.c:180), audio gate is radeon_audio != 0 (radeon_connectors.c:1766); detect paths call radeon_audio_detect() (radeon_connectors.c:1439, :1767) -> radeon_audio_get_pin() (radeon_audio.c:425) -> dce6_audio_get_pin() (dce6_afmt.c:86) on any audio-capable DCE6+ system.
  7. Note the sibling unguarded derefs at radeon_audio.c:483 (detect) and radeon_audio.c:266-267 (radeon_audio_enable walk) β€” the same event would usually crash there first for the calling encoder; the dce6_afmt.c walk additionally crashes on any other digital encoder with NULL enc_priv on the system.

Why the crash is a panic, not an exploit primitive

struct radeon_encoder_atom_dig is ~230 bytes; pin sits at a small fixed offset, so the access is a read from the zero page (unmapped on amd64 with vm.pmap.pg_ps_enabled-style protections; NULL page is never mappable by userland on DragonFly). No attacker-controlled data is read or written. Impact ceiling: kernel panic (DoS) on machines with anomalous VBIOS or under extreme memory pressure. Not a memcorrupt primitive.

Verification attempt on the audit guest

  • uname -a: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (see env.txt).
  • pciconf -l: only display device is vgapci0 ... chip=0x11111234 (Bochs QEMU std VGA). No ATI/AMD GPU -> radeon driver never attaches -> dce6_afmt.c code paths cannot execute on this guest.
  • Status therefore not_reproduced (impact none demonstrated at runtime); the finding stands on the source-level chain above, which is deterministic.

Fix

fix.diff (this directory) adds the NULL guard in dce6_afmt.c:105 and the primary sibling in radeon_audio.c:459 (radeon_audio_detect). A third identical walk in radeon_audio.c:266 (radeon_audio_enable) should get the same guard. Fix cannot be boot-validated without AMD hardware -> fix_status: not_testable.

Other classes hunted in dce6_afmt.c and killed (pass-2 negative result)

  • ELD/SAD walk bounds: sads[]/sad_count come from drm_edid_to_sad() (drm_edid.c:4034) which allocates exactly count = dbl/3 entries and returns that same count; caller radeon_audio_write_sad_regs (radeon_audio.c:375-381) rejects <= 0. dce6_afmt_write_sad_regs's for (j = 0; j < sad_count; j++) (dce6_afmt.c:246) is therefore in-bounds by construction.
  • sadb[0] (dce6_afmt.c:181, :208) is guarded by if (sad_count); drm_edid_to_speaker_allocation returns count==0 with sadb==NULL or a 3-byte kmemdup with count==3 β€” never a short non-NULL buffer.
  • pin-array bounds: num_pins <= 7 == RADEON_MAX_AFMT_BLOCKS (radeon_audio.c:316-328, radeon_mode.h:52, radeon.h:1769) β€” walks at dce6_afmt.c:75 and :96 are in-bounds.
  • div-by-zero: radeon_audio_decode_dfs_div() returns 0 for any out-of-range WDIVIDER field and dce6_afmt.c:326 guards if (div); clock*100 <= 60,000,000 cannot overflow u32.
  • register write indexes: all endpoint writes go through dce6_endpoint_rreg/wreg under rdev->end_idx_lock (dce6_afmt.c:49-52, :60-67) with offsets from the fixed pin_offsets[] table β€” no attacker-controlled index.
  • latency fields (dce6_afmt.c:144-156): connector->latency_present[] / video_latency[] / audio_latency[] are fixed-size fields written by the EDID CEA parser, always in-bounds here.

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 adds the NULL guards (dce6_afmt.c:105, radeon_audio.c detect) but cannot be boot-validated on this guest: no AMD GPU means neither baseline nor patched kernel can execute the DCE6 audio path. Fix is trivially correct by inspection.

fix.diff (applies cleanly to pristine sys/, patch --dry-run verified); no runtime validation possible - see env.txt.
↓ fix.diffper-fix-DF-5028

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md 'Proof chain' (7 steps, each path:line); env.txt documents the guest's display hardware (vgapci0 chip=0x11111234, Bochs std VGA) and stock X86_64_GENERIC lacking radeon hardware - the cited path cannot execute on this guest.

PoC changes

No runnable trigger possible on this guest (no AMD GPU; dce6_afmt executes only on DCE6+ ASICs). Verification was environment-level: confirmed the guest cannot execute the path; the finding stands on the deterministic source chain.

Verified recommended fix

NULL-check enc_priv before dereferencing dig->pin in dce6_audio_get_pin() (and the radeon_audio.c:483/:267 siblings): see fix.diff.

Verdict

dce6_audio_get_pin() (sys/dev/drm/radeon/dce6_afmt.c:104-105) dereferences dig->pin after dig = radeon_encoder->enc_priv without a NULL check for every encoder that radeon_encoder_is_digital() accepts; that predicate (radeon_encoders.c:423-445) tests only encoder_id, while radeon_add_atom_encoder() assigns enc_priv from radeon_atombios_get_lvds_info() (returns NULL when the VBIOS LVDS_Info table is missing/unparsable or kzalloc fails - radeon_atombios.c:1630-1640) and from radeon_atombios_set_dig_info() (returns NULL on kzalloc failure - atombios_encoders.c:2760-2763) for exactly those digital ids. Reachable by default (radeon_audio=-1, radeon_drv.c:180; gate radeon_audio != 0, radeon_connectors.c:1766) on any display detect via radeon_audio_detect (radeon_connectors.c:1439/:1767) -> radeon_audio_get_pin (radeon_audio.c:425) -> dce6_audio_get_pin; the walk crashes on ANY digital encoder system-wide with NULL enc_priv, and the sibling unguarded derefs are radeon_audio.c:483 and radeon_audio.c:267. Impact ceiling is a kernel NULL-page read = panic (DoS), not a corruption primitive. Not reproduced at runtime because the audit guest has no AMD GPU (Bochs VGA 1234:1111, env.txt): dce6 code only executes on SI/CIK ASICs; source-level proof chain is deterministic, hence confidence likely, severity Low (hardware+firmware anomaly or OOM precondition).