From 153eff7210d676d238f2f8aeb8f17c0c2ea3544d Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:25:09 -0700 Subject: [PATCH 1/7] test: add mock TDS FedAuth regression test for deferred access-token UAF (#594/#596) Adds tests/test_024_mock_tds_fedauth.py which uses the mssql-mock-tds in-process TDS server to assert that SQL_COPT_SS_ACCESS_TOKEN round-trips byte-for-byte through the ODBC driver to the server's Login7 FedAuth feature. This guards the deferred connect-attribute use-after-free fixed in PR #596: a corrupted deferred buffer would surface as a missing/garbage token captured by the mock, failing the test. Skips cleanly when mssql-mock-tds or cryptography are unavailable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_024_mock_tds_fedauth.py | 252 +++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 tests/test_024_mock_tds_fedauth.py diff --git a/tests/test_024_mock_tds_fedauth.py b/tests/test_024_mock_tds_fedauth.py new file mode 100644 index 00000000..6cd7ae7f --- /dev/null +++ b/tests/test_024_mock_tds_fedauth.py @@ -0,0 +1,252 @@ +""" +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Integration tests for FedAuth (access token) connect attributes against a +*mock TDS server*. + +These tests guard the fix delivered in PR #596 / issue #594: + + SQL_COPT_SS_ACCESS_TOKEN (1256) is a *deferred* ODBC connect attribute. + The MS ODBC driver stashes the caller's pointer at ``SQLSetConnectAttr`` + time and only dereferences it later, during ``SQLDriverConnect``, to build + the FedAuth login packet. PR #568 briefly copied the value into a + stack-local buffer, which was freed before the deferred read -> a + use-after-free that variously produced SIGBUS, a server reset, or the + error "Authentication token is missing in the federated authentication + message". + + PR #596 stores the value in Connection-owned member buffers so it stays + valid for the deferred dereference. + +The regression is invisible to ordinary unit tests because it only manifests +once a real driver actually transmits the token to a server. The +``mssql-mock-tds`` package gives us exactly that: an in-process TDS server that +records the access token it received in the Login7 FedAuth feature. If the +deferred buffer is ever corrupted again, the token captured by the server will +not match the one we sent (or no token will arrive at all) and these tests +fail. + +Requirements (both optional; tests skip cleanly when missing): + * ``mssql-mock-tds`` -> the mock server Python bindings. Install from the + public feed, e.g.:: + + pip install --index-url \\ + https://pkgs.dev.azure.com/sqlclientdrivers/public/_packaging/mssql-rs_Public/pypi/simple/ \\ + mssql-mock-tds + + * ``cryptography`` -> used to generate the throwaway TLS identity the mock + server needs (already pulled in transitively by + ``azure-identity``). +""" + +import datetime +import os +import secrets +import struct + +import pytest + +from mssql_python.constants import ConstantsDDBC + +# --------------------------------------------------------------------------- +# Optional-dependency probing. The whole module is skipped (not failed) when a +# dependency is unavailable so the suite stays green on machines/CI legs that do +# not install the mock server. +# --------------------------------------------------------------------------- +try: + import mssql_mock_tds + + MOCK_TDS_AVAILABLE = True +except ImportError: + mssql_mock_tds = None + MOCK_TDS_AVAILABLE = False + +try: + from cryptography import x509 + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives.serialization import pkcs12, NoEncryption + from cryptography.x509.oid import NameOID + + CRYPTOGRAPHY_AVAILABLE = True +except ImportError: + CRYPTOGRAPHY_AVAILABLE = False + + +pytestmark = pytest.mark.skipif( + not (MOCK_TDS_AVAILABLE and CRYPTOGRAPHY_AVAILABLE), + reason=( + "Requires the 'mssql-mock-tds' package and 'cryptography'. " + "Install mssql-mock-tds from the mssql-rs public feed to run these tests." + ), +) + +# Keep the per-connect wait short: the mock server records the FedAuth token +# while processing Login7 but does not complete the handshake the way the real +# ODBC driver expects, so the driver eventually times out. The token has +# already been captured by then, so a small login timeout keeps the tests fast. +_LOGIN_TIMEOUT_SECONDS = 3 + + +def _write_test_identity(directory): + """Generate a self-signed TLS identity (PKCS#12) the mock server can load. + + The mock server looks for a TLS identity at ``/tests/test_certificates`` + (among a few relative candidates). It prefers ``valid_cert.pem`` + ``key.pem`` + but those only build a native-tls Identity on non-Windows builds; the + ``identity.pfx`` path uses ``load_identity_from_file`` which works on every + platform, so we emit only the .pfx. + """ + cert_dir = os.path.join(directory, "tests", "test_certificates") + os.makedirs(cert_dir, exist_ok=True) + + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + name = x509.Name( + [ + x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), + x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), + ] + ) + now = datetime.datetime.now(datetime.timezone.utc) + cert = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - datetime.timedelta(days=1)) + .not_valid_after(now + datetime.timedelta(days=3650)) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False + ) + .sign(key, hashes.SHA256()) + ) + pfx = pkcs12.serialize_key_and_certificates( + name=b"mock-tds", + key=key, + cert=cert, + cas=None, + encryption_algorithm=NoEncryption(), + ) + with open(os.path.join(cert_dir, "identity.pfx"), "wb") as handle: + handle.write(pfx) + + +def _access_token_struct(raw_token): + """Build the ODBC SQL_COPT_SS_ACCESS_TOKEN value for *raw_token*. + + Mirrors ``mssql_python.auth.AADAuth.get_token_struct``: a little-endian + 4-byte length prefix followed by the UTF-16LE encoded token. The driver + unwraps this struct and transmits only the bare token in the Login7 FedAuth + feature, which is what the mock server records. + """ + token_bytes = raw_token.encode("utf-16-le") + return struct.pack(f"= 1, ( + "Mock server recorded no connection; the driver never reached " + f"Login7. Last connect error: {self_error!r}" + ) + assert mock_tls_server.has_received_token(token), ( + "Mock server did not receive the expected access token. This is the " + "#594/#596 deferred connect-attribute use-after-free signature " + f"(last token={mock_tls_server.get_last_access_token()!r})." + ) + assert mock_tls_server.get_last_access_token() == token + + def test_unique_access_token_transmitted_exactly(self, mock_tls_server): + """A random, unguessable token rules out any cached/constant fallback.""" + token = f"regression_token_{secrets.token_hex(24)}" + + _connect_with_token(mock_tls_server, token) + + assert mock_tls_server.get_last_access_token() == token, ( + "Unique access token was not transmitted byte-for-byte: sent " + f"{token!r}, server received " + f"{mock_tls_server.get_last_access_token()!r}." + ) + + def test_distinct_tokens_on_sequential_connects(self, mock_tls_server): + """Two connects on owned buffers must not clobber each other's token. + + PR #596 hardened the fix with per-attribute owned buffers. Exercising + two sequential connects with different tokens ensures the second token + is recorded without invalidating buffer ownership semantics. + """ + first = f"first_{secrets.token_hex(16)}" + second = f"second_{secrets.token_hex(16)}" + + _connect_with_token(mock_tls_server, first) + _connect_with_token(mock_tls_server, second) + + assert mock_tls_server.has_received_token(first) + assert mock_tls_server.has_received_token(second) + assert mock_tls_server.get_last_access_token() == second From 331a83fe566c72de18cfef0545dd0e855ed672aa Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:58:31 -0700 Subject: [PATCH 2/7] ci: install mssql-mock-tds in PR validation pipeline so the mock TDS fedauth test runs (not skips) Adds a reusable install step (eng/pipelines/steps/install-mock-tds.yml) backed by install-mock-tds.{sh,ps1}, mirroring the existing install-mssql-py-core pattern. The step installs mssql-mock-tds (pinned via eng/versions/mssql-mock-tds.version) plus cryptography from the public mssql-rs_Public PyPI feed, then verifies import. It is referenced from every test leg (Windows, macOS, and all Linux containers). The scripts are best-effort: on a platform with no compatible wheel (e.g. Alpine/musl) or if the sandbox feed is unavailable, they emit an Azure DevOps pipeline warning and exit 0, so the leg stays green and test_024_mock_tds_fedauth simply skips there instead of failing the build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 48 +++++++++++++++++ eng/pipelines/steps/install-mock-tds.yml | 69 ++++++++++++++++++++++++ eng/scripts/install-mock-tds.ps1 | 62 +++++++++++++++++++++ eng/scripts/install-mock-tds.sh | 67 +++++++++++++++++++++++ eng/versions/mssql-mock-tds.version | 1 + 5 files changed, 247 insertions(+) create mode 100644 eng/pipelines/steps/install-mock-tds.yml create mode 100644 eng/scripts/install-mock-tds.ps1 create mode 100644 eng/scripts/install-mock-tds.sh create mode 100644 eng/versions/mssql-mock-tds.version diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index 8cc7ea8e..ebef7f62 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -234,6 +234,10 @@ jobs: parameters: platform: windows + - template: steps/install-mock-tds.yml + parameters: + platform: windows + # Run tests for LocalDB - script: | python -m pytest -v --junitxml=test-results-localdb.xml --cov=. --cov-report=xml:coverage-localdb.xml --capture=tee-sys --cache-clear @@ -543,6 +547,10 @@ jobs: parameters: platform: unix + - template: steps/install-mock-tds.yml + parameters: + platform: unix + - script: | echo "Build successful, running tests now" python -m pytest -v --junitxml=test-results.xml --cov=. --cov-report=xml --capture=tee-sys --cache-clear @@ -799,6 +807,12 @@ jobs: containerName: test-container-$(distroName) venvActivate: 'source /opt/venv/bin/activate' + - template: steps/install-mock-tds.yml + parameters: + platform: container + containerName: test-container-$(distroName) + venvActivate: 'source /opt/venv/bin/activate' + - script: | # Uninstall ODBC Driver before running tests docker exec test-container-$(distroName) bash -c " @@ -1124,6 +1138,12 @@ jobs: containerName: test-container-$(distroName)-$(archName) venvActivate: 'source /opt/venv/bin/activate' + - template: steps/install-mock-tds.yml + parameters: + platform: container + containerName: test-container-$(distroName)-$(archName) + venvActivate: 'source /opt/venv/bin/activate' + - script: | # Uninstall ODBC Driver before running tests docker exec test-container-$(distroName)-$(archName) bash -c " @@ -1338,6 +1358,12 @@ jobs: containerName: test-container-rhel9 venvActivate: 'source myvenv/bin/activate' + - template: steps/install-mock-tds.yml + parameters: + platform: container + containerName: test-container-rhel9 + venvActivate: 'source myvenv/bin/activate' + - script: | # Uninstall ODBC Driver before running tests docker exec test-container-rhel9 bash -c " @@ -1563,6 +1589,12 @@ jobs: containerName: test-container-rhel9-arm64 venvActivate: 'source myvenv/bin/activate' + - template: steps/install-mock-tds.yml + parameters: + platform: container + containerName: test-container-rhel9-arm64 + venvActivate: 'source myvenv/bin/activate' + - script: | # Uninstall ODBC Driver before running tests docker exec test-container-rhel9-arm64 bash -c " @@ -1796,6 +1828,12 @@ jobs: containerName: test-container-alpine venvActivate: 'source /workspace/venv/bin/activate' + - template: steps/install-mock-tds.yml + parameters: + platform: container + containerName: test-container-alpine + venvActivate: 'source /workspace/venv/bin/activate' + - script: | # Uninstall ODBC Driver before running tests to use bundled libraries docker exec test-container-alpine bash -c " @@ -2047,6 +2085,12 @@ jobs: containerName: test-container-alpine-arm64 venvActivate: 'source /workspace/venv/bin/activate' + - template: steps/install-mock-tds.yml + parameters: + platform: container + containerName: test-container-alpine-arm64 + venvActivate: 'source /workspace/venv/bin/activate' + - script: | # Uninstall ODBC Driver before running tests to use bundled libraries docker exec test-container-alpine-arm64 bash -c " @@ -2398,6 +2442,10 @@ jobs: parameters: platform: unix + - template: steps/install-mock-tds.yml + parameters: + platform: unix + - script: | # Generate unified coverage (Python + C++) chmod +x ./generate_codecov.sh diff --git a/eng/pipelines/steps/install-mock-tds.yml b/eng/pipelines/steps/install-mock-tds.yml new file mode 100644 index 00000000..0ff4aefc --- /dev/null +++ b/eng/pipelines/steps/install-mock-tds.yml @@ -0,0 +1,69 @@ +# Step template: Install mssql-mock-tds (in-process mock TDS server) from the +# public mssql-rs_Public Azure Artifacts PyPI feed. +# +# This is required for tests/test_024_mock_tds_fedauth.py, which guards the +# deferred SQL_COPT_SS_ACCESS_TOKEN use-after-free fix (PR #596 / issue #594). +# Without the package the test skips. The backing scripts are best-effort: on a +# platform with no compatible wheel (e.g. Alpine/musl) they emit a pipeline +# warning and succeed, so the test simply skips on those legs. +# +# Usage: +# # Windows (host) +# - template: steps/install-mock-tds.yml +# parameters: +# platform: windows +# +# # macOS / Linux (host) +# - template: steps/install-mock-tds.yml +# parameters: +# platform: unix +# +# # Inside a Docker container +# - template: steps/install-mock-tds.yml +# parameters: +# platform: container +# containerName: test-container-$(distroName) +# venvActivate: 'source /opt/venv/bin/activate' + +parameters: + - name: platform + type: string + values: [windows, unix, container] + + - name: containerName + type: string + default: '' + + - name: venvActivate + type: string + default: 'source /opt/venv/bin/activate' + + - name: displaySuffix + type: string + default: '' + +steps: +# Windows: run the PowerShell script directly on the host +- ${{ if eq(parameters.platform, 'windows') }}: + - task: PowerShell@2 + displayName: 'Install mssql-mock-tds${{ parameters.displaySuffix }}' + inputs: + targetType: 'filePath' + filePath: 'eng/scripts/install-mock-tds.ps1' + +# Unix host (macOS, Linux without container) +- ${{ if eq(parameters.platform, 'unix') }}: + - script: | + chmod +x eng/scripts/install-mock-tds.sh + ./eng/scripts/install-mock-tds.sh + displayName: 'Install mssql-mock-tds${{ parameters.displaySuffix }}' + +# Inside a Docker container +- ${{ if eq(parameters.platform, 'container') }}: + - script: | + docker exec ${{ parameters.containerName }} bash -c " + ${{ parameters.venvActivate }} + chmod +x eng/scripts/install-mock-tds.sh + ./eng/scripts/install-mock-tds.sh + " + displayName: 'Install mssql-mock-tds in ${{ parameters.containerName }}${{ parameters.displaySuffix }}' diff --git a/eng/scripts/install-mock-tds.ps1 b/eng/scripts/install-mock-tds.ps1 new file mode 100644 index 00000000..941b4ffb --- /dev/null +++ b/eng/scripts/install-mock-tds.ps1 @@ -0,0 +1,62 @@ +<# +.SYNOPSIS + Installs the mssql-mock-tds in-process TDS server (plus cryptography, used to + mint the throwaway TLS identity it needs) from the public mssql-rs_Public + Azure Artifacts PyPI feed so that tests/test_024_mock_tds_fedauth.py runs in + CI instead of skipping. + +.DESCRIPTION + The package is currently published only as a dev pre-release on a sandbox + feed. To keep pipeline legs green where no compatible wheel exists, a failed + install is reported as a pipeline *warning* (not an error): the test then + skips cleanly. + + The package version is read from eng/versions/mssql-mock-tds.version. + +.PARAMETER FeedUrl + The PyPI-format index URL for the mssql-rs_Public feed. Public -- no auth. +#> + +param( + [string]$FeedUrl = "https://pkgs.dev.azure.com/sqlclientdrivers/public/_packaging/mssql-rs_Public/pypi/simple/" +) + +# Best-effort install: never fail the pipeline leg over a sandbox-feed dependency. +$ErrorActionPreference = 'Continue' +$ScriptDir = $PSScriptRoot +$RepoRoot = (Get-Item "$ScriptDir\..\..").FullName + +function Write-PipelineWarning([string]$Message) { + # Surface as an Azure DevOps pipeline warning while keeping the leg green. + Write-Host "##vso[task.logissue type=warning]$Message" + Write-Host "WARNING: $Message" +} + +$versionFile = Join-Path $RepoRoot "eng\versions\mssql-mock-tds.version" +if (-not (Test-Path $versionFile)) { + Write-PipelineWarning "Version file not found: $versionFile -- skipping mssql-mock-tds install." + exit 0 +} +$packageVersion = (Get-Content $versionFile -Raw).Trim() +if (-not $packageVersion) { + Write-PipelineWarning "Version file is empty: $versionFile -- skipping mssql-mock-tds install." + exit 0 +} + +Write-Host "=== Install mssql-mock-tds ($packageVersion) from $FeedUrl ===" + +& python -m pip install --extra-index-url $FeedUrl "mssql-mock-tds==$packageVersion" cryptography +if ($LASTEXITCODE -ne 0) { + Write-PipelineWarning "Could not install mssql-mock-tds==$packageVersion (no compatible wheel on this platform or feed unavailable) -- the mock TDS test will skip." + exit 0 +} + +Write-Host "Verifying import..." +& python -c "import mssql_mock_tds; print('mssql_mock_tds import OK')" +if ($LASTEXITCODE -ne 0) { + Write-PipelineWarning "mssql-mock-tds installed but failed to import -- the mock TDS test will skip." + exit 0 +} + +Write-Host "=== mssql-mock-tds installed successfully ===" +exit 0 diff --git a/eng/scripts/install-mock-tds.sh b/eng/scripts/install-mock-tds.sh new file mode 100644 index 00000000..13ff95a2 --- /dev/null +++ b/eng/scripts/install-mock-tds.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Installs the mssql-mock-tds in-process TDS server (plus cryptography, used to +# mint the throwaway TLS identity it needs) from the public mssql-rs_Public +# Azure Artifacts PyPI feed so that tests/test_024_mock_tds_fedauth.py runs in CI +# instead of skipping. +# +# The package is currently published only as a dev pre-release on a sandbox feed +# and ships wheels for a limited platform matrix (linux glibc x86_64/aarch64, +# macOS universal2, Windows amd64/arm64 -- but NOT musllinux/Alpine). To keep +# pipeline legs green where no compatible wheel exists, a failed install is +# reported as a pipeline *warning* (not an error): the test then skips cleanly. +# +# The package version is read from eng/versions/mssql-mock-tds.version (required). +# +# Usage: +# ./install-mock-tds.sh [--feed-url URL] + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PYTHON="${PYTHON:-$(command -v python || command -v python3)}" + +# Public PyPI-format index for the mssql-rs_Public feed (no auth required). +FEED_URL="${FEED_URL:-https://pkgs.dev.azure.com/sqlclientdrivers/public/_packaging/mssql-rs_Public/pypi/simple/}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --feed-url) FEED_URL="$2"; shift 2 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +warn() { + # Surface as an Azure DevOps pipeline warning while keeping the leg green. + echo "##vso[task.logissue type=warning]$1" + echo "WARNING: $1" +} + +version_file="$REPO_ROOT/eng/versions/mssql-mock-tds.version" +if [ ! -f "$version_file" ]; then + warn "Version file not found: $version_file -- skipping mssql-mock-tds install." + exit 0 +fi +PACKAGE_VERSION=$(tr -d '[:space:]' < "$version_file") +if [ -z "$PACKAGE_VERSION" ]; then + warn "Version file is empty: $version_file -- skipping mssql-mock-tds install." + exit 0 +fi + +echo "=== Install mssql-mock-tds ($PACKAGE_VERSION) from $FEED_URL ===" + +if "$PYTHON" -m pip install \ + --extra-index-url "$FEED_URL" \ + "mssql-mock-tds==$PACKAGE_VERSION" \ + cryptography; then + echo "Verifying import..." + if "$PYTHON" -c "import mssql_mock_tds; print('mssql_mock_tds import OK')"; then + echo "=== mssql-mock-tds installed successfully ===" + exit 0 + fi + warn "mssql-mock-tds installed but failed to import -- the mock TDS test will skip." + exit 0 +fi + +warn "Could not install mssql-mock-tds==$PACKAGE_VERSION (no compatible wheel on this platform, e.g. Alpine/musl, or feed unavailable) -- the mock TDS test will skip." +exit 0 diff --git a/eng/versions/mssql-mock-tds.version b/eng/versions/mssql-mock-tds.version new file mode 100644 index 00000000..75c5d7c7 --- /dev/null +++ b/eng/versions/mssql-mock-tds.version @@ -0,0 +1 @@ +0.1.0.dev20260630158804 From 3e5da9196ce5ae768c3e4dbc1b3a6cdfdbf85982 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:17:38 -0700 Subject: [PATCH 3/7] ci: bump mssql-mock-tds to 0.1.0.dev20260630158879 (adds musllinux/Alpine wheels) The new dev build ships musllinux_1_2 wheels (x86_64 + aarch64), so the Alpine test legs now install the mock server and run test_024_mock_tds_fedauth instead of skipping. Updated the stale comments that called out musl as an unsupported platform; the install scripts stay best-effort in case the sandbox feed is ever unavailable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/steps/install-mock-tds.yml | 6 +++--- eng/scripts/install-mock-tds.sh | 13 +++++++------ eng/versions/mssql-mock-tds.version | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/eng/pipelines/steps/install-mock-tds.yml b/eng/pipelines/steps/install-mock-tds.yml index 0ff4aefc..f827c960 100644 --- a/eng/pipelines/steps/install-mock-tds.yml +++ b/eng/pipelines/steps/install-mock-tds.yml @@ -3,9 +3,9 @@ # # This is required for tests/test_024_mock_tds_fedauth.py, which guards the # deferred SQL_COPT_SS_ACCESS_TOKEN use-after-free fix (PR #596 / issue #594). -# Without the package the test skips. The backing scripts are best-effort: on a -# platform with no compatible wheel (e.g. Alpine/musl) they emit a pipeline -# warning and succeed, so the test simply skips on those legs. +# Without the package the test skips. The backing scripts are best-effort: if a +# compatible wheel is ever missing or the sandbox feed is unavailable, they emit +# a pipeline warning and succeed, so the test simply skips on those legs. # # Usage: # # Windows (host) diff --git a/eng/scripts/install-mock-tds.sh b/eng/scripts/install-mock-tds.sh index 13ff95a2..ab98adf6 100644 --- a/eng/scripts/install-mock-tds.sh +++ b/eng/scripts/install-mock-tds.sh @@ -4,11 +4,12 @@ # Azure Artifacts PyPI feed so that tests/test_024_mock_tds_fedauth.py runs in CI # instead of skipping. # -# The package is currently published only as a dev pre-release on a sandbox feed -# and ships wheels for a limited platform matrix (linux glibc x86_64/aarch64, -# macOS universal2, Windows amd64/arm64 -- but NOT musllinux/Alpine). To keep -# pipeline legs green where no compatible wheel exists, a failed install is -# reported as a pipeline *warning* (not an error): the test then skips cleanly. +# The package is currently published as a dev pre-release on a sandbox feed and +# ships wheels for linux glibc (x86_64/aarch64), musllinux/Alpine +# (x86_64/aarch64), macOS universal2, and Windows (amd64/arm64). To keep +# pipeline legs green if a compatible wheel is ever missing or the sandbox feed +# is unavailable, a failed install is reported as a pipeline *warning* (not an +# error): the test then skips cleanly. # # The package version is read from eng/versions/mssql-mock-tds.version (required). # @@ -63,5 +64,5 @@ if "$PYTHON" -m pip install \ exit 0 fi -warn "Could not install mssql-mock-tds==$PACKAGE_VERSION (no compatible wheel on this platform, e.g. Alpine/musl, or feed unavailable) -- the mock TDS test will skip." +warn "Could not install mssql-mock-tds==$PACKAGE_VERSION (no compatible wheel on this platform or feed unavailable) -- the mock TDS test will skip." exit 0 diff --git a/eng/versions/mssql-mock-tds.version b/eng/versions/mssql-mock-tds.version index 75c5d7c7..6cf0296f 100644 --- a/eng/versions/mssql-mock-tds.version +++ b/eng/versions/mssql-mock-tds.version @@ -1 +1 @@ -0.1.0.dev20260630158804 +0.1.0.dev20260630158879 From 7fbdf1a2864d7828caa9c3e9613c1aff9193fde0 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:52:45 -0700 Subject: [PATCH 4/7] style: apply black formatting to test_024_mock_tds_fedauth.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_024_mock_tds_fedauth.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_024_mock_tds_fedauth.py b/tests/test_024_mock_tds_fedauth.py index 6cd7ae7f..a7a8a708 100644 --- a/tests/test_024_mock_tds_fedauth.py +++ b/tests/test_024_mock_tds_fedauth.py @@ -117,9 +117,7 @@ def _write_test_identity(directory): .serial_number(x509.random_serial_number()) .not_valid_before(now - datetime.timedelta(days=1)) .not_valid_after(now + datetime.timedelta(days=3650)) - .add_extension( - x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False - ) + .add_extension(x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False) .sign(key, hashes.SHA256()) ) pfx = pkcs12.serialize_key_and_certificates( @@ -174,8 +172,7 @@ def _connect_with_token(server, raw_token): import mssql_python conn_str = ( - f"Server={server.sql_address};Database=master;" - "Encrypt=yes;TrustServerCertificate=yes;" + f"Server={server.sql_address};Database=master;" "Encrypt=yes;TrustServerCertificate=yes;" ) attrs_before = { ConstantsDDBC.SQL_COPT_SS_ACCESS_TOKEN.value: _access_token_struct(raw_token), From dfa8685007ad6c67e1d9c0cbbd7fa5828c58ddfd Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:29:59 -0700 Subject: [PATCH 5/7] FIX: emit PEM identity on non-Windows so mock TDS TLS loads on macOS The mock server resolves its TLS identity by trying valid_cert.pem + key.pem first (via create_test_identity, OpenSSL, non-Windows only) and only then falls back to identity.pfx, which its Python binding loads with an EMPTY password. macOS' Security framework rejects a password-less PKCS#12 with 'The user name or passphrase you entered is not correct.', and we can't change the empty password the binding uses -- so the .pfx path can never work on macOS. Fix: write the PEM pair on non-Windows (Linux + macOS), where create_test_identity re-packs them into a 3DES PKCS#12 with a non-empty password that macOS accepts; keep emitting identity.pfx on Windows, where OpenSSL isn't bundled and Schannel loads a password-less PKCS#12 fine. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_024_mock_tds_fedauth.py | 54 +++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/tests/test_024_mock_tds_fedauth.py b/tests/test_024_mock_tds_fedauth.py index a7a8a708..d4542661 100644 --- a/tests/test_024_mock_tds_fedauth.py +++ b/tests/test_024_mock_tds_fedauth.py @@ -64,7 +64,7 @@ try: from cryptography import x509 - from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import pkcs12, NoEncryption from cryptography.x509.oid import NameOID @@ -90,13 +90,24 @@ def _write_test_identity(directory): - """Generate a self-signed TLS identity (PKCS#12) the mock server can load. + """Generate a self-signed TLS identity the mock server can load. The mock server looks for a TLS identity at ``/tests/test_certificates`` - (among a few relative candidates). It prefers ``valid_cert.pem`` + ``key.pem`` - but those only build a native-tls Identity on non-Windows builds; the - ``identity.pfx`` path uses ``load_identity_from_file`` which works on every - platform, so we emit only the .pfx. + (among a few relative candidates) and resolves it in this order: it first + tries ``valid_cert.pem`` + ``key.pem`` via ``create_test_identity`` (OpenSSL, + non-Windows only), then falls back to ``identity.pfx`` via + ``load_identity_from_file`` with an *empty* password. + + Those two paths need different files per platform: + + * **Non-Windows (Linux, macOS):** emit the PEM pair. ``create_test_identity`` + re-packs them into a 3DES-encrypted PKCS#12 with a non-empty password, + which macOS' Security framework accepts. A password-less ``identity.pfx`` + would *not* load on macOS -- ``Identity::from_pkcs12`` there rejects an + unencrypted PKCS#12 with "The user name or passphrase you entered is not + correct.", and we cannot influence the empty password the binding uses. + * **Windows:** ``create_test_identity`` is unavailable (no bundled OpenSSL), + so emit ``identity.pfx``. Schannel happily loads a password-less PKCS#12. """ cert_dir = os.path.join(directory, "tests", "test_certificates") os.makedirs(cert_dir, exist_ok=True) @@ -120,15 +131,28 @@ def _write_test_identity(directory): .add_extension(x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False) .sign(key, hashes.SHA256()) ) - pfx = pkcs12.serialize_key_and_certificates( - name=b"mock-tds", - key=key, - cert=cert, - cas=None, - encryption_algorithm=NoEncryption(), - ) - with open(os.path.join(cert_dir, "identity.pfx"), "wb") as handle: - handle.write(pfx) + + if os.name == "nt": + pfx = pkcs12.serialize_key_and_certificates( + name=b"mock-tds", + key=key, + cert=cert, + cas=None, + encryption_algorithm=NoEncryption(), + ) + with open(os.path.join(cert_dir, "identity.pfx"), "wb") as handle: + handle.write(pfx) + else: + cert_pem = cert.public_bytes(serialization.Encoding.PEM) + key_pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=NoEncryption(), + ) + with open(os.path.join(cert_dir, "valid_cert.pem"), "wb") as handle: + handle.write(cert_pem) + with open(os.path.join(cert_dir, "key.pem"), "wb") as handle: + handle.write(key_pem) def _access_token_struct(raw_token): From 3d601c53d959e14bfd389e6a8e5ab8c0ba70161e Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:31:31 -0700 Subject: [PATCH 6/7] FIX: drop non-deterministic last-token assertion in sequential-connect test The mock server stores connections in a HashMap keyed by client socket address, so get_last_access_token() (which returns values().last()) reflects arbitrary hash ordering rather than connect order. The Windows SQL2022 leg flaked because 'last' resolved to the first connect's token. Assert instead that both distinct tokens were received byte-for-byte (order independent) and that two connections were recorded. That is the actual property guarding the per-attribute owned buffers from #596 -- two connects must not clobber each other's token. The single-connection tests keep using get_last_access_token(), which is deterministic with one stored entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_024_mock_tds_fedauth.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_024_mock_tds_fedauth.py b/tests/test_024_mock_tds_fedauth.py index d4542661..06b06c08 100644 --- a/tests/test_024_mock_tds_fedauth.py +++ b/tests/test_024_mock_tds_fedauth.py @@ -259,8 +259,9 @@ def test_distinct_tokens_on_sequential_connects(self, mock_tls_server): """Two connects on owned buffers must not clobber each other's token. PR #596 hardened the fix with per-attribute owned buffers. Exercising - two sequential connects with different tokens ensures the second token - is recorded without invalidating buffer ownership semantics. + two sequential connects with different tokens ensures both tokens are + recorded byte-for-byte without one connection invalidating the other's + buffer ownership. """ first = f"first_{secrets.token_hex(16)}" second = f"second_{secrets.token_hex(16)}" @@ -268,6 +269,18 @@ def test_distinct_tokens_on_sequential_connects(self, mock_tls_server): _connect_with_token(mock_tls_server, first) _connect_with_token(mock_tls_server, second) - assert mock_tls_server.has_received_token(first) - assert mock_tls_server.has_received_token(second) - assert mock_tls_server.get_last_access_token() == second + # Both exact tokens must be present. We deliberately do NOT assert on + # get_last_access_token(): the mock stores connections in a HashMap keyed + # by client socket address, so "last" reflects non-deterministic hash + # ordering, not connect order. Byte-for-byte receipt of *both* distinct + # tokens is the property that actually guards the #596 owned buffers. + assert mock_tls_server.has_received_token( + first + ), f"Mock server did not receive the first token {first!r}." + assert mock_tls_server.has_received_token( + second + ), f"Mock server did not receive the second token {second!r}." + assert mock_tls_server.connection_count() >= 2, ( + "Expected at least two recorded connections for two sequential " + f"connects, got {mock_tls_server.connection_count()}." + ) From 63c77f6b0d043633d01561b64a2541a6e8d29a0e Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:35:08 -0700 Subject: [PATCH 7/7] FIX: bump mock-tds to 0.1.0.dev20260701159015 and disable pooling in test_024 The new mssql-mock-tds build handles the TransactionManager (0x0E) request that Python DB-API drivers send after FedAuth login, so mssql_python now completes the connect instead of hanging on Linux CI (microsoft/mssql-rs#86). The mock records the FedAuth token only when the client connection actually closes. mssql_python auto-enables ODBC connection pooling on the first connect(), so conn.close() returned the socket to the pool and the mock never observed the close -- leaving has_received_token empty and failing all three tests. Add an autouse fixture that disables pooling (a process-global ODBC setting) for the duration of these tests and restores the prior configuration afterwards, so each connection is torn down promptly and its token recorded. Also refresh the now-stale comments that claimed the mock never completes the handshake. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/versions/mssql-mock-tds.version | 2 +- tests/test_024_mock_tds_fedauth.py | 47 ++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/eng/versions/mssql-mock-tds.version b/eng/versions/mssql-mock-tds.version index 6cf0296f..8afe319d 100644 --- a/eng/versions/mssql-mock-tds.version +++ b/eng/versions/mssql-mock-tds.version @@ -1 +1 @@ -0.1.0.dev20260630158879 +0.1.0.dev20260701159015 diff --git a/tests/test_024_mock_tds_fedauth.py b/tests/test_024_mock_tds_fedauth.py index 06b06c08..2e990848 100644 --- a/tests/test_024_mock_tds_fedauth.py +++ b/tests/test_024_mock_tds_fedauth.py @@ -82,10 +82,9 @@ ), ) -# Keep the per-connect wait short: the mock server records the FedAuth token -# while processing Login7 but does not complete the handshake the way the real -# ODBC driver expects, so the driver eventually times out. The token has -# already been captured by then, so a small login timeout keeps the tests fast. +# Bound each connect so a future protocol change can never hang the suite. With +# the current mock the FedAuth Login7 is answered and the connection completes +# quickly; this login timeout is just a safety net. _LOGIN_TIMEOUT_SECONDS = 3 @@ -186,12 +185,43 @@ def mock_tls_server(tmp_path, monkeypatch): server.stop() +@pytest.fixture(autouse=True) +def _no_connection_pooling(): + """Disable connection pooling for the duration of each test. + + The mock records the FedAuth access token only once the client connection + is actually closed (its per-connection read loop moves the token into the + connection store when the socket goes away). ``mssql_python`` auto-enables + ODBC connection pooling on the first ``connect()``, so ``conn.close()`` would + merely return the socket to the pool and the mock would never observe the + close -- leaving ``has_received_token`` empty. Pooling is a process-global + ODBC environment setting, so we toggle it off here and restore the prior + configuration afterwards. + """ + from mssql_python.pooling import PoolingManager + import mssql_python + + was_enabled = PoolingManager.is_enabled() + was_initialized = PoolingManager.is_initialized() + + mssql_python.pooling(enabled=False) + try: + yield + finally: + if was_enabled: + mssql_python.pooling() + elif not was_initialized: + PoolingManager._reset_for_testing() + + def _connect_with_token(server, raw_token): """Best-effort connect to *server* using *raw_token* as the access token. - Returns the captured connection error (if any). The real ODBC driver times - out because the mock does not finish the handshake; that is expected and not - what these tests assert on -- they assert on the token the server received. + Returns the captured connection error (if any). Connection pooling is + disabled for these tests (see ``_no_connection_pooling``) so that closing the + connection actually tears down the socket and the mock records the token that + was transmitted in the Login7 FedAuth feature -- which is what these tests + assert on. """ import mssql_python @@ -206,7 +236,8 @@ def _connect_with_token(server, raw_token): conn = mssql_python.connect(conn_str, attrs_before=attrs_before) except Exception as exc: # noqa: BLE001 - handshake completion is not under test return exc - # If a future mock learns to complete the handshake, don't leak the handle. + # Close the connection so the mock observes the socket teardown and records + # the token from this connect. try: conn.close() except Exception: # noqa: BLE001