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

irq_service_dcn10[] leaves DC_IRQ_SOURCE_DC{1..6}_VLINE0 (enum 84..89) zero-initialized with .funcs==NULL; dal_irq_service_set/ack dereference info->funcs unguarded β€” DF-4713/DF-4932 family tail on the DCN10 (Raven) table

Summary

The table's last designator is index 83 while the shared enum runs to 90 - slots 84..89 stay zero and the shared dispatch derefs funcs->ack/.set unguarded. Verbatim replica on guest: baseline SIGSEGV exit 139 (deterministic x2), controls pass, 13x13 to_dal walk proves the IH path cannot reach the gap (latent; no in-tree caller passes 84..89). Fix (six vline0 dummies) validated on a full -Werror amdgpu.ko rebuild RC=0; sources reverted md5-verified. The family tail after DF-4932 (dce110).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4995 Β· 17 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 8.6 KB ↓ raw
dcn10_vline0gap.c β€” 28.0 KB view raw
build.sh β€” 128 B view raw
run.sh β€” 117 B view raw
build.log β€” 59 B view raw
run.log β€” 2.1 KB view raw
run.2.log β€” 2.1 KB view raw
panic.txt β€” 1.4 KB view raw
env.txt β€” 446 B view raw
fix.diff β€” 625 B view raw
amd_fix_apply.log β€” 1.1 KB view raw
amdgpu_module_build.log β€” 1.2 MB ↓ download
fix_build.log β€” 1.2 MB ↓ download
fix_run.log β€” 1005 B view raw
manifest.json β€” 2.0 KB view raw
verdict.json β€” 7.8 KB view raw

DF-4995 β€” irq_source_info_dcn10[] leaves DC_IRQ_SOURCE_DC{1..6}_VLINE0 with NULL .funcs; generic set/ack dereference it

Files: sys/dev/drm/amd/display/dc/irq/dcn10/irq_service_dcn10.c:245-331 (table), sys/dev/drm/amd/display/dc/irq/irq_service.c:118,152 (unguarded deref β€” shared, DF-4932/DF-4713 family)

Verbatim-replica verification (hardware-gated kernel trigger β€” no AMD GPU in the guest; the in-tree trigger is additionally caller-gated/latent, see VERDICT.md).

Build

sh build.sh     # cc -O2 -Wall -std=gnu11 [-DFIX_DF4995]

Run

sh run.sh       # baseline then fixed

Expected

Baseline (table + dispatch verbatim): - enum walk prints VUPDATE1=72 VBLANK1=78 DC1_VLINE0=84 DC6_VLINE0=89 DAL_IRQ_SOURCES_NUMBER=90 - exactly six entries have .funcs == NULL: table[84] … table[89] - controls: find_irq_source_info rejects index 90; PFLIP1 set programs its HUBPREQ0 enable register (generic path, dcn10 pflip .set==NULL); HPD1 ack runs the dcn10-local hpd_ack (polarity write observable: 0xffffffff β†’ 0xfffffffb); the to_dal_irq_source_dcn10 walk (13 src_id Γ— 13 ext_id incl. 0xdead) maps 168 combos to covered sources and 0 to 84..89 β€” the IH-driven entry can never synthesize a VLINE0 source (latent) - trigger: dal_irq_service_set(DC_IRQ_SOURCE_DC1_VLINE0) β†’ dal_irq_service_ack β†’ info->funcs->ack with funcs==NULL β†’ Segmentation fault, exit 139 (kernel equivalent: page fault at address ~offsetof(.ack) β‡’ panic). Deterministic across runs (run.2.log).

Fixed (vline0 dummy entries + !info->funcs guards in set/ack): the dummy handler logs and returns false; no crash; exit 0.

Fix compile of the real module: amd_fix_apply.log + amdgpu_module_build.log (fix.diff applied to guest /usr/src single-fix; amdgpu.ko fully rebuilt under -Werror, RC=0, zero error:/warning: diagnostics, irq_service_dcn10.c compiled at log:578 with the patched md5 recorded at log:790; sources reverted md5-verified pristine, make clean done).

VERDICT.md
↓ download raw

DF-4995 VERDICT β€” dcn10 irq table VLINE0 gap (NULL .funcs)

Bottom line

Reproduced at logic level with a verbatim userland replica of the DCN10 irq table (irq_service_dcn10.c:110-331 β€” hpd_ack, the funcs tables, the entry macros, the table body β€” plus to_dal_irq_source_dcn10.c:41-108 with the real DCN_1_0__SRCID/CTXID constants from amd/include/ivsrcid/irqsrcs_dcn_1_0.h), the enum (irq_types.h:40-148), and the generic dispatch (irq_service.c:77-158), run on the audit guest (DragonFly 6.5-DEVELOPMENT, cc 8.3).

The shared enum runs to DAL_IRQ_SOURCES_NUMBER=90, with DC_IRQ_SOURCE_DC1_VLINE0..DC6_VLINE0 = indices 84..89 (irq_types.h:140-147). The dcn10 table body's last designator is vblank_int_entry(5) == DC_IRQ_SOURCE_VBLANK6 = 83 (irq_service_dcn10.c:325-331): entries 84..89 stay zero-initialized, i.e. .funcs == NULL. This is the exact DF-4932 (dce110) family shape, now on the DCN10 (Raven) table β€” the family tail.

find_irq_source_info (irq_service.c:82) bounds-checks only the index, so for source 84..89 it happily returns &info[84..89], and dal_irq_service_ack (irq_service.c:152, invoked first by dal_irq_service_set at irq_service.c:116) dereferences info->funcs->ack with funcs==NULL β€” a read at NULL + offsetof(.ack) β‡’ page fault β‡’ hard panic. dal_irq_service_set (irq_service.c:118) has the same unguarded info->funcs->set.

Replica result (deterministic, run.log + run.2.log): - baseline: table[84..89] (.funcs == NULL), controls pass, dal_irq_service_set(DC_IRQ_SOURCE_DC1_VLINE0=84) β†’ Segmentation fault, exit 139 both runs; - fixed (-DFIX_DF4995: six vline0 dummy entries + !info->funcs guards): 0 NULL-funcs entries, dummy handlers log-and-return, exit 0.

Controls exercised (all pass, proving the dispatch itself is faithful): - OOB index 90 rejected by find_irq_source_info (the index bound is not the bug); - PFLIP1 set goes down the dcn10 generic path (.set==NULL, pflip_irq_info_funcs) and programs HUBPREQ0_DCSURF_SURFACE_FLIP_INTERRUPT (0x2200 β†’ 0x5 = ack_value 0x4 | enable_value 0x1 β€” the set-then-ack RMW shape); - HPD1 ack runs the dcn10-local hpd_ack replica verbatim: with DC_HPD_SENSE_DELAYED=1 the polarity field is cleared 0xffffffff β†’ 0xfffffffb β€” hpd_ack itself is correct, the file's only live bespoke function; - to_dal_irq_source_dcn10 walk (src_ids {0x3C..0x41, 0x4F..0x54, 9} Γ— ext_ids {0..11, 0xdead}): 168 combos map to covered sources, 0 combos map to 84..89 β€” the IH-arrival path (amdgpu_dm_irq.c:477-487 β†’ dc_interrupt_to_irq_source β†’ to_dal_irq_source) can never synthesize a VLINE0 source on DCN10.

Reachability β€” latent, traced honestly

Every in-tree caller of dc_interrupt_set/dc_interrupt_ack (dc.c:1682-1693) was traced:

Caller Sources passed Bounded by
dm_irq_state (amdgpu_dm_irq.c:536-559) dal_irq_type + acrtc->otg_inst, dal_irq_type ∈ {PFLIP1=26, VBLANK1=78} (VUPDATE never used) otg_inst ∈ [0,5] (tg instance, 6 OTGs) β‡’ ≀ PFLIP6/VBLANK6, all covered
dm_set_vblank (amdgpu_dm.c:3007-3015) IRQ_TYPE_VBLANK + acrtc->otg_inst same β‡’ ≀ VBLANK6=83
amdgpu_dm_hpd_init/fini (amdgpu_dm_irq.c:627-686) dc_link->irq_source_hpd/_rx HPD1+link_index ≀ HPD6RX, all covered
suspend/early-resume/late-resume (amdgpu_dm_irq.c:350-416) loops HPD1..HPD6RX covered
amdgpu_dm_irq_handler (amdgpu_dm_irq.c:477-487) to_dal_irq_source(...) result covered βˆͺ {INVALID} (proven by the walk control)

Nothing passes 84..89 into a dcn10 service today β‡’ no unprivileged trigger exists; the panic is one future caller away (a DM change using VLINE0 for freesync/PSR on DCN, a DCN port of a DCE path, or the DAL_VALID_IRQ_SRC_NUM off-by-one at irq_types.h:158 which admits src == DAL_IRQ_SOURCES_NUMBER β€” already noted under DF-4932). The guest has no AMD GPU (0 PCI 0x1002 devices; amdgpu not loaded), so the kernel-side trigger is hardware-gated anyway. No write primitive, no leak; the honest ceiling is a kernel panic (local DoS) if a trigger ever appears β€” identical classification to DF-4932/DF-4933 on dce110 and DF-4713 on the dispatch hole.

Severity Low (latent), confidence certain (the table gap and the NULL-deref shape are compile-time facts; the fault is deterministic in the replica).

Why this is a new finding, not a duplicate

DF-4932 filed the dce110 table's identical gap; this file (irq_service_dcn10.c) has its own table, and DF-4932's fix.diff covered only the dce110 table (its irq_service.c guards protect all asics, but the tree is read-only β€” nothing is applied). Each asic table must be made total over the shared enum; dcn10 is the last table in the family after dce80/dce110/dce120 were checked (dce120/dce80 share the shape β€” noted to maintainers under DF-4932; dcn10 is filed here).

Negative results (classes hunted and killed on this file)

  • DF-4933 family (vblank_set underflow): does NOT extend to dcn10 β€” vblank_irq_info_funcs = {.set=NULL,.ack=NULL} (irq_service_dcn10.c:152-155) and vupdate/vblank_int_entry set no .src_id; there is no dcn10_vblank_set and no pipe-offset arithmetic anywhere in the file; the generic set path uses only compile-time constants.
  • pflip HUBPREQ0..3 only: PFLIP5/PFLIP6/PFLIP_UNDERLAY0 are explicit dummy_irq_entry() (irq_service_dcn10.c:277-279) β€” covered, and dal_irq_service_dummy_set/ack are true no-ops (dce110:177-192, log+return false), so dummy entries can't MMIO stale reg 0.
  • to_dal out-of-range: switch with default: DC_IRQ_SOURCE_INVALID at both levels (irq_service_dcn10.c:100-106); INVALID is dummy-covered.
  • hpd_ack regs: status_reg/enable_reg set by every hpd_int_entry/ hpd_rx_int_entry designator; no runtime indexing.
  • construct/create (irq_service_dcn10.c:337-357): kzalloc NULL-checked; dal_irq_service_construct is void-returning in this tree (irq_service.c:53-63) β€” if init_data->ctx were NULL the service would carry ctx==NULL to dm_read_reg, but the only caller (dc_construct) always passes a live dc->ctx; upstream-identical shape, no attacker input reaches it. Info-level at most; noted, not filed.
  • "process" function: none exists in this DC version (irq_service.c has no dispatch-time process); the IH-side process NULL-call is DF-4483's amdgpu_irq.c hole, different file.
  • No user-controlled input reaches this file at all β€” its inputs are compile-time tables and GPU-generated IV src_id/ext_id.

Fix validation

  • fix.diff (file-local): six [DC_IRQ_SOURCE_DC{1..6}_VLINE0] = dummy_irq_entry() designators completing the table over the shared enum β€” applied single-fix to guest /usr/src (patch -V none -d /usr/src -p1, Hunk #1 succeeded at 328; patched md5 c31141c9ba1f50da042a2f63da779a97 recorded in amdgpu_module_build.log:790 at build time).
  • Full amdgpu.ko module rebuild in /usr/src/sys/dev/drm/amd/amdgpu (make -j6, -Werror per the module CFLAGS): RC=0, amdgpu.ko linked (3741272 bytes), irq_service_dcn10.c compiled at log:578, zero error:/warning: diagnostics (amdgpu_module_build.log).
  • Behavioral proof: replica fixed mode (-DFIX_DF4995) exercises exactly the patched shape β€” dummy handlers log and return, no crash, exit 0 (run.log FIXED section), vs baseline SIGSEGV 139.
  • Revert: reverse patch applied, md5 back to pristine 6ec2956a479058297a5c730b09c32423; make clean (0 .o/.ko left); /tmp scratch removed. Guest returned clean.
  • The shared-code !info->funcs guards in dal_irq_service_set/ack (irq_service.c:118/:152) are DF-4932's fix territory (defense-in-depth protecting every asic table); recommended alongside this table fix.

Artifacts

file what
dcn10_vline0gap.c verbatim replica (enum, struct, hpd_ack, funcs, macros, table, to_dal, dispatch)
build.sh / build.log cc -O2 -Wall -std=gnu11 (baseline + -DFIX_DF4995)
run.sh / run.log / run.2.log baseline SIGSEGV 139 vs fixed exit 0, deterministic Γ—2
fix.diff six vline0 dummy entries (git-apply-able, -p1)
amd_fix_apply.log pristine/patched md5s, hunk success
amdgpu_module_build.log full -Werror amdgpu.ko build, RC=0
env.txt guest uname/cc, no AMD GPU, no drm klds, pristine md5s

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed: fix.diff (six vline0 dummy entries) applied single-fix to guest /usr/src with 'patch -V none -d /usr/src -p1' (Hunk #1 succeeded at 328; patched md5 c31141c9ba1f50da042a2f63da779a97 verified at build time, amdgpu_module_build.log:790); full amdgpu.ko rebuilt under -Werror in /usr/src/sys/dev/drm/amd/amdgpu (make -j6, MODULE_BUILD_RC=0, amdgpu.ko linked 3741272 bytes, irq_service_dcn10.c compiled at log:578, zero error:/warning: diagnostics); behaviorally, the fixed-mode harness (same table shape: vline0 handled by dal_irq_service_dummy_ack/set which log and return false) survives the VLINE0 set with exit 0 while the baseline SIGSEGVs with 139; guest sources reverted md5-verified pristine (6ec2956a479058297a5c730b09c32423), make clean (0 .o/.ko), /tmp scratch removed, guest returned clean.

fix.diff; amd_fix_apply.log; amdgpu_module_build.log (also copied as fix_build.log); fix_run.log (fixed-mode run); run.log (baseline section); VERDICT.md 'Fix validation'
↓ fix.diffper-fix-DF-4995

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log + run.2.log (decisive: 'table[84..89] (.funcs == NULL)', '6 NULL-funcs entries in irq_source_info_dcn10[]', controls incl. 'control to_dal walk ... 0 to VLINE0 84..89 (ok β€” latent)', 'trigger: dal_irq_service_set(DC_IRQ_SOURCE_DC1_VLINE0=84) ... Segmentation fault ... EXIT=139' both runs; fixed 'survived the VLINE0 set (fixed behavior) EXIT=0'); panic.txt (fault signature); dcn10_vline0gap.c (verbatim table/dispatch/to_dal replica); amd_fix_apply.log (pristine/patched md5s) + amdgpu_module_build.log (-Werror full module build RC=0) + fix_run.log (fixed-mode run); VERDICT.md (full reachability trace + negative results)

PoC changes

Authored fresh (no seed), following the DF-4932 harness methodology. Iterations: (1) reg-field accessor macros needed the real dm_services.h:120-126 paste shape (reg_name ## __ ## reg_field ## _MASK/__SHIFT) and set_reg_field_value must ASSIGN back (dm_services.h) so the hpd_ack control's polarity write is observable (0xffffffff -> 0xfffffffb); (2) forward declaration of dal_irq_service_ack_generic placed before hpd_ack; (3) DragonFly patch(1) has no --no-backup-if-mismatch β€” used 'patch -V none -d /usr/src -p1' after the first interactive skip (caught by md5, file left pristine, redone). Added beyond the family harness: a to_dal_irq_source_dcn10 walk control (169 src/ext combos) proving the IH-driven entry can never return 84..89 β€” the latent-reachability evidence β€” and an hpd_ack control exercising this file's only live bespoke function.

Verified recommended fix

Make the dcn10 table total over the shared enum ([DC_IRQ_SOURCE_DC1..6_VLINE0] = dummy_irq_entry(), after vblank_int_entry(5)); for defense-in-depth also add the !info->funcs guards before the info->funcs->set/->ack dereferences in dal_irq_service_set/dal_irq_service_ack (irq_service.c:118/:152 β€” DF-4932's shared-code guard) so every asic table is protected against gaps.

Verdict

Reproduced at logic level with a verbatim userland replica of the dcn10 irq table (irq_service_dcn10.c:110-331 hpd_ack/funcs/entry-macros/table body, plus to_dal_irq_source_dcn10 :41-108 with the real DCN_1_0 SRCID/CTXID constants), the enum (irq_types.h:40-148), and the generic dispatch (irq_service.c:77-158), run on the audit guest. The shared enum runs to DAL_IRQ_SOURCES_NUMBER=90 but irq_source_info_dcn10[] stops at DC_IRQ_SOURCE_VBLANK6 (=83): DC_IRQ_SOURCE_DC1..6_VLINE0 (84..89) stay zero-initialized with .funcs==NULL, and find_irq_source_info (:82) bounds-checks only the index, so dal_irq_service_ack (:152, invoked first by dal_irq_service_set at :116) and dal_irq_service_set (:118) dereference info->funcs->ack/.set on NULL β€” replica set(DC_IRQ_SOURCE_DC1_VLINE0) SIGSEGVs deterministically (exit 139, run.log + run.2.log), the kernel-equivalent being a page fault at ~offsetof(.ack) => hard panic. This is the DF-4713/DF-4932 NULL-funcs-dispatch family rooted in this file's incomplete table β€” the family tail after dce80/dce110 (DF-4932 filed dce110's identical gap; each asic table is its own code). Reachability is latent and traced honestly: every in-tree dc_interrupt_set/ack caller was checked (dm_irq_state PFLIP/VBLANK+otg_inst bounded 0..5; dm_set_vblank VBLANK+otg_inst; hpd_init/fini HPD/HPDRX; suspend/resume loops HPD1..HPD6RX; the IH path amdgpu_dm_irq.c:477-487 can only receive to_dal-mapped covered sources or INVALID β€” the harness to_dal walk (13x13 src/ext combos incl. 0xdead) maps 168 to covered sources and 0 to 84..89) β€” nothing passes 84..89 into a dcn10 service today, so no unprivileged trigger exists; the panic is one future caller away (DM change using VLINE0 on DCN, or the DAL_VALID_IRQ_SRC_NUM off-by-one at irq_types.h:158 already noted under DF-4932). The guest also has no AMD GPU (0 PCI 0x1002 devices), so the kernel trigger is hardware-gated regardless. No write primitive, no leak; honest ceiling is a kernel panic (local DoS). Fix validated: fix.diff (six vline0 dummy entries completing the table over the shared enum) applied single-fix to guest /usr/src (patch -V none -p1, hunk succeeded at 328; patched md5 c31141c9 recorded at amdgpu_module_build.log:790 at build time); full amdgpu.ko rebuilt under -Werror (make -j6, RC=0, amdgpu.ko linked, irq_service_dcn10.c compiled at log:578, zero error:/warning: diagnostics); harness fixed mode (same shape: dummy handlers + funcs guards) exits 0 vs baseline 139; guest sources reverted md5-verified pristine (6ec2956a), make clean done, guest returned clean.