55import ctypes
66import ctypes .util
77import os
8- from typing import Optional
8+ from typing import Optional , cast
99
1010from cuda .pathfinder ._dynamic_libs .load_dl_common import LoadedDL
1111from cuda .pathfinder ._dynamic_libs .supported_nvidia_libs import SUPPORTED_LINUX_SONAMES
1212
1313CDLL_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
61130def 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 )
0 commit comments