Skip to main content

Module nvgpufilter

Module nvgpufilter 

Source
Expand description

Multi-GPU NVENC device filtering via kernel ioctl. Multi-GPU NVENC GET_ATTACHED_IDS / GET_PROBED_IDS ioctl filter — it exists so a container handed only a subset of the host’s GPUs can still open an NVENC session.

The problem: on NVIDIA driver 570-595, libnvidia-encode / libcuda / libnvcuvid enumerate every host GPU via the RM GET_ATTACHED_IDS ioctl and try to peer-init each one — including GPUs the container never exposed. A GPU whose /dev/nvidiaX node is absent or unopenable then makes nvEncOpenEncodeSessionEx fail with UNSUPPORTED_DEVICE, so the session cannot open at all even though a perfectly usable GPU is right there in the container.

The fix is to strip the unreachable GPUs out of that enumeration response before the libraries act on it. It GOT-patches ioctl in those NVIDIA libraries only — deliberately not an LD_PRELOAD object, which would shadow every ioctl in the process and need its own recursion guard. Because this crate’s own GOT is left untouched, the wrapper’s inner ioctl still resolves to the real libc instead of re-entering itself.

Everything hinges on at least one host GPU being hidden from the container, since that is the only situation the bug arises in: on 565-or-before / 610-or-later drivers (enumeration already correct) and whenever the container can see every host GPU, the strict-subset rule downstream makes the whole filter a no-op.

Structs§

Elf64Dyn 🔒
One Elf64_Dyn entry, modeled so the GOT patch can iterate a library’s .dynamic array: a d_tag (DT_*) paired with d_un, the 64-bit d_val/d_ptr union interpreted per tag.
Elf64Rela 🔒
One Elf64_Rela relocation entry, modeled so the patch can find each GOT slot and the symbol it binds: r_offset (the target GOT slot, base-relative), r_info (packed symbol index + relocation type, split by elf64_r_sym/elf64_r_type), and the unused r_addend.
NvRmControlParams 🔒
The param block the RM control ioctl hands back (NVOS54_PARAMETERS, 32 bytes, pointed at by arg) — modeled here so the filter can identify and validate an enumeration call before it touches the GPU-id array it carries.

Constants§

ATTACHED_PARAMS_SIZE 🔒
The params_size that marks an ATTACHED response and tells it apart from PROBED: both structs open with the same gpuIds[32], so only the exact total size (here gpuIds[32] = 4 * MAX_ATTACHED_GPUS) disambiguates them before the shared leading array is rewritten.
DT_JMPREL 🔒
ELF .dynamic tag DT_JMPREL (23): address of the PLT relocation table (.rela.plt).
DT_NULL 🔒
The DT_* .dynamic tags the GOT patch needs to locate a library’s symbol, string, and relocation tables — spelled out here because libc exposes PT_DYNAMIC but not these individual values. DT_NULL (0) terminates the array.
DT_PLTRELSZ 🔒
ELF .dynamic tag DT_PLTRELSZ (2): total byte size of the PLT relocation table.
DT_RELA 🔒
ELF .dynamic tag DT_RELA (7): address of the general relocation table (.rela.dyn).
DT_RELASZ 🔒
ELF .dynamic tag DT_RELASZ (8): total byte size of the .rela.dyn relocation table.
DT_STRTAB 🔒
ELF .dynamic tag DT_STRTAB (5): address of the dynamic string table (symbol names).
DT_SYMTAB 🔒
ELF .dynamic tag DT_SYMTAB (6): address of the dynamic symbol table.
GPU_GET_ATTACHED_IDS 🔒
Identifies the attached-GPU enumeration response the filter rewrites — RM control command NV0000_CTRL_CMD_GPU_GET_ATTACHED_IDS, matched against NvRmControlParams::cmd.
GPU_GET_PROBED_IDS 🔒
Identifies the probed-GPU enumeration response the filter rewrites — RM control command NV0000_CTRL_CMD_GPU_GET_PROBED_IDS, matched against NvRmControlParams::cmd.
INVALID_GPU_ID 🔒
Marks the end of the live ids: the RM fills unused gpuIds slots with this sentinel, so the first INVALID_GPU_ID terminates the populated prefix the filter scans and rewrites.
IOC_SIZEMASK 🔒
14-bit mask covering the _IOC_SIZE field ((1 << 14) - 1); shifted by IOC_SIZESHIFT and cleared from a request by ioc_no_size so the driver’s param-struct size cannot affect the match.
IOC_SIZESHIFT 🔒
Locates the _IOC_SIZE field so it can be cleared for size-agnostic request matching: the 14-bit encoded parameter size lives at bit 16 of an ioctl request (asm-generic/ioctl.h). Paired with IOC_SIZEMASK by ioc_no_size.
MAX_ATTACHED_GPUS 🔒
The RM ABI caps attached GPUs at 32, so this is the fixed gpuIds array capacity in both enumeration params structs — and the bound the filter’s scans and scratch buffers are sized to.
NV_ESC_RM_CONTROL 🔒
The ioctl command number (_IOC_NR, bits 0-7) that identifies the RM control escape — the low byte the request gate keys on to recognize NV_RM_CONTROL_REQUEST. Kept as a named constant only for documentation and the ioc_nr_extracts_low_byte unit test, hence #[allow(dead_code)].
NV_RM_CONTROL_REQUEST 🔒
The RM control ioctl the gate recognizes: _IOWR('F'=0x46, NR=0x2A, sizeof(NVOS54_PARAMETERS)=32), encoding DIR=READ|WRITE, TYPE=0x46, NR=0x2A, SIZE=0x20.
PROBED_PARAMS_SIZE 🔒
The params_size that marks a PROBED response and tells it apart from ATTACHED: this struct is gpuIds[32] followed by excludedGpuIds[32] (4 * MAX_ATTACHED_GPUS * 2). Only the shared leading gpuIds[32] is rewritten; the trailing excludedGpuIds[32] lists GPUs to exclude, not to use, so it is deliberately left untouched.
RELOC_ARCH_SUPPORTED 🔒
RELOC_GLOB_DAT 🔒
RELOC_JUMP_SLOT 🔒

Statics§

NVIDIACTL_RDEV 🔒
Identity of the RM control device, cached so the hook only rewrites responses that truly came from it: the rdev of /dev/nvidiactl, resolved once at install(). A value of 0 means it was never resolved, in which case the fd-identity gate is skipped and matching falls back to the request/size checks alone.

Functions§

bound_import 🔒
Where the first loaded object whose path contains object has its import of symbol bound: the address its GOT slot holds, which is the library’s own PLT stub while a lazy binding has not been resolved yet. None when no such object is loaded or it does not import symbol.
dyn_addr 🔒
Turn a DT_* d_ptr into an absolute address across libc implementations that disagree on what it holds: glibc pre-relocates these pointers to absolute, while musl leaves them file-relative, so the patch cannot assume either. The heuristic treats a value below the load base as a relative offset to add to base, and a value at or above base as already absolute.
elf64_r_sym 🔒
Extract the symbol-table index from an Elf64_Rela r_info field (its high 32 bits) — how the patch learns which symbol a relocation binds, so it can check for ioctl.
elf64_r_type 🔒
Extract the relocation type from an Elf64_Rela r_info field (its low 32 bits) — how the patch tells a JUMP_SLOT/GLOB_DAT GOT entry from relocations it must ignore.
filter_ids 🔒
Drop the hidden GPUs from a gpuIds[] array in place — but only ever a strict subset, so the filter can never accidentally blank out every GPU and strand the container with none. Ids that pass keep are compacted to the front and the rest back-filled with INVALID_GPU_ID.
filtered_ioctl 🔒 ⚠
The seam where a hidden GPU gets scrubbed out: the ioctl wrapper installed into the NVIDIA libraries’ GOT slots, which forwards to the real libc::ioctl and then post-processes the response. The three steps below exist only to make interposing on ioctl safe:
gpuid_to_minor 🔒
Map an RM gpuId to the /dev/nvidia minor that node_reachable needs: the id only carries a PCI address, not a device minor, so this bridges the two by scanning /proc/driver/nvidia/gpus, returning -1 when no match is found.
has_hidden_gpus 🔒
True when at least one host GPU is hidden from the container — the only situation that can trigger the peer-init bug, so it gates whether the filter installs at all.
install
Install the GET_ATTACHED_IDS/GET_PROBED_IDS GOT filter, at most once and only when a host GPU is hidden from the container. Idempotent and safe to call before every NVENC session open (guarded by a Once).
ioc_no_size 🔒
Strip the _IOC_SIZE field from a request (leaving DIR|TYPE|NR) so the gate stays bound to the exact RM control command without coupling to any one driver’s param-struct size — a driver that changes sizeof(NVOS54_PARAMETERS) must still match.
ioc_nr 🔒
Extract the _IOC_NR command byte (bits 0-7) of an ioctl request — the field that names the command. Used only by documentation and the ioc_nr_extracts_low_byte unit test, hence #[allow(dead_code)].
node_reachable 🔒
The reachability test the whole filter turns on: a GPU id is kept only if its /dev/nvidia{minor} node opens. Some runtimes create a node for every host GPU and enforce the allocation in the device cgroup, so an unallocated node exists but open() fails with EPERM; a presence test would count it as visible and leave the filter uninstalled.
page_prot 🔒
Reports the current PROT_* protection of the page holding addr (from /proc/self/maps, or -1 when the address is not found or maps is unreadable) for two reasons the GOT patch depends on: so it can refuse to dereference an address that a wrong relative/absolute guess landed in an unreadable page, and so it can restore a patched GOT page to its true original protection rather than a hardcoded read-only.
patch_ioctl_slot 🔒 ⚠
Repoint one ioctl GOT slot to filtered_ioctl.
patch_phdr_cb 🔒 ⚠
dl_iterate_phdr callback: for each loaded object matching a targeted NVIDIA library, find its PT_DYNAMIC segment and repoint its ioctl slots. Returns 0 to keep iterating.
path_opens 🔒
Whether path opens read-write from this process. The descriptor is closed at once.
rewrite_attached_ids 🔒 ⚠
Scrub the hidden GPUs out of a successful GET_ATTACHED_IDS / GET_PROBED_IDS response so the NVIDIA libraries never try to peer-init a GPU the container cannot reach: it drops every id whose /dev/nvidia node is absent. Split from filtered_ioctl so the whole thing runs under catch_unwind.
visit_import_slots 🔒 ⚠
Hand every GOT slot through which one loaded library calls symbol to visit, by walking its PT_DYNAMIC array — the only way to interpose without an LD_PRELOAD object, or to read where the dynamic linker bound an import — covering both the lazy-PLT and -fno-plt relocation forms because NVIDIA ships the latter.
visit_reloc_table 🔒 ⚠
Hand exactly the GOT slots of symbol in one relocation table to visit and nothing else — every candidate entry is name-checked so no other symbol the library imports is disturbed. It handles both the JUMP_SLOT and GLOB_DAT entry forms: an entry is skipped unless its relocation type is one of those with a nonzero symbol index whose name, resolved through symtab/strtab, is exactly symbol; the slot of a match lives at base + r_offset.