Skip to content

Commit eb69633

Browse files
committed
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').
1 parent 40f7fbf commit eb69633

3 files changed

Lines changed: 69 additions & 4 deletions

File tree

Lib/ctypes/util.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@
1717
def _get_build_version():
1818
"""Return the version of MSVC that was used to build Python.
1919
20-
For Python 2.3 and up, the version number is included in
21-
sys.version. For earlier versions, assume the compiler is MSVC 6.
20+
The version number is included in sys.version. Return None if it
21+
cannot be found there, for example because Python was not built
22+
with MSVC or because sys.version has been truncated.
2223
"""
2324
# This function was copied from Lib/distutils/msvccompiler.py
2425
prefix = "MSC v."
2526
i = sys.version.find(prefix)
2627
if i == -1:
27-
return 6
28+
# We don't know what version of the compiler this is.
29+
return None
2830
i = i + len(prefix)
29-
s, rest = sys.version[i:].split(" ", 1)
31+
s, sep, _ = sys.version[i:].partition(" ")
32+
if not sep:
33+
# sys.version was truncated inside the version number.
34+
return None
3035
majorVersion = int(s[:-2]) - 6
3136
if majorVersion >= 13:
3237
majorVersion += 1

Lib/test/test_ctypes/test_find.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ctypes.util
12
import os.path
23
import sys
34
import test.support
@@ -221,5 +222,56 @@ def test_find_nothing_with_wrong_ld_library_path(self):
221222
self.assertIsNone(result)
222223

223224

225+
@unittest.skipUnless(os.name == 'nt', 'Test only valid for Windows')
226+
class FindLibraryWindows(unittest.TestCase):
227+
# gh-154199: Python/getversion.c formats the compiler identification with
228+
# "%.80s", so a long banner (as emitted by clang-cl builds) can be cut off
229+
# before the "MSC v." marker. The build version must then be reported as
230+
# unknown instead of defaulting to MSVC 6, which would make find_msvcrt()
231+
# hand out msvcrt.dll -- a CRT that does not share its errno with the one
232+
# the _ctypes extension module was linked against.
233+
TRUNCATED_CLANG_VERSION = (
234+
'3.16.0a0 free-threading build (heads/main:0123456789ab, '
235+
'Jan 1 2026, 00:00:00) [Clang 22.1.8 '
236+
'(https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174d'
237+
)
238+
# Truncated just after the marker, so the version digits are missing.
239+
TRUNCATED_IN_MARKER_VERSION = (
240+
'3.16.0a0 (main:0123456789ab, Jan 1 2026, 00:00:00) '
241+
'[Clang 22.1.8 (https://example.invalid) 64 bit (AMD64) with MSC v.1944'
242+
)
243+
MSVC_VERSION = (
244+
'3.16.0a0 (main:0123456789ab, Jan 1 2026, 00:00:00) '
245+
'[MSC v.1944 64 bit (AMD64)]'
246+
)
247+
248+
@thread_unsafe('patches sys.version')
249+
def test_get_build_version_truncated(self):
250+
with unittest.mock.patch.object(sys, 'version',
251+
self.TRUNCATED_CLANG_VERSION):
252+
self.assertIsNone(ctypes.util._get_build_version())
253+
254+
@thread_unsafe('patches sys.version')
255+
def test_get_build_version_truncated_in_marker(self):
256+
with unittest.mock.patch.object(sys, 'version',
257+
self.TRUNCATED_IN_MARKER_VERSION):
258+
self.assertIsNone(ctypes.util._get_build_version())
259+
260+
@thread_unsafe('patches sys.version')
261+
def test_find_msvcrt_truncated(self):
262+
with unittest.mock.patch.object(sys, 'version',
263+
self.TRUNCATED_CLANG_VERSION):
264+
self.assertIsNone(ctypes.util.find_msvcrt())
265+
self.assertIsNone(find_library('c'))
266+
self.assertIsNone(find_library('m'))
267+
268+
@thread_unsafe('patches sys.version')
269+
def test_get_build_version_msvc(self):
270+
with unittest.mock.patch.object(sys, 'version', self.MSVC_VERSION):
271+
self.assertEqual(ctypes.util._get_build_version(), 14.4)
272+
# Recent versions of the CRT are not directly loadable.
273+
self.assertIsNone(ctypes.util.find_msvcrt())
274+
275+
224276
if __name__ == "__main__":
225277
unittest.main()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Fix :func:`ctypes.util.find_msvcrt` on Windows builds whose :data:`sys.version`
2+
does not contain the ``MSC v.`` marker, which happens on ``clang-cl`` builds
3+
because the compiler banner is truncated to 80 characters. Instead of assuming
4+
MSVC 6 and returning ``msvcrt.dll`` -- a C runtime that does not share its
5+
``errno`` with the one the :mod:`!_ctypes` extension module is linked against
6+
-- the compiler version is now reported as unknown, so
7+
:func:`!ctypes.util.find_msvcrt` and ``find_library('c')`` return ``None``,
8+
matching builds made with modern versions of MSVC.

0 commit comments

Comments
 (0)