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)
PoC verification
Evidence pack
findings/poc/DF-4933 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 theMAX_PIPES(=6 on dce110) array embedded incurrent_state(core_types.hstruct resource_ctx) β a read tens of KB past thedc_stateallocation.- 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 setarm pipes 0..5 β the reachable (DRM vblank/flip) path is provably fine. - Trigger:
VUPDATE1 setprintspipe_offset = (uint8_t)(-78) = 178,pipe_ctx[178] on a 6-entry array, then faults in the wildtg->funcsderef β 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
fixedfixed: 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
Confirmed kernel references
- sys/dev/drm/amd/display/dc/irq/dce110/irq_service_dce110.c:195
- sys/dev/drm/amd/display/dc/irq/dce110/irq_service_dce110.c:127
- sys/dev/drm/amd/display/dc/irq/dce110/irq_service_dce110.c:157
- sys/dev/drm/amd/display/dc/irq/dce110/irq_service_dce110.c:205
- sys/dev/drm/amd/display/dc/irq_types.h:154
- sys/dev/drm/amd/include/ivsrcid/ivsrcid_vislands30.h:30
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.
No comments yet.