Skip to content

fix(build): work around MSVC std::mutex ABI mismatch on Windows wheels#279

Merged
XuehaiPan merged 3 commits intometaopt:mainfrom
XuehaiPan:fix-msvcp140-abi-mismatch
May 5, 2026
Merged

fix(build): work around MSVC std::mutex ABI mismatch on Windows wheels#279
XuehaiPan merged 3 commits intometaopt:mainfrom
XuehaiPan:fix-msvcp140-abi-mismatch

Conversation

@XuehaiPan
Copy link
Copy Markdown
Member

@XuehaiPan XuehaiPan commented May 4, 2026

Description

Define Microsoft's documented _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR macro for the _C target on MSVC builds so that std::mutex's constructor and _Mtx_lock body bind to the same loaded msvcp140.dll regardless of whether another wheel preloaded an older copy of the MSVC C++ runtime.

This is a build-system-only change scoped to the _C target on MSVC; no source changes, and no effect on non-Windows wheels or on Windows toolchains other than MSVC (e.g., clang-cl + libc++, mingw-w64).

Motivation and Context

  • I have raised an issue to propose this change (required for new features and bug fixes)

Fixes #278.

The user-reported crash is a null-vptr dereference at msvcp140!mtx_do_lock+0x74, fired during optree._C's PyInit__C when imported after pyarrow. Full forensic trail (cdb dump, import-table inspection, address-range arithmetic) is in #278; the short version:

  • optree 0.19.0's Windows wheel is built with newer MSVC STL headers (VS 2022 17.10+) where std::mutex has a constexpr zero-init layout and the constructor emits no runtime init call. The wheel's import table reflects this: it imports only _Mtx_lock and _Mtx_unlock from msvcp140.dll, not _Mtx_init_in_situ.
  • pyarrow ships its own msvcp140.dll 14.28.29334.0 (Nov 2020) under the unmangled name. When pyarrow is imported first, the Windows loader caches that copy under the name msvcp140.dll. optree's _C.pyd, loaded later, binds its msvcp140.dll!_Mtx_lock IAT entry to pyarrow's old copy (confirmed: crash address 0x7ff8df0c2eb0 falls inside pyarrow's msvcp140.dll image range, and it's the only copy in the dump with PDB symbols loaded).
  • The old _Mtx_lock interprets the new mutex layout as the old ConcRT-backed object, virtual-dispatches through what it expects to be a critical-section vptr at mtx + 8, finds null, and dies.
  • The crash fires inside pybind11's internals_pp_manager::create_pp_content_once() (locking pp_set_mutex_), called from pybind11::detail::ensure_internals() at the top of PYBIND11_MODULE_PYINIT — before any optree code in BuildModule runs. So the crash cannot be avoided by deferring optree-side initialization.

_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR is Microsoft's documented escape hatch for newer-toolset / older-redistributable mismatches of exactly this shape (MS STL changelog). With it set, std::mutex's constructor reverts to a non-constexpr body that calls _Mtx_init_in_situ at runtime, so both the constructor and _Mtx_lock necessarily bind to the same loaded msvcp140.dll — the layout written by the constructor matches the layout read by the lock.

Verification

Build the Windows wheel with this change and confirm the import table now includes _Mtx_init_in_situ:

llvm-objdump -p optree/_C.cp312-win_amd64.pyd | rg '_Mtx_(init|destroy|lock|unlock)'

Expected: _Mtx_init_in_situ appears alongside _Mtx_lock and _Mtx_unlock. (_Mtx_destroy_in_situ will not appear: _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR only reverts the constructor; MSVC STL keeps ~mutex() as = default regardless. That's fine here because pp_set_mutex_ lives in a Meyers singleton and is never destructed in practice.) If only _Mtx_lock / _Mtx_unlock appear, the macro did not take effect.

End-to-end repro (run on the reporter's environment, or any Windows env with pyarrow + torch):

python -X dev -X faulthandler -c "import pyarrow, torch; import optree"

Expected before this PR: access violation. Expected after: clean import.

Tradeoffs

  • Per-mutex construction cost: each std::mutex ctor now does a cross-DLL _Mtx_init_in_situ call instead of being a constexpr no-op. optree constructs a handful of mutexes total over a process's lifetime; the cost is unmeasurable.
  • Disables constinit std::mutex patterns inside the _C target. None of optree's existing PYBIND11_CONSTINIT uses depend on this — they go through pybind11's gil_safe_call_once_and_store and std::once_flag, neither of which is a constinit std::mutex.

Removal Criterion

Documented in the inline CMake comment: remove once optree no longer ships wheels that may be loaded alongside an msvcp140.dll older than the toolset used to build the wheel.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds core functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation (update in the documentation)

Checklist

  • I have read the CONTRIBUTION guide. (required)
  • My change requires a change to the documentation.
  • I have updated the tests accordingly. (required for a bug fix or a new feature)
  • I have updated the documentation accordingly.
  • I have reformatted the code using make format. (required)
  • I have checked the code using make lint. (required)
  • I have ensured make test pass. (required)

@XuehaiPan XuehaiPan self-assigned this May 4, 2026
@XuehaiPan XuehaiPan added bug Something isn't working cxx Something related to the CXX source code labels May 4, 2026
@XuehaiPan XuehaiPan changed the title fix(build): work around MSVC std::mutex ABI mismatch on Windows wheels fix(build): work around MSVC std::mutex ABI mismatch on Windows wheels May 4, 2026
@XuehaiPan XuehaiPan force-pushed the fix-msvcp140-abi-mismatch branch from ff43af3 to 866a308 Compare May 4, 2026 13:11
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (7d80015) to head (d53e641).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #279   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           15        15           
  Lines         1565      1565           
  Branches       207       207           
=========================================
  Hits          1565      1565           
Flag Coverage Δ
unittests 100.00% <ø> (ø)
unittests-cp310-Linux 100.00% <ø> (ø)
unittests-cp310-Windows 100.00% <ø> (ø)
unittests-cp310-macOS 100.00% <ø> (ø)
unittests-cp311-Linux 100.00% <ø> (ø)
unittests-cp311-Windows 100.00% <ø> (ø)
unittests-cp311-macOS 100.00% <ø> (ø)
unittests-cp312-Linux 100.00% <ø> (ø)
unittests-cp312-Windows 100.00% <ø> (ø)
unittests-cp312-macOS 100.00% <ø> (ø)
unittests-cp313-Linux 100.00% <ø> (ø)
unittests-cp313-Windows 100.00% <ø> (ø)
unittests-cp313-macOS 100.00% <ø> (ø)
unittests-cp313t-Linux 100.00% <ø> (ø)
unittests-cp313t-Windows 100.00% <ø> (ø)
unittests-cp313t-macOS 100.00% <ø> (ø)
unittests-cp314-Linux 100.00% <ø> (ø)
unittests-cp314-Windows 100.00% <ø> (ø)
unittests-cp314-macOS 100.00% <ø> (ø)
unittests-cp314t-Linux 100.00% <ø> (ø)
unittests-cp314t-Windows 100.00% <ø> (ø)
unittests-cp314t-macOS 100.00% <ø> (ø)
unittests-cp39-Linux 100.00% <ø> (ø)
unittests-cp39-Windows 100.00% <ø> (ø)
unittests-cp39-macOS 100.00% <ø> (ø)
unittests-pp311-Linux 100.00% <ø> (ø)
unittests-pp311-Windows 100.00% <ø> (ø)
unittests-pp311-macOS 100.00% <ø> (ø)
unittests-pydebug 100.00% <ø> (ø)
unittests-pydebug-cp310d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp310d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp310d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp311d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp311d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp311d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp312d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp312d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp312d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp313d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp313d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp313d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp313td-Linux 100.00% <ø> (ø)
unittests-pydebug-cp313td-Windows 100.00% <ø> (ø)
unittests-pydebug-cp313td-macOS 100.00% <ø> (ø)
unittests-pydebug-cp314d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp314d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp314d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp314td-Linux 100.00% <ø> (ø)
unittests-pydebug-cp314td-Windows 100.00% <ø> (ø)
unittests-pydebug-cp314td-macOS 100.00% <ø> (ø)
unittests-pydebug-cp39d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp39d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp39d-macOS 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

XuehaiPan added 2 commits May 5, 2026 02:12
…GLIBCXX_USE_CXX11_ABI` style

Move the macro definition from a per-target `target_compile_definitions` in
`src/CMakeLists.txt` to a project-wide `add_definitions` in the root
`CMakeLists.txt`, parallel to `_GLIBCXX_USE_CXX11_ABI`. Add the macro to CI
workflow env, the cibuildwheel `environment-pass` list, and the Makefile
defaults so the same name flows through every input channel.

Also gain a documented disable channel: setting either
`_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR=` (empty env var) or
`cmake -D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR=` skips the `add_definitions`
call entirely, leaving the macro undefined so MSVC's `#ifdef` resolves to
false and the constexpr mutex layout is used. The dispatcher uses
`DEFINED ENV{X} OR DEFINED X` so both input channels behave symmetrically.
Useful for reproducing issue metaopt#278 once upstream pybind11 hardens
`internals_pp_manager::pp_set_mutex_`.

Switch the `add_definitions` form to bare `-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR`
(no `=value`) since MSVC's check is `#ifdef`, not `#if`. The status message
similarly reports just `set` or `not set` rather than echoing a value that
has no effect on behavior.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread CMakeLists.txt Outdated
Comment thread CMakeLists.txt
Comment thread CMakeLists.txt
Comment thread README.md
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread CMakeLists.txt
Comment thread CMakeLists.txt
Comment thread optree/_C.pyi
@XuehaiPan XuehaiPan merged commit 8cf3114 into metaopt:main May 5, 2026
163 of 468 checks passed
@XuehaiPan XuehaiPan deleted the fix-msvcp140-abi-mismatch branch May 5, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cxx Something related to the CXX source code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] optree 0.19.0 crashes on Windows

2 participants