Linux Media Controller development
 help / color / mirror / Atom feed
From: Michal TOMA <michaltoma@sicoop.com>
To: Zack Rusin <zack.rusin@broadcom.com>
Cc: Michal TOMA <michaltoma@sicoop.com>,
	bcm-kernel-feedback-list@broadcom.com, ian.forbes@broadcom.com,
	maarten.lankhorst@linux.intel.com, mripard@kernel.org,
	tzimmermann@suse.de, airlied@gmail.com, simona@ffwll.ch,
	sumit.semwal@linaro.org, christian.koenig@amd.com,
	dri-devel@lists.freedesktop.org, linux-kernel@vger.kernel.org,
	linux-media@vger.kernel.org, linaro-mm-sig@lists.linaro.org,
	stable@vger.kernel.org
Subject: [PATCH v2 3/3] drm/vmwgfx: Don't leak a GEM handle when referencing a surface by fd
Date: Thu, 10 Sep 2026 18:52:21 +0200	[thread overview]
Message-ID: <20260910165221.7558-4-michaltoma@sicoop.com> (raw)
In-Reply-To: <20260910165221.7558-1-michaltoma@sicoop.com>

DRM_VMW_GB_SURFACE_REF_EXT with DRM_VMW_HANDLE_PRIME first tries
ttm_prime_fd_to_handle(). If the fd is not a vmwgfx surface export,
vmw_surface_handle_reference() falls back to
vmw_buffer_prime_to_surface_base(). That function calls
drm_gem_prime_fd_to_handle() to turn the fd into a GEM handle in the
caller's file, then looks for a surface attached to that buffer.

That handle is never returned to userspace, since the ioctl reports the
surface handle and a separate backup buffer handle, and it is never
deleted. Every call that gets this far therefore leaves a GEM handle
behind in the file, whether it fails or succeeds, and whatever the
handle references stays alive until the file is closed.

For a dma-buf from another exporter, such as udmabuf, this is an
actual import: drm_gem_prime_fd_to_handle() attaches to the dma-buf,
maps it and creates a new GEM object. The surface lookup then always
fails, because such a buffer never has a surface. The WARN_ON() fires
and the ioctl returns -EINVAL, but the import stays referenced by the
stray handle.

Mesa's svga winsys passes dma-buf fds to DRM_VMW_GB_SURFACE_REF_EXT as
DRM_VMW_HANDLE_PRIME, and KWin 6.7 imports every wl_shm client buffer
as a udmabuf through EGL. On a Plasma Wayland session this pins one
window-sized buffer inside the compositor's DRM file for every
attempted import, for as long as the compositor runs, and logs a full
WARN backtrace each time. On a VirtualBox VMSVGA guest with 3D
acceleration on and the previous patch applied, 103 udmabufs (750 MiB)
had accumulated this way 40 minutes after boot. 99 of them had a
dma-buf refcount of 3, were still attached to the vmwgfx device and
were held by no process fd or mapping.

Look the buffer up through the dma-buf instead of importing it. Only
GEM buffers exported by this device can have a surface, so reject
anything else with drm_gem_is_prime_exported_dma_buf() before touching
it. For our own buffers, use dma_buf->priv directly: the dma-buf holds
a reference to the GEM object for as long as we hold the dma-buf, so
neither a handle nor an extra object reference is needed. The surface
lookup and the ttm_ref_object_add() reference that the callers rely on
are unchanged.

While restructuring the function, also:
 - replace the WARN_ON() on a buffer without a surface with a debug
   message. Userspace can trigger that case at will.
 - drop the base object reference taken by
   vmw_lookup_user_surface_for_buffer() when ttm_ref_object_add() fails.
   It was leaked before.

Build tested on drm-misc-fixes (4600b4d1a9ee) with W=1 (no warnings in
drivers/gpu/drm/vmwgfx/) and checkpatch.pl --strict. Runtime tested on
the same guest and 7.2.3 kernel as the previous patch, comparing a
vmwgfx.ko with only the previous patch against one with both. Calls
from a second render file:
 - udmabuf fd: -EINVAL both times. Before, each call left the import
   held by the file (refcount 3, attached) and logged a WARN. Now
   nothing is imported and nothing is logged.
 - fd of a dumb buffer exported from the primary node: -EINVAL both
   times. Before, a stray GEM handle and a WARN per call; now neither.
 - fd of a buffer exported before a GB surface was created on it, which
   reaches this function and succeeds: the exporter's surface is
   returned both times. Before, each call left a GEM handle in the
   file; now it does not.
 - fd of a buffer created together with its surface, which is exported
   as a TTM prime surface and does not reach this function: succeeds
   both times.
During a 5-minute KWin 6.7 test replaying a terminal workload, the
udmabuf count went from 4 to 20 (140 MiB) with only the previous patch,
with WARNs logged. With both patches it stayed at 6 during the run,
returned to 4 afterwards, and no WARN was logged.

Fixes: d6667f0ddf46 ("drm/vmwgfx: Fix handling of dumb buffers")
Cc: stable@vger.kernel.org # v6.11+
Assisted-by: LLM
Signed-off-by: Michal TOMA <michaltoma@sicoop.com>
---
Reproducer (results and environment are in the cover letter). Run as
root, since it counts udmabufs in debugfs:

  cc -o vmwgfx-surfref-fd-leak vmwgfx-surfref-fd-leak.c
  ./vmwgfx-surfref-fd-leak

// SPDX-License-Identifier: MIT
/*
 * Reproducer for "drm/vmwgfx: Don't leak a GEM handle when referencing a
 * surface by fd": DRM_VMW_GB_SURFACE_REF_EXT(DRM_VMW_HANDLE_PRIME) with a
 * udmabuf fd. Run as root, since it counts udmabufs in debugfs.
 *
 *   cc -o vmwgfx-surfref-fd-leak vmwgfx-surfref-fd-leak.c && ./vmwgfx-surfref-fd-leak
 *
 * The render node stays open after the calls. Expected lines 2 and 3
 * (udmabufs left while the node is open / after it is closed):
 *   with this fix:       0 / 0
 *   without it:          8 / 0 if "drm/vmwgfx: Release PRIME import in
 *                        the BO destroy path" is applied, otherwise
 *                        8 / 8 (pinned until reboot)
 */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/udmabuf.h>
#include <drm/drm.h>
#include <drm/vmwgfx_drm.h>

#define DRM_IOCTL_VMW_GB_SURFACE_REF_EXT \
	DRM_IOWR(DRM_COMMAND_BASE + DRM_VMW_GB_SURFACE_REF_EXT, \
		 union drm_vmw_gb_surface_reference_ext_arg)
#define N 8
#define SIZE (6UL << 20)

/* udmabufs of @size listed in dma_buf/bufinfo */
static int count_udmabufs(unsigned long size)
{
	FILE *f = fopen("/sys/kernel/debug/dma_buf/bufinfo", "r");
	char line[512];
	int n = 0;

	if (!f) {
		perror("/sys/kernel/debug/dma_buf/bufinfo");
		return -1;
	}
	while (fgets(line, sizeof(line), f)) {
		unsigned long sz;

		if (sscanf(line, "%lu", &sz) == 1 && sz == size &&
		    strstr(line, "udmabuf"))
			n++;
	}
	fclose(f);
	return n;
}

int main(void)
{
	int render = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
	int udm = open("/dev/udmabuf", O_RDWR | O_CLOEXEC);
	int before, einval = 0;

	if (render < 0 || udm < 0) {
		perror("open");
		return 1;
	}
	before = count_udmabufs(SIZE);
	for (int i = 0; i < N; i++) {
		int memfd = memfd_create("surfref", MFD_ALLOW_SEALING | MFD_CLOEXEC);
		struct udmabuf_create create = {
			.memfd = memfd, .flags = UDMABUF_FLAGS_CLOEXEC, .size = SIZE,
		};
		union drm_vmw_gb_surface_reference_ext_arg arg;
		int dmabuf;

		if (memfd < 0 || ftruncate(memfd, SIZE) ||
		    fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK)) {
			perror("memfd");
			return 1;
		}
		dmabuf = ioctl(udm, UDMABUF_CREATE, &create);
		if (dmabuf < 0) {
			perror("UDMABUF_CREATE");
			return 1;
		}
		memset(&arg, 0, sizeof(arg));
		arg.req.sid = dmabuf;
		arg.req.handle_type = DRM_VMW_HANDLE_PRIME;
		if (ioctl(render, DRM_IOCTL_VMW_GB_SURFACE_REF_EXT, &arg) &&
		    errno == EINVAL)
			einval++;
		close(dmabuf);
		close(memfd);
	}
	close(udm);
	usleep(500000);
	printf("GB_SURFACE_REF_EXT with a udmabuf fd: %d/%d calls failed with EINVAL\n",
	       einval, N);
	printf("6 MiB udmabufs left, render node open:   %d (before: %d)\n",
	       count_udmabufs(SIZE), before);
	close(render);
	usleep(500000);
	printf("6 MiB udmabufs left, render node closed: %d\n",
	       count_udmabufs(SIZE));
	return 0;
}

 drivers/gpu/drm/vmwgfx/vmwgfx_surface.c | 48 ++++++++++++++-----------
 1 file changed, 28 insertions(+), 20 deletions(-)

diff --git a/drivers/gpu/drm/vmwgfx/vmwgfx_surface.c b/drivers/gpu/drm/vmwgfx/vmwgfx_surface.c
index bd0563741..27f68fd9c 100644
--- a/drivers/gpu/drm/vmwgfx/vmwgfx_surface.c
+++ b/drivers/gpu/drm/vmwgfx/vmwgfx_surface.c
@@ -16,8 +16,11 @@
 #include "device_include/svga3d_surfacedefs.h"
 
 #include <drm/drm_dumb_buffers.h>
+#include <drm/drm_prime.h>
 #include <drm/ttm/ttm_placement.h>
 
+#include <linux/dma-buf.h>
+
 #define SVGA3D_FLAGS_64(upper32, lower32) (((uint64_t)upper32 << 32) | lower32)
 
 /**
@@ -931,33 +934,37 @@ u32 vmw_lookup_surface_handle_for_buffer(struct vmw_private *vmw,
 
 static int vmw_buffer_prime_to_surface_base(struct vmw_private *dev_priv,
 					    struct drm_file *file_priv,
-					    u32 fd, u32 *handle,
+					    u32 fd,
 					    struct ttm_base_object **base_p)
 {
 	struct ttm_base_object *base;
-	struct vmw_bo *bo;
+	struct dma_buf *dma_buf;
 	struct ttm_object_file *tfile = vmw_fpriv(file_priv)->tfile;
 	struct vmw_user_surface *user_srf;
 	int ret;
 
-	ret = drm_gem_prime_fd_to_handle(&dev_priv->drm, file_priv, fd, handle);
-	if (ret) {
-		drm_warn(&dev_priv->drm,
-			 "Wasn't able to find user buffer for fd = %u.\n", fd);
-		return ret;
-	}
+	dma_buf = dma_buf_get(fd);
+	if (IS_ERR(dma_buf))
+		return PTR_ERR(dma_buf);
 
-	ret = vmw_user_bo_lookup(file_priv, *handle, &bo);
-	if (ret) {
-		drm_warn(&dev_priv->drm,
-			 "Wasn't able to lookup user buffer for handle = %u.\n", *handle);
-		return ret;
+	/*
+	 * Only buffers exported by this device can have a user surface.
+	 * Look the buffer up through the dma-buf, which holds a reference
+	 * to it, instead of importing the fd into file_priv: the GEM handle
+	 * that would create is never returned to userspace, so nothing
+	 * would release it (or a foreign dma-buf) until the file is closed.
+	 */
+	if (!drm_gem_is_prime_exported_dma_buf(&dev_priv->drm, dma_buf)) {
+		ret = -EINVAL;
+		goto out;
 	}
 
-	user_srf = vmw_lookup_user_surface_for_buffer(dev_priv, bo, *handle);
-	if (WARN_ON(!user_srf)) {
-		drm_warn(&dev_priv->drm,
-			 "User surface fd %d (handle %d) is null.\n", fd, *handle);
+	user_srf = vmw_lookup_user_surface_for_buffer(dev_priv,
+						      to_vmw_bo(dma_buf->priv),
+						      fd);
+	if (!user_srf) {
+		drm_dbg_driver(&dev_priv->drm,
+			       "No user surface for buffer fd %d.\n", fd);
 		ret = -EINVAL;
 		goto out;
 	}
@@ -966,13 +973,15 @@ static int vmw_buffer_prime_to_surface_base(struct vmw_private *dev_priv,
 	ret = ttm_ref_object_add(tfile, base, NULL, false);
 	if (ret) {
 		drm_warn(&dev_priv->drm,
-			 "Couldn't add an object ref for the buffer (%d).\n", *handle);
+			 "Couldn't add an object ref for buffer fd %d (%d).\n",
+			 fd, ret);
+		ttm_base_object_unref(&base);
 		goto out;
 	}
 
 	*base_p = base;
 out:
-	vmw_user_bo_unref(&bo);
+	dma_buf_put(dma_buf);
 
 	return ret;
 }
@@ -996,7 +1005,6 @@ vmw_surface_handle_reference(struct vmw_private *dev_priv,
 			return vmw_buffer_prime_to_surface_base(dev_priv,
 								file_priv,
 								u_handle,
-								&handle,
 								base_p);
 	} else {
 		handle = u_handle;
-- 
2.55.0


  parent reply	other threads:[~2026-09-10 16:59 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-10 16:52 [PATCH v2 0/3] drm/vmwgfx: Fix dma-buf import leaks and a crash on imported BOs Michal TOMA
2026-09-10 16:52 ` [PATCH v2 1/3] drm/vmwgfx: Don't map or free the exporter's sg_table for " Michal TOMA
2026-10-07 19:28   ` Zack Rusin
2026-09-10 16:52 ` [PATCH v2 2/3] drm/vmwgfx: Release PRIME import in the BO destroy path Michal TOMA
2026-10-07 19:29   ` Zack Rusin
2026-09-10 16:52 ` Michal TOMA [this message]
2026-10-07 19:31   ` [PATCH v2 3/3] drm/vmwgfx: Don't leak a GEM handle when referencing a surface by fd Zack Rusin

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260910165221.7558-4-michaltoma@sicoop.com \
    --to=michaltoma@sicoop.com \
    --cc=airlied@gmail.com \
    --cc=bcm-kernel-feedback-list@broadcom.com \
    --cc=christian.koenig@amd.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=ian.forbes@broadcom.com \
    --cc=linaro-mm-sig@lists.linaro.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-media@vger.kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=simona@ffwll.ch \
    --cc=stable@vger.kernel.org \
    --cc=sumit.semwal@linaro.org \
    --cc=tzimmermann@suse.de \
    --cc=zack.rusin@broadcom.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox