关于 dbus 消息合法性的边界情况

Reading Time: 2 minutes

引言

最近项目上遇到了一个现象很奇妙的 bug, 一个提供 dbus 服务的 daemon 被不断地被 dbus-daemon 重启, 并且不断启动失败.

观察启动失败的日志, 发现其启动失败是因为有部份应该由该 daemon 独占的 资源/名字 已经被其他进程占领了. 而我们在观察到该 daemon 不断重启的同时, 如果我们 killall 之后手动起一个, 这个被我们手动启动的 daemon 可以正常被启动. 但是之后 dbus-daemon 还会重新尝试拉起新的实例.

最终定位到是因为该 daemon 意外地向 dbus 发送了一个非法的消息. 而根据 dbus-specification 中 Invalid Protocol and Spec Extensions 一节的描述:

For security reasons, the D-Bus protocol should be strictly parsed and validated, with the exception of defined extension points. Any invalid protocol or spec violations should result in immediately dropping the connection without notice to the other end.

发送非法消息的一方会被断开连接, 并且不会有任何通知被发送向连接的另一端. 这样一来, 旧的 daemon 就失去了 dbus 上的名字, 所以 dbus-daemon 认为服务不存在, 会拉起新的 daemon. 由此形成了奇妙的现象. 该 daemon 使用 godbus 这个库, 按理说, 这种库应该要防止用户发出非法消息, 也应该要提供某种在连接断开时通知用户的功能才对. 所以我开始看 dbus 的文档, 准备在 godbus 中规范错误消息的判断, 借此机会了解一下 dbus 的通信协议.

以下我们逐个说明 godbus 中没有判断到, 或者判断错误的非法消息.

签名的深度检查

dbus 上的签名是一种关于消息结构的描述. 这一点是个大坑, dbus specification 甚至可以说是写错了, 其中 Valid Signatures 一节写到:

The maximum depth of container type nesting is 32 array type codes and 32 open parentheses. This implies that the maximum total depth of recursion is 64, for an “array of array of array of … struct of struct of struct of …” where there are 32 array and 32 struct.

问题就出在 “32 open parentheses” 这个地方, dbus 签名中一共会出现两种括号, 分别是描述结构体的 () 和描述字典项的 {}. 这里说的 32 个到底是加起来是 32 个还是分别 32 个呢? 按后半句理解应该是加起来一共 32 个, 但是仔细看看后半句, 可以发现写文档的人并没有考虑到字典项的问题.

其实 dbus specification 一开始 就说了:

… any attempt to reimplement D-Bus will probably require looking at the reference implementation and/or asking questions on the D-Bus mailing list about intended behavior. …

所以落实到细节的时候, 还是得直接看代码. 这里 是参考实现中检查签名合法与否的函数 _dbus_validate_signature_with_reason:

// ...
struct_depth = 0;
array_depth = 0;
dict_entry_depth = 0;
// ...
if (array_depth > DBUS_MAXIMUM_TYPE_RECURSION_DEPTH)
  {
    result = DBUS_INVALID_EXCEEDED_MAXIMUM_ARRAY_RECURSION;
    goto out;
  }
// ...
if (struct_depth > DBUS_MAXIMUM_TYPE_RECURSION_DEPTH)
  {
    result = DBUS_INVALID_EXCEEDED_MAXIMUM_STRUCT_RECURSION;
    goto out;
  }
// ...
if (dict_entry_depth > DBUS_MAXIMUM_TYPE_RECURSION_DEPTH)
  {
    result = DBUS_INVALID_EXCEEDED_MAXIMUM_DICT_ENTRY_RECURSION;
    goto out;
  }
// ...

这里的 DBUS_INVALID_EXCEEDED_MAXIMUM_DICT_ENTRY_RECURSION 值为 32, 这个函数实际上并没有检查整个签名的总深度, 也就是说如果只看签名的话, 签名的最大总深度是 32*3. 单纯看签名的话, a{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{s((((((((((((((((((((((((((((((((i))))))))))))))))))))))))))))))))}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} 居然是合法的.

实测, 只要传的这个字典是空的, 这个消息真的可以被发出来. 但是一但数组有内容了, 这个消息本身就是非法的了. 因为在函数 validate_body_helper (它的定义在 这里) 中:

if (total_depth > (DBUS_MAXIMUM_TYPE_RECURSION_DEPTH * 2))
  {
    return DBUS_INVALID_NESTED_TOO_DEEPLY;
  }

确实保证了 “消息” 的总深度是不能超过 64 的.

但是空的数组并不会被遍历:

// ...
array_end = p + claimed_len; // claimed_len 是数组的长度
// ...
else
  {
    while (p < array_end)
      {
        validity = validate_body_helper (&sub, byte_order, FALSE,
                                         total_depth + 1,
                                         p, end, &p);
// ...

所以空数组的签名深度超过了 64 这种情况并不能被检查出来. 那么由于 Variant 类型本身能套 64 层娃. 所以如果算上签名的深度的话, 消息的最大的深度可以到达 64 + 32 * 3, qdbus 打印结果的时候大概长这样:

[Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Variant: [Argument: a{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{sa{s((((((((((((((((((((((((((((((((i))))))))))))))))))))))))))))))))}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} {}]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]

还是挺壮观的, 想过去甚至可以用这种方式在签名里面传递信息 (雾

其他检查

  1. 数组的内容编码后总长度不能大于 2^26 字节;
  2. 结构体不能是空的;
  3. 字符串不能含有非 UTF-8 字符;
  4. 单个签名的长度不能大于 255.

以上都已经提了 PR, 基本都给合并了.

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注