Compilers aren't supposed to crash — especially not when compiling perfectly valid code like this: // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.25; contract A { function a () public pure returns ( uint256 ) { return 1 ** 2 ; } } Yet running Solidity's compiler (solc) on this file on a standard Ubuntu 22.04 system (G++ 11.4, Boost 1.74) causes an immediate segmentation fault. At first, this seemed absurd. The code just returns 1 to the power of 2 — no memory tricks, unsafe casting, or undefined behavior. And yet, it crashes. Another minimal example? // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.25; contract A { function a () public pure { uint256 [ 1 ] data; } } Still crashes. So what’s going on? We traced it down to a seemingly unrelated C++ line deep in the compiler backend: if ( * lengthValue == 0 ) { ... } That single comparison — a boost::rational compared to 0 — causes infinite recursion in G++ < 14 when compiled under C++20. And the resulting stack overflow crashes solc. This post unpacks how this happened — and why none of the individual components are technically "broken": A 12-year-old overload resolution bug in G++ An outdated symmetric comparison pattern in Boost A subtle but impactful rewrite rule in C++20 Put together, they form a perfect storm — one that takes down Solidity compilation on default Linux setups, even though your code is perfectly fine. If you follow the Solidity build documentation (v0.8.30), you'll see it recommends: Boost ≥ 1.67 GCC ≥ 11 Ubuntu 22.04, for example, ships with: G++ 11.4.0 Boost 1.74.0 So far, so good. However, Solidity enabled C++20 in January 2025: This wasn't accompanied by an update to the versions of dependencies in the documentation. As we'll soon see, that's what opened the trapdoor. In C++, when you write an expression like a == b , the compiler chooses among available operator== implementations by comparing their match quality. A member function like a.operator==(b) usually has higher priority than a non-member function like operator==(a, b) — unless the types differ too much or are ambiguous. That’s the rule. But G++ didn’t always follow it. In 2012, a bug was filed: GCC Bug 53499 – overload resolution favors non-member function. The issue? In expressions where: A class rational has a templated operator== member function has a templated member function There's also a more generic free operator==(rational, U) function Clang correctly chooses the member function. G++ (before v14) chooses the non-member function. Why? Because G++ mishandles templated conversion + non-exact match, overvaluing a non-member function with worse match quality. It does not correctly apply the overload resolution ranking rules defined in CWG532: Member/nonmember operator template partial ordering. Let’s see this in action: #include template < typename IntType > class rational { public: template < class T > bool operator ==( const T & i ) const { std::cout << "clang++ resolved member" << std::endl; return true ; } }; template < class Arg , class IntType > bool operator ==( const rational < IntType > & a , const Arg & b ) { std::cout << "g++ <14 resolved non-member" << std::endl; return false ; } int main () { rational < int > r; return r == 0 ; } Compile with g++<14: g++ -std=c++17 main.cpp -o test && ./test Output (on g++ 11.4): g++ <14 resolved non-member Output (on g++ 11.4): Compile with clang++: clang++ -std=c++17 main.cpp -o test && ./test Output: clang++ resolved member In short, the wrong function gets picked. G++ was broken here until v14. C++20 introduced the spaceship operator <=> and defaulted comparison rewrites. When you define a two-argument operator== , C++20 may implicitly define the "reversed" version: If you define: bool operator==(T1, T2); Then T2 == T1 may call the same function by reversing the arguments. This rewrite is recursive: a == b becomes b == a , which becomes a == b again, and so on — if not handled carefully. This is great for reducing boilerplate — unless the call becomes ambiguous or self-referential. The old Boost rational class (prior to v1.75) defined both member function and non-member function of operator== : template < class Arg , class IntType > template < typename IntType > class rational { ... public: ... template < class T > BOOST_CONSTEXPR typename boost::enable_if_c:: value , bool >:: type operator == ( const T & i ) const { return ((den == IntType ( 1 )) && (num == i)); } ... } template < class Arg , class IntType > BOOST_CONSTEXPR inline typename boost:: enable_if_c < rational_detail::is_compatible_integer< Arg , IntType >::value, bool > ::type operator == ( const Arg & b , const rational < IntType > & a ) { return a == b; } This was designed under C++17 semantics. Back then, rhs == lhs would fall back to member overloads if available. All good. But under C++20 with G++ < 14 : G++ incorrectly chooses this non-member operator first C++20 reverses the comparison Which calls the same function again with arguments flipped And so on... This creates infinite recursion. A minimal example: // g++ -std=c++20 -o crash main.cpp && ./crash #include int main () { boost::rational < int > r; return r == 0 ; } Expected output: nothing. Actual: segmentation fault (stack overflow). This exact pattern was reported and fixed in Boost rational, but only in version 1.75+. Here’s the one-line fix: template BOOST_CONSTEXPR inline typename boost::enable_if_c < rational_detail::is_compatible_integer::value, bool>::type operator == (const Arg& b, const rational& a) { - return a == b; + return a.operator==(b); } Instead of calling a == b — which triggers overload resolution again — the patched version directly calls the member function operator== . This prevents C++20 from triggering recursive rewrites. The Solidity codebase uses boost::rational to represent certain compile-time constant expressions. One snippet that can trigger this issue appears in DeclarationTypeChecker::endVisit : if (Expression const * length = _typeName . length ()) { std::optional < rational > lengthValue; if ( length -> annotation (). type && length -> annotation (). type -> category () == Type::Category::RationalNumber) ... else if (std::optional < ConstantEvaluator::TypedRational > value = ConstantEvaluator:: evaluate (...)) lengthValue = value -> value ; if ( ! lengthValue) ... else if ( * lengthValue == 0 ) // <-- Infinite recursion happens here ... } Under normal circumstances, this expression is benign. But: G++ < 14 wrongly prefers Boost's non-member operator C++20 reverses the arguments The non-member operator recursively calls itself 💥: segmentation fault. If a system uses any of the following: G++ < 14 (e.g., Ubuntu 22.04 uses 11.4) Boost < 1.75 (e.g., 1.74 ships with Ubuntu) C++20 enabled (default in recent Solidity builds) They will encounter this crash as soon as it processes a Solidity source with a length expression like T[0] or anything involving compile-time rational comparisons. Update Boost to ≥ 1.75 Pin G++ to v14 or later This isn’t a security vulnerability. It doesn’t corrupt memory or allow code execution. But it is a reminder of the fragility of modern build stacks. A bug introduced in 2012, fixed in 2024, quietly broke one of the most used blockchain compiler toolchains — all without any code in the Solidity repo being “wrong.” Every layer here — Boost, G++, the C++20 spec, and Solidity — behaved “as documented.” But together, they composed into undefined behavior. The lesson? Always test critical software under multiple compilers and library versions — especially when enabling a new language standard.