Skip to content

Commit f365f51

Browse files
authored
Merge branch 'main' into jahnvi/conda-recipe
2 parents 95c62f6 + 9dd3b52 commit f365f51

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

‎mssql_python/pybind/ddbc_bindings.cpp‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@
2626
// Macro definitions
2727
//-------------------------------------------------------------------------------------------------
2828

29+
#ifdef _WIN32
30+
// Constrained DLL search flags (Windows 8+ / Win7 + KB2533623). Defined
31+
// defensively in case the build's SDK headers gate them behind an older
32+
// _WIN32_WINNT than this project targets.
33+
//
34+
// Note: LOAD_LIBRARY_SEARCH_DEFAULT_DIRS is deliberately NOT used. It also
35+
// includes LOAD_LIBRARY_SEARCH_USER_DIRS -- directories any in-process module
36+
// registered via AddDllDirectory/SetDllDirectory -- which is outside the
37+
// trusted set we want. We combine APPLICATION_DIR + SYSTEM32 + DLL_LOAD_DIR
38+
// explicitly instead.
39+
#ifndef LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
40+
#define LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR 0x00000100
41+
#endif
42+
#ifndef LOAD_LIBRARY_SEARCH_APPLICATION_DIR
43+
#define LOAD_LIBRARY_SEARCH_APPLICATION_DIR 0x00000200
44+
#endif
45+
#ifndef LOAD_LIBRARY_SEARCH_SYSTEM32
46+
#define LOAD_LIBRARY_SEARCH_SYSTEM32 0x00000800
47+
#endif
48+
#endif // _WIN32
49+
2950
#ifndef SQL_C_DATE
3051
#define SQL_C_DATE (9)
3152
#endif
@@ -1047,9 +1068,19 @@ DriverHandle LoadDriverLibrary(const std::string& driverPath) {
10471068
// fs::path::c_str() returns wchar_t* on Windows with correct encoding
10481069
namespace fs = std::filesystem;
10491070
fs::path pathObj(driverPath);
1050-
HMODULE handle = LoadLibraryW(pathObj.c_str());
1071+
// Resolve the vendored driver's dependencies with a constrained search
1072+
// path. LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR adds the driver's own folder for
1073+
// its dependency lookups; APPLICATION_DIR and SYSTEM32 add the host
1074+
// application directory and System32. This excludes the current working
1075+
// directory, %PATH%, and process-wide user DLL directories (registered via
1076+
// AddDllDirectory/SetDllDirectory) -- all of which the legacy LoadLibraryW
1077+
// order, or the broader LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, would include.
1078+
HMODULE handle = LoadLibraryExW(
1079+
pathObj.c_str(), nullptr,
1080+
LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32 |
1081+
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);
10511082
if (!handle) {
1052-
LOG("LoadDriverLibrary: LoadLibraryW failed for path='%s' - %s", driverPath.c_str(),
1083+
LOG("LoadDriverLibrary: LoadLibraryExW failed for path='%s' - %s", driverPath.c_str(),
10531084
GetLastErrorMessage().c_str());
10541085
ThrowStdException("Failed to load library: " + driverPath);
10551086
}
@@ -1199,7 +1230,10 @@ DriverHandle LoadDriverOrThrowException() {
11991230
fs::path authDllPath = dllDir / "mssql-auth.dll";
12001231
if (fs::exists(authDllPath)) {
12011232
// Use fs::path::c_str() which returns wchar_t* on Windows with proper encoding
1202-
HMODULE hAuth = LoadLibraryW(authDllPath.c_str());
1233+
HMODULE hAuth = LoadLibraryExW(
1234+
authDllPath.c_str(), nullptr,
1235+
LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32 |
1236+
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);
12031237
if (hAuth) {
12041238
LOG("LoadDriverOrThrowException: mssql-auth.dll loaded "
12051239
"successfully from '%s'",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
Regression guard for the Windows package-local DLL load path.
3+
4+
The vendored ODBC driver (``msodbcsql18.dll``) and Entra auth DLL
5+
(``mssql-auth.dll``) must be loaded with a constrained search path --
6+
``LoadLibraryExW`` with ``LOAD_LIBRARY_SEARCH_APPLICATION_DIR |
7+
LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR`` -- rather than
8+
the legacy ``LoadLibraryW`` search order, which also consults the current
9+
working directory and ``%PATH``. The broader ``LOAD_LIBRARY_SEARCH_DEFAULT_DIRS``
10+
is intentionally avoided because it also searches user DLL directories
11+
(registered process-wide via ``AddDllDirectory`` / ``SetDllDirectory``).
12+
13+
This is a source-contract test on purpose. The restriction only manifests at
14+
DLL-resolution time on Windows, which cannot be observed without dropping a
15+
file on disk; a success-path "does it still load" check passes on the
16+
unhardened code too (any host with ``msvcp140.dll`` in System32), so it guards
17+
nothing. Asserting the loader keeps using the constrained API is the
18+
deterministic, platform-independent way to fail if the hardening is reverted.
19+
"""
20+
21+
import re
22+
from pathlib import Path
23+
24+
_LOADER_SRC = Path(__file__).resolve().parents[1] / "mssql_python" / "pybind" / "ddbc_bindings.cpp"
25+
26+
27+
def _code_without_comments(text):
28+
# Drop // line comments so prose that mentions LoadLibraryW is not matched.
29+
return "\n".join(re.sub(r"//.*", "", line) for line in text.splitlines())
30+
31+
32+
def test_loader_source_present():
33+
assert _LOADER_SRC.is_file(), f"loader source not found at {_LOADER_SRC}"
34+
35+
36+
def test_no_unhardened_loadlibrary_call():
37+
code = _code_without_comments(_LOADER_SRC.read_text(encoding="utf-8"))
38+
# A bare LoadLibraryW(...) call resolves dependencies via the legacy search
39+
# order, which includes the current directory and %PATH%.
40+
assert re.search(r"\bLoadLibraryW\s*\(", code) is None, (
41+
"ddbc_bindings.cpp contains a bare LoadLibraryW call; the vendored "
42+
"driver and auth DLLs must be loaded with LoadLibraryExW and the "
43+
"constrained search flags instead."
44+
)
45+
46+
47+
def test_driver_and_auth_loads_use_constrained_search():
48+
code = _code_without_comments(_LOADER_SRC.read_text(encoding="utf-8"))
49+
# Match each LoadLibraryExW( ... ); call and require the constrained flag
50+
# set inside that call's own argument list -- not merely somewhere in the
51+
# file (the flag names also appear in the #define block, so a file-wide
52+
# substring check would still pass if a call's flags were replaced).
53+
calls = re.findall(r"LoadLibraryExW\s*\(.*?\)\s*;", code, re.DOTALL)
54+
assert len(calls) >= 2, "expected LoadLibraryExW for both the driver and the auth DLL loads"
55+
required = (
56+
"LOAD_LIBRARY_SEARCH_APPLICATION_DIR",
57+
"LOAD_LIBRARY_SEARCH_SYSTEM32",
58+
"LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR",
59+
)
60+
for call in calls:
61+
for flag in required:
62+
assert flag in call, f"a LoadLibraryExW call is missing {flag}: {call}"
63+
# DEFAULT_DIRS re-introduces LOAD_LIBRARY_SEARCH_USER_DIRS (process-wide
64+
# AddDllDirectory/SetDllDirectory dirs), which is outside the trusted
65+
# set; the loads must name the individual directories instead.
66+
assert "LOAD_LIBRARY_SEARCH_DEFAULT_DIRS" not in call, (
67+
"a LoadLibraryExW call uses LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, which "
68+
"includes user DLL directories: " + call
69+
)

0 commit comments

Comments
 (0)