RFC-0034:空的终止字符串

RFC-0034:Null 终止字符串
状态已拒绝
领域
  • FIDL
说明

我们提议 FIDL 中的字符串将作为 null 终止(同时保持字符串的大小,与目前一样)。此外,更改 C 绑定,以将 uint8_t* 用于字符串数据

作者
提交日期(年-月-日)2019-02-08
审核日期(年-月-日)2019-02-21

遭拒的理由

总结

我们建议:

  1. FIDL 中的字符串将以 null 结尾(同时保持字符串的大小,与目前一样);

  2. 更改 C 绑定,以将 uint8_t* 用于字符串数据。

设计初衷

当前的 FIDL 字符串编码方式很容易意外编写不安全的代码。Fuchsia 系统中一些权限最高的代码使用 C 和低级别 C++ 绑定,因此应该为这些层中暴露的风险赋予额外的权重。

我们无意中遇到此类 bug,但对 Fuchsia 树中的代码进行系统性审核并非易事,并且无法阻止这些 bug 在我们的树或第三方代码中反复出现。

设计

提议更改 FIDL 字符串的编码,以包含值为 0 的单个终止符字节。这不会使 FIDL 字符串与 C 字符串兼容,但 FIDL 字符串用作 C 字符串,会被解读为比预期短,而不是比预期长,后者更安全。

有线格式

有线编码字符串的新定义将是(突出显示的更改):

  • 代表文本的 UTF-8 编码字符的可变长度序列。
  • 可为 null;null 字符串和空字符串是不同的。
  • 可以指定最大大小,例如,string:40 表示字符串最大为 40 字节(不包括 null 终止符)。
  • 字符串内容具有 null 终止符。
  • 编码器和解码器必须验证字符串的最后一个字节(如长度所示)后面是否有 null 字节。
  • 存储为一条 16 字节的记录,其中包含:
    • size:64 位无符号代码单元(字节),不包括 null 终止符
    • data:64 位存在状态指示或指向外行字符串数据的指针
  • 当针对传输而编码时,data 表示存在相应内容:
    • 0:字符串为 null
    • UINTPTR_MAX:字符串为非 null 值,数据是下一个外行对象
  • 在解码以供使用时,data 是指向内容的指针:
    • 0:字符串为 null
    • <valid pointer>:字符串为非 null 值,data 位于指示的内存地址

字符串的表示方式如下:

  • string:不可为 null 的字符串(如果遇到 null 数据,则会发生验证错误)
  • string?:可为 null 的字符串
  • string:Nstring:N?:长度上限为 N 个代码单元的字符串

这将构成重大的传输格式更改,具体而言,一个长度可被 8 整除的字符串的长度将增加 8 个字节(它将对齐到 8 个字节,并且以 Null 终止,导致另外添加 7 个字节的内边距重新调整为 8)。

需要更新编码器,以向编码字符串添加 null 终止,并验证字符串内容中是否有 null 字符。解码器需要更新,以检查字符串内容中是否有 null 字符,但长度指示的是 null 终止符。

C 绑定

目前,C 绑定将字符串表示为 char*size_t。如果该 char* 传递给需要 C 字符串的函数,则可能会被错误地解译。绑定将更改为使用 uint8_t*,因此将字符串数据指针传递给 strchr()printf("%s") 将无法编译。

实施策略

这是一项重大的传输格式变更。在使用 FIDL 的所有用例中,都需要仔细协调其部署。

在某些构建时标志后,需要更新以下代码:

  • //zircon/system/ulib/fidl/walker.h(用于正确验证字符串)
  • //zircon/system/host/fidl/lib/flat_ast.cpp(更新 StringType::Shape
  • //zircon/system/host/fidl/lib/c_generator.cpp(更新 EmitLinerarizeMessageProduceInterfaceClientImplementation 等)
  • //sdk/lib/fidl/cpp/string.cc
  • //garnet/public/lib/fidl/rust/fidl/src/encoding.rs
  • //third_party/go/src/syscall/zx/fidl/encoding.go(更新 marshalStringunmarshalString
  • //third_party/go/src/syscall/zx/fidl/encoding_new.go(更新 mString
  • //sdk/dart/fidl/lib/src/types.dart(更新 StringType
  • llcpp 绑定
  • 适用于其他语言的树外绑定

应使用 fidl_compatibility_test 对这些测试进行测试,运行测试并确认预期的系统稳定性。

实际上,此更改需要与发布团队和 Chromium 等外部团队进行协调。

工效学设计

这会使 C 和低级别 C++ 绑定更易于使用,并且不会影响其他绑定。

文档和示例

应更新线上传输格式文档(如上所述)。

向后兼容性

此变更与 API 兼容,但与 ABI 不兼容。

性能

这对构建性能没有影响,但会对性能产生以下细微影响:

  • 每个字符串平均多花一个字节进行传输

安全性

这项变更旨在修复使用 FIDL 时内存不安全语言的潜在安全漏洞。

测试

系统将使用兼容性测试套件进行测试。应扩展该套件,以确保能够正确处理长度为 7 字节、8 字节和 9 字节的字符串等极端情况。

缺点、替代方案和未知情况

这会使 FIDL 消息的大小平均增加每个字符串一个字节。对于长度不能被 8 整除的字符串,不会发生任何变化。对于长度可被 8 整除的字符串,其长度将增加 8 个字节。

替代选项

取消应用启动

我们可以保持原样,并依靠代码审核和文档来确保人们不会编写错误的代码。通过采用新的低级别 C++ 绑定,该问题也会在一定程度上缓解。这种细微的问题导致的 bug 类别的潜在危险过于严重,以至于无法保持原样。

为 Null 终止,但禁止嵌入的 null 字节 / 使用修改后的 UTF-8

(这是原始提案)

'\0' 是一个有效的 UTF-8 字符,存在于 FIDL 用户想要交换的 UTF-8 字符串中。禁止 null 字节会导致 FIDL 不适合很多用途,并且使用修改后的 UTF-8 会产生额外的开销,即将包含可能为 null 字节的普通 UTF-8 转换为修改后的 UTF-8。

仅使用 uint8_t 而不是 char

如果 FIDL 字符串数据指针是 uint8_t(而非 char),那么如果没有类型转换,它们就无法传递给标准字符串函数或 printf。这有助于发现此类 bug,但不会阻止它们。

在调试 build 中使用有效的 ASCII 填充内边距

8 个字符串中的 7 个后跟填充零字节。在调试 build 中,我们可以将这些零更改为有效的 ASCII 字符,以便在输出或解析这些 bug 时显示出来。它们很容易从裂缝中钻出

始终将字符串移出行

C/C++ 绑定可以为字符串和 null 终止符分配空间,并复制和终止接收到的所有字符串。这会对 C 和低级别 C++ 绑定产生不可接受的性能成本。

将字符串移出了 ASan build 的行

地址排错程序将检查代码没有溢出堆分配。对于 ASan 构建,我们可以为堆上的字符串分配空间,复制其中的字节并将该指针返回给调用方。将其集成到 C 绑定中非常重要,可能意味着隐藏 ASan 和发布 build 之间行为的显著差异,并且对在 Fuchsia 构建系统之外工作的开发者没有帮助。

停止使用 C 和 C++

如果 Google 在 2019 年无法编写出安全的 C++,那么 C++ 就不是安全的。遗憾的是,C 和 C++ 是许多设备驱动程序开发者选择的语言,许多供应商可能会选择将其现有的 C/C++ 驱动程序移植到我们的平台。

不要向 C 公开原始字符

我们可以向 C 公开不透明结构,并要求 C 中的任何字符串访问,以将字符串从已解码的消息缓冲区复制出去。

早期技术和参考资料

DBusCapt'n Proto 和 CORBA1 发送长度和 null 终止,protobuf 不返回 null 终止。


  1. CORBA 规范 3.3 第 2 部分,第 9.3.2.7 节。