From bf20afd245139fe0c2a598255f518b817c31ba4d Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 23 Sep 2026 19:47:05 +0100 Subject: [PATCH 1/2] FIX: enforce POSIX native binary hardening AB#48377 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/pybind/CMakeLists.txt | 43 +++++ tests/test_040_native_binary_hardening.py | 183 ++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 tests/test_040_native_binary_hardening.py diff --git a/mssql_python/pybind/CMakeLists.txt b/mssql_python/pybind/CMakeLists.txt index 77d599bd5..a126f053a 100644 --- a/mssql_python/pybind/CMakeLists.txt +++ b/mssql_python/pybind/CMakeLists.txt @@ -255,6 +255,13 @@ endif() message(STATUS "Final Python library directory: ${PYTHON_LIB_DIR}") +# Single-config POSIX generators ignore `cmake --build --config Release`. +# Make the optimized release mode explicit instead of relying on simdutf to +# populate this project-wide cache variable as a FetchContent side effect. +if(UNIX AND NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() + include(FetchContent) message(STATUS "Downloading simdutf v8.2.0 source archive with FetchContent") set(simdutf_fetchcontent_args @@ -387,6 +394,42 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang endif() endif() +# Harden the Python extension against exploitation of a separate memory-safety +# defect. Mach-O receives stack protection; ELF-specific flags stay Linux-only. +if(UNIX) + target_compile_options(ddbc_bindings PRIVATE -fstack-protector-strong) +endif() + +if(UNIX AND NOT APPLE) + include(CheckCXXSourceCompiles) + set(DDBC_REQUIRED_FLAGS_SAVED "${CMAKE_REQUIRED_FLAGS}") + set(CMAKE_REQUIRED_FLAGS + "${CMAKE_REQUIRED_FLAGS} -O2 -Werror -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3" + ) + check_cxx_source_compiles(" + #include + #if !defined(__USE_FORTIFY_LEVEL) || __USE_FORTIFY_LEVEL < 3 + #error _FORTIFY_SOURCE=3 is unavailable + #endif + int main() { return 0; } + " DDBC_SUPPORTS_FORTIFY_SOURCE_3) + set(CMAKE_REQUIRED_FLAGS "${DDBC_REQUIRED_FLAGS_SAVED}") + if(DDBC_SUPPORTS_FORTIFY_SOURCE_3) + set(DDBC_FORTIFY_LEVEL 3) + else() + set(DDBC_FORTIFY_LEVEL 2) + endif() + target_compile_options(ddbc_bindings PRIVATE + $<$>:-U_FORTIFY_SOURCE> + $<$>:-D_FORTIFY_SOURCE=${DDBC_FORTIFY_LEVEL}> + ) + target_link_options(ddbc_bindings PRIVATE + -Wl,-z,relro + -Wl,-z,now + -Wl,-z,noexecstack + ) +endif() + # Add macOS-specific string conversion fix if(APPLE) message(STATUS "Enabling macOS string conversion fix") diff --git a/tests/test_040_native_binary_hardening.py b/tests/test_040_native_binary_hardening.py new file mode 100644 index 000000000..48556e985 --- /dev/null +++ b/tests/test_040_native_binary_hardening.py @@ -0,0 +1,183 @@ +"""Regression guards for POSIX native-extension hardening.""" + +import struct +import sys +from pathlib import Path + +import pytest + +_ROOT = Path(__file__).resolve().parents[1] +_CMAKE = _ROOT / "mssql_python" / "pybind" / "CMakeLists.txt" +_PT_DYNAMIC = 2 +_PT_GNU_STACK = 0x6474E551 +_PT_GNU_RELRO = 0x6474E552 +_PF_X = 0x1 +_DT_NULL = 0 +_DT_BIND_NOW = 24 +_DT_FLAGS = 30 +_DF_BIND_NOW = 0x8 +_DT_FLAGS_1 = 0x6FFFFFFB +_DF_1_NOW = 0x1 + + +def _make_elf64(*, relro=True, bind_now=True, executable_stack=False): + header_size = 64 + program_header_size = 56 + program_count = 3 + dynamic_offset = header_size + program_header_size * program_count + dynamic = struct.pack("" + program_offset = struct.unpack_from(endian + "Q", data, 0x20)[0] + entry_size = struct.unpack_from(endian + "H", data, 0x36)[0] + entry_count = struct.unpack_from(endian + "H", data, 0x38)[0] + + if ( + not program_offset + or not entry_count + or entry_size < 56 + or program_offset + entry_count * entry_size > len(data) + ): + raise ValueError("invalid ELF program headers") + + has_relro = False + stack_executable = None + dynamic_segment = None + for index in range(entry_count): + offset = program_offset + index * entry_size + program_type = struct.unpack_from(endian + "I", data, offset)[0] + flags = struct.unpack_from(endian + "I", data, offset + 4)[0] + file_offset = struct.unpack_from(endian + "Q", data, offset + 8)[0] + file_size = struct.unpack_from(endian + "Q", data, offset + 32)[0] + if file_offset + file_size > len(data): + raise ValueError("ELF segment extends beyond the file") + if program_type == _PT_GNU_RELRO: + has_relro = True + elif program_type == _PT_GNU_STACK: + stack_executable = bool(flags & _PF_X) + elif program_type == _PT_DYNAMIC: + dynamic_segment = file_offset, file_size + + if dynamic_segment is None: + raise ValueError("ELF has no PT_DYNAMIC segment") + + dynamic_offset, dynamic_size = dynamic_segment + dynamic_entry_size = 16 + if dynamic_size % dynamic_entry_size: + raise ValueError("ELF dynamic segment has a partial entry") + + bind_now = False + terminated = False + for offset in range(dynamic_offset, dynamic_offset + dynamic_size, dynamic_entry_size): + tag = struct.unpack_from(endian + "q", data, offset)[0] + value = struct.unpack_from(endian + "Q", data, offset + 8)[0] + if tag == _DT_NULL: + terminated = True + break + bind_now = bind_now or tag == _DT_BIND_NOW + bind_now = bind_now or tag == _DT_FLAGS and bool(value & _DF_BIND_NOW) + bind_now = bind_now or tag == _DT_FLAGS_1 and bool(value & _DF_1_NOW) + + if not terminated: + raise ValueError("ELF dynamic segment lacks DT_NULL") + return has_relro, bind_now, stack_executable + + +def test_elf_hardening_parser_reads_program_and_dynamic_flags(): + assert _elf_hardening(_make_elf64()) == (True, True, False) + assert _elf_hardening(_make_elf64(relro=False)) == (False, True, False) + assert _elf_hardening(_make_elf64(bind_now=False)) == (True, False, False) + assert _elf_hardening(_make_elf64(executable_stack=True)) == (True, True, True) + + +@pytest.mark.skipif( + not _CMAKE.is_file(), + reason="requires a source checkout; isolated wheel tests omit the source tree", +) +def test_posix_hardening_flags_are_explicit(): + cmake = _CMAKE.read_text(encoding="utf-8") + + assert "set(CMAKE_BUILD_TYPE Release" in cmake + assert "-fstack-protector-strong" in cmake + assert "-U_FORTIFY_SOURCE" in cmake + assert "DDBC_SUPPORTS_FORTIFY_SOURCE_3" in cmake + assert "-D_FORTIFY_SOURCE=${DDBC_FORTIFY_LEVEL}" in cmake + assert "$>" in cmake + assert "if(UNIX AND NOT APPLE)" in cmake + for flag in ("-Wl,-z,relro", "-Wl,-z,now", "-Wl,-z,noexecstack"): + assert flag in cmake + + +@pytest.mark.skipif(sys.platform != "linux", reason="ELF hardening applies to Linux") +def test_linux_extension_has_linker_hardening(): + from mssql_python import ddbc_bindings + + extension = Path(ddbc_bindings.module_path) + has_relro, bind_now, stack_executable = _elf_hardening(extension.read_bytes()) + assert has_relro, "native extension is missing PT_GNU_RELRO" + assert bind_now, "native extension is missing immediate binding (BIND_NOW)" + assert stack_executable is not None, "native extension has no PT_GNU_STACK declaration" + assert stack_executable is False, "native extension requests an executable stack" From dd1b21c7fc4aaa4bac198612631727e9c6ea65af Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Thu, 24 Sep 2026 07:17:56 +0100 Subject: [PATCH 2/2] FIX: isolate concurrent auth connection mocks Give each concurrent connect call its own configured native connection mock so MagicMock child creation cannot race during close. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_008_auth.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_008_auth.py b/tests/test_008_auth.py index 6fd2d6574..2b3ef50da 100644 --- a/tests/test_008_auth.py +++ b/tests/test_008_auth.py @@ -1565,7 +1565,13 @@ def test_multiple_connections_share_same_token_provider(self, mock_ddbc_conn): @patch("mssql_python.connection.ddbc_bindings.Connection") def test_concurrent_connections_with_same_token_provider(self, mock_ddbc_conn): """Concurrent connect() calls with one token provider should succeed.""" - mock_ddbc_conn.return_value = MagicMock() + + def create_native_connection(*_args, **_kwargs): + native_connection = MagicMock() + native_connection.get_autocommit.return_value = True + return native_connection + + mock_ddbc_conn.side_effect = create_native_connection mock_cred = MagicMock() mock_cred.get_token.return_value = MagicMock(token=SAMPLE_TOKEN) from mssql_python import connect