DMA_HEAP_IOCTL_ALLOC allocates a dma-buf and installs an fd into the caller's fd table via fd_install() before dma_heap_ioctl() copies the result back to userspace. If the trailing copy_to_user() fails, the ioctl returns -EFAULT and userspace never learns the fd number, but the fd (and the underlying dma-buf reference) remain in the caller's fd table and are leaked for the lifetime of the process.
The failure is easily reachable from userspace: pass a struct dma_heap_allocation_data that lives in a page whose protection is flipped to PROT_READ between copy_from_user() and copy_to_user() (e.g. via mprotect()). Each such ioctl leaks one dmabuf fd; repeating the call quickly fills /proc/<pid>/fd with anonymous "/dmabuf:" entries that only go away when the process exits.
Fix it by closing the installed fd (and clearing the fd field of the kernel-side copy) when copy_to_user() fails after a successful allocation, so the error path matches what userspace observes: no fd was returned, therefore no fd is left behind.
Fixes: c02a81fba74f ("dma-buf: Add dma-buf heaps framework") Cc: stable@vger.kernel.org Signed-off-by: Baineng Shou shoubaineng@gmail.com ---
Reproducer (full source, gcc -o poc poc.c; run as root):
// poc.c -- leak one dma-buf fd per DMA_HEAP_IOCTL_ALLOC // when copy_to_user() fails #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <linux/dma-heap.h>
int main(int argc, char **argv) { int n = argc > 1 ? atoi(argv[1]) : 100; long ps = sysconf(_SC_PAGESIZE);
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC); if (heap < 0) return perror("open"), 1;
for (int i = 0; i < n; i++) { /* Put a valid request in a page, then make the page * read-only: copy_from_user() still succeeds and the * dma-buf is allocated and fd_install()'d, but the * trailing copy_to_user() fails and the fd, never * returned to us, is leaked. */ struct dma_heap_allocation_data *req = mmap(NULL, ps, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memset(req, 0, sizeof(*req)); req->len = ps; req->fd_flags = O_RDWR | O_CLOEXEC;
mprotect(req, ps, PROT_READ); ioctl(heap, DMA_HEAP_IOCTL_ALLOC, req); /* -EFAULT */ munmap(req, ps); }
printf("done: check ls -l /proc/%d/fd for %d leaked fds\n", getpid(), n); pause(); return 0; }
Before the fix, ./poc 10 leaves 10 anonymous dmabuf fds in the caller's fd table:
# ls -l /proc/$(pgrep poc)/fd lrwx------ 1 root root 64 Jan 1 00:03 3 -> /dev/dma_heap/system lrwx------ 1 root root 64 Jan 1 00:03 4 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 5 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 6 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 7 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 8 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 9 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 10 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 11 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 12 -> /dmabuf: lrwx------ 1 root root 64 Jan 1 00:03 13 -> /dmabuf:
After the fix, only /dev/dma_heap/system remains open; the anonymous "/dmabuf:" entries are gone.
drivers/dma-buf/dma-heap.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/drivers/dma-buf/dma-heap.c b/drivers/dma-buf/dma-heap.c index a76bf3f8b071..0dd7a84b06bf 100644 --- a/drivers/dma-buf/dma-heap.c +++ b/drivers/dma-buf/dma-heap.c @@ -18,6 +18,7 @@ #include <linux/uaccess.h> #include <linux/xarray.h> #include <uapi/linux/dma-heap.h> +#include <linux/fdtable.h>
#define DEVNAME "dma_heap"
@@ -181,8 +182,16 @@ static long dma_heap_ioctl(struct file *file, unsigned int ucmd, goto err; }
- if (copy_to_user((void __user *)arg, kdata, out_size) != 0) + if (copy_to_user((void __user *)arg, kdata, out_size) != 0) { + if (kcmd == DMA_HEAP_IOCTL_ALLOC && ret == 0) { + struct dma_heap_allocation_data *h = (void *)kdata; + + close_fd(h->fd); + h->fd = -1; + } ret = -EFAULT; + } + err: if (kdata != stack_kdata) kfree(kdata);