编译器错误导致编译器错误:一个12年前的G++错误如何导致Solidity崩溃
Compiler Bug Causes Compiler Bug: How a 12-Year-Old G++ Bug Took Down Solidity

原始链接: https://osec.io/blog/2025-08-11-compiler-bug-causes-compiler-bug/

## Solidity 编译器崩溃:完美风暴 一个看似有效的 Solidity 合约——甚至只是简单地返回 12——都可能在默认的 Linux 设置(如 Ubuntu 22.04)上导致 Solidity 编译器 (solc) 崩溃。根本原因并非 Solidity 的错误,而是 G++(版本 <14)中一个 12 年前的错误、Boost 库中过时的模式以及 C++20 中引入的一个微妙的重写规则之间的复杂交互。 具体来说,涉及 `boost::rational` 和 0 的比较在用 C++20 编译时会导致 G++<14 中的无限递归。这是因为 G++ 错误地优先考虑了非成员 `operator==`,而不是成员函数,而 C++20 的参数反转功能随后创建了一个递归循环。 Solidity 在 2024 年初启用了 C++20,但未在文档中更新依赖项版本,从而暴露了此问题。解决方法包括将 Boost 更新到 1.75 或更高版本(直接调用成员函数),或使用 G++ 14 或更高版本。这凸显了现代构建堆栈的脆弱性,以及在采用新的语言标准时进行全面测试(跨编译器和库版本)的重要性。

## Solidity 与 12 年 G++ 漏洞 一篇最近的博文详细描述了一个 12 年前的 G++ 编译器漏洞如何险些导致 Solidity 崩溃,Solidity 是以太坊智能合约的语言。该漏洞源于 Boost 库内的复杂 C++ 模板元编程,导致编译过程中出现无限递归。虽然该漏洞本身不会产生*错误*代码,但可能被利用来向编译器进程注入恶意代码——类似于 XZ 后门。 该事件凸显了依赖复杂代码库的风险,即使对于看似无关的组件也是如此。评论员们争论了 C++ 日益增长的复杂性,并质疑在关键的、基于合约的系统(如智能合约)中使用编译器是否明智,因为这些系统中的错误可能导致严重的财务后果。另一些人指出,智能合约本质上容易受到漏洞攻击,因为代码本身 inherent 的不可靠性。 讨论还涉及在多个编译器和库版本上进行彻底测试的重要性,以及在 C++ 等不断发展的语言中维护向后兼容性的挑战。最终,该事件作为一个警示故事,提醒人们复杂软件生态系统中隐藏的漏洞。
相关文章

原文

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:

Ubuntu 22.04, for example, ships with:

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.


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.


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.


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.


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.

联系我们 contact @ memedata.com