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 字符串编码很容易无意中编写出不安全的代码。C 和低级 C++ 绑定在 Fuchsia 系统中的一些特权最高的代码中使用,因此应对这些层面暴露的风险给予额外重视。

我们无意中发现了此类 bug,但对 Fuchsia 树中的代码进行系统性审核非常困难,也无法防止这些 bug 在我们的树或第三方代码中再次出现。

设计

此提案建议更改 FIDL 字符串的编码,以包含一个值为零的终止符字节。这并不会使 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 是下一个非线性对象
  • 解码以供使用时,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/hlcpp/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 不兼容。

性能

这不会影响 build 性能,但会产生以下轻微性能影响:

  • 每个字符串的传输平均需要多 1 个字节

安全

此更改专门用于修复使用 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 填充内边距

八个字符串中有七个字符串后面会跟随零填充字节。在调试 build 中,我们可以将这些零更改为有效的 ASCII 字符,以便在输出或解析时显示这些 bug。这些问题很容易被忽略。

始终将字符串移出行

C/C++ 绑定可以为字符串和 null 终止符分配空间,并复制和终止收到的所有字符串。这会给 C 和低级别 C++ 绑定带来不可接受的性能开销。

将字符串移出 ASan build 的行

地址值过滤器会检查代码是否会超出堆分配。对于 ASan build,我们可以在堆上为字符串分配空间,将字节复制到该空间,然后将该指针返回给调用方。将其集成到 C 绑定中并非易事,这可能会导致 ASan 和发布 build 之间的行为存在明显差异,并且不会帮助在 Fuchsia build 系统之外工作的开发者。

停止使用 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 节。