Zend: Fix swapped arguments in MSVC InterlockedCompareExchange CAS#21759
Open
EdmondDantes wants to merge 1 commit intophp:masterfrom
Open
Zend: Fix swapped arguments in MSVC InterlockedCompareExchange CAS#21759EdmondDantes wants to merge 1 commit intophp:masterfrom
EdmondDantes wants to merge 1 commit intophp:masterfrom
Conversation
The MSVC fallback implementations of zend_atomic_bool_compare_exchange_ex and zend_atomic_int_compare_exchange_ex passed *expected as the Exchange argument and desired as the Comparand argument to InterlockedCompareExchange*. The WinAPI signature is InterlockedCompareExchange(Destination, Exchange, Comparand) — i.e. (new, old) — whereas the C11 / __atomic / __sync CAS intrinsics take (expected, desired) i.e. (old, new). The two arguments were therefore swapped: the function wrote the old value back on success and compared against the new value, so the CAS became a no-op in every case except the degenerate *expected == desired one (where it still reported success without having performed a real exchange). On x86/x64 with HAVE_C11_ATOMICS this code path is not compiled, which is why the bug has gone unnoticed. It is reachable on MSVC builds that do not define HAVE_C11_ATOMICS (e.g. older toolchains or the !C11 branch selected by the preprocessor), where any caller relying on CAS semantics — notably zend_atomic_int_fetch_add-style loops — silently fails to update the value. Swap Exchange and Comparand to match the documented WinAPI order.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The MSVC fallback implementations of
zend_atomic_bool_compare_exchange_exandzend_atomic_int_compare_exchange_exinZend/zend_atomic.hpass their arguments toInterlockedCompareExchange*in the wrong order.The WinAPI signature is:
i.e.
(new, old). Every other CAS intrinsic used in this file — C11__c11_atomic_compare_exchange_strong, GCC__atomic_compare_exchange, legacy__sync_val_compare_and_swap— takes(expected, desired)i.e.(old, new). The MSVC branch was written as if it followed the same convention, soExchangeandComparandended up swapped.Effect:
desired(the intended new value) instead of*expected.*expected(the intended old value) back into the destination — essentially a no-op.prevcomes back holding the destination's actual value, which is then compared against*expected:truewithout having performed any real exchange. Callers observe "success" but the store never happened.*expectedand returnsfalse, so CAS loops do eventually converge — but every iteration is wasted and the store on the final iteration is still the no-op above.In short: on this code path CAS silently degrades to "load, maybe report success, never store".
How it was found
While working on a downstream project I noticed
zend_atomic_int_fetch_add-style refcount updates silently failing on a Windows MSVC build. Tracing it back, every CAS iteration was reporting success without actually updating the value, which pointed at this function. Swapping the two arguments to match the WinAPI order fixes the refcount behaviour.Why it has gone unnoticed
This branch is selected by:
On modern MSVC + clang-cl builds
HAVE_C11_ATOMICSis defined and the__c11_atomic_*branch is used instead, which is correct. The buggy code is only reached on MSVC configurations that do not advertise C11 atomics support (older toolchains, or builds where the feature test fails for whatever reason). Callers that only usezend_atomic_*_load/store/exchangeare unaffected — those wrapInterlockedOr/InterlockedExchange, which only take one data argument and are correct. It's specifically thecompare_exchangehelpers (and therefore anything built on top of them, e.g. fetch-add loops) that are broken.The fix
Swap
ExchangeandComparandso the call matches the documented WinAPI contract:No behavioural change on any non-MSVC / C11-atomics build.
Suspicions / things worth a second look
A few things I'd like more eyes on, since I only have one MSVC toolchain in front of me:
!HAVE_C11_ATOMICSMSVC branch still reachable in supported builds? If every supported MSVC configuration now goes throughHAVE_C11_ATOMICS, this bug is latent — worth fixing anyway, but it would be good to know whether it actually manifests in CI or only in the wild. If the branch is dead code, it might be cleaner to delete it entirely in a follow-up.InterlockedCompareExchange*on x86/x64 is a full barrier, so seq-cst semantics are preserved. I haven't audited ARM64 MSVC — someone more familiar with that target should double-check there isn't an implicit assumption baked in here.zend_atomic_*_compare_exchange_exunder this specific#ifbranch, which is why this slipped through. Might be worth adding one; happy to do it in a follow-up if reviewers agree.boolround-trip throughInterlockedCompareExchange8.InterlockedCompareExchange8operates onchar. The cast(bool) InterlockedCompareExchange8(...)is fine for the return, but the*expected/desiredarguments are implicitly widened frombool(0/1) tochar, which is well-defined. Just flagging it for completeness.Test plan
HAVE_C11_ATOMICSdefined — should be a no-op (branch not compiled).HAVE_C11_ATOMICS, if such a configuration still exists.