Skip to content

Commit 127f798

Browse files
authored
[pathfinder] RTLD_DI_LINKMAP-based new implementation of abs_path_for_dynamic_library() (#834)
* Use RTLD_DI_LINKMAP in abs_path_for_dynamic_library(), to eliminate need for EXPECTED_LIB_SYMBOLS * load_dl_linux.py: factor out get_candidate_sonames() and also use from check_if_already_loaded_from_elsewhere(), for consistency with load_with_system_search() * load_dl_windows.py: avoid unnecessary function-level imports (this was just an oversight) * Bump cuda-pathfinder version to `1.1.1a1` * Harden/polish the new abs_path_for_dynamic_library() implementation. * Eliminate `class LinkMap(ctypes.Structure)` and rely purely on pointer arithmetic instead. Use `os.fsdecode()` instead of `l_name.decode()` to avoid `UnicodeDecodeError` * Ensure `_dl_last_error()` does not raise `UnicodeDecodeError` * Add `validate_abs_path()` in test_load_nvidia_dynamic_lib.py * Change back to more intutive approach, using `_LinkMapLNameView` as name. Explain safety constraints in depth. * l_origin + basename(l_name) → abs_path
1 parent 0932878 commit 127f798

5 files changed

Lines changed: 117 additions & 89 deletions

File tree

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,126 @@
55
import ctypes
66
import ctypes.util
77
import os
8-
from typing import Optional
8+
from typing import Optional, cast
99

1010
from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
1111
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_LINUX_SONAMES
1212

1313
CDLL_MODE = os.RTLD_NOW | os.RTLD_GLOBAL
1414

15-
LIBDL_PATH = ctypes.util.find_library("dl") or "libdl.so.2"
16-
LIBDL = ctypes.CDLL(LIBDL_PATH)
17-
LIBDL.dladdr.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
18-
LIBDL.dladdr.restype = ctypes.c_int
1915

16+
def _load_libdl() -> ctypes.CDLL:
17+
# In normal glibc-based Linux environments, find_library("dl") should return
18+
# something like "libdl.so.2". In minimal or stripped-down environments
19+
# (no ldconfig/gcc, incomplete linker cache), this can return None even
20+
# though libdl is present. In that case, we fall back to the stable SONAME.
21+
name = ctypes.util.find_library("dl") or "libdl.so.2"
22+
try:
23+
return ctypes.CDLL(name)
24+
except OSError as e:
25+
raise RuntimeError(f"Could not load {name!r} (required for dlinfo/dlerror on Linux)") from e
26+
27+
28+
LIBDL = _load_libdl()
29+
30+
# dlinfo
31+
LIBDL.dlinfo.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
32+
LIBDL.dlinfo.restype = ctypes.c_int
33+
34+
# dlerror (thread-local error string; cleared after read)
35+
LIBDL.dlerror.argtypes = []
36+
LIBDL.dlerror.restype = ctypes.c_char_p
37+
38+
# First appeared in 2004-era glibc. Universally correct on Linux for all practical purposes.
39+
RTLD_DI_LINKMAP = 2
40+
RTLD_DI_ORIGIN = 6
2041

21-
class DlInfo(ctypes.Structure):
22-
"""Structure used by dladdr to return information about a loaded symbol."""
42+
43+
class _LinkMapLNameView(ctypes.Structure):
44+
"""
45+
Prefix-only view of glibc's `struct link_map` used **solely** to read `l_name`.
46+
47+
Background:
48+
- `dlinfo(handle, RTLD_DI_LINKMAP, ...)` returns a `struct link_map*`.
49+
- The first few members of `struct link_map` (including `l_name`) have been
50+
stable on glibc for decades and are documented as debugger-visible.
51+
- We only need the offset/layout of `l_name`, not the full struct.
52+
53+
Safety constraints:
54+
- This is a **partial** definition (prefix). It must only be used via a pointer
55+
returned by `dlinfo(...)`.
56+
- Do **not** instantiate it or pass it **by value** to any C function.
57+
- Do **not** access any members beyond those declared here.
58+
- Do **not** rely on `ctypes.sizeof(LinkMapPrefix)` for allocation.
59+
60+
Rationale:
61+
- Defining only the leading fields avoids depending on internal/unstable
62+
tail members while keeping code more readable than raw pointer arithmetic.
63+
"""
2364

2465
_fields_ = (
25-
("dli_fname", ctypes.c_char_p), # path to .so
26-
("dli_fbase", ctypes.c_void_p),
27-
("dli_sname", ctypes.c_char_p),
28-
("dli_saddr", ctypes.c_void_p),
66+
("l_addr", ctypes.c_void_p), # ElfW(Addr)
67+
("l_name", ctypes.c_char_p), # char*
2968
)
3069

3170

32-
def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> Optional[str]:
33-
"""Get the absolute path of a loaded dynamic library on Linux.
71+
# Defensive assertions, mainly to document the invariants we depend on
72+
assert _LinkMapLNameView.l_addr.offset == 0
73+
assert _LinkMapLNameView.l_name.offset == ctypes.sizeof(ctypes.c_void_p)
3474

35-
Args:
36-
libname: The name of the library
37-
handle: The library handle
3875

39-
Returns:
40-
The absolute path to the library file, or None if no expected symbol is found
76+
def _dl_last_error() -> Optional[str]:
77+
msg_bytes = cast(Optional[bytes], LIBDL.dlerror())
78+
if not msg_bytes:
79+
return None # no pending error
80+
# Never raises; undecodable bytes are mapped to U+DC80..U+DCFF
81+
return msg_bytes.decode("utf-8", "surrogateescape")
4182

42-
Raises:
43-
OSError: If dladdr fails to get information about the symbol
44-
"""
45-
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import EXPECTED_LIB_SYMBOLS
4683

47-
for symbol_name in EXPECTED_LIB_SYMBOLS[libname]:
48-
symbol = getattr(handle, symbol_name, None)
49-
if symbol is not None:
50-
break
51-
else:
52-
return None
84+
def l_name_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
85+
lm_view = ctypes.POINTER(_LinkMapLNameView)()
86+
rc = LIBDL.dlinfo(ctypes.c_void_p(handle._handle), RTLD_DI_LINKMAP, ctypes.byref(lm_view))
87+
if rc != 0:
88+
err = _dl_last_error()
89+
raise OSError(f"dlinfo failed for {libname=!r} (rc={rc})" + (f": {err}" if err else ""))
90+
if not lm_view: # NULL link_map**
91+
raise OSError(f"dlinfo returned NULL link_map pointer for {libname=!r}")
5392

54-
addr = ctypes.cast(symbol, ctypes.c_void_p)
55-
info = DlInfo()
56-
if LIBDL.dladdr(addr, ctypes.byref(info)) == 0:
57-
raise OSError(f"dladdr failed for {libname=!r}")
58-
return info.dli_fname.decode() # type: ignore[no-any-return]
93+
l_name_bytes = lm_view.contents.l_name
94+
if not l_name_bytes:
95+
raise OSError(f"dlinfo returned empty link_map->l_name for {libname=!r}")
96+
97+
path = os.fsdecode(l_name_bytes)
98+
if not path:
99+
raise OSError(f"dlinfo returned empty l_name string for {libname=!r}")
100+
101+
return path
102+
103+
104+
def l_origin_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
105+
l_origin_buf = ctypes.create_string_buffer(4096)
106+
rc = LIBDL.dlinfo(ctypes.c_void_p(handle._handle), RTLD_DI_ORIGIN, l_origin_buf)
107+
if rc != 0:
108+
err = _dl_last_error()
109+
raise OSError(f"dlinfo failed for {libname=!r} (rc={rc})" + (f": {err}" if err else ""))
110+
111+
path = os.fsdecode(l_origin_buf.value)
112+
if not path:
113+
raise OSError(f"dlinfo returned empty l_origin string for {libname=!r}")
114+
115+
return path
116+
117+
118+
def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
119+
l_name = l_name_for_dynamic_library(libname, handle)
120+
l_origin = l_origin_for_dynamic_library(libname, handle)
121+
return os.path.join(l_origin, os.path.basename(l_name))
122+
123+
124+
def get_candidate_sonames(libname: str) -> list[str]:
125+
candidate_sonames = list(SUPPORTED_LINUX_SONAMES.get(libname, ()))
126+
candidate_sonames.append(f"lib{libname}.so")
127+
return candidate_sonames
59128

60129

61130
def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]:
@@ -72,9 +141,8 @@ def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]:
72141
>>> if loaded is not None:
73142
... print(f"Library already loaded from {loaded.abs_path}")
74143
"""
75-
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_LINUX_SONAMES
76144

77-
for soname in SUPPORTED_LINUX_SONAMES.get(libname, ()):
145+
for soname in get_candidate_sonames(libname):
78146
try:
79147
handle = ctypes.CDLL(soname, mode=os.RTLD_NOLOAD)
80148
except OSError:
@@ -96,9 +164,7 @@ def load_with_system_search(libname: str) -> Optional[LoadedDL]:
96164
Raises:
97165
RuntimeError: If the library is loaded but no expected symbol is found
98166
"""
99-
candidate_sonames = list(SUPPORTED_LINUX_SONAMES.get(libname, ()))
100-
candidate_sonames.append(f"lib{libname}.so")
101-
for soname in candidate_sonames:
167+
for soname in get_candidate_sonames(libname):
102168
try:
103169
handle = ctypes.CDLL(soname, CDLL_MODE)
104170
abs_path = abs_path_for_dynamic_library(libname, handle)

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
from typing import Optional
99

1010
from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
11+
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import (
12+
LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY,
13+
SUPPORTED_WINDOWS_DLLS,
14+
)
1115

1216
# Mirrors WinBase.h (unfortunately not defined already elsewhere)
1317
WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100
@@ -110,7 +114,6 @@ def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]:
110114
>>> if loaded is not None:
111115
... print(f"Library already loaded from {loaded.abs_path}")
112116
"""
113-
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_WINDOWS_DLLS
114117

115118
for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()):
116119
handle = kernel32.GetModuleHandleW(dll_name)
@@ -129,8 +132,6 @@ def load_with_system_search(libname: str) -> Optional[LoadedDL]:
129132
Returns:
130133
A LoadedDL object if successful, None if the library cannot be loaded
131134
"""
132-
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_WINDOWS_DLLS
133-
134135
for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()):
135136
handle = kernel32.LoadLibraryExW(dll_name, None, 0)
136137
if handle:
@@ -153,10 +154,6 @@ def load_with_abs_path(libname: str, found_path: str) -> LoadedDL:
153154
Raises:
154155
RuntimeError: If the DLL cannot be loaded
155156
"""
156-
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import (
157-
LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY,
158-
)
159-
160157
if libname in LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY:
161158
add_dll_directory(found_path)
162159

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/supported_nvidia_libs.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
# SUPPORTED_LIBNAMES
77
# SUPPORTED_WINDOWS_DLLS
88
# SUPPORTED_LINUX_SONAMES
9-
# EXPECTED_LIB_SYMBOLS
109

1110
import sys
1211

@@ -401,39 +400,3 @@ def is_suppressed_dll_file(path_basename: str) -> bool:
401400
# nvrtc64_120_0.dll
402401
return path_basename.endswith(".alt.dll") or "-builtins" in path_basename
403402
return path_basename.startswith(("cudart32_", "nvvm32"))
404-
405-
406-
# Based on `nm -D --defined-only` output for Linux x86_64 distributions.
407-
EXPECTED_LIB_SYMBOLS = {
408-
"nvJitLink": (
409-
"__nvJitLinkCreate_12_0", # 12.0 through 12.9
410-
"nvJitLinkVersion", # 12.3 and up
411-
),
412-
"nvrtc": ("nvrtcVersion",),
413-
"nvvm": ("nvvmVersion",),
414-
"cudart": ("cudaRuntimeGetVersion",),
415-
"nvfatbin": ("nvFatbinVersion",),
416-
"cublas": ("cublasGetVersion",),
417-
"cublasLt": ("cublasLtGetVersion",),
418-
"cufft": ("cufftGetVersion",),
419-
"cufftw": ("fftwf_malloc",),
420-
"curand": ("curandGetVersion",),
421-
"cusolver": ("cusolverGetVersion",),
422-
"cusolverMg": ("cusolverMgCreate",),
423-
"cusparse": ("cusparseGetVersion",),
424-
"nppc": ("nppGetLibVersion",),
425-
"nppial": ("nppiAdd_32f_C1R_Ctx",),
426-
"nppicc": ("nppiColorToGray_8u_C3C1R_Ctx",),
427-
"nppidei": ("nppiCopy_8u_C1R_Ctx",),
428-
"nppif": ("nppiFilterSobelHorizBorder_8u_C1R_Ctx",),
429-
"nppig": ("nppiResize_8u_C1R_Ctx",),
430-
"nppim": ("nppiErode_8u_C1R_Ctx",),
431-
"nppist": ("nppiMean_8u_C1R_Ctx",),
432-
"nppisu": ("nppiFree",),
433-
"nppitc": ("nppiThreshold_8u_C1R_Ctx",),
434-
"npps": ("nppsAdd_32f_Ctx",),
435-
"nvblas": ("dgemm",),
436-
"cufile": ("cuFileGetVersion",),
437-
# "cufile_rdma": ("rdma_buffer_reg",),
438-
"nvjpeg": ("nvjpegCreate",),
439-
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
__version__ = "1.1.1a0"
4+
__version__ = "1.1.1a1"

cuda_pathfinder/tests/test_load_nvidia_dynamic_lib.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,6 @@ def test_supported_libnames_windows_libnames_requiring_os_add_dll_directory_cons
4646
)
4747

4848

49-
def test_supported_libnames_all_expected_lib_symbols_consistency():
50-
assert tuple(sorted(supported_nvidia_libs.SUPPORTED_LIBNAMES_ALL)) == tuple(
51-
sorted(supported_nvidia_libs.EXPECTED_LIB_SYMBOLS.keys())
52-
)
53-
54-
5549
def test_runtime_error_on_non_64bit_python():
5650
with (
5751
patch("struct.calcsize", return_value=3), # fake 24-bit pointer
@@ -68,6 +62,12 @@ def build_child_process_failed_for_libname_message(libname, result):
6862
)
6963

7064

65+
def validate_abs_path(abs_path):
66+
assert abs_path, f"empty path: {abs_path=!r}"
67+
assert os.path.isabs(abs_path), f"not absolute: {abs_path=!r}"
68+
assert os.path.isfile(abs_path), f"not a file: {abs_path=!r}"
69+
70+
7171
def child_process_func(libname):
7272
import os
7373

@@ -76,6 +76,7 @@ def child_process_func(libname):
7676
loaded_dl_fresh = load_nvidia_dynamic_lib(libname)
7777
if loaded_dl_fresh.was_already_loaded_from_elsewhere:
7878
raise RuntimeError("loaded_dl_fresh.was_already_loaded_from_elsewhere")
79+
validate_abs_path(loaded_dl_fresh.abs_path)
7980

8081
loaded_dl_from_cache = load_nvidia_dynamic_lib(libname)
8182
if loaded_dl_from_cache is not loaded_dl_fresh:
@@ -86,6 +87,7 @@ def child_process_func(libname):
8687
raise RuntimeError("loaded_dl_no_cache.was_already_loaded_from_elsewhere")
8788
if not os.path.samefile(loaded_dl_no_cache.abs_path, loaded_dl_fresh.abs_path):
8889
raise RuntimeError(f"not os.path.samefile({loaded_dl_no_cache.abs_path=!r}, {loaded_dl_fresh.abs_path=!r})")
90+
validate_abs_path(loaded_dl_no_cache.abs_path)
8991

9092
sys.stdout.write(f"{loaded_dl_fresh.abs_path!r}\n")
9193

0 commit comments

Comments
 (0)