Solidity 编译器在特定环境下 (G++ < 14, Boost < 1.75, 启用 C++20) 编译包含长度表达式的 Solidity 代码时会因编译器缺陷而崩溃。这是由于 G++ 的一个长期存在的重载解析bug,加上 C++20 的对称比较特性以及旧版本的 Boost 库中的代码共同作用导致的无限递归问题。
一个来自 2012 年的微妙的 G++ 漏洞、C++20 的新比较规则和遗留的 Boost 代码可能会冲突,从而导致 Solidity 的编译器在有效的代码上崩溃。我们将揭示这个令人惊讶的连锁反应以及如何修复它。
编译器不应该崩溃 —— 尤其是在编译像这样完全有效的代码时:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
contract A {
function a() public pure returns (uint256) {
return 1 ** 2;
}
}
然而,在标准的 Ubuntu 22.04 系统(G++ 11.4,Boost 1.74)上对此文件运行 Solidity 的编译器 (solc) 会导致立即的段错误。
起初,这似乎很荒谬。这段代码只是返回 1 的 2 次方 —— 没有内存技巧、不安全的强制转换或未定义的行为。
然而,它崩溃了。
另一个最小的例子?
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
contract A {
function a() public pure {
uint256[1] data;
}
}
仍然崩溃。
那么发生了什么?
我们将其追溯到编译器后端中一段看似无关的 C++ 代码行:
if (*lengthValue == 0) { ... }
当在 C++20 下编译时,这个简单的比较 —— 将一个 boost::rational
与 0 进行比较 —— 会导致 G++ < 14 中的无限递归。 并且由此产生的堆栈溢出导致 solc 崩溃。
这篇文章将揭示这是如何发生的 —— 以及为什么没有一个单独的组件在技术上是“损坏的”:
组合在一起,它们形成了一场完美的风暴 —— 即使你的代码完全没问题,也会导致默认 Linux 设置上的 Solidity 编译失败。
如果你遵循 Solidity 构建文档 (v0.8.30),你会看到它建议:
例如,Ubuntu 22.04 附带:
到目前为止,一切都很好。
然而,Solidity 在 2025 年 1 月启用了 C++20:
这并没有伴随文档中依赖项版本的更新。正如我们很快将看到的,这就是打开陷阱的原因。
在 C++ 中,当你编写像 a == b
这样的表达式时,编译器会通过比较它们的匹配质量来选择可用的 operator==
实现。像 a.operator==(b)
这样的成员函数通常比像 operator==(a, b)
这样的非成员函数具有更高的优先级 —— 除非类型差异太大或存在歧义。
这就是规则。但是 G++ 并不总是遵循它。
在 2012 年,提交了一个漏洞:GCC 漏洞 53499 – 重载解析偏向于非成员函数。 问题是什么?在以下表达式中:
rational<T>
具有模板化的 operator==
成员函数operator==(rational<T>, U)
函数Clang 正确地选择了成员函数。
G++(v14 之前)选择了非成员函数。
为什么?因为 G++ 错误地处理了模板化转换 + 非精确匹配,高估了匹配质量较差的非成员函数。它没有正确应用 CWG532: 成员/非成员运算符模板偏序 中定义的重载解析排序规则。
让我们看看实际效果:
#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;
}
g++ -std=c++17 main.cpp -o test && ./test
输出(在 g++ 11.4 上):
g++ <14 resolved non-member
clang++ -std=c++17 main.cpp -o test && ./test
输出:
clang++ resolved member
简而言之,选择了错误的函数。G++ 在 v14 之前在这里被破坏了。
C++20 引入了 宇宙飞船运算符 <=>
和默认的比较重写。
当你定义一个双参数的 operator==
时,C++20 可能会隐式地定义“反向”版本:
bool operator==(T1, T2);
T2 == T1
可能会通过反转参数来调用同一个函数。此重写是递归的:a == b
变成 b == a
,然后再次变成 a == b
,依此类推 —— 如果不小心处理。
这非常适合减少样板代码 —— 除非调用变得模糊或自引用。
旧的 Boost rational
类(在 v1.75 之前)定义了 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;
}
这是在 C++17 的语义下设计的。那时,如果可用,rhs == lhs
会回退到成员重载。一切都很好。
但是在 C++20
和 G++ < 14
下:
这会产生无限递归。
一个最小的例子:
// g++ -std=c++20 -o crash main.cpp && ./crash
#include <boost/rational.hpp>
int main() {
boost::rational<int> r;
return r == 0;
}
预期输出:无。
实际输出:段错误(堆栈溢出)。
这个确切的模式已在 Boost rational 中报告并修复,但仅在 1.75+ 版本中。
这是一行修复:
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);
}
已修补的版本不是调用 a == b
—— 这会再次触发重载解析 —— 而是直接调用成员函数 operator==
。
这可以防止 C++20 触发递归重写。
Solidity 代码库使用 boost::rational
来表示某些编译时常量表达式。
一个可以触发此问题的代码片段出现在 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) // <-- 无限递归发生在这里
...
}
在正常情况下,此表达式是良性的。但是:
💥:段错误。
如果系统使用以下任何一项:
只要它处理具有长度表达式(如 T[0]
)或涉及编译时有理数比较的任何内容的 Solidity 源代码,它们就会遇到此崩溃。
将 Boost 更新到 ≥ 1.75
将 G++ 锁定到 v14 或更高版本
这不是一个安全漏洞。它不会破坏内存或允许代码执行。
但它是对现代构建堆栈脆弱性的一种提醒。一个在 2012 年引入,在 2024 年修复的漏洞,悄悄地破坏了最常用的区块链编译器工具链之一 —— 所有这些都没有 Solidity 存储库中的任何代码是“错误的”。
这里的每一层 —— Boost、G++、C++20 规范和 Solidity —— 都“按文档”运行。但在一起,它们组成了未定义的行为。
教训是什么?始终在多个编译器和库版本下测试关键软件 —— 尤其是在启用新的语言标准时。
- 原文链接: osec.io/blog/2025-08-11-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!