From a9fdc87b6059e274389870b7ea43f96753ace144 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 1 Sep 2026 12:43:17 +0530 Subject: [PATCH 1/5] CHORE: load bundled Windows driver and auth DLLs from package-local directories On Windows the vendored ODBC driver (msodbcsql18.dll) and Entra auth DLL (mssql-auth.dll) were loaded with LoadLibraryW, so their dependencies - including the bundled VC++ runtime - were resolved via the default process search order, which also consults the current working directory and %PATH%. Load them with LoadLibraryExW using LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, and register the driver directory plus its co-located vcredist subfolder via AddDllDirectory, so dependency resolution is confined to trusted, package-local directories (System32, the application directory, and the package's own folders). The vcredist subfolder is added explicitly because LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR only covers each DLL's own directory. macOS and Linux are unaffected: the change is inside _WIN32 blocks, and the shipped Unix libraries already carry @loader_path / $ORIGIN. Adds a Windows-only regression test that plants a bogus msvcp140.dll on the CWD and %PATH% and confirms the driver still loads from the package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 ++ mssql_python/pybind/ddbc_bindings.cpp | 45 ++++++++++++++- tests/test_026_windows_dll_search.py | 83 +++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 tests/test_026_windows_dll_search.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ec2ae5c6..b5ce2e666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), explicit and drop the bundled binaries. ### Changed +- Windows: the bundled ODBC driver and Entra auth DLLs are now loaded with a + package-local DLL search path (System32, the driver's own directory, and its + co-located `vcredist` runtime) instead of the default process search order, + so their dependencies resolve deterministically from trusted, package-local + directories. No API or behavior change; macOS and Linux are unaffected. - Connection strings and string connection parameters that contain a NUL (`\x00`) character are now rejected up front with `InterfaceError` instead of being silently truncated at the NUL by the underlying driver. diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index cd3a45fa6..7e49a275e 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -18,6 +18,7 @@ #include // For std::memcpy #include #include +#include // std::once_flag / std::call_once #include // std::forward #include // CPython datetime API (PyDateTime_IMPORT, PyDateTime_GET_*, etc.) @@ -26,6 +27,18 @@ // Macro definitions //------------------------------------------------------------------------------------------------- +#ifdef _WIN32 +// Constrained DLL search flags (Windows 8+ / Win7 + KB2533623). Defined +// defensively in case the build's SDK headers gate them behind an older +// _WIN32_WINNT than this project targets. +#ifndef LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR +#define LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR 0x00000100 +#endif +#ifndef LOAD_LIBRARY_SEARCH_DEFAULT_DIRS +#define LOAD_LIBRARY_SEARCH_DEFAULT_DIRS 0x00001000 +#endif +#endif // _WIN32 + #ifndef SQL_C_DATE #define SQL_C_DATE (9) #endif @@ -1047,9 +1060,19 @@ DriverHandle LoadDriverLibrary(const std::string& driverPath) { // fs::path::c_str() returns wchar_t* on Windows with correct encoding namespace fs = std::filesystem; fs::path pathObj(driverPath); - HMODULE handle = LoadLibraryW(pathObj.c_str()); + // Resolve the vendored driver and its dependencies from trusted, + // package-local directories only. LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR adds the + // driver's own folder for its dependency lookups, and + // LOAD_LIBRARY_SEARCH_DEFAULT_DIRS restricts the rest of the search to + // System32, the application directory, and directories registered via + // AddDllDirectory (see LoadDriverOrThrowException) -- excluding the current + // working directory and %PATH%, which the legacy LoadLibraryW search order + // would otherwise include. + HMODULE handle = LoadLibraryExW( + pathObj.c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); if (!handle) { - LOG("LoadDriverLibrary: LoadLibraryW failed for path='%s' - %s", driverPath.c_str(), + LOG("LoadDriverLibrary: LoadLibraryExW failed for path='%s' - %s", driverPath.c_str(), GetLastErrorMessage().c_str()); ThrowStdException("Failed to load library: " + driverPath); } @@ -1196,10 +1219,26 @@ DriverHandle LoadDriverOrThrowException() { : "x86"; fs::path dllDir = fs::path(moduleDir) / "libs" / "windows" / archDir; + + // Register the driver directory and its co-located `vcredist` subfolder as + // trusted DLL search directories. The vendored VC++ runtime ships under + // `vcredist`, which LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR does not reach on its + // own, so it is added explicitly here. Done once per process; the loads + // below (and in LoadDriverLibrary) opt into this search list via + // LOAD_LIBRARY_SEARCH_DEFAULT_DIRS instead of the legacy order. + static std::once_flag dllSearchDirsOnce; + std::call_once(dllSearchDirsOnce, [&dllDir]() { + AddDllDirectory(dllDir.c_str()); + fs::path vcredistDir = dllDir / "vcredist"; + AddDllDirectory(vcredistDir.c_str()); + }); + fs::path authDllPath = dllDir / "mssql-auth.dll"; if (fs::exists(authDllPath)) { // Use fs::path::c_str() which returns wchar_t* on Windows with proper encoding - HMODULE hAuth = LoadLibraryW(authDllPath.c_str()); + HMODULE hAuth = LoadLibraryExW( + authDllPath.c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); if (hAuth) { LOG("LoadDriverOrThrowException: mssql-auth.dll loaded " "successfully from '%s'", diff --git a/tests/test_026_windows_dll_search.py b/tests/test_026_windows_dll_search.py new file mode 100644 index 000000000..e24b29763 --- /dev/null +++ b/tests/test_026_windows_dll_search.py @@ -0,0 +1,83 @@ +""" +Windows-only regression coverage for package-local DLL loading. + +The vendored ODBC driver and Entra auth DLLs are loaded with a constrained, +package-local search path so their dependencies (notably the bundled VC++ +runtime under ``vcredist``) resolve from trusted directories rather than the +current working directory or ``%PATH%``. This test plants a bogus +``msvcp140.dll`` in both a ``%PATH%`` entry and the process working directory +and confirms the driver still loads from the package -- i.e. the bundled +runtime resolution did not regress once those legacy directories are no longer +consulted. +""" + +import os +import subprocess +import sys +import textwrap + +import pytest + +pytestmark = pytest.mark.skipif( + sys.platform != "win32", + reason="Package-local DLL search path applies to Windows only.", +) + +# A minimal, invalid PE image. A loader that resolved msvcp140.dll from CWD or +# %PATH% would try to load this and fail; the package-local search path must +# ignore it and use the bundled runtime instead. +_JUNK_DLL = b"MZ" + b"\x00" * 256 + + +def _plant_junk(directory): + with open(os.path.join(directory, "msvcp140.dll"), "wb") as handle: + handle.write(_JUNK_DLL) + + +def test_driver_loads_despite_planted_dll_on_cwd_and_path(tmp_path): + cwd_dir = tmp_path / "cwd" + path_dir = tmp_path / "onpath" + cwd_dir.mkdir() + path_dir.mkdir() + _plant_junk(str(cwd_dir)) + _plant_junk(str(path_dir)) + + env = dict(os.environ) + env["PATH"] = str(path_dir) + os.pathsep + env.get("PATH", "") + + # Force the ODBC driver to load in a fresh interpreter by attempting a + # connection to an unreachable endpoint. Handle allocation (which loads the + # driver and its dependencies) happens before the network handshake, so a + # short login timeout is enough. We assert only that the failure is NOT a + # driver/runtime load failure -- the connection itself is expected to fail. + child = textwrap.dedent( + """ + import mssql_python + _LOAD_FAILURES = ( + "Failed to load the driver", + "Failed to load mssql-auth.dll", + "Failed to load library", + ) + try: + mssql_python.connect( + "Server=127.0.0.1,1;Database=x;Uid=x;Pwd=x;" + "Encrypt=no;TrustServerCertificate=yes", + timeout=1, + ) + except Exception as exc: # noqa: BLE001 - any non-load error is acceptable + message = str(exc) + for needle in _LOAD_FAILURES: + assert needle not in message, message + print("DRIVER_LOADED_OK") + """ + ) + + result = subprocess.run( + [sys.executable, "-c", child], + cwd=str(cwd_dir), + env=env, + capture_output=True, + text=True, + ) + + assert "DRIVER_LOADED_OK" in result.stdout, (result.stdout, result.stderr) From de227c32ece8daab27585f808295e4fcbbc30777 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 1 Sep 2026 13:03:09 +0530 Subject: [PATCH 2/5] CHORE: apply Black formatting to Windows DLL search test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_026_windows_dll_search.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_026_windows_dll_search.py b/tests/test_026_windows_dll_search.py index e24b29763..b9016f6e1 100644 --- a/tests/test_026_windows_dll_search.py +++ b/tests/test_026_windows_dll_search.py @@ -50,8 +50,7 @@ def test_driver_loads_despite_planted_dll_on_cwd_and_path(tmp_path): # driver and its dependencies) happens before the network handshake, so a # short login timeout is enough. We assert only that the failure is NOT a # driver/runtime load failure -- the connection itself is expected to fail. - child = textwrap.dedent( - """ + child = textwrap.dedent(""" import mssql_python _LOAD_FAILURES = ( "Failed to load the driver", @@ -69,8 +68,7 @@ def test_driver_loads_despite_planted_dll_on_cwd_and_path(tmp_path): for needle in _LOAD_FAILURES: assert needle not in message, message print("DRIVER_LOADED_OK") - """ - ) + """) result = subprocess.run( [sys.executable, "-c", child], From a4a7346c31f36a3f5ccc6a4a33cc479a2c5b8060 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 1 Sep 2026 13:14:14 +0530 Subject: [PATCH 3/5] CHORE: guard the Windows loader against reverting to unhardened LoadLibraryW Replace the success-path load smoke test with a source-contract regression guard. The previous test only confirmed the driver still loads, which also passes on the unhardened code (any host with msvcp140.dll in System32), so it guarded nothing. The new test fails if the loader reintroduces a bare LoadLibraryW call or drops the constrained-search flags / vcredist registration -- i.e. it fails on the pre-fix source and passes on the current source. Deterministic and platform-independent (no DLL, no live driver). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_026_windows_dll_search.py | 114 +++++++++++---------------- 1 file changed, 45 insertions(+), 69 deletions(-) diff --git a/tests/test_026_windows_dll_search.py b/tests/test_026_windows_dll_search.py index b9016f6e1..4066de3c9 100644 --- a/tests/test_026_windows_dll_search.py +++ b/tests/test_026_windows_dll_search.py @@ -1,81 +1,57 @@ """ -Windows-only regression coverage for package-local DLL loading. - -The vendored ODBC driver and Entra auth DLLs are loaded with a constrained, -package-local search path so their dependencies (notably the bundled VC++ -runtime under ``vcredist``) resolve from trusted directories rather than the -current working directory or ``%PATH%``. This test plants a bogus -``msvcp140.dll`` in both a ``%PATH%`` entry and the process working directory -and confirms the driver still loads from the package -- i.e. the bundled -runtime resolution did not regress once those legacy directories are no longer -consulted. +Regression guard for the Windows package-local DLL load path. + +The vendored ODBC driver (``msodbcsql18.dll``) and Entra auth DLL +(``mssql-auth.dll``) must be loaded with a constrained, package-local search +path -- ``LoadLibraryExW`` with ``LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | +LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR`` and the driver + ``vcredist`` directories +registered via ``AddDllDirectory`` -- rather than the legacy ``LoadLibraryW`` +search order, which also consults the current working directory and ``%PATH%``. + +This is a source-contract test on purpose. The restriction only manifests at +DLL-resolution time on Windows, which cannot be observed without dropping a +file on disk; a success-path "does it still load" check passes on the +unhardened code too (any host with ``msvcp140.dll`` in System32), so it guards +nothing. Asserting the loader keeps using the constrained API is the +deterministic, platform-independent way to fail if the hardening is reverted. """ -import os -import subprocess -import sys -import textwrap - -import pytest - -pytestmark = pytest.mark.skipif( - sys.platform != "win32", - reason="Package-local DLL search path applies to Windows only.", -) +import re +from pathlib import Path -# A minimal, invalid PE image. A loader that resolved msvcp140.dll from CWD or -# %PATH% would try to load this and fail; the package-local search path must -# ignore it and use the bundled runtime instead. -_JUNK_DLL = b"MZ" + b"\x00" * 256 +_LOADER_SRC = Path(__file__).resolve().parents[1] / "mssql_python" / "pybind" / "ddbc_bindings.cpp" -def _plant_junk(directory): - with open(os.path.join(directory, "msvcp140.dll"), "wb") as handle: - handle.write(_JUNK_DLL) +def _code_without_comments(text): + # Drop // line comments so prose that mentions LoadLibraryW is not matched. + return "\n".join(re.sub(r"//.*", "", line) for line in text.splitlines()) -def test_driver_loads_despite_planted_dll_on_cwd_and_path(tmp_path): - cwd_dir = tmp_path / "cwd" - path_dir = tmp_path / "onpath" - cwd_dir.mkdir() - path_dir.mkdir() - _plant_junk(str(cwd_dir)) - _plant_junk(str(path_dir)) +def test_loader_source_present(): + assert _LOADER_SRC.is_file(), f"loader source not found at {_LOADER_SRC}" - env = dict(os.environ) - env["PATH"] = str(path_dir) + os.pathsep + env.get("PATH", "") - # Force the ODBC driver to load in a fresh interpreter by attempting a - # connection to an unreachable endpoint. Handle allocation (which loads the - # driver and its dependencies) happens before the network handshake, so a - # short login timeout is enough. We assert only that the failure is NOT a - # driver/runtime load failure -- the connection itself is expected to fail. - child = textwrap.dedent(""" - import mssql_python - _LOAD_FAILURES = ( - "Failed to load the driver", - "Failed to load mssql-auth.dll", - "Failed to load library", - ) - try: - mssql_python.connect( - "Server=127.0.0.1,1;Database=x;Uid=x;Pwd=x;" - "Encrypt=no;TrustServerCertificate=yes", - timeout=1, - ) - except Exception as exc: # noqa: BLE001 - any non-load error is acceptable - message = str(exc) - for needle in _LOAD_FAILURES: - assert needle not in message, message - print("DRIVER_LOADED_OK") - """) - - result = subprocess.run( - [sys.executable, "-c", child], - cwd=str(cwd_dir), - env=env, - capture_output=True, - text=True, +def test_no_unhardened_loadlibrary_call(): + code = _code_without_comments(_LOADER_SRC.read_text(encoding="utf-8")) + # A bare LoadLibraryW(...) call resolves dependencies via the legacy search + # order, which includes the current directory and %PATH%. + assert re.search(r"\bLoadLibraryW\s*\(", code) is None, ( + "ddbc_bindings.cpp contains a bare LoadLibraryW call; the vendored " + "driver and auth DLLs must be loaded with LoadLibraryExW and the " + "constrained search flags instead." ) - assert "DRIVER_LOADED_OK" in result.stdout, (result.stdout, result.stderr) + +def test_driver_and_auth_loads_use_constrained_search(): + code = _code_without_comments(_LOADER_SRC.read_text(encoding="utf-8")) + # Both the driver and the auth DLL are loaded with the hardened API. + assert ( + len(re.findall(r"\bLoadLibraryExW\s*\(", code)) >= 2 + ), "expected LoadLibraryExW for both the driver and the auth DLL loads" + assert "LOAD_LIBRARY_SEARCH_DEFAULT_DIRS" in code + assert "LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR" in code + # The bundled VC++ runtime ships under `vcredist`, which must be registered + # as a trusted search directory (LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR only + # covers each DLL's own folder). + assert "AddDllDirectory(" in code + assert "vcredist" in code From 6ead192b484d5415ce73a93253ade8ad461ea5ad Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 1 Sep 2026 14:27:00 +0530 Subject: [PATCH 4/5] CHORE: simplify Windows loader hardening to LoadLibraryExW flags only Drop the AddDllDirectory registration (and its once_flag / ): the bundled VC++ runtime is already loaded from the trusted copy next to the pybind .pyd (built /MD) before the driver loads, so registering the vcredist directory added a permanent process-global search-dir side effect for no resolution benefit. The per-load LoadLibraryExW flags (LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR) are the whole change: they keep the current directory and %PATH% out of the driver's and auth DLL's dependency search without mutating any global process state. Also revert the CHANGELOG entry and trim the regression test's assertions to match (no more AddDllDirectory / vcredist checks). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 ----- mssql_python/pybind/ddbc_bindings.cpp | 29 ++++++--------------------- tests/test_026_windows_dll_search.py | 15 +++++--------- 3 files changed, 11 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ce2e666..2ec2ae5c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,11 +45,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), explicit and drop the bundled binaries. ### Changed -- Windows: the bundled ODBC driver and Entra auth DLLs are now loaded with a - package-local DLL search path (System32, the driver's own directory, and its - co-located `vcredist` runtime) instead of the default process search order, - so their dependencies resolve deterministically from trusted, package-local - directories. No API or behavior change; macOS and Linux are unaffected. - Connection strings and string connection parameters that contain a NUL (`\x00`) character are now rejected up front with `InterfaceError` instead of being silently truncated at the NUL by the underlying driver. diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 7e49a275e..7342c0f56 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -18,7 +18,6 @@ #include // For std::memcpy #include #include -#include // std::once_flag / std::call_once #include // std::forward #include // CPython datetime API (PyDateTime_IMPORT, PyDateTime_GET_*, etc.) @@ -1060,14 +1059,12 @@ DriverHandle LoadDriverLibrary(const std::string& driverPath) { // fs::path::c_str() returns wchar_t* on Windows with correct encoding namespace fs = std::filesystem; fs::path pathObj(driverPath); - // Resolve the vendored driver and its dependencies from trusted, - // package-local directories only. LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR adds the - // driver's own folder for its dependency lookups, and - // LOAD_LIBRARY_SEARCH_DEFAULT_DIRS restricts the rest of the search to - // System32, the application directory, and directories registered via - // AddDllDirectory (see LoadDriverOrThrowException) -- excluding the current - // working directory and %PATH%, which the legacy LoadLibraryW search order - // would otherwise include. + // Resolve the vendored driver's dependencies with a constrained search + // path. LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR adds the driver's own folder for + // its dependency lookups, and LOAD_LIBRARY_SEARCH_DEFAULT_DIRS restricts the + // rest of the search to System32 and the application directory -- excluding + // the current working directory and %PATH%, which the legacy LoadLibraryW + // search order would otherwise include. HMODULE handle = LoadLibraryExW( pathObj.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); @@ -1219,20 +1216,6 @@ DriverHandle LoadDriverOrThrowException() { : "x86"; fs::path dllDir = fs::path(moduleDir) / "libs" / "windows" / archDir; - - // Register the driver directory and its co-located `vcredist` subfolder as - // trusted DLL search directories. The vendored VC++ runtime ships under - // `vcredist`, which LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR does not reach on its - // own, so it is added explicitly here. Done once per process; the loads - // below (and in LoadDriverLibrary) opt into this search list via - // LOAD_LIBRARY_SEARCH_DEFAULT_DIRS instead of the legacy order. - static std::once_flag dllSearchDirsOnce; - std::call_once(dllSearchDirsOnce, [&dllDir]() { - AddDllDirectory(dllDir.c_str()); - fs::path vcredistDir = dllDir / "vcredist"; - AddDllDirectory(vcredistDir.c_str()); - }); - fs::path authDllPath = dllDir / "mssql-auth.dll"; if (fs::exists(authDllPath)) { // Use fs::path::c_str() which returns wchar_t* on Windows with proper encoding diff --git a/tests/test_026_windows_dll_search.py b/tests/test_026_windows_dll_search.py index 4066de3c9..af7f20b38 100644 --- a/tests/test_026_windows_dll_search.py +++ b/tests/test_026_windows_dll_search.py @@ -2,11 +2,11 @@ Regression guard for the Windows package-local DLL load path. The vendored ODBC driver (``msodbcsql18.dll``) and Entra auth DLL -(``mssql-auth.dll``) must be loaded with a constrained, package-local search -path -- ``LoadLibraryExW`` with ``LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | -LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR`` and the driver + ``vcredist`` directories -registered via ``AddDllDirectory`` -- rather than the legacy ``LoadLibraryW`` -search order, which also consults the current working directory and ``%PATH%``. +(``mssql-auth.dll``) must be loaded with a constrained search path -- +``LoadLibraryExW`` with ``LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | +LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR`` -- rather than the legacy ``LoadLibraryW`` +search order, which also consults the current working directory and ``%PATH%`` +when resolving those DLLs' dependencies. This is a source-contract test on purpose. The restriction only manifests at DLL-resolution time on Windows, which cannot be observed without dropping a @@ -50,8 +50,3 @@ def test_driver_and_auth_loads_use_constrained_search(): ), "expected LoadLibraryExW for both the driver and the auth DLL loads" assert "LOAD_LIBRARY_SEARCH_DEFAULT_DIRS" in code assert "LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR" in code - # The bundled VC++ runtime ships under `vcredist`, which must be registered - # as a trusted search directory (LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR only - # covers each DLL's own folder). - assert "AddDllDirectory(" in code - assert "vcredist" in code From 7c4ac71eee52b61ecbb4a30baeae6526e7515365 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 1 Sep 2026 17:16:03 +0530 Subject: [PATCH 5/5] CHORE: bind the loader test's flag checks to each LoadLibraryExW call The previous assertions checked that the search-flag names appeared anywhere in the source, but they also appear in the #define block -- so gutting both LoadLibraryExW calls' flags to 0 still passed. Match each LoadLibraryExW(...) call and require both flags within that call's own argument list instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_026_windows_dll_search.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_026_windows_dll_search.py b/tests/test_026_windows_dll_search.py index af7f20b38..5a57bda6c 100644 --- a/tests/test_026_windows_dll_search.py +++ b/tests/test_026_windows_dll_search.py @@ -44,9 +44,16 @@ def test_no_unhardened_loadlibrary_call(): def test_driver_and_auth_loads_use_constrained_search(): code = _code_without_comments(_LOADER_SRC.read_text(encoding="utf-8")) - # Both the driver and the auth DLL are loaded with the hardened API. - assert ( - len(re.findall(r"\bLoadLibraryExW\s*\(", code)) >= 2 - ), "expected LoadLibraryExW for both the driver and the auth DLL loads" - assert "LOAD_LIBRARY_SEARCH_DEFAULT_DIRS" in code - assert "LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR" in code + # Match each LoadLibraryExW( ... ); call and require BOTH flags inside that + # call's own argument list -- not merely somewhere in the file (the flag + # names also appear in the #define block, so a file-wide substring check + # would still pass if a call's flags were replaced with 0). + calls = re.findall(r"LoadLibraryExW\s*\(.*?\)\s*;", code, re.DOTALL) + assert len(calls) >= 2, "expected LoadLibraryExW for both the driver and the auth DLL loads" + for call in calls: + assert "LOAD_LIBRARY_SEARCH_DEFAULT_DIRS" in call, ( + "a LoadLibraryExW call is missing LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: " + call + ) + assert "LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR" in call, ( + "a LoadLibraryExW call is missing LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: " + call + )