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.