Skip to content

[GSD-13310] Unbounded dmabuf fd leak in zeMemOpenIpcHandle, not released by zeMemCloseIpcHandle #974

Description

@tsg-

Pre-submission Checklist

  • I am using the latest GPU driver version (releases)
  • I have searched for similar issues and found none

GPU Hardware

Intel Arc Pro B70

DRI Devices Information

$ ls -ls /dev/dri/*
0 crw-rw----+ 1 root video  226,   0 Aug 23 06:09 /dev/dri/card0
0 crw-rw----+ 1 root video  226,   1 Aug 23 06:09 /dev/dri/card1
0 crw-rw----+ 1 root render 226, 128 Aug 23 06:09 /dev/dri/renderD128

$ ls -la /dev/dri/by-path/
total 0
drwxr-xr-x  2 root root 100 Aug 23 06:09 .
drwxr-xr-x  3 root root 120 Aug 23 06:09 ..
lrwxrwxrwx+ 1 root root   8 Aug 23 06:09 pci-0000:02:00.0-card -> ../card0
lrwxrwxrwx+ 1 root root   8 Aug 23 06:09 pci-0000:ad:00.0-card -> ../card1
lrwxrwxrwx+ 1 root root  13 Aug 23 06:09 pci-0000:ad:00.0-render -> ../renderD128

The GPU is 0000:ad:00.0 (card1 / renderD128). card0 at 0000:02:00.0 is the
onboard display controller and has no render node.

GPU Detailed Information (lspci output)

$ sudo lspci -vvv -k -s 0000:ad:00.0
ad:00.0 VGA compatible controller: Intel Corporation Battlemage G31 [Intel Graphics] (prog-if 00 [VGA controller])
        Subsystem: Intel Corporation Device 1701
        Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr+ Stepping- SERR+ FastB2B- DisINTx-
        Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
        Latency: 0, Cache Line Size: 32 bytes
        Interrupt: pin ? routed to IRQ 1064
        NUMA node: 1
        IOMMU group: 50
        Region 0: Memory at e0000000 (64-bit, non-prefetchable) [size=16M]
        Region 2: Memory at 227000000000 (64-bit, prefetchable) [size=32G]
        Expansion ROM at e1000000 [disabled] [size=2M]
        Capabilities: [70] Express (v2) Endpoint, IntMsgNum 0
                DevCap: MaxPayload 256 bytes, PhantFunc 0, Latency L0s unlimited, L1 unlimited
                        ExtTag+ AttnBtn- AttnInd- PwrInd- RBE+ FLReset+ SlotPowerLimit 0W TEE-IO-
                DevCtl: CorrErr- NonFatalErr- FatalErr+ UnsupReq-
                        RlxdOrd+ ExtTag+ PhantFunc- AuxPwr- NoSnoop+ FLReset-
                        MaxPayload 256 bytes, MaxReadReq 512 bytes
                DevSta: CorrErr- NonFatalErr- FatalErr- UnsupReq- AuxPwr- TransPend-
                LnkCap: Port #0, Speed 2.5GT/s, Width x1, ASPM L0s L1, Exit Latency L0s <64ns, L1 <1us
                LnkSta: Speed 2.5GT/s, Width x1
        Capabilities: [ac] MSI: Enable+ Count=1/1 Maskable+ 64bit+
        Capabilities: [d0] Power Management version 3
        Capabilities: [100 v1] Alternative Routing-ID Interpretation (ARI)
        Capabilities: [200 v1] Address Translation Service (ATS)
                ATSCap: Invalidate Queue Depth: 00
                ATSCtl: Enable+, Smallest Translation Unit: 00
        Capabilities: [420 v1] Physical Resizable BAR
                BAR 2: current size: 32GB, supported: 256MB 512MB 1GB 2GB 4GB 8GB 16GB 32GB
        Capabilities: [400 v1] Latency Tolerance Reporting
        Kernel driver in use: xe
        Kernel modules: xe

Trimmed to the interesting parts — PCIe equalization/preset tables removed.

Driver Version

26.31.39395.13

Installed GPU Driver Packages

$ sudo dpkg --list | grep -iE "igc|gmm|opencl|level-zero|fc|level_zero|ocloc|libze"
ii  clinfo                    3.0.25.02.14-1build1         amd64
ii  intel-ocloc               26.31.39395.13-1~26.04~ppa1  amd64
ii  intel-opencl-icd          26.31.39395.13-1~26.04~ppa1  amd64
ii  libigc2                   2.40.13-1~26.04              amd64
ii  libigdfcl2                2.40.13-1~26.04              amd64
ii  libigdgmm12:amd64         22.10.0-1~26.04~ppa1         amd64
ii  libze-dev:amd64           1.32.0-1~26.04~ppa1          amd64
ii  libze-intel-gpu1          26.31.39395.13-1~26.04~ppa1  amd64
ii  libze1:amd64              1.32.0-1~26.04~ppa1          amd64
ii  ocl-icd-libopencl1:amd64  2.3.4-1                      amd64

Driver Installation Details

Prebuilt .deb packages installed with apt from the Intel PPA
(ppa:kobuk-team/intel-graphics-staging, resolute suite) on Ubuntu 26.04 LTS.
Nothing built from source, nothing side-loaded, no ZE_ENABLE_ALT_DRIVERS and no
LD_LIBRARY_PATH override — this is the stock supported install. LD_DEBUG=libs
confirms the system libraries are the ones doing the work:

$ LD_DEBUG=libs ./zeprobe-reuse 1 2>&1 | grep 'calling init'
calling init: /usr/lib/x86_64-linux-gnu/libze_intel_gpu.so.1
calling init: /usr/lib/x86_64-linux-gnu/libigdfcl.so.2
calling init: /usr/lib/x86_64-linux-gnu/libigc.so.2

Linux Distribution

Other (please specify below)

Other Linux Distribution

Ubuntu 26.04 LTS (resolute)

Kernel Version & Boot Parameters

$ uname -a
Linux smc-test02 7.0.0+ #1 SMP PREEMPT_DYNAMIC Tue Jul 21 15:00:43 UTC 2026 x86_64 GNU/Linux

$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-7.0.0+ root=/dev/mapper/ubuntu--vg-ubuntu--lv ro
i915.force_probe=!e210 xe.force_probe=e210 nvme_core.multipath=N
intel_iommu=on iommu=pt xe.max_vfs=16 console=tty0 console=ttyS1,115200n8
crashkernel=2G-4G:320M,4G-32G:512M,32G-64G:1024M,64G-128G:2048M,128G-:4096M

Actual Behavior

Each zeMemOpenIpcHandle() call leaves behind one extra /dmabuf: fd that never goes
away. zeMemCloseIpcHandle() doesn't release it — if I skip the Close call entirely
the fd count comes out exactly the same. zeContextDestroy() doesn't reclaim them
either. Every call returns ZE_RESULT_SUCCESS, so nothing warns you it's happening.

30 balanced Open/Close pairs on a single IPC handle leave 30 fds behind:

before opens: 1          <- the fd carried by the IPC handle
after open/close 1: 3    <- +2 (the extra one looks like one-time driver init)
after open/close 2: 4    <- +1
...
after open/close 30: 32
after cleanup: 31        <- Put + Free + ContextDestroy gave back exactly one

Counting after every individual call in a full cycle shows where it comes from:

after zeMemAllocDevice       dmabuf_fds=0
after zeMemGetIpcHandle      dmabuf_fds=1     (handle carries fd 4)
after zeMemOpenIpcHandle     dmabuf_fds=3     +2 here, +1 on later iterations
after zeMemCloseIpcHandle    dmabuf_fds=3     no change
after zeMemPutIpcHandle      dmabuf_fds=2     releases the handle's fd
after zeMemFree              dmabuf_fds=2
after zeContextDestroy       dmabuf_fds=2

To get ahead of the obvious question: this isn't me forgetting to close an fd I was
handed. The only descriptor I ever see is the one inside ze_ipc_mem_handle_t, and
zeMemPutIpcHandle() clearly does release that one — you can watch it go from 1 back
to 0 above. What leaks is a second fd created inside zeMemOpenIpcHandle() that I'm
never given a reference to, so there's nothing I could close even if I wanted to, and
zeMemCloseIpcHandle() leaves it alone.

Since the loop reuses one handle, this also shows the driver isn't refcounting and
handing back the same mapping — every Open produces a distinct mapping, and closing
it frees nothing.

Expected Behavior

zeMemCloseIpcHandle() should release whatever zeMemOpenIpcHandle() allocated,
internal dmabuf fds included, so the process fd count stays flat no matter how many
balanced Open/Close pairs you do. Failing that, zeContextDestroy() should at least
clean up on the way out.

Reproduction Rate

Always reproduces - 100%

Steps to Reproduce

  1. Save the program from the Source Code field below as zeprobe-reuse.c.
  2. Build it: gcc -O0 -g -o zeprobe-reuse zeprobe-reuse.c -lze_loader
  3. Run it: ./zeprobe-reuse 30

It allocates one device buffer, exports one IPC handle, then does 30 balanced
zeMemOpenIpcHandle/zeMemCloseIpcHandle pairs against that same handle, counting
entries in /proc/self/fd whose readlink target contains dmabuf after each pair.
Nothing else happens inside the loop — no allocations, no Get/Put — so the growth
can only come from the Open/Close pair.

The count should stay flat. Instead it grows by one per iteration, without bound, until the process hits RLIMIT_NOFILE. ./zeprobe-reuse 1000 ends at after open/close 1000: 1002 and after cleanup: 1001 — 1000 leaked, plus the handle's own fd and one from driver init. At that point the process is already close to the default 1024 soft limit.

Is this a regression?

  • Yes, this is a regression - functionality that previously worked is now broken

Last Known Working Driver Version

None known - appears to have never worked

First Known Failing Driver Version

Unknown - oldest version tested (26.18.38308.1) is already affected

API Call Logs

Nothing to show, which is part of the problem: every call in the sequence returns
ZE_RESULT_SUCCESS. zeMemCloseIpcHandle() reports success while releasing nothing,
so the leak is completely silent from the API's point of view.

strace Logs

Not attached, but happy to provide if useful. /proc/self/fd readlink targets render
as /dmabuf:, which is how the reproducer identifies them.

System Logs / dmesg Output

Nothing in dmesg correlates with the leaking calls — no xe errors or warnings
appear during a probe run, and the count grows identically whether or not anything is
logged.

Backtrace (if crash or hang occurred)

Not applicable, there's no crash. The process runs to completion and exits cleanly;
it just accumulates descriptors until it would hit RLIMIT_NOFILE.

Source Code / Reproducer

#include <level_zero/ze_api.h>

#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define ZCHECK(_call)                                                         \
    do {                                                                      \
        ze_result_t _result = (_call);                                        \
        if (_result != ZE_RESULT_SUCCESS) {                                   \
            fprintf(stderr, "%s failed: 0x%x\n", #_call, _result);            \
            return 1;                                                         \
        }                                                                     \
    } while (0)

static int count_dmabuf_fds(void)
{
    char path[256];
    char target[256];
    struct dirent *entry;
    DIR *dir;
    ssize_t length;
    int count = 0;

    dir = opendir("/proc/self/fd");
    if (dir == NULL) {
        return -1;
    }

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_name[0] == '.') {
            continue;
        }

        snprintf(path, sizeof(path), "/proc/self/fd/%s", entry->d_name);
        length = readlink(path, target, sizeof(target) - 1);
        if (length <= 0) {
            continue;
        }

        target[length] = '\0';
        if (strstr(target, "dmabuf") != NULL) {
            ++count;
        }
    }

    closedir(dir);
    return count;
}

int main(int argc, char **argv)
{
    int iterations = (argc > 1) ? atoi(argv[1]) : 30;
    ze_context_desc_t context_desc = {};
    ze_device_mem_alloc_desc_t alloc_desc = {};
    ze_driver_handle_t driver;
    ze_device_handle_t device;
    ze_context_handle_t context;
    ze_ipc_mem_handle_t handle;
    uint32_t driver_count = 1;
    uint32_t device_count = 1;
    void *allocation;
    void *mapping;
    int i;

    ZCHECK(zeInit(0));
    ZCHECK(zeDriverGet(&driver_count, &driver));
    ZCHECK(zeDeviceGet(driver, &device_count, &device));

    context_desc.stype = ZE_STRUCTURE_TYPE_CONTEXT_DESC;
    alloc_desc.stype   = ZE_STRUCTURE_TYPE_DEVICE_MEM_ALLOC_DESC;
    ZCHECK(zeContextCreate(driver, &context_desc, &context));
    ZCHECK(zeMemAllocDevice(context, &alloc_desc, 4096, 64, device,
                            &allocation));
    ZCHECK(zeMemGetIpcHandle(context, allocation, &handle));

    printf("before opens: %d\n", count_dmabuf_fds());
    for (i = 1; i <= iterations; ++i) {
        ZCHECK(zeMemOpenIpcHandle(context, device, handle, 0, &mapping));
        ZCHECK(zeMemCloseIpcHandle(context, mapping));
        printf("after open/close %d: %d\n", i, count_dmabuf_fds());
    }

    ZCHECK(zeMemPutIpcHandle(context, handle));
    ZCHECK(zeMemFree(context, allocation));
    ZCHECK(zeContextDestroy(context));
    printf("after cleanup: %d\n", count_dmabuf_fds());
    return 0;
}

Command Line / Application Details

gcc -O0 -g -o zeprobe-reuse zeprobe-reuse.c -lze_loader
./zeprobe-reuse 30

No environment variables set. Single GPU visible, so no ZE_AFFINITY_MASK either.

oneAPI Version (if applicable)

Not used - reproducer links only libze_loader from the distro packages

Screenshots / Video

Not applicable — console output only, included above.

Additional Notes

I tried this on two unrelated machines across three driver releases and got to the same result, so it doesn't seem tied to a particular box, SKU or release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OS: LinuxIssue specific to Linux distributions (Ubuntu, Fedora, RHEL, etc.)Type: BugGeneral bug report, unexpected behavior or crash

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions