Skip to content

ctypes.util.find_msvcrt() returns "msvcrt.dll" on clang-cl Windows builds, breaking ctypes.get_errno() #154199

Description

@SynaptSea

Bug description:

On a Windows CPython built with clang-cl (build.bat "/p:PlatformToolset=ClangCL", documented in PCbuild/readme.txt; gh-77532), ctypes.util.find_msvcrt() returns 'msvcrt.dll' instead of None. The cause is sys.version truncation: Python/getversion.c caps the compiler segment at 80 chars (%.80s), which cuts off the "MSC v." marker that ctypes.util._get_build_version() sniffs for, so it falls back to "assume MSVC 6". The practical impact: CDLL(find_library("c"), use_errno=True) loads the OS-private legacy msvcrt.dll, whose errno is a different variable from the ucrt errno that _ctypes saves/restores around foreign calls — ctypes.get_errno() returns stale values (typically 0) with no exception raised.

Observable symptom: test.test_ctypes.test_errno.Test.test_open fails on clang-cl builds, while on MSVC builds find_msvcrt() returns None and the same test silently skips (skipTest("Unable to find C library")), which is why this has gone unnoticed.

import sys, errno
from ctypes import CDLL, c_char_p, c_int, get_errno
from ctypes.util import find_library, find_msvcrt

print(sys.version)
print(find_msvcrt())          # 'msvcrt.dll'  — expected None, as on MSVC builds
libc = CDLL(find_library("c"), use_errno=True)
libc._open.argtypes = c_char_p, c_int
print(libc._open(b"", 0))     # -1 (call fails, as intended)
print(get_errno())            # 0  — expected errno.ENOENT == 2

Observed on a clang-cl build of main:

3.16.0a0 free-threading build (heads/ft-opt-dirty:2f032556f42, Jul 16 2026, 11:20:12) [Clang 22.1.8 (https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174d
msvcrt.dll
-1
0
$ python -m unittest -v test.test_ctypes.test_errno
...
FAIL: test_open (test.test_ctypes.test_errno.Test.test_open)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "...\Lib\test\test_ctypes\test_errno.py", line 25, in test_open
    self.assertEqual(get_errno(), errno.ENOENT)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 0 != 2

The same build additionally shows both CRTs loaded after the call (ctypes.util.dllist() lists ucrtbase.dll and msvcrt.dll), which is the divergence in one line.

Root cause chain (permalinks sha-pinned to main @ 16d0c89, 2026-07-18; _get_build_version/find_msvcrt/find_library are textually unchanged on today's main, merely shifted a few lines by later unrelated ctypes commits):

  1. PC/pyconfig.h#L161 builds the clang banner as "[Clang " __clang_version__ "] 64 bit (AMD64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]" (added in gh-77532: Minor tweaks to allow compiling with PlatformToolset=ClangCL on Windows #101352) — so the MSC v. marker is intended to be present.
  2. But Python/getversion.c#L18-L20 composes sys.version with "%.80s (%.80s) %.80s" (free-threaded builds use the "%.80s free-threading build (%.80s) %.80s" variant — same %.80s cap), truncating the compiler segment at 80 chars. Official LLVM Windows binaries have a long __clang_version__ (22.1.8 (https://github.com/llvm/llvm-project <40-hex-sha>) — 86 chars by itself, 93 with the [Clang prefix), so the tail carrying both markers is always truncated away. The full Py_GetCompiler() string on my build is 130 chars:
    [Clang 22.1.8 (https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174dcb5aa28b59d1)] 64 bit (AMD64) with MSC v.1951 CRT]
    
    while the sys.version compiler segment ends at ...ca7933e47d3a3451d81e72ac174d — 28 characters into the commit hash, without MSC v., (AMD64), or the closing bracket. (An MSVC banner is ~27 chars and is never truncated, which is why this is clang-specific. The same truncation already broke sysconfig.get_platform() on clang-cl builds — sysconfig.get_platform() could return wrong value because sys.version is truncated #145410, fixed in March 2026 by moving the check into C: gh-145410: Add _sysconfig.get_platform() function #146145.)
  3. Lib/ctypes/util.py#L10-L33: _get_build_version() sniffs sys.version for "MSC v." and, when absent, returns 6 ("assume the compiler is MSVC 6", a distutils-era assumption).
  4. Lib/ctypes/util.py#L35-L54: find_msvcrt() maps version <= 6 to 'msvcrt.dll', and find_library("c") returns that.
  5. msvcrt.dll maintains its own CRT state; the interpreter and _ctypes use the ucrt. The errno set by msvcrt.dll's _open is never seen by the use_errno machinery → get_errno() == 0.

Note that the 'msvcrt.dll' return is reachable only through this banner-sniff failure: when the marker survives (e.g. MSVC's [MSC v.1943 64 bit (AMD64)]), _get_build_version() yields 14.x and find_msvcrt() returns None — the code comment there cites issue23606, "CRT is no longer directly loadable" (gh-67794/bpo-23606). A clang-cl build links the same ucrt as the MSVC build, so 'msvcrt.dll' is simply the wrong answer for it. The MSVC-6 fallback would still be meaningful for a toolchain that genuinely links msvcrt.dll (classic MinGW), which is worth weighing when choosing between the options below.

Suggested fix directions (deferring to the maintainers):

  • Minimal: make _get_build_version() return None instead of 6 when "MSC v." is absent, so find_msvcrt()/find_library("c") behave exactly as on modern MSVC builds and test_errno skips consistently. This trades the VC6-era fallback away; if the MinGW case above is considered worth preserving, the next option handles both.
  • Alternative: make find_msvcrt() report the CRT the interpreter is actually linked against (ucrtbase.dll / the api-ms-win-crt-* stable ABI) independent of the compiler banner — correct for every toolchain, but it would reverse the bpo-23606 decision that modern MSVC returns None.
  • Longer-term: stop deriving build facts from sys.version string-sniffing altogether; the truncation shows the banner is not a reliable data channel (sysconfig.get_platform() could return wrong value because sys.version is truncated #145410 already moved sysconfig off it).

I'm happy to submit a PR for whichever direction a maintainer prefers, with a regression test that exercises _get_build_version() against a truncated clang-style banner.

Side observations I can file separately if useful: the %.80s cap leaves sys.version ending mid-commit-hash without its closing bracket on official-LLVM clang builds, and the clang _Py_COMPILER string literal has unbalanced brackets (one [, two ]).

Environment / build: CPython main @ f62050d (2026-07-16), built per PCbuild with build.bat -p x64 --disable-gil --pgo --tail-call-interp "/p:PlatformToolset=ClangCL", LLVM 22.1.8 official Windows release, Windows 11 x64. Also reproduced identically on an earlier (June 2026) clang-cl build, and I confirmed the contrasting skip behaviour on an MSVC build of the same source. My working branch carries only build-system tuning (PCbuild props/vcxproj plus a zlib-ng header) and build-regenerated files (hence the -dirty banner); the files involved here — Lib/ctypes/util.py, Lib/test/test_ctypes/test_errno.py, Python/getversion.c, PC/pyconfig.h — are unmodified from upstream. The failure is not related to free-threading — the mechanism only involves the compiler banner segment.

I'm aware clang-cl is PY_SUPPORT_TIER 0; filing since PCbuild documents and actively maintains the toolset (gh-77532, and it is still being kept working — e.g. gh-146210 in 2026, gh-131278/gh-130090/gh-131035 in 2025), the defect is in a stdlib banner-sniff rather than the toolchain, and the same latent defect affects any build whose banner lacks the marker.

Related: gh-77532 / GH-101352 (clang banner introduction), gh-67794 (bpo-23606, find_msvcrtNone for modern MSVC), gh-70914 (user report that find_msvcrt() returns None on post-VC6 builds; closed as a duplicate of bpo-23606), gh-145916 (soft-deprecation of ctypes.util.find_library — its linked PR touches only the docs, so it removes neither this defect nor the test failure), gh-145410 / GH-146145 (same truncation, sysconfig side). The same truncated banner also breaks the vendored get_platform() in Lib/test/test_importlib/test_windows.py; filed separately as gh-154200.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    OS-windowsstdlibStandard Library Python modules in the Lib/ directorytopic-ctypestype-bugAn unexpected behavior, bug, or error

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions