告别 LEB128 的规范性陷阱,Bijou64 用结构设计解决安全问题
点击查看原文>
研究实验室 Ink & Switch 发布了 bijou64,这是一种可变长度整数(varint)编码格式,确保每个整数都映射到唯一有效的字节序列。该设计由 Brooklyn Zelenka 开发,通过消除经常影响二进制协议的一类规范性漏洞,在基准测试中也超过了标准标量 LEB128 解码器。
传统的可变长度整数格式(例如 LEB128)会将整数拆分为多个 7 位数据块,并在每个字节中加入一个表示后续字节存在的标志位。这种方式导致同一个数字可能存在多个有效字节序列(例如,0 可以编码为 0x00,也可以编码为 0x80 0x00)。在使用加密签名、内容寻址或分布式共识的系统中,这些冗余编码会形成攻击面。虽然规范要求解码器拒绝非规范输入,但 Zelenka 指出,开发者经常会遗漏这些独立的验证检查,或者为了优化性能将其移除,从而导致安全漏洞。这类问题类似于历史上 PKCS#1 v1.5、GnuTLS 以及比特币交易可塑性漏洞中的问题。
Bijou64 通过让替代编码在结构层面上无法存在,解决了这一问题。Zelenka 在文章中这样描述:
这是 bijou64 设计目标中希望彻底消除的一类漏洞。它不是通过增加更多检查来实现,而是移除了那个真正重要的检查,并让格式本身保证:即使完全没有规范性检查,对于任何给定值,存在的唯一编码就是规范编码。
该设计依赖两种主要的结构机制:
直接值(0–247):这一范围内的初始字节直接表示整数值,不需要额外元数据。
标签字节和偏移量(248–255):这一范围内的字节表示后续需要跟随多少个负载字节。每个长度级别都会增加一个固定常量偏移量,因此较小的值无法通过填充方式被编码到更大的字节长度中。
由于负载部分是连续的大端整数,编译器可以将解码转换为一次加载操作和字节交换操作。在 x86 和 ARM 硬件上,bijou64 对小数字的解码速度大约是 LEB128 的两倍,而对于较大的数字,由于避免了 LEB128 中的位掩码操作以及遍历连续位时产生的大量分支判断,其速度提升可达到 8 到 10 倍。
Hacker News 社区针对该格式提出了多个权衡方面的讨论。一位评论者引用 BONJSON 格式的基准测试结果,认为仅与标量解码器进行比较,忽略了高性能解析的发展方向:
测试证明,当你转向 SIMD 指令时,ULEB128 或 sentinel values 每次都会获胜,因为它们拥有并行化机会。真正讽刺的是,即使是 SIMD 文本解析也会超过这个方案!SIMD 的能力就是这么强大。
另一位评论者 dzaima 则质疑了其核心安全论点,指出 bijou64 缩小了需要检查的边界范围,但并没有完全消除运行时验证:
忘记检查 first_byte==255 情况下的范围限制,然后直接让它发生 64 位溢出,这和遗漏 LEB128 的范围检查一样,是一种完全合理的漏洞。从某种意义上来说,bijou64 甚至可能更容易出问题:它会让人认为较小输入根本不需要任何范围检查,因此开发者可能忘记为最大长度情况添加特殊处理。
其他反馈还指出,像 LEB128 这样的非规范格式在 WebAssembly 和 DWARF 的链接器中被有意使用,用于为未链接引用填充空间,以便进行原地修补。这说明可变长度填充对于某些特定工具链来说仍然是一项有意保留的需求。
参考实现 bijou64 已使用 Rust 发布在 crates.io 上,采用 MIT/Apache 2.0 双许可证。同时,项目还提供了 npm 上的 JavaScript 封装,以及由社区维护的 Elixir、Go、Perl 和 Java 移植版本。规范中也涵盖了不同位宽版本 bijou32 和 bijou128。
原文连接:
https://www.infoq.com/news/2026/07/bijou64-canonical-varint/
本文来源:InfoQ