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

drm_mode_page_flip_ioctl: -EDEADLK retry bypasses the only drm_crtc_vblank_put - one vblank reference leaked per page flip that succeeds after a deadlock retry

Summary

vblank_get is taken before the acquire ctx (outside the retry loop); the sole put executes only on a non-EDEADLK exit - after the goto retry block. Any flip that hits -EDEADLK, backs off, then succeeds returns ret==0 and leaks the vblank ref: monotonic refcount growth, vblank IRQ permanently enabled, eventual counter saturation. DRM_MASTER-gated; live only on radeon/legacy-DCE amdgpu (page_flip_target); latent on stock DFly today (no in-tree producer of the inversion against this ioctl's lock order); byte-identical upstream and still in master. Bracket-the-reference fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4437 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
verdict.json β€” 3.0 KB view raw
manifest.json β€” 1.0 KB view raw
fix.diff β€” 1.3 KB view raw
env.txt β€” 733 B view raw

DF-4437 β€” drm_mode_page_flip_ioctl: -EDEADLK retry bypasses drm_crtc_vblank_put (vblank refcount leak)

Severity / class

Low (local, DRM_MASTER-gated, latent trigger; no memory corruption). Upstream-inherited: identical logic in Linux v5.0 and, as of 2026-09, in upstream master (drm_mode_page_flip_ioctl tail unchanged).

Root cause

sys/dev/drm/drm_plane.c, drm_mode_page_flip_ioctl:

  • :1053 r = drm_crtc_vblank_get(crtc); β€” taken ONCE, before drm_modeset_acquire_init(), but only when crtc->funcs->page_flip_target exists.
  • :1089 retry: label INSIDE the modeset loop.
  • :1186-1190 on ret == -EDEADLK: drm_modeset_backoff(), then goto retry β€” jumping straight back into the loop.
  • :1195-1196 if (ret && crtc->funcs->page_flip_target) drm_crtc_vblank_put(crtc); β€” the ONLY put, placed AFTER the EDEADLK block and gated on ret != 0.

Control flow: EDEADLK (ret != 0) β†’ out: β†’ goto retry β†’ second iteration succeeds β†’ ret == 0 β†’ tail put never executes β†’ one vblank reference leaked per page flip that succeeded after a deadlock retry.

Reachability on DragonFly (honest assessment)

  • The ioctl is DRM_MASTER-gated (sys/dev/drm/drm_ioctl.c:673), so the attacker is the display master β€” on a console with no compositor running, any local user can become master via DRM_IOCTL_SET_MASTER; otherwise the compositor itself (multi-threaded) races itself.
  • The path is live only for CRTC drivers implementing page_flip_target: in-tree that is radeon (sys/dev/drm/radeon/radeon_display.c:666) and amdgpu legacy DCE (sys/dev/drm/amd/amdgpu/amdgpu_display.c:146, wired in dce_v8/v10/ v11/virtual). amdgpu DC and simple-kms drivers are unaffected.
  • Producing -EDEADLK inside this ioctl requires a ww-modeset-lock inversion (this function takes crtc->mutex then plane->mutex; every in-tree core path β€” setplane/cursor/atomic/setcrtc β€” uses the same crtc-before-plane order, and the legacy drivers below don't invert). The inversion producer exists in mainline (e.g. drm_atomic_helper_update_plane locks plane state before crtc state) but is only compiled for atomic drivers, which on DF don't carry page_flip_target. So on stock DragonFly today the leak is LATENT: certain code defect, no identified in-tree trigger producer. Confidence "likely" reflects exactly this.

Impact ceiling

Each occurrence increments the CRTC's vblank refcount without a matching put: vblank IRQ stays permanently enabled (perf/idle-power), and drm_crtc_vblank_get refcount grows monotonically; after ~2^31 leaks the counter overflows (warn spam / saturation). No kernel memory corruption; worst case is a local display-subsystem resource-exhaustion DoS on the affected ASICs, requiring sustained winning deadlock-retries.

Why not guest-tested

The QEMU audit guest has no DRM device at all (/dev/dri absent; only an unbound QEMU std-VGA chip β€” see env.txt), and the leak needs a page_flip_target-capable GPU (radeon / pre-DC amdgpu) plus a lock-inversion producer that stock DF does not currently ship. Low severity, non-trivial local trigger β‡’ QEMU verification not warranted per audit contract.

Fix

fix.diff β€” bracket the ref with a vblank_ref_held flag: (re)take the reference at the top of the retry loop when not held, and drop it at out: before the EDEADLK decision, clearing the flag; every exit path then pairs get/put exactly once.

VERDICT.md
↓ download raw

DF-4437 VERDICT β€” certain code defect, latent trigger on stock DF

Reproduced?

No runtime verification performed (status: untested). Two independent blockers on the audit guest: (1) no DRM device exists in the guest (/dev/dri absent, only an unbound QEMU std-VGA chip β€” env.txt); (2) the leak needs a page_flip_target-capable driver (radeon / pre-DC amdgpu) raced by a ww-modeset-lock-inversion producer, which stock DragonFly does not currently ship for those drivers. Low severity, non-trivial local trigger β‡’ QEMU verification not mandated by the audit contract.

Why the logic defect is certain (control-flow proof)

sys/dev/drm/drm_plane.c, drm_mode_page_flip_ioctl:

  • :1049-1055 if crtc->funcs->page_flip_target: r = drm_crtc_vblank_get(crtc); β€” reference taken exactly once, BEFORE drm_modeset_acquire_init() at :1088, i.e. outside the retry loop.
  • :1088-1089 drm_modeset_acquire_init(...); retry:
  • :1179 out: … :1186-1190 if (ret == -EDEADLK) { ret = drm_modeset_backoff(&ctx); if (!ret) goto retry; }
  • :1195-1196 if (ret && crtc->funcs->page_flip_target) drm_crtc_vblank_put(crtc);

Trace: iteration 1 returns -EDEADLK (from drm_modeset_lock at :1090/:1093 or the driver callback at :1160/:1165) β†’ out: β†’ EDEADLK branch β†’ goto retry (skipping the put at :1195) β†’ iteration 2 succeeds β†’ ret == 0 β†’ the ret && guard makes the put a no-op β†’ the vblank reference acquired at :1053 is never released. One leaked reference per successfully retried flip; repeatable per contention event.

This tail is byte-identical in Linux v5.0 (the DF copy's origin) and in upstream master as of 2026-09 β€” the defect is upstream-inherited and still latent upstream.

Reachability analysis (what keeps this at Low)

  • Gate: DRM_IOCTL_MODE_PAGE_FLIP is DRM_MASTER-gated (sys/dev/drm/drm_ioctl.c:673). Unprivileged reach requires the attacker to BE the master (possible on a headless/console box where they win DRM_IOCTL_SET_MASTER) or to be the compositor.
  • Live drivers: radeon (sys/dev/drm/radeon/radeon_display.c:666) and amdgpu legacy DCE v8/v10/v11/virtual (sys/dev/drm/amd/amdgpu/amdgpu_display.c:146) implement page_flip_target; DC-based amdgpu does not.
  • EDEADLK producer: this ioctl takes crtc->mutex then plane->mutex. Every in-tree core path (setplane lock_all, cursor ioctl, atomic check, setcrtc) uses the same crtc-before-plane order, so no ww cycle is generated with them; drm_atomic_helper_update_plane (which locks plane state before crtc state, creating a real inversion) exists only in atomic drivers that don't carry page_flip_target. Conclusion: on stock DF the trigger currently has no producer; the defect becomes exploitable the day such a producer is compiled in. Confidence "likely" (defect certain, trigger latent).

Impact ceiling

Per leaked reference: vblank->refcount grows by one and the vblank IRQ can never be disabled for that CRTC (idle-power/perf regression, warn spam on counter saturation after ~2^31 events). No kernel memory corruption, no info leak. Local display-subsystem DoS at worst.

Fix validation

Not applicable (no runtime repro). fix.diff brackets the reference with a vblank_ref_held flag so every exit path and every retry pairs get/put exactly once.

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

No behavior validation possible (no GPU, no inversion producer in guest); fix.diff get/put-brackets every exit and retry path; applies cleanly (patch -p1 --dry-run OK, offset 2 lines)

findings/poc/DF-4437/fix.diff (patch -p1 --dry-run: OK)
↓ fix.diffper-fix-DF-4437

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4437/VERDICT.md (full control-flow trace + reachability analysis + upstream parity); env.txt (guest DRM absence); fix.diff (applies cleanly with 2-line offset, patch --dry-run verified)

PoC changes

No PoC authored: needs a page_flip_target-capable GPU (radeon/pre-DC amdgpu) plus a lock-inversion producer not shipped on DF; guest has no DRM device

Verified recommended fix

Bracket the vblank reference with a vblank_ref_held flag: (re)acquire at retry-loop top when not held, release at out: before the -EDEADLK backoff decision

Verdict

Certain control-flow defect, latent trigger on stock DragonFly. drm_mode_page_flip_ioctl takes drm_crtc_vblank_get (drm_plane.c:1053) once, before the modeset retry loop, but the only drm_crtc_vblank_put (:1195-1196) sits AFTER the -EDEADLK 'goto retry' (:1186-1190) and is gated on 'ret' - so a flip that succeeds after a deadlock retry returns ret==0 and never drops the reference: one vblank refcount leak per retried flip, keeping the vblank IRQ permanently enabled and growing the CRTC refcount monotonically. Path is live only for page_flip_target drivers (radeon radeon_display.c:666, amdgpu legacy DCE amdgpu_display.c:146) and is DRM_MASTER-gated (drm_ioctl.c:673). On stock DF no in-tree path produces the required ww-lock inversion against this ioctl's crtc-then-plane order (the plane-then-crtc locker drm_atomic_helper_update_plane exists only in atomic drivers, which don't carry page_flip_target), so the defect is latent today and becomes a local display DoS the day such a producer ships. Byte-identical logic in Linux v5.0 and current upstream master. Untested: guest has no DRM device (/dev/dri absent) and lacks radeon/amdgpu hardware. Low severity.