Coverage for cuda/pathfinder/_dynamic_libs/load_dl_linux.py: 72.64%
106 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 02:41 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 02:41 +0000
1# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2# SPDX-License-Identifier: Apache-2.0
4from __future__ import annotations
6import contextlib
7import ctypes
8import ctypes.util
9import os
10import sys
11from typing import TYPE_CHECKING, cast
13from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
15if TYPE_CHECKING:
16 from cuda.pathfinder._dynamic_libs.lib_descriptor import LibDescriptor
18if sys.platform == "linux":
19 CDLL_MODE = os.RTLD_NOW | os.RTLD_GLOBAL
20else:
21 CDLL_MODE = 0
24def _load_libdl() -> ctypes.CDLL:
25 # In normal glibc-based Linux environments, find_library("dl") should return
26 # something like "libdl.so.2". In minimal or stripped-down environments
27 # (no ldconfig/gcc, incomplete linker cache), this can return None even
28 # though libdl is present. In that case, we fall back to the stable SONAME.
29 name = ctypes.util.find_library("dl") or "libdl.so.2"
30 try:
31 return ctypes.CDLL(name)
32 except OSError as e:
33 raise RuntimeError(f"Could not load {name!r} (required for dlinfo/dlerror on Linux)") from e
36LIBDL = _load_libdl()
38# dlinfo
39LIBDL.dlinfo.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
40LIBDL.dlinfo.restype = ctypes.c_int
42# dlerror (thread-local error string; cleared after read)
43LIBDL.dlerror.argtypes = []
44LIBDL.dlerror.restype = ctypes.c_char_p
46# First appeared in 2004-era glibc. Universally correct on Linux for all practical purposes.
47RTLD_DI_LINKMAP = 2
48RTLD_DI_ORIGIN = 6
51class _LinkMapLNameView(ctypes.Structure):
52 """
53 Prefix-only view of glibc's `struct link_map` used **solely** to read `l_name`.
55 Background:
56 - `dlinfo(handle, RTLD_DI_LINKMAP, ...)` returns a `struct link_map*`.
57 - The first few members of `struct link_map` (including `l_name`) have been
58 stable on glibc for decades and are documented as debugger-visible.
59 - We only need the offset/layout of `l_name`, not the full struct.
61 Safety constraints:
62 - This is a **partial** definition (prefix). It must only be used via a pointer
63 returned by `dlinfo(...)`.
64 - Do **not** instantiate it or pass it **by value** to any C function.
65 - Do **not** access any members beyond those declared here.
66 - Do **not** rely on `ctypes.sizeof(LinkMapPrefix)` for allocation.
68 Rationale:
69 - Defining only the leading fields avoids depending on internal/unstable
70 tail members while keeping code more readable than raw pointer arithmetic.
71 """
73 _fields_ = (
74 ("l_addr", ctypes.c_void_p), # ElfW(Addr)
75 ("l_name", ctypes.c_char_p), # char*
76 )
79# Defensive assertions, mainly to document the invariants we depend on
80assert _LinkMapLNameView.l_addr.offset == 0
81assert _LinkMapLNameView.l_name.offset == ctypes.sizeof(ctypes.c_void_p)
84def _dl_last_error() -> str | None:
85 msg_bytes = cast(bytes | None, LIBDL.dlerror())
86 if not msg_bytes:
87 return None # no pending error
88 # Never raises; undecodable bytes are mapped to U+DC80..U+DCFF
89 return msg_bytes.decode("utf-8", "surrogateescape")
92def l_name_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
93 lm_view = ctypes.POINTER(_LinkMapLNameView)() 1abc
94 rc = LIBDL.dlinfo(ctypes.c_void_p(handle._handle), RTLD_DI_LINKMAP, ctypes.byref(lm_view)) 1abc
95 if rc != 0: 1abc
96 err = _dl_last_error()
97 raise OSError(f"dlinfo failed for {libname=!r} (rc={rc})" + (f": {err}" if err else ""))
98 if not lm_view: # NULL link_map** 1abc
99 raise OSError(f"dlinfo returned NULL link_map pointer for {libname=!r}")
101 l_name_bytes = lm_view.contents.l_name 1abc
102 if not l_name_bytes: 1abc
103 raise OSError(f"dlinfo returned empty link_map->l_name for {libname=!r}")
105 path = os.fsdecode(l_name_bytes) 1abc
106 if not path: 1abc
107 raise OSError(f"dlinfo returned empty l_name string for {libname=!r}")
109 return path 1abc
112def l_origin_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
113 l_origin_buf = ctypes.create_string_buffer(4096) 1abc
114 rc = LIBDL.dlinfo(ctypes.c_void_p(handle._handle), RTLD_DI_ORIGIN, l_origin_buf) 1abc
115 if rc != 0: 1abc
116 err = _dl_last_error()
117 raise OSError(f"dlinfo failed for {libname=!r} (rc={rc})" + (f": {err}" if err else ""))
119 path = os.fsdecode(l_origin_buf.value) 1abc
120 if not path: 1abc
121 raise OSError(f"dlinfo returned empty l_origin string for {libname=!r}")
123 return path 1abc
126def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
127 l_name = l_name_for_dynamic_library(libname, handle) 1abc
128 l_origin = l_origin_for_dynamic_library(libname, handle) 1abc
129 return os.path.join(l_origin, os.path.basename(l_name)) 1abc
132if sys.platform == "linux":
134 def check_if_already_loaded_from_elsewhere(desc: LibDescriptor) -> LoadedDL | None:
135 for soname in desc.linux_sonames: 1abcd
136 try: 1abcd
137 handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD) 1abcd
138 except OSError: 1abcd
139 continue 1abcd
140 else:
141 return LoadedDL(
142 abs_path_for_dynamic_library(desc.name, handle),
143 True,
144 handle._handle,
145 "was-already-loaded-from-elsewhere",
146 )
147 return None 1abcd
149 def _load_lib(desc: LibDescriptor, filename: str) -> ctypes.CDLL:
150 cdll_mode = CDLL_MODE 1abc
151 if desc.requires_rtld_deepbind: 1abc
152 cdll_mode |= os.RTLD_DEEPBIND
153 return ctypes.CDLL(filename, cdll_mode) 1abc
154else:
156 def check_if_already_loaded_from_elsewhere(_desc: LibDescriptor) -> LoadedDL | None:
157 raise RuntimeError(f"check_if_already_loaded_from_elsewhere() is not supported on platform {sys.platform!r}")
159 def _load_lib(_desc: LibDescriptor, _filename: str) -> ctypes.CDLL:
160 raise RuntimeError(f"_load_lib() is not supported on platform {sys.platform!r}")
163def load_with_system_search(desc: LibDescriptor) -> LoadedDL | None:
164 """Try to load a library using the native Linux dynamic-loader search path.
166 Args:
167 desc: Descriptor for the library to load
169 Returns:
170 A LoadedDL object if successful, None if the library cannot be loaded
172 """
173 for soname in desc.linux_sonames: 1abce
174 try: 1abce
175 handle = _load_lib(desc, soname) 1abce
176 except OSError: 1ae
177 pass 1ae
178 else:
179 abs_path = abs_path_for_dynamic_library(desc.name, handle) 1abc
180 assert abs_path 1abc
181 return LoadedDL(abs_path, False, handle._handle, "system-search") 1abc
182 return None 1ae
185def _work_around_known_bugs(libname: str, found_path: str) -> None:
186 if libname == "nvrtc":
187 # Work around bug/oversight in
188 # nvidia_cuda_nvrtc-13.0.48-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl
189 # Issue: libnvrtc.so.13 RUNPATH is not set.
190 # This workaround is highly specific
191 # - for simplicity.
192 # - to not mask bugs in future nvidia-cuda-nvrtc releases.
193 # - because a more general workaround is complicated.
194 dirname, basename = os.path.split(found_path)
195 if basename == "libnvrtc.so.13":
196 dep_basename = "libnvrtc-builtins.so.13.0"
197 dep_path = os.path.join(dirname, dep_basename)
198 if os.path.isfile(dep_path):
199 # In case of failure, defer to primary load, which is almost certain to fail, too.
200 with contextlib.suppress(OSError):
201 ctypes.CDLL(dep_path, CDLL_MODE)
204def load_with_abs_path(desc: LibDescriptor, found_path: str, found_via: str | None = None) -> LoadedDL:
205 """Load a dynamic library from the given path.
207 Args:
208 desc: Descriptor for the library to load.
209 found_path: The absolute path to the library file.
210 found_via: Label indicating how the path was discovered.
212 Returns:
213 A LoadedDL object representing the loaded library.
215 Raises:
216 RuntimeError: If the library cannot be loaded.
217 """
218 _work_around_known_bugs(desc.name, found_path)
219 try:
220 handle = _load_lib(desc, found_path)
221 except OSError as e:
222 raise RuntimeError(f"Failed to dlopen {found_path}: {e}") from e
223 return LoadedDL(found_path, False, handle._handle, found_via)