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

dce110_vblank_set underflows pipe_offset for VUPDATE sources (vupdate_int_entry initializes no .src_id) β€” pipe_ctx[178] far-OOB read feeding a wild arm_vert_intr indirect call

Summary

The vblank set handler is installed for VUPDATE entries too, but they carry no .src_id - the re-derive maps to INVALID and pipe_offset=(uint8_t)(0-78)=178, reading pipe_ctx[178] (a MAX_PIPES=6 array, tens of KB OOB) whose garbage tg is immediately indirect-called. Double-baked: even correctly remapped VUPDATE wraps because the subtraction base is VBLANK. No in-tree VUPDATE-set caller (latent, one caller away from a controlled-call-capable primitive). Replica: VUPDATE1 arms pipe 0x5a5a... and faults; index-derived-pipe fix validated on amdgpu.ko -Werror.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4933 Β· 11 files
FileTypeDescriptionSize
dce110_vupd.c β€” 18.1 KB view raw
build.sh β€” 109 B view raw
run.sh β€” 117 B view raw
build.log β€” 250 B view raw
run.log β€” 2.3 KB view raw
run.2.log β€” 2.3 KB view raw
amd_fix_apply.log β€” 568 B view raw
amdgpu_module_build.log β€” 12.5 KB view raw
fix.diff β€” 1.0 KB view raw
env.txt β€” 965 B view raw
VERDICT.md β€” 4.4 KB ↓ raw
VERDICT.md
↓ download raw

DF-4933 VERDICT

Status: reproduced (logic level, verbatim userland replica on the audit guest). Kernel trigger hardware-gated (no AMD GPU) and caller-gated/latent: no in-tree caller ever .sets a VUPDATE source on dce110 (grep across amdgpu_dm/dc: IRQ_TYPE_VUPDATE/DC_IRQ_SOURCE_VUPDATE* never passed to dc_interrupt_set). The defect is a genuine memory-safety shape one caller away from an OOB read + wild indirect call.

The defect

vblank_irq_info_funcs (.set = dce110_vblank_set) is installed for BOTH the VBLANK entries and the VUPDATE entries (irq_service_dce110.c:79-82,127-158). vblank_int_entry sets .src_id = VISLANDS30_IV_SRCID_D1_VERTICAL_INTERRUPT0 + reg_num (19..24), but vupdate_int_entry (:127-141) initializes no .src_id/.ext_id β€” both struct members are 0 for DC_IRQ_SOURCE_VUPDATE1..6.

dce110_vblank_set (:195-219) re-derives the pipe from the entry:

enum dc_irq_source dal_irq_src =
        dc_interrupt_to_irq_source(irq_service->ctx->dc,
                                   info->src_id,     /* 0 for VUPDATE */
                                   info->ext_id);    /* 0 for VUPDATE */
uint8_t pipe_offset = dal_irq_src - IRQ_TYPE_VBLANK;
...
core_dc->current_state->res_ctx.pipe_ctx[pipe_offset].stream_res.tg
...
if (!tg || !tg->funcs->arm_vert_intr(tg, 2))
  • to_dal_irq_source_dce110(0, 0) β†’ default: β†’ DC_IRQ_SOURCE_INVALID (0) (:390-392; IV src-id 0 is not the V_UPDATE range 7,9,…,17).
  • pipe_offset = (uint8_t)(0 - 78) (IRQ_TYPE_VBLANK = DC_IRQ_SOURCE_VBLANK1 = 78 per irq_types.h:133,154) = 178.
  • pipe_ctx[178] on the MAX_PIPES(=6 on dce110) array embedded in current_state (core_types.h struct resource_ctx) β€” a read tens of KB past the dc_state allocation.
  • The garbage tg (if non-NULL) is called through: tg->funcs->arm_vert_intr.

Secondary correctness note: even with a src_id the V_UPDATE IV ids are non-consecutive (7,9,11,13,15,17 β€” ivsrcid_vislands30.h:30-64), and even a correctly mapped VUPDATE source still underflows because the base is IRQ_TYPE_VBLANK, not IRQ_TYPE_VUPDATE (72..77 - 78 still wraps). The fix therefore derives the pipe from the table index, not from src-id remapping.

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

  • Controls: VBLANK1..6 set arm pipes 0..5 β€” the reachable (DRM vblank/flip) path is provably fine.
  • Trigger: VUPDATE1 set prints pipe_offset = (uint8_t)(-78) = 178, pipe_ctx[178] on a 6-entry array, then faults in the wild tg->funcs deref β€” SIGBUS exit 138 both runs (poison pointer 0x5a5a… is non-canonical β‡’ #GP; in-kernel either #GP or a non-canonical/invalid deref β‡’ panic).
  • Fixed: [fixed] source=72 -> pipe_offset=0 … β€” all VUPDATE and VBLANK sources arm the correct pipe; exit 0.

Impact ceiling

If a caller is ever introduced (e.g. DM starting to use IRQ_TYPE_VUPDATE, a DCN-port reusing this file, or any out-of-tree/updated caller), this is a far-OOB kernel read whose result is used as an indirect call target β€” panic at minimum; with heap grooming of the memory following the dc_state allocation a controlled-function-pointer call is conceivable. No such control is demonstrated here (the replica demonstrates the deterministic fault), so the honest impact is a kernel panic. Not reachable by an unprivileged local user through any existing in-tree path.

Fix validation

fix.diff replaces the src-id remap with an index-derived pipe: source = info - irq_service->info; pipe_offset = source - VUPDATE1 (VUPDATE range) else source - VBLANK1 β€” provably 0..5 for every VUPDATE/VBLANK table entry, independent of .src_id. Applied to the guest's /usr/src together with DF-4932's diff; first build attempt caught that removing the dc_ctx local broke the DC_ERROR macro (logger_interface.h:85 consumes dc_ctx) β€” the final diff keeps it. amdgpu.ko rebuilt under -Werror: RC=0, zero errors, irq_service_dce110.o linked (amdgpu_module_build.log). Harness fixed mode: correct pipes for all 12 sources, no crash. 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 (index-derived pipe_offset) applied to guest /usr/src with DF-4932's diff; amdgpu.ko rebuilt under -Werror (RC=0, zero errors, irq_service_dce110.o linked, amdgpu_module_build.log); harness baseline faults (Bus error exit 138) vs fixed arming the correct pipe for all 12 sources (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-4933

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log + run.2.log (decisive: 'table: vupdate src_id=0 ext_id=0 | vblank1 src_id=19'; controls 'control VBLANK1..6 set: armed pipe 0..5'; trigger 'pipe_offset = (uint8_t)(-78) = 178', 'pipe_ctx[178] on a 6-entry array; tg = 0x5a5a5a5a5a5a5a5a' then Bus error exit=138 both runs; fixed '[fixed] source=72 -> pipe_offset=0' … exit=0); dce110_vupd.c (verbatim replica); amd_fix_apply.log + amdgpu_module_build.log (-Werror module build); VERDICT.md

PoC changes

Authored fresh (no seed). Iterations: forward declarations + tg_stub ordering (gcc 8.3), unbuffered stdout for pre-crash evidence; the kernel fix.diff itself needed one revision β€” removing the dc_ctx local broke the DC_ERROR macro (logger_interface.h:85 consumes dc_ctx) and was caught by the -Werror module build, exactly the validation the module rebuild exists for.

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_offset = (source in VUPDATE1..VUPDATE6) ? source - DC_IRQ_SOURCE_VUPDATE1 : source - DC_IRQ_SOURCE_VBLANK1; (optionally also give vupdate_int_entry a .src_id = VISLANDS30_IV_SRCID_D1_V_UPDATE_INT + reg_num*2 for consistency).

Verdict

Reproduced at logic level with a verbatim userland replica of dce110_vblank_set (:195-219), to_dal_irq_source_dce110 (:315-393), the vupdate/vblank entry macros (:127-158) and the set dispatch (irq_service.c:77-124), run on the audit guest. vupdate_int_entry installs .set=dce110_vblank_set but initializes no .src_id/.ext_id (both 0 for DC_IRQ_SOURCE_VUPDATE1..6), so the function's re-derivation dc_interrupt_to_irq_source(dc,0,0) returns DC_IRQ_SOURCE_INVALID and pipe_offset=(uint8_t)(0-IRQ_TYPE_VBLANK=78) underflows to 178; core_dc->current_state->res_ctx.pipe_ctx[178] is a far-OOB read on the MAX_PIPES(6) array whose result (garbage tg) is immediately called through tg->funcs->arm_vert_intr(tg,2). Replica (current_state at the head of an 8MiB 0x5a-poisoned mapping, pipes 0..5 valid): VBLANK1..6 controls arm the correct pipes β€” proving the in-tree-reachable vblank path is fine β€” then set(VUPDATE1) faults deterministically (SIGBUS exit 138 both runs; kernel equivalent #GP/#PF => panic). Note the defect is double-baked: V_UPDATE IV ids are non-consecutive (7,9,..,17) and even a correctly remapped VUPDATE source still underflows because the subtraction base is IRQ_TYPE_VBLANK rather than IRQ_TYPE_VUPDATE. Reachability is latent and stated plainly: no in-tree caller ever .set's a VUPDATE source on dce110 (IRQ_TYPE_VUPDATE/DC_IRQ_SOURCE_VUPDATE* appear nowhere in amdgpu_dm/dc against dc_interrupt_set; dm_irq_state only uses PFLIP/VBLANK+otg_inst), and the guest has no AMD GPU β€” so no unprivileged trigger exists today; the memory-safety shape (OOB read feeding an indirect call) is one future caller away, with heap grooming of the memory after the dc_state allocation a conceivable route to a controlled call (not demonstrated; honest ceiling is a kernel panic). Fix validated: fix.diff (pipe derived from the table index with VUPDATE/VBLANK range selection β€” provably 0..5, independent of src_id; dc_ctx retained for the DC_ERROR macro) applied to guest /usr/src with DF-4932's diff; amdgpu.ko rebuilt under -Werror (RC=0, zero errors, irq_service_dce110.o linked); harness fixed mode arms the correct pipe for all 12 VUPDATE/VBLANK sources (exit 0); guest sources reverted md5-verified pristine.