Anubis 与可重现 WebAssembly 构建的挑战"}],
Anubis 项目正在实现基于 WebAssembly 的工作量证明 (PoW) 检查,以允许管理员使用非 SHA256 方法来保护网站。为了在客户端和服务器端保持检查逻辑的单一事实来源,Anubis 使用了 WebAssembly;然而,为了支持禁用 WebAssembly 的用户,该项目使用 Binaryen 项目中的 wasm2js 工具将该 WebAssembly 重新编译为 JavaScript。
实现可重现构建——即相同的输入字节始终产生相同的输出字节——对于提交到 Anubis 仓库的 wasm2js 二进制文件的信任和验证至关重要。然而,这一过程揭示了现代编译器工具链中显著的非确定性。
编译器非确定性的来源
编译器并不总是其输入的确定性函数。几个因素可能导致相同的源代码在不同的构建或环境中产生不同的二进制输出。
构建时元数据
C/C++ 开发中最常见的非确定性来源之一是使用内置宏,如 __DATE__ 和 __TIME__。这些宏会在二进制文件中盖上执行时的精确时间戳,从而确保即使源代码保持不变,每一次构建都会产生不同的字节。
隐式工具链依赖
编译器通常依赖于系统中 $PATH 中可用的外部工具。在 wasi-sdk 的情况下,Clang 可能会在后台调用 wasm-opt 来优化 WebAssembly 输出。这会产生对主机上安装的特定版本 wasm-opt 的依赖。例如,在安装了 wasm-opt 版本 108 的机器上进行的构建可能会失败,或者产生与在安装了版本 130 的机器上进行的构建不同的结果,特别是在处理 WebAssembly Exceptions 扩展时。
为了缓解这一问题,Anubis 构建过程在链接步骤中使用 --no-wasm-opt 标志来移除这种外部依赖。
特定架构的二进制差异
即使控制了元数据和外部工具,低层级的代码生成也可能根据主机架构的内存布局而有所不同。
地址敏感型代码生成
Clang 的异常处理路径包含地址敏感型代码生成,其中原始指针值可能会泄露到 try_table 块的排序中。这导致二进制文件在不同构建之间,或者在不同架构(例如 x86_64 与 arm64)上构建时,由于不同的指针迭代顺序而产生几个字节的差异。
为了在单一架构内解决此问题,采取了以下步骤:
- 禁用 ASLR:使用
setarch --addr-no-randomize在构建期间禁用地址空间随机化。 - 校验和验证:为 x86_64 和 arm64 架构创建已知的良好 SHA256 校验和。
- CI 验证:实现了一个 CI 任务,在两种架构上重新构建模块并根据记录的校验和进行验证。
社区对可重现性的观点
对确定性构建的追求引发了开发者社区中的几次技术讨论:
- 封闭式构建系统:一些贡献者建议,像 Nix 这样的工具通过使用沙盒来捕获时间调用并将其替换为常量(epoch 0)来解决这些问题,从而确保确定性。
- LLVM Bugs:技术分析表明,LLVM 中对
DenseMap的非确定性迭代可能是指针泄露问题的根源,建议迁移到MapVector以保证确定性的迭代顺序。 - PoW 伦理:一些用户对强制客户端运行工作量证明循环以防止爬虫所带来的能耗和可访问性影响提出了担忧。
"如果 Clang 因为指针地址而生成非确定性输出,那么这就是一个 bug... 最常见的情况是如果某个代码路径正在对一个非确定性的 DenseMap 进行迭代。"
虽然构建现在在特定架构内是确定性的,但实现跨架构的可重现性仍然是 LLVM 的上游挑战。