理解Rust和Solana智能合约中的整数溢出和下溢问题

  • Sec3dev
  • 发布于 2021-10-15 23:41
  • 阅读 37

本文探讨了Rust和Solana智能合约中的整数溢出和下溢问题,解析了这些问题的根源以及对区块链安全的影响,并讨论了如何在Solana智能合约中有效预防这些问题。文章强调Rust在调试模式和发布模式下的不同表现,可能导致对安全性的误解。

在智能合约中,整数溢出/下溢是相当普遍的,因为区块链应用程序经常在财务数据上进行数学计算。

Rust 是一种在 SolanaPolkadot 等区块链中使用的流行语言。对许多开发者来说,认为 Rust 是内存安全的,因此不存在算术溢出/下溢,这可能是一个误解。本文解释了为什么 Rust 程序仍然会遭受算术错误,这些问题如何影响区块链安全,以及如何在智能合约中处理它们。

为什么本质上会出现整数溢出/下溢

就像家里每一件家具都占用空间一样,在计算机程序中,每一条数据也需要一个存储其值的空间。这个空间是有限的。如果某个数据的值(例如账户的余额)在计算后超出了其空间的大小,就会发生溢出/下溢。

在 Rust 中,无符号整数可以有以下类型:u8、u16、u32、u64、u128 和 usize。类型表示用于存储整数的位数:u8 可以保存 0 到 255 之间的值,u16 可以保存 0 到 65535 之间的值,依此类推。例如,x = x + 1 — 如果一个 u8 整数 x 被更改为超出其范围的值,假设为 256 或 -1,那么就会发生溢出/下溢。

Rust 不会防止整数溢出/下溢

这有点令人惊讶 — Rust 在调试模式和发布模式下对整数溢出/下溢的行为不同。在调试模式下,Rust 会添加内置的溢出/下溢检查,并在运行时发生溢出/下溢时引发 panic。

然而,在发布(或优化)模式下,Rust 默认情况下 静默地忽略 这种行为,并计算 二补数包装(例如,255+1 对 u8 类型 返回 0_)。你可以通过 [这个链接](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&code=fn%20main()%20%7B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20let%20(x%2C%20y)%20%3D%20(2345%2C%206789)%3B%0A%0A%20%20%20%20%20%20%20%20let%20overflow%20%3D%20multiply_will_overflow(x%2C%20y)%3B%0A%0A%20%20%20%20%20%20%20%20println!(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%7B%7D%20*%20%7B%7D%20%7B%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20x%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20overflow%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22overflows%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22doesn%27t%20overflow%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20let%20(x%2C%20y)%20%3D%20(2345678%2C%209012345)%3B%0A%0A%20%20%20%20%20%20%20%20let%20overflow%20%3D%20multiply_will_overflow(x%2C%20y)%3B%0A%0A%20%20%20%20%20%20%20%20println!(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%7B%7D%20*%20%7B%7D%20%7B%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20x%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20overflow%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22overflows%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22doesn%27t%20overflow%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20let%20(x%2C%20y)%20%3D%20(2345678901%2C%209012345678)%3B%0A%0A%20%20%20%20%20%20%20%20let%20overflow%20%3D%20multiply_will_overflow(x%2C%20y)%3B%0A%0A%20%20%20%20%20%20%20%20println!(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%7B%7D%20*%20%7B%7D%20%7B%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20x%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20overflow%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22overflows%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22doesn%27t%20overflow%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D%0A%7D%0A%0Afn%20multiply_will_overflow(x%3A%20i64%2C%20y%3A%20i64)%20-%3E%20bool%20%7B%0A%20%20%20%20x.checked_mul(y).is_none()%0A%7D%0A) .

注意:Rust 中的溢出检查可以在发布模式下启用 https://doc.rust-lang.org/cargo/reference/profiles.html#overflow-checks

换句话说,在调试时看似 panic 的 Rust 溢出在生产环境中可能不复存在。

尽管 Rust 在发布模式下忽略溢出是有合理原因的(例如性能),但这种不一致的行为可能导致对 Rust 算术操作安全性的错觉。这对区块链和智能合约尤其危险

Solana 程序中的常见算术错误

用 Rust 编写的 Solana 智能合约以及 Solana 的核心运行时(验证器代码)都经历了相当多的算术错误。

以下是来自 Github 的部分列表(点击链接可查看拉取请求):

  • Solana 智能合约 溢出/下溢:12345
  • Solana 核心运行时 溢出/下溢:123456789

图 1. jet-v1 中的整数下溢/溢出修复 fixed in jet-v1

上面的例子显示了在 jet-v1 协议中 total_loan_notes -= note_amount(第 245 行) 的整数下溢和 total_deposit += token_amount(第 246 行) 的溢出。这两个变量是 u64 类型。修复方法很简单:将 - 替换为 checked_sub,将 + 替换为 checked_add。这些将在下溢或溢出时返回 None,而不是包装运算。

图 2. Solana bpf loader 中的整数溢出修复 fixed in Solana bpf loader

上面的例子显示了在 Solana bpf loader 中发生的整数溢出。乘法 num_accounts size_of::<AccountMeta>() 和加法 + data_len 都可能导致溢出。修复方法是将 替换为 saturating_mul,将 + 替换为 saturating_add。这些将对数值在数值边界内进行饱和,而不是溢出。

图 3. Solana watchtower 中的算术错误修复 fixed in Solana watchtower

上面的例子展示了 Solana 中另一种类型的算术错误。两个整数之间的除法 / total_current_stake * 100 和 total_stake 可能会因截断小数而损失精度。修复该问题的方法是将这些变量的类型更改为浮点类型 f64。这样,结果 current_stake_percent 也具有 f64 类型的精度。

如何防止 Solana 智能合约中的算术错误

在 Rust 中编写 Solana 智能合约时,有三种常见方法来处理算术错误:

  1. *_用 checked_add、checked_sub、checked_mul 和 checked_div、checked_pow 替换 +、-、、/、** 分别。由于在 Solana 中这些检查的开销几乎可以忽略(与 以太坊的高Gas费 相比),因此始终进行这些检查可能是一个好主意。
  2. *_用 saturating_add、saturating_sub、saturating_mul 和 saturating_pow 替换 +、-、、/,_** 分别。这保证了计算出的值将在数值边界内,而不会发生溢出/下溢。
  3. 另一种选择,通过在 [profile.release] 下设置 overflow-checks = true 来启用发布模式下的溢出检查。(致谢:freax13
  4. 注意整数除法 /。为了避免损失精度,改变整数为浮点类型,如上述代码示例(图 3)所示。然而,一般来说,不建议在浮点数中处理货币,因为浮点数无法准确表示大多数十进制数字,错误会随着时间的推移积累。相反,应使用适当的十进制类型。这篇文章 很好地解释了背后的原因。(致谢:Plasma_000simukis

工具自动化

此外,拥有一个能够全面检查所有代码路径中算术错误的工具毫无疑问是明智的,就像检查拼写错误一样。

X-Ray 是一个先进的工具,内置支持检测 Solana 智能合约中的常见安全陷阱,包括算术溢出/下溢。

  • 原文链接: sec3.dev/blog/understand...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Sec3dev
Sec3dev
https://www.sec3.dev/