Skip to content

drm/apple: M3 Pro GPU memory leak fix: release replaced framebuffers on 14.7 firmware - #325

Merged
chriskearney merged 1 commit into
integration/aurorafrom
fix/v14-retained-fb-leak
Nov 2, 2026
Merged

chriskearney merged 1 commit into
integration/aurorafrom
fix/v14-retained-fb-leak

Conversation

@erykwieliczko

@erykwieliczko erykwieliczko commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

When a commit replaces the primary plane's framebuffer, apple_plane_prepare_fb() takes a reference on the old one and apple_atomic_commit_tail() parks it on the DCP's swapped_out_fbs list, so it stays alive while the display may still scan it out. The template IOMFB path (iomfb_template.c) releases those entries when the firmware completes the swap. The native 14.7 path (iomfb_v14_7.c) never does: nothing on it calls dcp_release_retained_framebuffers() or dcp_release_all_retained_framebuffers().

So on the M3 every replaced framebuffer stays referenced until the driver goes away, and with it the imported GEM object, its dma-buf attachment, its display IOMMU mapping, and the buffer itself once its owner frees it. That covers every board on the 14.7 path: T6030 (M3 Pro), J516C/J514C (M3 Max), J613/J615 (M3 Air), and external displays on that firmware. I tested on the M3 Pro only.

Measured on a J514S (M3 Pro, 3024x1964) at the KWin login screen, on unfixed integration/aurora (4fdd0fc), with 600 pointer moves (each one a compositor redraw and a primary-plane flip):

before after ~3,500 swaps
framebuffers held by appledrm 6 69
references on them 194 3,706 (exactly +1 per swap)
  • Display mappings: KWin's three 22.8 MiB swapchain buffers ended up with 135 dma-buf attachments to the display. Each leaked framebuffer holds its own 22.8 MiB display mapping and added 16 KiB of IOMMU page tables (SecPageTables).
  • A freed buffer kept alive: the boot splash's framebuffer (allocated by plymouthd, 22.7 MiB) stayed allocated after plymouthd had exited. The leaked reference was its last one.
  • It keeps going: two more runs went on the same way, to 137 and then 171 framebuffers, with references growing by exactly the number of swaps.

Fix

  • iomfb_v14_7_flush(): release all retained framebuffers once dcp_v14_present() reports a completed swap.
  • dcp_v14_blank(): release them once the blank has completed.

The 14.7 session already holds its own reference on the framebuffer it shows (active_fb). When a swap fails, or an external display refuses one, nothing is released. Anything that may still be scanned out stays pinned, as before.

The 14.7 backlight KUnit fixture now sets up the retained-framebuffer list as probe does, because dcp_poweroff() reaches the release through dcp_v14_blank().

Testing

J514S (M3 Pro), 14.7 DCP firmware, kernel 7.2.11-11-dsc3-ARCH. I built appledrm from 4fdd0fc with and without this commit, booted each build in turn (identified by build-id), and ran the same measurement:

unfixed 4fdd0fc with this commit
framebuffers, before → after 600 pointer moves 6 → 69 3 → 3
references 194 → 3,706 5 → 5
SecPageTables +16 KiB per leaked framebuffer flat (1,088 kB)
plymouthd's framebuffer still allocated freed when KWin took over
swap failures 0 0
  • Checks: checkpatch.pl --strict is clean, and check-commits.py is clean (sign-off present). A W=1 build of iomfb_v14_7.c has no warnings, before or after.
  • KUnit: CI's first run caught a real problem. v14_bl_dpms_dispatch reaches dcp_v14_blank() through dcp_poweroff(), and the 14.7 backlight fixture built its DCP without the retained-framebuffer list that probe initialises, so the new release oopsed on it. The fixture now initialises the list the way probe (and dcp_dropped_swap_test.c) does. iomfb_v14_7.c compiles with the 14.7 KUnit cases enabled, and CI is running KUnit again.
  • Not tested: M3 Max and M3 Air boards, and external displays on 14.7.

🤖 Generated with Claude Code

Every commit that replaces the primary plane's framebuffer takes a
reference on the old one (apple_plane_prepare_fb()), and commit_tail
parks it on the DCP's swapped_out_fbs list until the display no longer
scans it out. The template IOMFB path releases those entries when the
firmware completes the swap. The native 14.7 path never does, so on the
M3 every replaced framebuffer stays referenced until the driver goes
away, and with it the imported GEM object, its dma-buf attachment, its
display IOMMU mapping, and the buffer itself once its owner frees it.

Release the retained framebuffers once a 14.7 swap has completed, and
after the panel is blanked. The session already holds its own reference
on the framebuffer it shows (active_fb), and a failed swap still keeps
everything pinned, as before.

The 14.7 backlight KUnit fixture now sets up the retained framebuffer
list as probe does: dcp_poweroff() reaches the release through
dcp_v14_blank().

On a J514S (M3 Pro, 3024x1964) at the KWin login screen, 600 pointer
moves (about 3500 swaps) on unfixed integration/aurora took the display
driver from 6 to 69 framebuffers and from 194 to 3706 references: one
reference per swap. Each leaked framebuffer held its own 22.8 MiB
display mapping (16 KiB more IOMMU page tables each), and KWin's three
swapchain buffers ended up with 135 attachments. The boot splash's
22.7 MiB framebuffer stayed allocated after plymouthd had exited. With
this change the same run kept 3 framebuffers and 5 references, and the
splash framebuffer was freed once KWin took over.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Assisted-by: Claude:claude-opus-5-5

@chriskearney chriskearney left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed and boot-tested 41a2b44 on an M3 Pro J516S/T6030. Approving the bounded framebuffer-lifetime fix based on source review and the hardware comparison below. This is not a claim that the full OMT suite passed.

The 14.7 link waits for both the RPC reply and matching swap-complete event before returning success. active_fb keeps its own reference; failed/refused presents keep the retained pins; blank drains only after a successful black swap. I found no source blocker. Exact-head CI passed, including KUnit (1742 passed, 3 skipped); I did not run KUnit locally. Direct retained-list success/failure/failed-blank regression tests would be a useful follow-up.

Compared against 14-1 built from the PR base 4fdd0fc, using the installed configuration as build input and verifying the candidate's loaded appledrm build ID against the built artifact:

Measurement Base Candidate
Internal swaps under load 3535 3584
Framebuffer-reference increase after internal load +3580 0
Framebuffers, start → after window churn 6 → 30 7 → 7
Reference total, start → after churn 125 → 7141 11 → 11
SecPageTables change after churn +384 kB +32 kB

The protocol included internal redraw load, external presentation, internal DPMS off/on (inactive CRTC and active_fb=0 verified), and five window/fullscreen cycles. No DART faults, flip failures, refcount/UAF reports or kernel WARN/BUG appeared during this protocol.

OMT caveat: the candidate job remains tests_failed: H.264 and HEVC on-screen checks saw five correct bars but cyan #1a2338. Retained before/after desktop captures show persistent crash notifications covering the cyan sampling region. Independently reconstructing OMT's sample grid from those captures predicts both the baseline's passing median and the candidate's panel-color median. Notification occlusion is therefore a strong inference, not proof: the actual video-check screenshots were deleted. The full suite did not pass; lab-kernel package/boot checks also fail, and gpu.driver fails in both runs. An mpv userspace crash/notification also occurs on the base and remains a separate issue. These results have not been rescored or rerun.

Scope: M3 Pro internal panel plus presentation to one Sony 4K120 display through a TS4. The external route changed between runs (DCPEXT1 → DCPEXT0); this is a matched internal-panel comparison, not matched external qualification. Three startup HPD timeouts on inactive DCPEXT1 in the candidate were unchanged after the initial measurement; the same warning sequence exists on the base. No M3 Air/Max, suspend, hot-plug or long-soak coverage.

@chriskearney
chriskearney added this pull request to the merge queue Nov 2, 2026
Merged via the queue into integration/aurora with commit 66c68a2 Nov 2, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants