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

irq_source_info_dce110[] leaves DC_IRQ_SOURCE_DC{1..6}_VLINE0 (enum 84..89) zero-initialized with .funcs==NULL; dal_irq_service_set/ack dereference info->funcs without a NULL check (DF-4713 family)

Summary

The 90-entry table fills only 0..83 - the six VLINE0 entries stay zero and find_irq_source_info checks only the index, so a set/ack of a VLINE0 source reads NULL+offsetof in the unguarded funcs deref. No in-tree caller passes 84..89 today (traced every set/ack caller) - a loaded trap incl. the shared shape in dce120/dce80 and the DAL_VALID_IRQ_SRC_NUM <= off-by-one noted. Replica harness: table walk shows the six NULLs, trigger SIGSEGVs in ack; dummy-entries + guards fix validated on amdgpu.ko -Werror.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4932 Β· 11 files
FileTypeDescriptionSize
dce110_vline0gap.c β€” 19.7 KB view raw
build.sh β€” 109 B view raw
run.sh β€” 117 B view raw
build.log β€” 50 B view raw
run.log β€” 1.8 KB view raw
run.2.log β€” 1.8 KB view raw
amd_fix_apply.log β€” 568 B view raw
amdgpu_module_build.log β€” 12.5 KB view raw
fix.diff β€” 1.1 KB view raw
env.txt β€” 965 B view raw
VERDICT.md β€” 4.8 KB ↓ raw
VERDICT.md
↓ download raw

DF-4932 VERDICT

Status: reproduced (logic level, verbatim userland replica on the audit guest). Kernel trigger hardware-gated (no AMD GPU β€” 0 PCI 0x1002 devices) and β€” importantly β€” caller-gated/latent: no in-tree caller of dc_interrupt_set()/ dc_interrupt_ack() passes a DC_IRQ_SOURCE_DC*_VLINE0 source on a dce110 service today. The defect is proven; the panic requires a future/alternate caller.

The defect

enum dc_irq_source (sys/dev/drm/amd/display/dc/irq_types.h:40-148) runs to DAL_IRQ_SOURCES_NUMBER = 90 and includes DC_IRQ_SOURCE_DC1_VLINE0 .. DC_IRQ_SOURCE_DC6_VLINE0 = 84..89 (VBLANK1..6 = 78..83).

irq_source_info_dce110[DAL_IRQ_SOURCES_NUMBER] (irq_service_dce110.c:226-313) is initialized with designated initializers covering exactly enum values 0..83 (INVALID, HPD, HPDRX, I2C_DDC, DPSINK, TIMER, PFLIP, PFLIP_UNDERLAY0, GPIOPAD0-30, DC1-6UNDERFLOW, DMCU_SCP, VBIOS_SW, VUPDATE1-6, VBLANK1-6). Entries 84..89 (the VLINE0 block) receive no initializer β€” they stay zero: enable_reg=0 … .funcs=NULL.

The generic dispatch only bounds-checks the index:

  • find_irq_source_info() β€” irq_service.c:82: if (source >= DAL_IRQ_SOURCES_NUMBER || source < DC_IRQ_SOURCE_INVALID) return NULL; β€” 84..89 pass.
  • dal_irq_service_ack() β€” irq_service.c:152: if (info->funcs->ack) … β€” reads *(NULL + offsetof(ack)) β‡’ fault.
  • dal_irq_service_set() β€” irq_service.c:116,118: acks first (same deref), then if (info->funcs->set).

This is the same NULL-funcs-dispatch shape as the reproduced DF-4713 family (amdgpu_irq.c), here rooted in this file's incomplete table rather than an unregistered DRM-level source.

Reachability audit (why "latent")

Every in-tree path into the service was traced:

  • dc_interrupt_set/ack (core/dc.c:1681-1693) do no source validation beyond the generic index check.
  • Callers: dm_irq_state (amdgpu_dm_irq.c:520-556) passes IRQ_TYPE_PFLIP/IRQ_TYPE_VBLANK + acrtc->otg_inst with otg_inst from status->primary_otg_inst = pipe_ctx->stream_res.tg->inst (amdgpu_dm.c:4856, dc_resource.c:2055) β€” hardware-bounded 0..5 on dce110; amdgpu_dm_hpd_init/fini pass dc_link->irq_source_hpd{,_rx} (HPD/HPDRX); amdgpu_dm.c:3014 passes IRQ_TYPE_VBLANK + otg_inst (same bound).
  • The interrupt arrival path (amdgpu_dm_irq_handler β†’ dc_interrupt_to_irq_source β†’ to_dal_irq_source_dce110) can only return {INVALID, VBLANK1-6, VUPDATE1-6, PFLIP1-6, HPD1-6, HPD RX1-6} β€” all covered; INVALID maps to the dummy entry.

So nothing passes 84..89 into a dce110 service in this tree. The gap becomes a panic the moment any code (a DCN-port, a DM change, or DAL_VALID_IRQ_SRC_NUM's own off-by-one β€” irq_types.h:158 allows src == DAL_IRQ_SOURCES_NUMBER, see notes) starts forwarding VLINE0 or edge-of-enum sources. dce120/dce80 tables have the identical shape (context note for the maintainers).

Reproduction (run.log / run.2.log, audit guest, cc 8.3)

  • Table walk: exactly table[84] … table[89] (.funcs == NULL); 6 gaps.
  • Control: index 90 rejected; PFLIP1 enable works via the generic RMW path.
  • Trigger: dal_irq_service_set(DC_IRQ_SOURCE_DC1_VLINE0=84) crashes inside dal_irq_service_ack at the info->funcs->ack load β€” SIGSEGV exit 139, identical across two runs.
  • Fixed: dummy entries + !info->funcs guards β†’ dal_irq_service_dummy_ack/set log and return false; exit 0.

Impact ceiling

A NULL-dereference in kernel context β‡’ hard panic (local DoS) on the machine owning the display. No write primitive, no info leak, no escalation chain. Not triggerable by an unprivileged local user through any existing path.

Fix validation

fix.diff = six dummy_irq_entry() rows for DC_IRQ_SOURCE_DC{1..6}_VLINE0 in the dce110 table + if (!info->funcs) {DC_LOG_ERROR; return false;} guards in dal_irq_service_set/dal_irq_service_ack (protects every asic table against any future gap, incl. dce120/dce80). Applied to the guest's /usr/src together with DF-4933's diff (patched md5s 724635b5…/305f0881… vs pristine 95266b5b…/35607b1a… β€” the first build attempt accidentally skipped the irq_service.c hunks (interactive patch on an already-patched tree); redone from pristine with both files verified patched at build time). amdgpu.ko rebuilt under -Werror: RC=0, zero errors, irq_service.o irq_service_dce110.o linked (amdgpu_module_build.log). Harness fixed mode: no crash, graceful dummy handling. Guest sources reverted md5-verified pristine.

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 (vline0 dummy entries + funcs guards) applied to guest /usr/src with DF-4933's diff; both patched files md5-verified at build time; amdgpu.ko rebuilt under -Werror (RC=0, zero errors, irq_service.o irq_service_dce110.o linked, amdgpu_module_build.log); harness baseline SIGSEGV (exit 139) vs fixed graceful dummy handling (exit 0); guest sources reverted md5-verified pristine.

fix.diff; amd_fix_apply.log; amdgpu_module_build.log; run.log (fixed section); VERDICT.md
↓ fix.diffper-fix-DF-4932

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log + run.2.log (decisive: 'table[84..89] (.funcs == NULL)', controls OK, 'trigger: dal_irq_service_set(DC_IRQ_SOURCE_DC1_VLINE0=84) -> dal_irq_service_ack -> info->funcs->ack with funcs==NULL' then Segmentation fault exit=139 both runs; fixed exit=0); dce110_vline0gap.c (verbatim table/dispatch replica); amd_fix_apply.log (patched vs pristine md5s) + amdgpu_module_build.log (-Werror build); VERDICT.md (full reachability trace)

PoC changes

Authored fresh (no seed). Iterations: forward declarations for the prototype-scoped struct parameters (gcc 8.3), unbuffered stdout so the pre-crash table walk and controls reach the log before SIGSEGV, and the fix-build redo after patch(1)'s interactive skip of already-applied hunks was caught by md5-at-build-time verification.

Verified recommended fix

Make the dce110 table total over the shared enum ([DC_IRQ_SOURCE_DC1..6_VLINE0] = dummy_irq_entry()) and add !info->funcs guards (log + return false) before the info->funcs->set/->ack dereferences in dal_irq_service_set/dal_irq_service_ack (irq_service.c:118/:152) so every asic table is protected against gaps.

Verdict

Reproduced at logic level with a verbatim userland replica of the dce110 irq table (irq_service_dce110.c:226-313 entry macros and body), the enum (irq_types.h:40-155), and the generic dispatch (irq_service.c:77-158) run on the audit guest. The table covers enum sources 0..83 only; DC_IRQ_SOURCE_DC1..6_VLINE0 (84..89 < DAL_IRQ_SOURCES_NUMBER=90) stay zero-initialized with .funcs==NULL, and find_irq_source_info (:82) bounds-checks only the index, so dal_irq_service_ack (:152) and dal_irq_service_set (:118) dereference info->funcs->ack/.set on NULL β€” replica set(DC_IRQ_SOURCE_DC1_VLINE0) SIGSEGVs deterministically (exit 139, both runs), the kernel-equivalent being a page fault at ~offsetof(.ack) => hard panic. This is the DF-4713 NULL-funcs-dispatch family rooted in this file's incomplete table. Reachability is latent: every in-tree dc_interrupt_set/ack caller was traced (dm_irq_state PFLIP/VBLANK+otg_inst hardware-bounded 0..5 via tg->inst per dc_resource.c:2055 and amdgpu_dm.c:4856; hpd_init/fini HPD/HPDRX; amdgpu_dm.c:3014 VBLANK+otg_inst) and the IH-arrival path (to_dal_irq_source_dce110) can only return covered sources or INVALID (dummy-handled) β€” nothing passes 84..89 into a dce110 service today, so no unprivileged trigger exists; the panic is one future caller away (DCN ports, DM changes, or the DAL_VALID_IRQ_SRC_NUM off-by-one at irq_types.h:158 which admits src==DAL_IRQ_SOURCES_NUMBER). dce120/dce80 tables share the shape (noted for maintainers). No write primitive, no leak; ceiling is a kernel panic. Fix validated: fix.diff (six vline0 dummy entries + !info->funcs guards in set/ack, protecting every asic table) applied to guest /usr/src together with DF-4933's; both files verified patched at build time (irq_service.c md5 724635b5, dce110 305f0881 β€” an initial attempt accidentally skipped the irq_service.c hunks and was redone from pristine); amdgpu.ko rebuilt under -Werror, RC=0, zero errors, irq_service.o irq_service_dce110.o linked; harness fixed mode handles VLINE0 gracefully (exit 0); guest sources reverted md5-verified pristine.