Skip to content

Commit cf5da9c

Browse files
committed
Fix Windows already-loaded detection for newer CUPTI builds
Fall back to enumerating loaded modules when basename-based GetModuleHandleW lookups miss an already loaded DLL so pathfinder can recognize newer Windows CUPTI loads consistently and keep the regression covered. Made-with: Cursor
1 parent 82e6bb8 commit cf5da9c

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import ctypes.wintypes
88
import os
99
import struct
10+
from collections.abc import Iterator
1011
from typing import TYPE_CHECKING
1112

1213
from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
@@ -22,11 +23,16 @@
2223

2324
# Set up kernel32 functions with proper types
2425
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
26+
psapi = ctypes.windll.psapi # type: ignore[attr-defined]
2527

2628
# GetModuleHandleW
2729
kernel32.GetModuleHandleW.argtypes = [ctypes.wintypes.LPCWSTR]
2830
kernel32.GetModuleHandleW.restype = ctypes.wintypes.HMODULE
2931

32+
# GetCurrentProcess
33+
kernel32.GetCurrentProcess.argtypes = []
34+
kernel32.GetCurrentProcess.restype = ctypes.wintypes.HANDLE
35+
3036
# LoadLibraryExW
3137
kernel32.LoadLibraryExW.argtypes = [
3238
ctypes.wintypes.LPCWSTR, # lpLibFileName
@@ -47,6 +53,15 @@
4753
kernel32.AddDllDirectory.argtypes = [ctypes.wintypes.LPCWSTR]
4854
kernel32.AddDllDirectory.restype = ctypes.c_void_p # DLL_DIRECTORY_COOKIE
4955

56+
# EnumProcessModules
57+
psapi.EnumProcessModules.argtypes = [
58+
ctypes.wintypes.HANDLE,
59+
ctypes.POINTER(ctypes.wintypes.HMODULE),
60+
ctypes.wintypes.DWORD,
61+
ctypes.POINTER(ctypes.wintypes.DWORD),
62+
]
63+
psapi.EnumProcessModules.restype = ctypes.wintypes.BOOL
64+
5065

5166
def ctypes_handle_to_unsigned_int(handle: ctypes.wintypes.HMODULE) -> int:
5267
"""Convert ctypes HMODULE to unsigned int."""
@@ -101,6 +116,41 @@ def abs_path_for_dynamic_library(libname: str, handle: ctypes.wintypes.HMODULE)
101116
return buffer.value
102117

103118

119+
def _iter_loaded_module_handles() -> Iterator[ctypes.wintypes.HMODULE]:
120+
process_handle = kernel32.GetCurrentProcess()
121+
capacity = 64
122+
module_size = ctypes.sizeof(ctypes.wintypes.HMODULE)
123+
while True:
124+
module_handles = (ctypes.wintypes.HMODULE * capacity)()
125+
needed = ctypes.wintypes.DWORD()
126+
ok = psapi.EnumProcessModules(
127+
process_handle,
128+
module_handles,
129+
ctypes.sizeof(module_handles),
130+
ctypes.byref(needed),
131+
)
132+
if not ok:
133+
error_code = ctypes.GetLastError() # type: ignore[attr-defined]
134+
raise RuntimeError(f"EnumProcessModules failed (error code: {error_code})")
135+
count = needed.value // module_size
136+
if count <= capacity:
137+
for raw_handle in module_handles[:count]:
138+
if raw_handle is None:
139+
continue
140+
yield ctypes.wintypes.HMODULE(int(raw_handle))
141+
return
142+
capacity = count
143+
144+
145+
def _find_loaded_module(dll_names: tuple[str, ...]) -> tuple[ctypes.wintypes.HMODULE, str] | None:
146+
wanted = {dll_name.casefold() for dll_name in dll_names}
147+
for handle in _iter_loaded_module_handles():
148+
abs_path = abs_path_for_dynamic_library("loaded module", handle)
149+
if os.path.basename(abs_path).casefold() in wanted:
150+
return handle, abs_path
151+
return None
152+
153+
104154
def check_if_already_loaded_from_elsewhere(desc: LibDescriptor, have_abs_path: bool) -> LoadedDL | None:
105155
for dll_name in desc.windows_dlls:
106156
handle = kernel32.GetModuleHandleW(dll_name)
@@ -112,6 +162,14 @@ def check_if_already_loaded_from_elsewhere(desc: LibDescriptor, have_abs_path: b
112162
# activate it even if the library was already loaded from elsewhere.
113163
add_dll_directory(abs_path)
114164
return LoadedDL(abs_path, True, ctypes_handle_to_unsigned_int(handle), "was-already-loaded-from-elsewhere")
165+
# Observed on newer Windows CUPTI builds: GetModuleHandleW(basename)
166+
# can miss an already loaded DLL, so fall back to enumerating loaded modules.
167+
loaded = _find_loaded_module(desc.windows_dlls)
168+
if loaded is not None:
169+
handle, abs_path = loaded
170+
if have_abs_path and desc.requires_add_dll_directory:
171+
add_dll_directory(abs_path)
172+
return LoadedDL(abs_path, True, ctypes_handle_to_unsigned_int(handle), "was-already-loaded-from-elsewhere")
115173
return None
116174

117175

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import sys
5+
6+
import pytest
7+
8+
if sys.platform != "win32":
9+
pytest.skip("Windows-only tests", allow_module_level=True)
10+
11+
from cuda.pathfinder._dynamic_libs import load_dl_windows
12+
from cuda.pathfinder._dynamic_libs.lib_descriptor import LIB_DESCRIPTORS
13+
14+
15+
def test_check_if_already_loaded_falls_back_to_enumerated_modules(tmp_path, mocker):
16+
desc = LIB_DESCRIPTORS["cupti"]
17+
expected_path = tmp_path / desc.windows_dlls[0]
18+
handles = (0x111, 0x222)
19+
20+
mocker.patch.object(load_dl_windows.kernel32, "GetModuleHandleW", return_value=0)
21+
mocker.patch.object(load_dl_windows, "_iter_loaded_module_handles", return_value=iter(handles))
22+
mocker.patch.object(
23+
load_dl_windows,
24+
"abs_path_for_dynamic_library",
25+
side_effect=(
26+
r"C:\Windows\System32\kernel32.dll",
27+
str(expected_path),
28+
),
29+
)
30+
add_dll_directory = mocker.patch.object(load_dl_windows, "add_dll_directory")
31+
32+
result = load_dl_windows.check_if_already_loaded_from_elsewhere(desc, have_abs_path=False)
33+
34+
assert result is not None
35+
assert result.abs_path == str(expected_path)
36+
assert result.was_already_loaded_from_elsewhere is True
37+
assert result.found_via == "was-already-loaded-from-elsewhere"
38+
assert result._handle_uint == handles[1]
39+
add_dll_directory.assert_not_called()
40+
41+
42+
def test_check_if_already_loaded_fallback_preserves_add_dll_directory_side_effect(tmp_path, mocker):
43+
desc = LIB_DESCRIPTORS["nvrtc"]
44+
expected_path = tmp_path / desc.windows_dlls[0]
45+
46+
mocker.patch.object(load_dl_windows.kernel32, "GetModuleHandleW", return_value=0)
47+
mocker.patch.object(load_dl_windows, "_iter_loaded_module_handles", return_value=iter((0x333,)))
48+
mocker.patch.object(load_dl_windows, "abs_path_for_dynamic_library", return_value=str(expected_path))
49+
add_dll_directory = mocker.patch.object(load_dl_windows, "add_dll_directory")
50+
51+
result = load_dl_windows.check_if_already_loaded_from_elsewhere(desc, have_abs_path=True)
52+
53+
assert result is not None
54+
assert result.abs_path == str(expected_path)
55+
assert result.was_already_loaded_from_elsewhere is True
56+
add_dll_directory.assert_called_once_with(str(expected_path))

0 commit comments

Comments
 (0)