From eb69633f69803166ab4cbc89f070cc482bc7b533 Mon Sep 17 00:00:00 2001 From: Evan <36235551+SynaptSea@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:12:33 -0700 Subject: [PATCH] gh-154199: Fix find_msvcrt() on builds with a truncated sys.version Python/getversion.c formats the compiler identification with \\%.80s\\, so the long banner emitted by clang-cl builds is cut off before the \\MSC v.\\ marker that _get_build_version() looks for. It then fell back to \\ eturn 6\\, i.e. 'assume MSVC 6', and find_msvcrt() handed out msvcrt.dll -- a CRT that does not share its errno with the ucrt that _ctypes is linked against, so ctypes.get_errno() silently returned stale values. Report the build version as unknown instead, so find_msvcrt() and find_library('c') return None exactly as they do on modern MSVC builds. Also handle a second truncation case found while fixing this one: when the cut lands inside the version number itself the marker is present but the digits are not, and the unpacking assignment raised ValueError out of find_library('c'). --- Lib/ctypes/util.py | 13 +++-- Lib/test/test_ctypes/test_find.py | 52 +++++++++++++++++++ ...-07-19-21-40-00.gh-issue-154199.Kq3mVt.rst | 8 +++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-19-21-40-00.gh-issue-154199.Kq3mVt.rst diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index a73422598b2cd9..efb528d1750765 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -17,16 +17,21 @@ def _get_build_version(): """Return the version of MSVC that was used to build Python. - For Python 2.3 and up, the version number is included in - sys.version. For earlier versions, assume the compiler is MSVC 6. + The version number is included in sys.version. Return None if it + cannot be found there, for example because Python was not built + with MSVC or because sys.version has been truncated. """ # This function was copied from Lib/distutils/msvccompiler.py prefix = "MSC v." i = sys.version.find(prefix) if i == -1: - return 6 + # We don't know what version of the compiler this is. + return None i = i + len(prefix) - s, rest = sys.version[i:].split(" ", 1) + s, sep, _ = sys.version[i:].partition(" ") + if not sep: + # sys.version was truncated inside the version number. + return None majorVersion = int(s[:-2]) - 6 if majorVersion >= 13: majorVersion += 1 diff --git a/Lib/test/test_ctypes/test_find.py b/Lib/test/test_ctypes/test_find.py index 8bc84c3d2ef9f8..d9523a54680071 100644 --- a/Lib/test/test_ctypes/test_find.py +++ b/Lib/test/test_ctypes/test_find.py @@ -1,3 +1,4 @@ +import ctypes.util import os.path import sys import test.support @@ -221,5 +222,56 @@ def test_find_nothing_with_wrong_ld_library_path(self): self.assertIsNone(result) +@unittest.skipUnless(os.name == 'nt', 'Test only valid for Windows') +class FindLibraryWindows(unittest.TestCase): + # gh-154199: Python/getversion.c formats the compiler identification with + # "%.80s", so a long banner (as emitted by clang-cl builds) can be cut off + # before the "MSC v." marker. The build version must then be reported as + # unknown instead of defaulting to MSVC 6, which would make find_msvcrt() + # hand out msvcrt.dll -- a CRT that does not share its errno with the one + # the _ctypes extension module was linked against. + TRUNCATED_CLANG_VERSION = ( + '3.16.0a0 free-threading build (heads/main:0123456789ab, ' + 'Jan 1 2026, 00:00:00) [Clang 22.1.8 ' + '(https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174d' + ) + # Truncated just after the marker, so the version digits are missing. + TRUNCATED_IN_MARKER_VERSION = ( + '3.16.0a0 (main:0123456789ab, Jan 1 2026, 00:00:00) ' + '[Clang 22.1.8 (https://example.invalid) 64 bit (AMD64) with MSC v.1944' + ) + MSVC_VERSION = ( + '3.16.0a0 (main:0123456789ab, Jan 1 2026, 00:00:00) ' + '[MSC v.1944 64 bit (AMD64)]' + ) + + @thread_unsafe('patches sys.version') + def test_get_build_version_truncated(self): + with unittest.mock.patch.object(sys, 'version', + self.TRUNCATED_CLANG_VERSION): + self.assertIsNone(ctypes.util._get_build_version()) + + @thread_unsafe('patches sys.version') + def test_get_build_version_truncated_in_marker(self): + with unittest.mock.patch.object(sys, 'version', + self.TRUNCATED_IN_MARKER_VERSION): + self.assertIsNone(ctypes.util._get_build_version()) + + @thread_unsafe('patches sys.version') + def test_find_msvcrt_truncated(self): + with unittest.mock.patch.object(sys, 'version', + self.TRUNCATED_CLANG_VERSION): + self.assertIsNone(ctypes.util.find_msvcrt()) + self.assertIsNone(find_library('c')) + self.assertIsNone(find_library('m')) + + @thread_unsafe('patches sys.version') + def test_get_build_version_msvc(self): + with unittest.mock.patch.object(sys, 'version', self.MSVC_VERSION): + self.assertEqual(ctypes.util._get_build_version(), 14.4) + # Recent versions of the CRT are not directly loadable. + self.assertIsNone(ctypes.util.find_msvcrt()) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-19-21-40-00.gh-issue-154199.Kq3mVt.rst b/Misc/NEWS.d/next/Library/2026-07-19-21-40-00.gh-issue-154199.Kq3mVt.rst new file mode 100644 index 00000000000000..1a8f9dc0601c79 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-21-40-00.gh-issue-154199.Kq3mVt.rst @@ -0,0 +1,8 @@ +Fix :func:`ctypes.util.find_msvcrt` on Windows builds whose :data:`sys.version` +does not contain the ``MSC v.`` marker, which happens on ``clang-cl`` builds +because the compiler banner is truncated to 80 characters. Instead of assuming +MSVC 6 and returning ``msvcrt.dll`` -- a C runtime that does not share its +``errno`` with the one the :mod:`!_ctypes` extension module is linked against +-- the compiler version is now reported as unknown, so +:func:`!ctypes.util.find_msvcrt` and ``find_library('c')`` return ``None``, +matching builds made with modern versions of MSVC.