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§
- Elf64
Dyn 🔒 - One
Elf64_Dynentry, modeled so the GOT patch can iterate a library’s.dynamicarray: ad_tag(DT_*) paired withd_un, the 64-bitd_val/d_ptrunion interpreted per tag. - Elf64
Rela 🔒 - One
Elf64_Relarelocation 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 byelf64_r_sym/elf64_r_type), and the unusedr_addend. - NvRm
Control 🔒Params - The param block the RM control ioctl hands back (
NVOS54_PARAMETERS, 32 bytes, pointed at byarg) — 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_sizethat marks an ATTACHED response and tells it apart from PROBED: both structs open with the samegpuIds[32], so only the exact total size (heregpuIds[32]=4 * MAX_ATTACHED_GPUS) disambiguates them before the shared leading array is rewritten. - DT_
JMPREL 🔒 - ELF
.dynamictagDT_JMPREL(23): address of the PLT relocation table (.rela.plt). - DT_NULL 🔒
- The
DT_*.dynamictags the GOT patch needs to locate a library’s symbol, string, and relocation tables — spelled out here because libc exposesPT_DYNAMICbut not these individual values.DT_NULL(0) terminates the array. - DT_
PLTRELSZ 🔒 - ELF
.dynamictagDT_PLTRELSZ(2): total byte size of the PLT relocation table. - DT_RELA 🔒
- ELF
.dynamictagDT_RELA(7): address of the general relocation table (.rela.dyn). - DT_
RELASZ 🔒 - ELF
.dynamictagDT_RELASZ(8): total byte size of the.rela.dynrelocation table. - DT_
STRTAB 🔒 - ELF
.dynamictagDT_STRTAB(5): address of the dynamic string table (symbol names). - DT_
SYMTAB 🔒 - ELF
.dynamictagDT_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 againstNvRmControlParams::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 againstNvRmControlParams::cmd. - INVALID_
GPU_ 🔒ID - Marks the end of the live ids: the RM fills unused
gpuIdsslots with this sentinel, so the firstINVALID_GPU_IDterminates the populated prefix the filter scans and rewrites. - IOC_
SIZEMASK 🔒 - 14-bit mask covering the
_IOC_SIZEfield ((1 << 14) - 1); shifted byIOC_SIZESHIFTand cleared from a request byioc_no_sizeso the driver’s param-struct size cannot affect the match. - IOC_
SIZESHIFT 🔒 - Locates the
_IOC_SIZEfield 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 withIOC_SIZEMASKbyioc_no_size. - MAX_
ATTACHED_ 🔒GPUS - The RM ABI caps attached GPUs at 32, so this is the fixed
gpuIdsarray 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 recognizeNV_RM_CONTROL_REQUEST. Kept as a named constant only for documentation and theioc_nr_extracts_low_byteunit 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_sizethat marks a PROBED response and tells it apart from ATTACHED: this struct isgpuIds[32]followed byexcludedGpuIds[32](4 * MAX_ATTACHED_GPUS * 2). Only the shared leadinggpuIds[32]is rewritten; the trailingexcludedGpuIds[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
rdevof/dev/nvidiactl, resolved once atinstall(). 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
objecthas its import ofsymbolbound: the address its GOT slot holds, which is the library’s own PLT stub while a lazy binding has not been resolved yet.Nonewhen no such object is loaded or it does not importsymbol. - dyn_
addr 🔒 - Turn a
DT_*d_ptrinto 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 loadbaseas a relative offset to add tobase, and a value at or abovebaseas already absolute. - elf64_
r_ 🔒sym - Extract the symbol-table index from an
Elf64_Relar_infofield (its high 32 bits) — how the patch learns which symbol a relocation binds, so it can check forioctl. - elf64_
r_ 🔒type - Extract the relocation type from an
Elf64_Relar_infofield (its low 32 bits) — how the patch tells aJUMP_SLOT/GLOB_DATGOT 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 passkeepare compacted to the front and the rest back-filled withINVALID_GPU_ID. - filtered_
ioctl 🔒 ⚠ - The seam where a hidden GPU gets scrubbed out: the
ioctlwrapper installed into the NVIDIA libraries’ GOT slots, which forwards to the reallibc::ioctland then post-processes the response. The three steps below exist only to make interposing onioctlsafe: - gpuid_
to_ 🔒minor - Map an RM
gpuIdto the/dev/nvidiaminor thatnode_reachableneeds: 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_IDSGOT 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 aOnce). - ioc_
no_ 🔒size - Strip the
_IOC_SIZEfield 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 changessizeof(NVOS54_PARAMETERS)must still match. - ioc_nr 🔒
- Extract the
_IOC_NRcommand byte (bits 0-7) of an ioctl request — the field that names the command. Used only by documentation and theioc_nr_extracts_low_byteunit 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 butopen()fails withEPERM; a presence test would count it as visible and leave the filter uninstalled. - page_
prot 🔒 - Reports the current
PROT_*protection of the page holdingaddr(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
ioctlGOT slot tofiltered_ioctl. - patch_
phdr_ 🔒 ⚠cb dl_iterate_phdrcallback: for each loaded object matching a targeted NVIDIA library, find itsPT_DYNAMICsegment and repoint itsioctlslots. Returns 0 to keep iterating.- path_
opens 🔒 - Whether
pathopens 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_IDSresponse so the NVIDIA libraries never try to peer-init a GPU the container cannot reach: it drops every id whose/dev/nvidianode is absent. Split fromfiltered_ioctlso the whole thing runs undercatch_unwind. - visit_
import_ 🔒 ⚠slots - Hand every GOT slot through which one loaded library calls
symboltovisit, by walking itsPT_DYNAMICarray — 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-pltrelocation forms because NVIDIA ships the latter. - visit_
reloc_ 🔒 ⚠table - Hand exactly the GOT slots of
symbolin one relocation table tovisitand nothing else — every candidate entry is name-checked so no other symbol the library imports is disturbed. It handles both theJUMP_SLOTandGLOB_DATentry forms: an entry is skipped unless its relocation type is one of those with a nonzero symbol index whose name, resolved throughsymtab/strtab, is exactlysymbol; the slot of a match lives atbase + r_offset.