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

dce120 construct(): ipps/transforms NULL checks index slot [i] while allocation went to slot [j] β€” on Vega20 with harvested pipes the check trips on a never-written kzalloc slot and kills the entire display pool init (spurious false-failure)

Summary

With harvesting enabled the fill index j lags pipe index i; allocations go to [j] but NULL checks read [i] - slot [i] was never written (kzalloc pool), so the check ALWAYS sees NULL and construct() aborts display init on any Vega20 with a fused display pipe. The dangerous inverse (missed real alloc failure leaving NULL in pool) is provably impossible; unwind is clean. Hardware-conditional boot-time functional DoS, not attacker-triggerable; same mismatch still in upstream v6.6. Check-the-allocated-slot fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4448 Β· 4 files
FileTypeDescriptionSize
VERDICT.md β€” 3.2 KB ↓ raw
fix.diff β€” 683 B view raw
manifest.json β€” 795 B view raw
verdict.json β€” 2.1 KB view raw
VERDICT.md
↓ download raw

DF-4448 β€” dce120 construct(): NULL checks index [i] while allocations go to [j]

File: sys/dev/drm/amd/display/dc/dce120/dce120_resource.c Severity: Low Β· CWE: CWE-129 (improper validation of array index) / CWE-754

Root cause

With harvesting enabled (ASICREV_IS_VEGA20_P, dce120_resource.c:918), the controller-creation loop skips fused-off pipes (:1022-1027), so the fill index j lags the hardware pipe index i (j++ at :1075). Allocations are correctly placed at slot [j] β€” but two of the NULL checks read slot [i]:

  • pool->base.ipps[j] = dce120_ipp_create(ctx, i); then if (pool->base.ipps[i] == NULL) (:1049-1050)
  • pool->base.transforms[j] = dce120_transform_create(ctx, i); then if (pool->base.transforms[i] == NULL) (:1057-1058)

When a pipe was skipped (j < i), slot [i] has never been written (the pool is kzalloc'd in dce120_create_resource_pool, :1131-1132), so the check reads a guaranteed-NULL slot:

  • Missed-NULL direction β€” impossible (proved): slot [i] can only be non-NULL if some earlier iteration wrote ipps[j_m] == i with j_m <= i_m < i_k β€” contradiction. The bug therefore never leaves a NULL object in the pool.
  • False-failure direction β€” certain: the check trips on the never-written slot and goto controller_create_fail / goto res_create_fail (:1054, :1062) β€” construct() returns false, destruct() unwinds, and dce120_create_resource_pool() returns NULL (:1140-1141). On Vega20 parts with any fused display pipe, the whole display-computer pool fails to initialize at driver probe β†’ amdgpu_dm/DC dead (display-console DoS), with DC: failed to create input pixel processor! / DC: failed to create transform! in dmesg.

All error labels collapse to the same destruct(pool) block (:1116-1124), and destruct() is NULL-guarded for every slot β€” so the spurious failure unwinds cleanly (no leak, no double-free; verified destruct :515-584, dal_irq_service_destroy NULLs its target irq_service.c:65-75).

Distinct from known DF-1628 (opps [j] NULL leaves the pool with a NULL OPP because the goto is missing there entirely). Upstream v6.6 still carries the same [i]/[j] mismatch (verified against torvalds/linux v6.6 dce120_resource.c) β€” long-standing, never fixed upstream.

Verification attempt

Guest (env: DragonFly 6.5-DEVELOPMENT, no AMD GPU, no /dev/dri) cannot execute the DC construct path; the i/j invariant analysis above is the proof. The behavior requires Vega20 hardware with fused pipes (mmCC_DC_PIPE_DIS bits, read_pipe_fuses :901-907).

Check the slot that was actually allocated:

-       if (pool->base.ipps[i] == NULL) {
+       if (pool->base.ipps[j] == NULL) {
...
-       if (pool->base.transforms[i] == NULL) {
+       if (pool->base.transforms[j] == NULL) {

See fix.diff.

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

Two-line index fix authored against the read-only sys/ tree. Not buildable/runnable on the audit guest (no AMD GPU / no DC construct path).

findings/poc/DF-4448/fix.diff
↓ fix.diffper-fix-DF-4448

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4448/VERDICT.md (i/j invariant proof); dce120_resource.c:1049-1055 and :1057-1063 vs allocations at [j]; j++ at :1075; kzalloc pool at :1131

PoC changes

n/a

Verified recommended fix

NULL-check the slot that was allocated: ipps[j] and transforms[j] (fix.diff, 2-line change).

Verdict

In dce120 construct() with harvesting enabled (Vega20), ipps and transforms are allocated into slot [j] but NULL-checked at slot [i]; after a fused pipe is skipped (j<i) the check reads a never-written kzalloc slot, always NULL, so construct() spuriously fails and the whole DCE12 pool (and amdgpu_dm display init) dies at probe on any Vega20 with fused display pipes. Missed-NULL direction proved impossible (slot [i] cannot be non-NULL when j<i at check time), unwind is clean (all destruct paths NULL-guarded, dal_irq_service_destroy NULLs). Distinct from DF-1628 (opps missing goto). Upstream v6.6 still carries the same mismatch.