Skip to content

Commit 86dcf9c

Browse files
committed
Harden Windows Arm64 utility discovery
1 parent a59bffc commit 86dcf9c

5 files changed

Lines changed: 255 additions & 21 deletions

File tree

cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,30 @@ def _windows_installed_nsight_root(product: str) -> str | None:
9191
access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY
9292
product_key_path = rf"{_NSIGHT_REGISTRY_ROOT}\{product}"
9393
try:
94-
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, product_key_path, 0, access) as product_key:
95-
current_version, _ = winreg.QueryValueEx(product_key, "CurrentVersion")
96-
if not isinstance(current_version, str) or not current_version:
97-
raise RuntimeError(f"Invalid CurrentVersion value in {product_key_path!r}")
98-
with winreg.OpenKey(product_key, current_version, 0, access) as version_key:
99-
install_root, _ = winreg.QueryValueEx(version_key, None)
94+
product_context = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, product_key_path, 0, access)
10095
except FileNotFoundError:
10196
return None
10297

103-
if not isinstance(install_root, str) or not install_root:
104-
raise RuntimeError(f"Invalid installation directory for {product_key_path!r} version {current_version!r}")
98+
try:
99+
with product_context as product_key:
100+
current_version, _ = winreg.QueryValueEx(product_key, "CurrentVersion")
101+
if not isinstance(current_version, str) or not current_version.strip():
102+
raise RuntimeError(
103+
f"Invalid CurrentVersion value {current_version!r} in "
104+
f"Nsight {product!r} registry registration at {product_key_path!r}"
105+
)
106+
with winreg.OpenKey(product_key, current_version, 0, access) as version_key:
107+
install_root, _ = winreg.QueryValueEx(version_key, None)
108+
except FileNotFoundError as exc:
109+
raise RuntimeError(
110+
f"Incomplete Nsight {product!r} registry registration at {product_key_path!r}"
111+
) from exc
112+
113+
if not isinstance(install_root, str) or not install_root.strip():
114+
raise RuntimeError(
115+
f"Invalid installation directory {install_root!r} in Nsight {product!r} "
116+
f"registry registration at {product_key_path!r} version {current_version!r}"
117+
)
105118
return install_root
106119

107120

@@ -182,6 +195,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
182195
Raises:
183196
UnsupportedBinaryError: If ``utility_name`` is not in the supported set
184197
(see ``SUPPORTED_BINARY_UTILITIES``).
198+
RuntimeError: If a native Windows architecture needed for an
199+
architecture-specific utility layout cannot be determined, or an
200+
installed Nsight product has incomplete or invalid registry data.
185201
186202
Windows on ARM (WoA) Note:
187203
Binary utilities execute in separate processes and do not need to match

cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"arm64": 0xAA64,
1212
}
1313

14+
_WINDOWS_ARCH_BY_PE_MACHINE = {machine: arch for arch, machine in WINDOWS_PE_MACHINE_BY_ARCH.items()}
15+
1416

1517
class UnsupportedArchError(RuntimeError):
1618
"""Raised when Python reports an unsupported Windows architecture."""
@@ -36,8 +38,8 @@ def windows_python_arch() -> str:
3638
raise UnsupportedArchError(raw_platform_tag)
3739

3840

39-
def windows_machine_arch() -> str:
40-
"""Return the native Windows machine architecture."""
41+
def _windows_machine_arch_from_platform() -> str:
42+
"""Return the Windows architecture reported by Python's platform module."""
4143
raw_machine = platform.machine()
4244
machine = raw_machine.lower().replace("_", "-")
4345

@@ -50,6 +52,62 @@ def windows_machine_arch() -> str:
5052
raise RuntimeError(f"Unsupported Windows machine architecture: {raw_machine!r}")
5153

5254

55+
def _windows_native_machine() -> int | None:
56+
"""Return the native Windows PE machine type, or None on older Windows."""
57+
import ctypes
58+
from ctypes import wintypes
59+
60+
try:
61+
# These ctypes attributes are absent from the type stubs on non-Windows hosts.
62+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined]
63+
except OSError as exc:
64+
raise RuntimeError("Failed to load kernel32 while detecting the native Windows architecture") from exc
65+
66+
get_current_process = kernel32.GetCurrentProcess
67+
try:
68+
is_wow64_process2 = kernel32.IsWow64Process2
69+
except AttributeError:
70+
return None
71+
72+
get_current_process.argtypes = ()
73+
get_current_process.restype = wintypes.HANDLE
74+
is_wow64_process2.argtypes = (
75+
wintypes.HANDLE,
76+
ctypes.POINTER(wintypes.USHORT),
77+
ctypes.POINTER(wintypes.USHORT),
78+
)
79+
is_wow64_process2.restype = wintypes.BOOL
80+
81+
process_machine = wintypes.USHORT()
82+
native_machine = wintypes.USHORT()
83+
if not is_wow64_process2(
84+
get_current_process(),
85+
ctypes.byref(process_machine),
86+
ctypes.byref(native_machine),
87+
):
88+
error_code = ctypes.get_last_error() # type: ignore[attr-defined]
89+
error = ctypes.WinError(error_code) # type: ignore[attr-defined]
90+
raise RuntimeError(
91+
f"IsWow64Process2 failed while detecting the native Windows architecture "
92+
f"(Windows error {error_code}): {error}"
93+
) from error
94+
return native_machine.value
95+
96+
97+
def windows_machine_arch() -> str:
98+
"""Return the native Windows machine architecture, ignoring process emulation."""
99+
native_machine = _windows_native_machine()
100+
if native_machine is None:
101+
# IsWow64Process2 predates x64-on-Arm emulation, so this fallback is only
102+
# needed on older Windows versions where platform.machine() is sufficient.
103+
return _windows_machine_arch_from_platform()
104+
105+
try:
106+
return _WINDOWS_ARCH_BY_PE_MACHINE[native_machine]
107+
except KeyError:
108+
raise RuntimeError(f"Unsupported native Windows PE machine type: 0x{native_machine:04x}") from None
109+
110+
53111
def windows_pe_matches_arch(path: str, target_arch: str) -> bool:
54112
"""Return whether a Windows Portable Executable (PE) targets the requested architecture.
55113

cuda_pathfinder/docs/source/install.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Runtime Requirements
99

1010
``cuda.pathfinder`` is a pure-Python package with no runtime dependencies:
1111

12-
* Linux (x86-64, arm64) and Windows (x86-64)
12+
* Linux (x86-64, arm64) and Windows (x86-64, arm64)
1313
* Python 3.10 - 3.14
1414

1515
Installing from PyPI

cuda_pathfinder/tests/test_find_nvidia_binaries.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ def fake_is_executable_candidate(path):
5858
return checked
5959

6060

61+
def _patch_winreg(mocker):
62+
winreg = mocker.MagicMock()
63+
winreg.HKEY_LOCAL_MACHINE = object()
64+
winreg.KEY_READ = 0x20019
65+
winreg.KEY_WOW64_64KEY = 0x0100
66+
mocker.patch.object(binary_finder_module.importlib, "import_module", return_value=winreg)
67+
return winreg
68+
69+
6170
@pytest.mark.usefixtures("clear_find_binary_cache")
6271
def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, mocker):
6372
conda_prefix = os.path.join(os.sep, "conda")
@@ -425,13 +434,9 @@ def test_windows_installed_nsight_root_reads_64_bit_registry(mocker):
425434
product_context.__enter__.return_value = product_key
426435
version_context = mocker.MagicMock()
427436
version_context.__enter__.return_value = version_key
428-
winreg = mocker.MagicMock()
429-
winreg.HKEY_LOCAL_MACHINE = object()
430-
winreg.KEY_READ = 0x20019
431-
winreg.KEY_WOW64_64KEY = 0x0100
437+
winreg = _patch_winreg(mocker)
432438
winreg.OpenKey.side_effect = (product_context, version_context)
433439
winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1))
434-
mocker.patch.object(binary_finder_module.importlib, "import_module", return_value=winreg)
435440

436441
assert binary_finder_module._windows_installed_nsight_root("Systems") == install_root
437442
access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY
@@ -448,6 +453,99 @@ def test_windows_installed_nsight_root_reads_64_bit_registry(mocker):
448453
)
449454

450455

456+
@pytest.mark.agent_authored(model="gpt-5.6")
457+
def test_windows_installed_nsight_root_returns_none_when_product_key_is_absent(mocker):
458+
winreg = _patch_winreg(mocker)
459+
winreg.OpenKey.side_effect = FileNotFoundError("Nsight Systems is not installed")
460+
461+
assert binary_finder_module._windows_installed_nsight_root("Systems") is None
462+
463+
464+
@pytest.mark.agent_authored(model="gpt-5.6")
465+
def test_windows_installed_nsight_root_rejects_missing_current_version(mocker):
466+
product_context = mocker.MagicMock()
467+
product_context.__enter__.return_value = mocker.MagicMock()
468+
winreg = _patch_winreg(mocker)
469+
winreg.OpenKey.return_value = product_context
470+
winreg.QueryValueEx.side_effect = FileNotFoundError("CurrentVersion is missing")
471+
472+
with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info:
473+
binary_finder_module._windows_installed_nsight_root("Systems")
474+
475+
assert isinstance(exc_info.value.__cause__, FileNotFoundError)
476+
477+
478+
@pytest.mark.parametrize("current_version", (None, "", " ", 2026))
479+
@pytest.mark.agent_authored(model="gpt-5.6")
480+
def test_windows_installed_nsight_root_rejects_invalid_current_version(mocker, current_version):
481+
product_context = mocker.MagicMock()
482+
product_context.__enter__.return_value = mocker.MagicMock()
483+
winreg = _patch_winreg(mocker)
484+
winreg.OpenKey.return_value = product_context
485+
winreg.QueryValueEx.return_value = (current_version, 1)
486+
487+
with pytest.raises(RuntimeError, match=r"Invalid CurrentVersion value .*Nsight 'Systems' registry registration"):
488+
binary_finder_module._windows_installed_nsight_root("Systems")
489+
490+
491+
@pytest.mark.agent_authored(model="gpt-5.6")
492+
def test_windows_installed_nsight_root_rejects_missing_version_key(mocker):
493+
product_key = mocker.MagicMock()
494+
product_context = mocker.MagicMock()
495+
product_context.__enter__.return_value = product_key
496+
winreg = _patch_winreg(mocker)
497+
winreg.OpenKey.side_effect = (product_context, FileNotFoundError("Version key is missing"))
498+
winreg.QueryValueEx.return_value = ("2026.1.3", 1)
499+
500+
with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info:
501+
binary_finder_module._windows_installed_nsight_root("Systems")
502+
503+
assert isinstance(exc_info.value.__cause__, FileNotFoundError)
504+
505+
506+
@pytest.mark.agent_authored(model="gpt-5.6")
507+
def test_windows_installed_nsight_root_rejects_missing_installation_directory(mocker):
508+
product_context = mocker.MagicMock()
509+
product_context.__enter__.return_value = mocker.MagicMock()
510+
version_context = mocker.MagicMock()
511+
version_context.__enter__.return_value = mocker.MagicMock()
512+
winreg = _patch_winreg(mocker)
513+
winreg.OpenKey.side_effect = (product_context, version_context)
514+
winreg.QueryValueEx.side_effect = (("2026.1.3", 1), FileNotFoundError("Installation directory is missing"))
515+
516+
with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info:
517+
binary_finder_module._windows_installed_nsight_root("Systems")
518+
519+
assert isinstance(exc_info.value.__cause__, FileNotFoundError)
520+
521+
522+
@pytest.mark.parametrize("install_root", (None, "", " ", 2026))
523+
@pytest.mark.agent_authored(model="gpt-5.6")
524+
def test_windows_installed_nsight_root_rejects_invalid_installation_directory(mocker, install_root):
525+
product_context = mocker.MagicMock()
526+
product_context.__enter__.return_value = mocker.MagicMock()
527+
version_context = mocker.MagicMock()
528+
version_context.__enter__.return_value = mocker.MagicMock()
529+
winreg = _patch_winreg(mocker)
530+
winreg.OpenKey.side_effect = (product_context, version_context)
531+
winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1))
532+
533+
with pytest.raises(
534+
RuntimeError,
535+
match=r"Invalid installation directory .*Nsight 'Systems' registry registration.*version '2026.1.3'",
536+
):
537+
binary_finder_module._windows_installed_nsight_root("Systems")
538+
539+
540+
@pytest.mark.agent_authored(model="gpt-5.6")
541+
def test_windows_installed_nsight_root_propagates_access_errors(mocker):
542+
winreg = _patch_winreg(mocker)
543+
winreg.OpenKey.side_effect = PermissionError("Registry access denied")
544+
545+
with pytest.raises(PermissionError, match="Registry access denied"):
546+
binary_finder_module._windows_installed_nsight_root("Systems")
547+
548+
451549
@pytest.mark.usefixtures("clear_find_binary_cache")
452550
def test_find_binary_first_matching_dir_wins(monkeypatch, mocker):
453551
conda_prefix = os.path.join(os.sep, "conda")

cuda_pathfinder/tests/test_search_steps.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
from __future__ import annotations
77

8+
import ctypes
89
import os
10+
from ctypes import wintypes
911

1012
import pytest
1113

@@ -148,22 +150,82 @@ def test_rejects_unknown_sysconfig_tag(self, mocker):
148150

149151

150152
class TestWindowsMachineArch:
153+
@pytest.mark.parametrize(
154+
("native_machine", "expected"),
155+
((0x8664, "x64"), (0xAA64, "arm64")),
156+
)
157+
@pytest.mark.agent_authored(model="gpt-5.6")
158+
def test_uses_native_pe_machine(self, mocker, native_machine, expected):
159+
mocker.patch.object(windows_arch_mod, "_windows_native_machine", return_value=native_machine)
160+
platform_machine = mocker.patch.object(windows_arch_mod.platform, "machine", return_value="AMD64")
161+
162+
assert windows_arch_mod.windows_machine_arch() == expected
163+
platform_machine.assert_not_called()
164+
165+
@pytest.mark.agent_authored(model="gpt-5.6")
166+
def test_rejects_unknown_native_pe_machine(self, mocker):
167+
mocker.patch.object(windows_arch_mod, "_windows_native_machine", return_value=0x014C)
168+
169+
with pytest.raises(RuntimeError, match=r"Unsupported native Windows PE machine type: 0x014c"):
170+
windows_arch_mod.windows_machine_arch()
171+
151172
@pytest.mark.parametrize(
152173
("reported_machine", "expected"),
153174
(("AMD64", "x64"), ("x86_64", "x64"), ("ARM64", "arm64"), ("aarch64", "arm64")),
154175
)
155176
@pytest.mark.agent_authored(model="gpt-5.6")
156-
def test_normalizes_platform_machine(self, mocker, reported_machine, expected):
177+
def test_old_windows_fallback_normalizes_platform_machine(self, mocker, reported_machine, expected):
178+
mocker.patch.object(windows_arch_mod, "_windows_native_machine", return_value=None)
157179
mocker.patch.object(windows_arch_mod.platform, "machine", return_value=reported_machine)
158180

159181
assert windows_arch_mod.windows_machine_arch() == expected
160182

161183
@pytest.mark.agent_authored(model="gpt-5.6")
162-
def test_rejects_unknown_machine(self, mocker):
163-
mocker.patch.object(windows_arch_mod.platform, "machine", return_value="mips64")
184+
def test_native_machine_returns_none_when_is_wow64_process2_is_unavailable(self, mocker):
185+
kernel32 = mocker.Mock(spec=["GetCurrentProcess"])
186+
mocker.patch.object(ctypes, "WinDLL", create=True, return_value=kernel32)
164187

165-
with pytest.raises(RuntimeError, match=r"Unsupported Windows machine architecture: 'mips64'"):
166-
windows_arch_mod.windows_machine_arch()
188+
assert windows_arch_mod._windows_native_machine() is None
189+
190+
@pytest.mark.agent_authored(model="gpt-5.6")
191+
def test_native_machine_configures_api_and_returns_native_machine(self, mocker):
192+
kernel32 = mocker.Mock()
193+
kernel32.GetCurrentProcess.return_value = wintypes.HANDLE(1)
194+
195+
def report_native_machine(_process, _process_machine, native_machine):
196+
native_machine._obj.value = 0xAA64
197+
return True
198+
199+
kernel32.IsWow64Process2.side_effect = report_native_machine
200+
mocker.patch.object(ctypes, "WinDLL", create=True, return_value=kernel32)
201+
202+
assert windows_arch_mod._windows_native_machine() == 0xAA64
203+
assert kernel32.GetCurrentProcess.argtypes == ()
204+
assert kernel32.GetCurrentProcess.restype is wintypes.HANDLE
205+
assert kernel32.IsWow64Process2.argtypes == (
206+
wintypes.HANDLE,
207+
ctypes.POINTER(wintypes.USHORT),
208+
ctypes.POINTER(wintypes.USHORT),
209+
)
210+
assert kernel32.IsWow64Process2.restype is wintypes.BOOL
211+
212+
@pytest.mark.agent_authored(model="gpt-5.6")
213+
def test_native_machine_raises_contextual_error_when_api_call_fails(self, mocker):
214+
kernel32 = mocker.Mock()
215+
kernel32.GetCurrentProcess.return_value = wintypes.HANDLE(1)
216+
kernel32.IsWow64Process2.return_value = False
217+
mocker.patch.object(ctypes, "WinDLL", create=True, return_value=kernel32)
218+
mocker.patch.object(ctypes, "get_last_error", create=True, return_value=87)
219+
windows_error = OSError(87, "The parameter is incorrect")
220+
mocker.patch.object(ctypes, "WinError", create=True, return_value=windows_error)
221+
222+
with pytest.raises(
223+
RuntimeError,
224+
match=r"IsWow64Process2 failed while detecting the native Windows architecture \(Windows error 87\)",
225+
) as exc_info:
226+
windows_arch_mod._windows_native_machine()
227+
228+
assert exc_info.value.__cause__ is windows_error
167229

168230

169231
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)