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

Unguarded irq_service->funcs->to_dal_irq_source double-deref in dal_irq_service_to_irq_source plus swallowed dal_irq_service_construct failure β€” latent NULL-deref hardening gap (asymmetric with every sibling entry point)

Summary

The only unvalidated dereference chain in the dispatcher (every sibling validates), compounded by a void-returning construct whose failure the family constructors swallow (dce110/80/120/dcn10 wire funcs unconditionally). Reachability traced dead in-tree (ctx kzalloc-checked, all 8 create-sites NULL-check irqs) - pure defense-in-depth for future refactors. KLD harness compiles the VERBATIM dispatcher against a synthetic table with canaries: T1 proves the zeroed-object fault on :165 while also proving the surrounding negatives (bounds accept exactly 0..89; canaries intact; DF-4731 macro defused at these paths; DFly's >= is STRICTER than upstream v4.17's > which would accept one-past-end). guard+INVALID-return fix authored.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5778 Β· 11 files
FileTypeDescriptionSize
df5778_harness.c β€” 18.8 KB view raw
Makefile β€” 213 B ↓ download
build.sh β€” 147 B view raw
run.sh β€” 175 B view raw
build.log β€” 1003 B view raw
run.log β€” 3.8 KB view raw
env.txt β€” 736 B view raw
VERDICT.md β€” 8.3 KB ↓ raw
fix.diff β€” 2.6 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 5.1 KB view raw
VERDICT.md
↓ download raw

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.

  1. 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 dcs.
  • 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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Pure-hardening early-return guards on paths provably non-NULL in-tree; no AMD GPU on the guest to build an amdgpu kernel and no way to drive the objects to NULL, so a fix-build validation is not possible (house precedent DF-4731). Guard logic desk-checked against every cited call site; callers already tolerate DC_IRQ_SOURCE_INVALID (dummy slot).

fix.diff (guards at irq_service.c:165 and dc.c:1673/1690); VERDICT.md section 5
↓ fix.diffper-fix-DF-5778

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 'T1 zeroed irq_service=... svc->funcs (off+16) = 0' + 'T1 VERDICT: ... NO guard'; run.log 'B1 RESULT: PASS' (bounds) and 'B2 ... PASS (intact)'; run.log B3/B4 lines; VERDICT.md section 2 reachability trace with path:line for all 8 create-sites; env.txt (no AMD GPU); fix.diff

PoC changes

no seed PoC; harness authored fresh. Build iterations on guest: (1) not on out-of-tree include path -> absolute /usr/src/sys/conf/kmod.mk; (2) no sys/stddef.h on DFly -> address arithmetic for the member offset; (3)(4) -Werror=missing-prototypes -> added verbatim prototypes from irq_service_interface.h:28-47 and irq_service.h:72-83; (5) guest root shell is csh -> run.sh pipes commands through /bin/sh (as vm.sh does).

Verified recommended fix

Guard dal_irq_service_to_irq_source (reject !irq_service/!funcs/!member with DC_IRQ_SOURCE_INVALID) and give dc_interrupt_to_irq_source/dc_interrupt_ack the same dc/res_pool->irqs NULL check dc_interrupt_set already has (fix.diff)

Verdict

Structural/latent finding verified statically and by production-code KLD harness on the DragonFly guest (no AMD GPU; synthetic-table family standard). The harness compiles the verbatim dispatcher (irq_service.c:77-169, irq_service.h structs, irq_types.h enum+macro) and proves: T1 the exact irq_service.c:165 deref chain on a zero-initialised irq_service hits svc->funcs (off+16)==NULL with NO guard on the path (fault-free probe); B1 find_irq_source_info accepts exactly 0..89 and rejects one-past-end 90 and all larger/(enum)-1 values with canaries intact (table walk in-bounds β€” executable negative result for the pass-2 bounds question); B3 the DF-4731 DAL_VALID_IRQ_SRC_NUM off-by-one is defused at this file's set/ack paths by the >= bound; B4 upstream Linux v4.17 used > (accepts 90 β†’ &info[90] one-past-end) while the DFly tree's >= (irq_service.c:82) is stricter/correct β€” no port-introduced defect, byte-identical otherwise; T2-T4 verbatim happy path, dummy-slot log-and-fail with zero register traffic, and generic RMW confined to in-table regs. The in-kernel NULL-deref manifestation cannot be exercised: no AMD hardware on the guest and no in-tree caller can produce the NULL object (all 8 resource create-sites NULL-check pool->base.irqs; dc->ctx is kzalloc-checked at core/dc.c:602-607 before pool creation at :671). Severity Info, defense-in-depth.