# DF-5778 VERDICT — full narrative

**File:** `sys/dev/drm/amd/display/dc/irq/irq_service.c` (169 LOC, pass 2 GLM 5.3)
**Finding:** Unguarded service-object dereference in `dal_irq_service_to_irq_source`
plus swallowed `dal_irq_service_construct` failure — latent NULL-deref hardening gap.
**Class:** CWE-476 (NULL pointer dereference, latent/defense-in-depth).
**Severity:** Info. **Confidence:** certain (guards certainly absent; reachability
certainly nil via in-tree callers — both proven below).

## 1. The defect (what is missing, exactly)

1. `irq_service.c:160-169` — `dal_irq_service_to_irq_source()` executes

   ```c
   return irq_service->funcs->to_dal_irq_source(irq_service, src_id, ext_id);
   ```

   with **no** check of `irq_service`, `irq_service->funcs`, or the
   `to_dal_irq_source` member. Contrast the sibling entry points in the same
   file, which *do* validate before dereferencing:
   - `dal_irq_service_set()` (:101-114) and `dal_irq_service_ack()`
     (:138-150) validate via `find_irq_source_info()` (:77-86).
   - `dal_irq_service_construct()` (:53-63) validates `init_data`/`ctx`.
   - `dal_irq_service_destroy()` (:65-75) validates the handle.
   Even the DC wrappers are asymmetric: `dc_interrupt_set()`
   (core/dc.c:1681-1688) checks `dc == NULL`, while
   `dc_interrupt_to_irq_source()` (core/dc.c:1673-1679) and
   `dc_interrupt_ack()` (core/dc.c:1690-1693) check nothing.

2. `irq_service.c:53-63` — `dal_irq_service_construct()` returns `void`.
   On bad `init_data`/`ctx` it bails (`BREAK_TO_DEBUGGER(); return;`)
   leaving the object zeroed. Every family constructor swallows that —
   e.g. dce110's `construct()` (irq_service_dce110.c:398-405) ignores the
   outcome, unconditionally wires `info`/`funcs`, and
   `dal_irq_service_dce110_create()` (:407-418) hands the object back as
   live. A `ctx == NULL` service would later reach
   `dm_read_reg(irq_service->ctx, addr)` (dm_services.h:66-77), whose
   `ctx->cgs_device` deref faults. Same shape in dce80/dce120/dcn10.

## 2. Reachability trace — why this is latent, not live

- `init_data.ctx = dc->ctx` at all 8 create sites (dce100_resource.c:960,
  dce110_resource.c:1306, dce112_resource.c:1252, dce120_resource.c:1010,
  dce80_resource.c:967/1177/1383, dcn10_resource.c:1362).
- `dc->ctx` is `kzalloc`'d and checked *before* resource-pool creation
  (core/dc.c:602-607 succeeds or `goto fail`; pool created at :671-678
  only afterwards) ⇒ `ctx` can never be NULL when the family constructors
  run ⇒ the construct-failure branch is dead in-tree.
- `pool->base.irqs = dal_irq_service_*_create()` is NULL-checked at **all
  8** sites (`if (!pool->base.irqs) goto res_create_fail;`/
  `irqs_create_fail`/`fail`) ⇒ `res_pool->irqs` is non-NULL in any live
  `dc`, and `irq_service->funcs` is unconditionally wired by the family
  constructors ⇒ `dal_irq_service_to_irq_source`'s chain is non-NULL on
  every reachable call.
- Callers of the wrappers: `amdgpu_dm_irq_handler` (amdgpu_dm_irq.c:472-487)
  with a valid `adev->dm.dc`; `register_hpd_handlers` loops
  (amdgpu_dm.c:1336/1358/1421/1444); `dm_irq_state`/`amdgpu_dm_set_*`
  (amdgpu_dm_irq.c:357-670) — all with constructed `dc`s.
- No userland input reaches these entry points with pointer-shaped data;
  the only externally-shaped inputs are `src_id`/`ext_id` (hardware IV
  ring) which flow *through* `funcs->to_dal_irq_source`, not into the
  pointer chain.

**Conclusion:** no in-tree caller can produce the faulting object today.
The gap matters for robustness (future callers, partial-init refactors,
driver-teardown windows) — hence Info, defense-in-depth.

## 3. Verification (what ran, on what)

Guest: DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC, with-src snapshot),
cc 8.3, **no AMD GPU** (vgapci 0x1234:0x1111 QEMU VGA; /dev/dri absent).
Per the reservation note for this file, the evidence class is a
production-code KLD harness with synthetic tables.

`df5778_harness.c` compiles the **verbatim** production code — the
complete `enum dc_irq_source` + `DAL_VALID_IRQ_SRC_NUM`
(irq_types.h:36-158), the four structs (irq_service.h:36-70), the
interface prototypes (irq_service_interface.h:28-47), and the six
dispatcher function bodies (irq_service.c:77-169, byte-identical; the
`dm_read_reg`/`dm_write_reg`/`DC_LOG_ERROR` macro interposition mirrors
how dm_services.h itself provides them as macros) — against a synthetic
`[DAL_IRQ_SOURCES_NUMBER]` table with physically-adjacent guard canaries
and a register recorder standing in for cgs MMIO.

Decisive run (`run.log`, module loaded/unloaded cleanly, guest healthy):

- **T1 (the finding):** a zero-initialised `irq_service` (the object the
  swallowed-construct path would leave) has `svc->funcs` (offset +16) ==
  NULL; the replication of the exact irq_service.c:165 deref chain shows
  the verbatim line would fault, with no guard on the path. Fault-free
  probe — the guest is not panicked by design.
- **B1 (bounds, pass-2 angle (a)):** `find_irq_source_info` accepts
  exactly 0..89 (0 = INVALID dummy slot, by design) and rejects
  one-past-end 90, 91, 95, 255, 65535, 0x7FFFFFFF, 0xFFFFFFFF
  (`(enum)-1`); every accepted pointer lands in-table. Canaries intact
  (B2). **The table walk is in-bounds** — executable negative result.
- **B3:** `DAL_VALID_IRQ_SRC_NUM(90) == true` (the DF-4731 macro
  off-by-one, filed on amdgpu_dm_irq.c — not re-reported here) while
  `find_irq_source_info(90) == NULL`: this file's `>=` defuses the macro
  on the set/ack paths.
- **B4 (divergence, pass-2 angle (c)):** upstream Linux v4.17
  `find_irq_source_info` used `source > DAL_IRQ_SOURCES_NUMBER` — which
  *accepts* 90 and returns `&info[90]`, a one-past-end read of the `[90]`
  family tables. The DFly tree uses `>=` (irq_service.c:82): stricter and
  correct. Verified by fetching torvalds/linux v4.17
  (drivers/gpu/drm/amd/display/dc/irq/irq_service.c) during this audit
  and by emulating the upstream predicate in the harness. Apart from this
  predicate (and the `static` qualifier on `find_irq_source_info`), the
  file is byte-identical to upstream v4.17 — no port-introduced defects.
- **T2:** verbatim `dal_irq_service_to_irq_source` happy path executes
  correctly through the synthetic funcs.
- **T3:** dummy-slot dispatch (the family `dummy_irq_entry()` wiring,
  funcs non-NULL with log-and-fail members) produces **zero** register
  traffic and clean `false` returns — behaviorally distinct from the
  zero-filled VLINE0 slots of the DF-2168/4932/4995/5054/5078 family
  (known; not re-reported).
- **T4:** generic RMW dispatch reads/writes exactly the in-table synthetic
  registers with correct masking, ack-before-set ordering as in production.

Status: **untested** in the poc_results sense used for structural
findings (house precedent DF-4731): the in-kernel manifestation cannot be
exercised on this guest (no AMD GPU, and no in-tree caller can produce
the NULL object). The structure of the missing guard and the correctness
of everything around it *were* executed and verified.

## 4. Fix

`fix.diff` (authored post-verification, not applied to the read-only
`sys/` tree):
- `dal_irq_service_to_irq_source`: reject `!irq_service`, missing
  `funcs`, or missing member with `DC_IRQ_SOURCE_INVALID` (callers
  already handle INVALID — dummy slot).
- `dc_interrupt_to_irq_source`/`dc_interrupt_ack` (core/dc.c): the same
  `dc`/`res_pool->irqs` guard `dc_interrupt_set` already has.

A deeper (optional) hardening would make `dal_irq_service_construct`
return `bool` and have the four family `create()` functions return NULL
on failure; not included to keep the diff minimal and reviewable.

## 5. Fix validation

`fix_status: not_testable` — the guards are pure hardening on paths that
cannot be driven to NULL on this guest (no AMD GPU; in-tree callers
proven non-NULL). The harness's T1 probe is the baseline demonstration of
the unguarded chain; with the guard applied the same object would return
`DC_IRQ_SOURCE_INVALID` by inspection (single early-return). No kernel
rebuild is warranted for an Info hardening finding; the guard logic was
desk-checked line-by-line against the cited call sites.

## 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.
