intel / intel/compute-runtime

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

Open
#974 2 comments 0 reactions 0 assignees View on GitHub
OS: Linux Status: Merged Type: Bug
Dominant language
C++
Stars
1.4k
Forks
300
PR merge metrics
No merged PRs in 30d

Description

### Pre-submission Checklist

- [x] I am using the latest GPU driver version ([releases](https://github.com/intel/compute-runtime/releases))
- [x] 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- SERR- &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

```c
#include

#include
#include
#include
#include
#include

#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.

Contributor guide

Open the contributing guide

Research direction

Start by building and running the supplied zeprobe-reuse.c reproducer with gcc -O0 -g -o zeprobe-reuse zeprobe-reuse.c -lze_loader, then trace the implementations of zeMemOpenIpcHandle and zeMemCloseIpcHandle. Confirm that balanced calls leave /dmabuf descriptors in /proc/self/fd; done means the count stays flat and cleanup also releases internal descriptors.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, cpp, linux
Domain
api, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.