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

dce80 vupdate entries carry no .src_id, so dce110_vblank_set (wired by dce80's own vblank_irq_info_funcs) re-derives DC_IRQ_SOURCE_INVALID and underflows pipe_offset to 178 β†’ far-OOB pipe_ctx[178] read feeding a wild arm_vert_intr indirect call β€” the DF-4933 twin on the DCE80 table wiring

Summary

vupdate entries init no .src_id but use vblank_irq_info_funcs whose .set re-maps (0,0)->INVALID then computes (uint8_t)(0-78)=178 - OOB pipe_ctx index feeding a wild tg->funcs->arm_vert_intr call; even a remapped VUPDATE source still underflows (wrong base). Latent (no in-tree VUPDATE .set caller). Replica: SIGBUS through poisoned tg x2; index-derived-offset fix in the shared callee (fixes both dce80 and dce110 tables); amdgpu.ko rebuild clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5055 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
dce80_vupd.c β€” 22.9 KB view raw
build.sh β€” 128 B view raw
run.sh β€” 117 B view raw
build.log β€” 365 B view raw
run.log β€” 2.9 KB view raw
env.txt β€” 381 B view raw
fix.diff β€” 1.7 KB view raw
amd_fix_apply.log β€” 850 B view raw
amdgpu_module_build.log β€” 1.2 MB ↓ download
manifest.json β€” 2.3 KB view raw
verdict.json β€” 5.7 KB view raw

DF-5055 β€” dce80 vupdate entries carry no .src_id, so dce110_vblank_set (wired by dce80's own vblank_irq_info_funcs) underflows pipe_offset to 178 β†’ far-OOB pipe_ctx[] read feeding a wild arm_vert_intr call β€” the DF-4933 twin on the DCE80 table

What

irq_service_dce80.c defines its own vblank_irq_info_funcs = { .set = dce110_vblank_set, .ack = NULL } (dce80:82-85) and assigns it to BOTH the vupdate entries (vupdate_int_entry, dce80:132-146) and the vblank entries (vblank_int_entry, dce80:148-163). The vblank macro sets .src_id = VISLANDS30_IV_SRCID_D1_VERTICAL_INTERRUPT0 + reg_num (19..24) β€” correct. The vupdate macro initializes no .src_id: for DC_IRQ_SOURCE_VUPDATE1..6 the info has src_id=0/ext_id=0. dce110_vblank_set (irq_service_dce110.c:195-219) then re-derives the source via dc_interrupt_to_irq_source(dc, 0, 0) β†’ to_dal_irq_source_dce110 default β†’ DC_IRQ_SOURCE_INVALID (0), and

uint8_t pipe_offset = dal_irq_src - IRQ_TYPE_VBLANK;   /* 0 - 78 = 178 */
struct timing_generator *tg =
    core_dc->current_state->res_ctx.pipe_ctx[pipe_offset].stream_res.tg;
if (enable) {
    if (!tg || !tg->funcs->arm_vert_intr(tg, 2)) { ... }
}

becomes pipe_ctx[178] on the MAX_PIPES(6) array β€” a far-OOB read whose garbage tg is immediately called through tg->funcs->arm_vert_intr. DF-4933 filed this for the dce110 table; the DCE80 table's macros wire the identical trap independently.

Reproduce (on the QEMU guest; no AMD GPU needed β€” verbatim userland replica)

# build:  sh build.sh    # cc -O2 -Wall -std=gnu11 [-DFIX_DF5055]
# run:    sh run.sh      # baseline then fixed

The replica carries the dce80 vupdate/vblank entry macros + funcs table, dce110_vblank_set, to_dal_irq_source_dce110, and the set dispatch verbatim, with a poisoned (0x5a) 8MiB current_state mapping: pipes 0..5 carry real timing generators, everything past the array is garbage.

Expected output

Baseline (run.log): VBLANK1..6 controls arm pipes 0..5 (the in-tree-reachable path is fine), then dal_irq_service_set(DC_IRQ_SOURCE_VUPDATE1) prints pipe_offset = (uint8_t)(0-78) = 178 and SIGBUS exit 138 inside tg->funcs->arm_vert_intr (tg = 0x5a5a5a5a5a5a5a5a from the poisoned pipe_ctx[178]). Deterministic x2 (second run in run.log).

Fixed (-DFIX_DF5055, index-derived pipe_offset): all 12 VUPDATE/VBLANK sources arm the correct pipes 0..5, exit 0. Also validated by the full -Werror amdgpu.ko rebuild with the fix applied (RC=0, irq_service_dce110.o linked, amdgpu_module_build.log).

Severity rationale

Low (family rating, matching DF-4933): latent β€” no in-tree caller ever .sets a VUPDATE source (suspend/resume touch HPD/HPDRX only; vblank enable is IRQ_TYPE_VBLANK+otg_inst; the IH path converts through to_dal_irq_source_dce110 and amdgpu_dm never registers the V_UPDATE src_ids 7,9,..,17). Guest has no AMD GPU. The memory-safety shape β€” OOB read feeding an indirect call β€” is one future caller away, with heap grooming around the dc_state allocation a conceivable route to a controlled call (not demonstrated; honest ceiling is a kernel panic). Note the defect is double-baked: even a correctly remapped VUPDATE source would still underflow, because the subtraction base is IRQ_TYPE_VBLANK, not IRQ_TYPE_VUPDATE.

VERDICT.md
↓ download raw

DF-5055 β€” VERDICT

Status: reproduced (panic-equivalent SIGBUS in the verbatim replica), fix validated (module -Werror rebuild + replica fixed mode). Impact: far-OOB read feeding a wild indirect call (Low, latent). Confidence: certain.

The bug (path:line)

  • irq_service_dce80.c:82-85 β€” the dce80 file's OWN vblank_irq_info_funcs = { .set = dce110_vblank_set, .ack = NULL }.
  • irq_service_dce80.c:145 β€” vupdate_int_entry assigns those funcs to DC_IRQ_SOURCE_VUPDATE1..6 (designators 72..77) while initializing no .src_id/.ext_id (macro body dce80:132-146).
  • irq_service_dce80.c:161-162 β€” vblank_int_entry DOES set .src_id = VISLANDS30_IV_SRCID_D1_VERTICAL_INTERRUPT0 + reg_num (19..24, consecutive β€” ivsrcid_vislands30.h:66-156), so only the vupdate side is broken.
  • irq_service_dce110.c:195-219 β€” dce110_vblank_set: dal_irq_src = dc_interrupt_to_irq_source(dc, 0, 0) β†’ to_dal_irq_source_dce110(0,0) β†’ default β†’ DC_IRQ_SOURCE_INVALID; uint8_t pipe_offset = dal_irq_src - IRQ_TYPE_VBLANK β†’ (uint8_t)(0-78) = 178; pipe_ctx[178] on the 6-entry array β†’ garbage tg; !tg || !tg->funcs->arm_vert_intr(tg, 2) loads and CALLS through the garbage pointer.
  • Twin of DF-4933 (dce110's own vupdate entries have the same shape); this filing documents the DCE80 table's independent wiring of it.

Reproduction (verbatim userland replica on the audit guest)

dce80_vupd.c: dce80 vupdate/vblank macros + funcs table, dce110_vblank_set (body verbatim; only the #ifdef'd pipe_offset line differs in fixed mode), to_dal_irq_source_dce110 (verbatim, real VISLANDS30 constants), dc_interrupt_to_irq_source (dc.c:1672-1678 collapse), the set dispatch (irq_service.c:77-124), and a poisoned 8MiB current_state (0x5a) with valid tgs in pipes 0..5.

run.log (baseline): - table: vupdate1 src_id=0 ext_id=0 | vblank1 src_id=19 β€” the asymmetry, straight out of the table; - controls: VBLANK1..6 set arm pipes 0..5 via the real conversion path (in-tree-reachable path proven fine); - trigger: set(VUPDATE1) β†’ prints pipe_offset = (uint8_t)(0-78) = 178 β†’ Bus error, exit 138 (pipe_ctx[178].stream_res.tg = 0x5a5a5a5a5a5a5a5a β†’ tg->funcs load faults); - ---- SECOND BASELINE RUN ---- β†’ second=138 (deterministic x2). Fixed (-DFIX_DF5055): [fixed] VUPDATE1 -> pipe 0 armed via index-derived offset; exit 0.

Fix validation (fix_status: fixed)

  • fix.diff (DF-4933's shape, authored against the real dce110 tree): a vblank_pipe_offset(irq_service, info) helper derives the pipe from the TABLE INDEX β€” (info - irq_service->info), VUPDATE range β†’ source - DC_IRQ_SOURCE_VUPDATE1, else source - DC_IRQ_SOURCE_VBLANK1 β€” provably 0..5 for both ranges, independent of src_id. The one-line call-site change replaces the underflowing subtraction. Applies clean (patch -p1, Hunk #1 at 192 + Hunk #2 at 226).
  • Together with DF-5054's diff: full make -j6 amdgpu.ko rebuild under -Werror: RC=0, zero diagnostics, irq_service_dce110.o linked (amdgpu_module_build.log untrimmed, 777 lines).
  • Behavioral: replica fixed mode arms the correct pipe for all 12 VUPDATE/VBLANK sources, exit 0 (re-run post-build).
  • Revert: reverse patch, md5 back to pristine 35607b1aeb1dc71fa3108f6573b6c5fd; make clean; guest reset to the with-src snapshot afterwards.

Reachability / ceiling (honest)

Latent (identical to DF-4933's verdict): no in-tree caller .set's a VUPDATE source. Traced: amdgpu_dm_irq.c:353/380/406 (HPD/HPDRX only), amdgpu_dm.c:3014-3015 (VBLANK+otg_inst), amdgpu_dm_irq.c:570 (PFLIP); the IH conversion path can return VUPDATE only for V_UPDATE src_ids 7,9,..,17 (odd, non-consecutive β€” see the to_dal switch) which amdgpu_dm never registers handlers for. Guest has no AMD GPU β†’ replica-harness methodology (family precedent). If a future caller appears: OOB read β†’ wild indirect call; a groomed heap around the dc_state allocation could in principle aim tg (not demonstrated β€” honest ceiling is a kernel panic).

Also checked on this file (pass-2 negatives)

  • See DF-5054's VERDICT for the dce80 table sweep (VLINE0 gap = its own finding; hpd_rx/pflip NULL member funcs are safe; irq_service_funcs type has a single member; hpd/pflip/gpio/crtc pin ranges in range; DAL_VALID_IRQ_SRC_NUM off-by-one noted, not re-filed).

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 (vblank_pipe_offset index-derived helper + one-line call-site change in dce110_vblank_set) applied to guest /usr/src (patched md5 f6754cc7d3bac717925e9e1cc398a902, together with DF-5054's diff); full make -j6 amdgpu.ko rebuild under -Werror RC=0, zero diagnostics, irq_service_dce110.o linked (amdgpu_module_build.log); replica fixed mode arms the correct pipe for all 12 VUPDATE/VBLANK sources (exit 0) vs baseline SIGBUS 138; sources reverted md5-verified pristine (35607b1aeb1dc71fa3108f6573b6c5fd), module objects cleaned, guest reset to the with-src snapshot.

fix.diff; amd_fix_apply.log; amdgpu_module_build.log; run.log (baseline crash + fixed exit 0); VERDICT.md
↓ fix.diffper-fix-DF-5055

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (decisive: 'table: vupdate1 src_id=0 ext_id=0 | vblank1 src_id=19'; controls 'control VBLANK1..6 set' arming pipes 0..5; trigger 'pipe_offset = (uint8_t)(0-78) = 178' -> Bus error exit 138; '---- SECOND BASELINE RUN ---- second=138'; fixed '[fixed] VUPDATE1 -> pipe 0 armed via index-derived offset; exit 0'); dce80_vupd.c (verbatim replica); amd_fix_apply.log + amdgpu_module_build.log (-Werror amdgpu.ko rebuild RC=0); fix.diff; VERDICT.md

PoC changes

Authored fresh (no seed), mirroring DF-4933's dce110_vupd replica onto the DCE80 wiring: two compile/logic iterations (forward-decl ordering; stub signature mismatch; fixed-mode exit logic initially treated a successful return as BAD and was corrected to expect index-derived success).

Verified recommended fix

Derive pipe_offset from the table index instead of re-mapping info->src_id: source = (enum dc_irq_source)(info - irq_service->info); pipe = (source in VUPDATE1..VUPDATE6) ? source - DC_IRQ_SOURCE_VUPDATE1 : source - DC_IRQ_SOURCE_VBLANK1 (covers dce80 and dce110 tables alike).

Verdict

Reproduced at logic level with a verbatim userland replica of the dce80 vupdate/vblank entry macros + the dce80-local vblank_irq_info_funcs {.set=dce110_vblank_set} (irq_service_dce80.c:82-85,132-163), dce110_vblank_set itself (irq_service_dce110.c:195-219), to_dal_irq_source_dce110, and the set dispatch (irq_service.c:77-124), run on the audit guest against a 0x5a-poisoned 8MiB current_state with valid timing generators in pipes 0..5. The vupdate entries carry src_id=0/ext_id=0 (macro initializes no .src_id, unlike the vblank entries' 19..24), so dce110_vblank_set's re-derivation to_dal_irq_source_dce110(0,0) returns DC_IRQ_SOURCE_INVALID and uint8_t pipe_offset = dal_irq_src - IRQ_TYPE_VBLANK' underflows to 178; core_dc->current_state->res_ctx.pipe_ctx[178] reads the poisoned mapping (tg=0x5a5a5a5a5a5a5a5a) and the very nexttg->funcs->arm_vert_intr(tg,2)' faults β€” SIGBUS exit 138, deterministic x2. VBLANK1..6 controls arm pipes 0..5 through the REAL conversion path, proving the in-tree-reachable vblank path healthy β€” this is the DF-4933 (dce110) twin wired independently by the DCE80 table's own macros. Latent like DF-4933: no in-tree caller ever .set's a VUPDATE source (suspend/resume HPD/HPDRX-only, vblank enable = IRQ_TYPE_VBLANK+otg_inst, pflip = IRQ_TYPE_PFLIP; the IH converter returns VUPDATE only for V_UPDATE src_ids 7,9,..,17 which amdgpu_dm never registers), and the guest has no AMD GPU. The defect is double-baked upstream (even a correctly remapped VUPDATE source underflows, since the base is IRQ_TYPE_VBLANK not IRQ_TYPE_VUPDATE). Ceiling if a future caller appears: far-OOB read feeding a wild indirect call β€” a groomed heap around the dc_state allocation could in principle aim tg (not demonstrated; honest ceiling is a kernel panic). FIX VALIDATED: fix.diff (vblank_pipe_offset helper deriving the pipe from the TABLE INDEX β€” provably 0..5 for both ranges, DF-4933's fix shape) -> replica fixed mode arms the correct pipe for all 12 VUPDATE/VBLANK sources, exit 0; full amdgpu.ko rebuild with the fix on guest /usr/src under -Werror RC=0, zero diagnostics, irq_service_dce110.o linked; sources reverted md5-verified pristine.