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

GEM object refcount leak on amdgpufb_create error path (VRAM buffer permanently leaked)

  • File: sys/dev/drm/amd/amdgpu/amdgpu_fb.c
  • Lines: 228, 237–252 (error paths), 308–316 (broken cleanup)
  • Severity: Medium
  • CVSS 3.1: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:L
  • CWE: CWE-401 Missing Release of Memory after Effective Lifetime
  • Confidence: certain
  • Status: new
  • Related: DF-1969 (radeon_fb.c identical-class bug)

Summary

When drm_fb_helper_alloc_fbi() (line 237) or amdgpu_display_framebuffer_init() (line 248) fails after amdgpufb_create_pinned_object() (line 228) succeeds, the out: error label has broken cleanup: the if (abo) { } block is empty (lines 309-311) and if (fb && ret) at line 312 is false because fb is still NULL at those error points.

The GEM object's reference count is never decremented, permanently leaking the pinned VRAM buffer object.

Root cause

In amdgpufb_create(), after amdgpufb_create_pinned_object(rfbdev, &mode_cmd, &gobj) succeeds at line 228, gobj holds a reference to a pinned+kmap'd VRAM buffer object.

Two error paths follow:

  • Path A: drm_fb_helper_alloc_fbi(helper) at line 237 returns IS_ERR(info) β†’ goto out (line 240). At this point fb is still NULL (set only at line 255, after framebuffer_init succeeds).
  • Path B: amdgpu_display_framebuffer_init() at line 248 fails β†’ goto out (line 252). fb is still NULL.

At the out: label (line 308): - if (abo) { } (line 309) is an EMPTY BLOCK β€” clearly someone intended cleanup here but left it incomplete. - if (fb && ret) (line 312) is FALSE because fb == NULL on both paths.

So gobj is never released. amdgpufb_destroy_pinned_object() (line 115-127), which does amdgpu_bo_kunmap + amdgpu_bo_unpin + drm_gem_object_put_unlocked, is never called.

The GEM object and its backing VRAM allocation (size = pitches[0] * ALIGN(height, 8), page-aligned, pinned in VRAM) are leaked for the lifetime of the kernel.

Later, amdgpu_fbdev_destroy() (line 321-336) checks if (rfb->base.obj[0]) (line 327) before cleanup. On Path B, amdgpu_display_framebuffer_init sets obj[0]=NULL on failure (amdgpu_display.c:516), so the destroy function skips cleanup entirely. On Path A, obj[0] was never set (still zero from kzalloc). In both cases, the leak is permanent.

Additionally, amdgpu_fbdev_init() at line 383-384 ignores the return value of drm_fb_helper_initial_config and always returns 0, so the driver proceeds as if fbdev init succeeded.

Threat model

Attacker position: local attacker with DRM device access or the ability to trigger display hotplug/modeset events.

Trigger: repeat fbdev re-probe failures to leak several MB of pinned VRAM per attempt. Over time this exhausts VRAM, causing subsequent modeset/scanout allocations to fail β†’ local DoS.

On amdgpu, the framebuffer surface size can be large (e.g., 16384*4 * ALIGN(8640,8) β‰ˆ 572 MB for a 16K display). Even a single leak of this size is significant; repeated leaks via hotplug cycles are devastating.

The attack requires the fb_probe path to fail (e.g., low VRAM, or GART bind failure from amdgpu_ttm_alloc_gart at line 185), which an attacker can influence by consuming VRAM via other DRM allocations.

Proof of concept

  1. Open the amdgpu DRM device (/dev/drmX).
  2. Allocate large VRAM GEM objects via DRM_IOCTL_AMDKGPU_GEM_CREATE or DRM_IOCTL_MODE_CREATE_DUMB until VRAM is nearly exhausted.
  3. Trigger a fbdev re-probe β€” on DragonFly this happens via connector hotplug (unplug/replug display) or by triggering drm_fb_helper_hotplug_event.
  4. amdgpufb_create_pinned_object succeeds (enough VRAM for one object) but amdgpu_ttm_alloc_gart (line 185) fails because GART space is exhausted, or drm_fb_helper_alloc_fbi fails under memory pressure.
  5. The GEM object is leaked.
  6. Repeat until VRAM is permanently consumed.

Verification: check systat -vmstat or amdgpu VRAM usage sysfs before/after β€” the leaked objects never appear as freeable.

Success criterion: fbdev/modeset failures after repeated cycling, or OOM from VRAM exhaustion.

Replace the broken out: error path with proper cleanup. The GEM object must be released on all error paths, and the latent kfree(fb) must be removed (see DF-1983).

--- a/sys/dev/drm/amd/amdgpu/amdgpu_fb.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_fb.c
@@ -305,16 +305,14 @@ static int amdgpufb_create(struct drm_fb_helper *helper,
    vga_switcheroo_client_fb_set(adev->ddev->pdev, info);
    return 0;

 out:
-   if (abo) {
-
-   }
-   if (fb && ret) {
-       drm_gem_object_put_unlocked(gobj);
-       drm_framebuffer_unregister_private(fb);
-       drm_framebuffer_cleanup(fb);
-       kfree(fb);
-   }
+   /*
+    * Release the pinned GEM object on any error path.  fb is embedded
+    * in the kzalloc'd rfbdev (never separately allocated), so never
+    * kfree() it -- see amdgpu_mode.h:316-321.
+    */
+   if (gobj)
+       amdgpufb_destroy_pinned_object(gobj);
    return ret;
 }

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1982 Β· 4 files
FileTypeDescriptionSize
README.md readme PoC trigger description 317 B ↓ raw
VERDICT.md verdict verification narrative 932 B ↓ raw
fix.diff suggested-fix git-apply-able fix 480 B view raw
fix_build_summary.txt build-log combined 16-finding kernel build rc=0 826 B view raw
README.md readme PoC trigger description
↓ download raw

DF-1982 PoC

See the parent finding markdown at findings/DF-1982-*.md for the full threat model and PoC steps. This directory is the evidence-pack slot for the PoC runner; the runner will populate it with sources, build.sh / run.sh, full untrimmed logs, env.txt, VERDICT.md, and manifest.json after verification.

VERDICT.md verdict verification narrative
↓ download raw

DF-1982 Verification

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).

The cited defect exists in the audited source at sys/dev/drm/amd/amdgpu/amdgpu_fb.c:305-316. amdgpu is not in GENERIC and requires real AMD GPU hardware.

Mechanism (source-only confirmation)

amdgpufb_create error label out: at L308-316 has an empty "if (abo) {}" block (309-311) where cleanup was intended. On error paths where fb was never set up (alloc_fbi failure at L240, framebuffer_init failure at L253), the GEM object reference acquired earlier is never released β†’ permanent VRAM leak. fb=&rfbdev->rfb.base is only assigned at L257, after the possible error gotos.

Fill the empty if (abo) block: on error paths where fb==NULL (framebuffer never set up), call drm_gem_object_put_unlocked(gobj) to release the leaked GEM reference.

The full git apply-able diff lives in fix.diff in this folder.

Confirmed kernel references

Detail

Exploit chain

none (non-corruption: resource leak only)

Evidence (decisive lines)

Combined kernel build: 16 fix.diffs applied, make -j6 nativekernel => rc=0, 0 warnings, 0 errors.

PoC changes

Created VERDICT.md, fix.diff (fill empty if(abo) with GEM ref release), manifest.json, env.txt, build.sh, run.sh.

Verified recommended fix

Fill the empty if (abo) block: on error paths where fb==NULL, call drm_gem_object_put_unlocked(gobj). Matches finding proposal.

Verdict

SOURCE-CONFIRMED (HW/module gated). amdgpufb_create error label out: (amdgpu_fb.c:305-316) has empty 'if (abo) {}' block (L309-311) where cleanup was intended. On error paths where fb was never set up (alloc_fbi failure L240, framebuffer_init failure L253), GEM object reference is never released -> permanent VRAM leak. fb only assigned at L257, after possible error gotos. Confirmed by source trace. Not runnable: amdgpu module, no AMD HW.