The Euclidean Remainder Fallacy
How MSVC's /O1–/O2 optimizer confused C++ truncated modulo with math-class modulo — and how Parasoft C/C++test would have caught it before a single optimized binary was built.
How MSVC's /O1–/O2 optimizer confused C++ truncated modulo with math-class modulo — and how Parasoft C/C++test would have caught it before a single optimized binary was built.
Bug
MSVC /O1–/O2 miscompiles 6U < (v % 5) when v is negative
Regression
MSVC v19.51
Reporter
Ofek Shilon
#include <cstdio> int test(int v) { return 6U < (v % 5) ? 1111 : 2; } int main() { int res = test(-1); printf("%d\n", res); return 0; }
→ prints 2
Optimizer assumes v % 5 ∈ [0, 4], concludes 6U < [0,4] is always false, constant-folds to 2.
→ prints 1111
-1 % 5 == -1 (C++ truncated modulo). -1 promotes to UINT_MAX. 6U < UINT_MAX → true.
The optimizer's range analysis assigned v % 5 the wrong range.
| Property | Euclidean Modulo (what optimizer assumed) | C++ Truncated Modulo (what standard requires) |
|---|---|---|
| Range of v % 5 | [0, 4] (wrong) | [-4, 4] (right) |
| -1 % 5 | 4 (wrong) | -1 (right) |
| Sign of remainder | Always non-negative (wrong) | Follows dividend (right) |
| Standard reference | Math class (wrong) | [expr.mul]/4 (right) |
| Effect on 6U < (v % 5) | “Always false” → fold to 2 | Must evaluate at runtime |
When the signed remainder -1 meets the unsigned literal 6U, the usual arithmetic conversions promote it to unsigned int → UINT_MAX (4,294,967,295). The comparison 6U < UINT_MAX is trivially true. But the optimizer never gets there — it already folded the expression using the wrong range.
“MSVC's optimizer discovered Euclidean modulo and decided the C++ standard was simply wrong. -1 % 5 is -1 in C++, not 4 — and once that -1 meets 6U, it becomes UINT_MAX, making the comparison trivially true. The optimizer ‘helpfully’ replaced truncated modulo with math-class modulo, concluded 6U < [0,4] is always false, and folded to 2. A textbook example of why ‘obviously always false’ is the most dangerous phrase in compiler optimization.”
#pragma optimize("", off) int test(int v) { return 6U < (v % 5) ? 1111 : 2; } #pragma optimize("", on)
Kills the bug without changing semantics or affecting the rest of the translation unit.
int test(int v) { int rem = v % 5; unsigned urem = (unsigned)rem; return 6U < urem ? 1111 : 2; }
Makes the signed→unsigned conversion visible so the optimizer can't skip it.
int test(int v) { unsigned uv = (unsigned)v; return 6U < (uv % 5u) ? 1111 : 2; }
⚠️ This changes the result: UINT_MAX % 5 == 0, not -1. Only use if you genuinely want unsigned modular arithmetic.
Guaranteed correct, but disables all optimizations for the file. Use only if the bug is widespread in a single translation unit.
Yes — through two independent mechanisms that form a closed loop.
Flags the signed/unsigned comparison pattern before the code ever reaches the optimizer. The exact coding error the optimizer made is literally what CERT-C and MISRA-C rules exist to prevent.
A C++test unit test with a negative-input boundary case passes under /Od and fails under /O2 — catching the miscompilation at CI build time, not in production.
This rule exists precisely to catch the assumption that v % d ∈ [0, d-1]. This is literally the exact error the MSVC optimizer made. The static analysis tool designed to catch programmer errors would have caught the compiler's error — because the optimizer fell for the same fallacy the rule exists to prevent.
Flags implicit signed→unsigned conversions — the -1 → UINT_MAX promotion the optimizer missed.
The broader CWE category covering the signed→unsigned implicit promotion chain.
If Parasoft C/C++test had been run on this code before the /O1 build: INT10-C would have flagged 6U < (v % 5), a developer would have reviewed the finding and either made the conversion explicit, cast v to unsigned first, or added a runtime guard — preventing the vulnerable code from ever reaching the miscompiling optimizer.
| Rule | Category | What It Flags | Relevance |
|---|---|---|---|
| Rule 10.4 | Required | Both operands of an operator should not be of different essential type categories | 6U (unsigned) vs v % 5 (signed) — mixed-type comparison |
| Rule 10.6 | Required | Value of unsigned expression should not be implicitly converted to greater width | The promotion chain that turns -1 into UINT_MAX |
| Rule 12.4 | Advisory | Comparison should not be made against a constant outside the operand's range | 6U constant vs v % 5 signed range |
Three MISRA-C findings on a single expression. In any MISRA-compliant project, this code would never have reached code review — let alone the optimizer. The signed/unsigned mismatch is flagged at the essential-type level, before any optimization pass runs.
TEST(ModuloSuite, NegativeInput) { // -1 % 5 == -1 (C++ truncated) // -1 → UINT_MAX on unsigned promotion // 6U < UINT_MAX → true → 1111 int result = test(-1); ASSERT_EQUAL(1111, result); }
Correct codegen: -1 % 5 = -1 → UINT_MAX → 6U < UINT_MAX = true → 1111
Miscompiled: optimizer folds to 2 instead of 1111
CERT-C INT10-C + MISRA-C 10.4 fire on 6U < (v % 5). Developer reviews finding, makes conversion explicit or adds guard.
The miscompiling optimizer runs, but the code has already been hardened by step 1.
test(-1) expects 1111. If the optimizer still miscompiles, the test FAILS and the build is blocked.
(Optional) Instrumented run catches unexpected unsigned wraparound at the comparison site.
The static analysis tool designed to catch programmer errors would have caught the compiler's error
because the optimizer made the exact same mistake that CERT-C INT10-C exists to prevent.
Assumed v % 5 ∈ [0, 4] — Euclidean range. Ignored negative remainders. Folded the comparison.
Assuming v % 5 ∈ [0, 4] — Euclidean range. Ignoring negative remainders. The exact same fallacy.
The Israeli Center for Static Code Analysis — Parasoft Distribution & Integration
in/Engineering-Software-Lab-ESL ↗Analysis & Presentation by
Daniel (Dani) Liezrowice in/liezrowice ↗CEO & Co-Founder, ESL — Engineering Software Lab