Aug 11, 2025

Compiler Bug Causes Compiler Bug: How a 12-Year-Old G++ Bug Took Down Solidity

A subtle G++ bug from 2012, C++20's new comparison rules, and legacy Boost code can collide to crash Solidity's compiler on valid code. We unpack the surprising chain reaction and how to fix it.

Heading image of Compiler Bug Causes Compiler Bug: How a 12-Year-Old G++ Bug Took Down Solidity

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.


Background: The Setup

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:

Enable C++20 in Solidity

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.


Part I: A 12-Year-Old G++ Bug in Overload Resolution

What’s Overload Resolution?

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.

The Bug

In 2012, a bug was filed: GCC Bug 53499 – overload resolution favors non-member function. The issue? In expressions where:

  • A class rational<T> has a templated operator== member function
  • There's also a more generic free operator==(rational<T>, 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.

A Minimal Reproducer

Let’s see this in action:

#include <iostream>

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
    
  • 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.


Part II: C++20’s Symmetric Comparison Feature

What Changed in C++20?

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.


Part III: The Boost Trapdoor

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<rational_detail::is_compatible_integer<T, IntType>::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 <boost/rational.hpp>

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 <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;
+     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.


Part IV: How This Breaks Solidity

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.


Part V: What Environments are Affected?

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.


Recommendations

  • Update Boost to ≥ 1.75
  • Pin G++ to v14 or later

Conclusion

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.

Subscribe to our blogs