【问题标题】:Does Linux kernel list implementation cause UB?Linux内核列表实现会导致UB吗?
【发布时间】:2021-02-27 17:46:30
【问题描述】:

先决条件:

  1. 根据C standard,会产生无效指针的指针算术会导致未定义的行为。
  2. Linux 源代码seems to conform 符合 C 标准,希望与大多数架构兼容。
  3. Linux's list implementation 包含以下代码(保留格式,可能另一个问题的想法是如何使用 Stackoverflow 语法设置适当的制表宽度):
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

#define list_next_entry(pos, member) \
    list_entry((pos)->member.next, typeof(*(pos)), member)

#define list_first_entry(ptr, type, member) \
    list_entry((ptr)->next, type, member)

#define list_entry_is_head(pos, head, member)               \
    (&pos->member == (head))

#define list_for_each_entry(pos, head, member)              \
    for (pos = list_first_entry(head, typeof(*pos), member);    \
         !list_entry_is_head(pos, head, member);            \
         pos = list_next_entry(pos, member))
  1. 上述列表实现的典型用例具有类型为struct A 的结构,其中包含struct B 类型结构列表的头部。

Q:假设offsetof(struct B, entry_in_list) > offsetof(struct A, list_head) 并实现了以下循环:

struct A* A_ptr = something_meaningful;
struct B* pos = NULL;
list_for_each_entry(pos, &A_ptr->list_head, entry_in_list) {
  do_something();
}

然后list_next_entry(pos, member) 的最后一次(循环退出之前)评估将扩展到:

container_of(A_ptr->list_head, struct B, entry_in_list) = 
 = (char*)A_ptr->list_head - offsetof(struct B, entry_in_list) =
 = (char*)A_ptr + offsetof(struct A, list_head) - offsetof(struct B, entry_in_list) 

,根据我们的假设,它将指向 A 结构之前的区域。假设这个区域不包含分配的内存,container_of() 宏的结果将是一个无效的指针,从而导致 Linux 中的 UB(一般情况下为 OFC)。这个推理是合理的还是我弄错了?

或者该标准的某些部分普遍认为不值得遵循?

【问题讨论】:

  • Linux内核使用了一些GCC扩展如typeof,也对C实现做了一些假设。
  • @IanAbbott 感谢您的评论。但似乎不仅要对编译器(以及 C 实现)做出假设,而且还要对将使用该通用接口的体系结构做出假设。我相信 GCC 可能只是将此 C 代码转换为汇编,假设它不违反 C 标准,并且只有当 CPU 看到分配给与pos 关联的寄存器的无效指针时,UB 才会被披露。
  • 您是正确的,list_for_each_entrylist_entry_is_head 宏在 C 标准中快速而松散。当循环终止条件预计为假时,pos 变量未指向struct B 对象,因此在list_entry_is_head 中访问&pos->entry_in_list 会在此处调用UB。
  • @IanAbbott 我相信这算是一个完整的答案:)

标签: c list linked-list linux-kernel


【解决方案1】:

正如 OP 所怀疑的那样,list_for_each_entry(pos, head, member) 宏的实现依赖于 C 语言中的未定义行为,以便循环终止条件 !list_entry_is_head(pos, head, member) 变为 false。

假设列表非空,那么在最后一次迭代之后,for 循环的第三个“前进”表达式会在struct list_head 之前的地址offsetof(typeof(*pos), member) 字节处生成一个指向无效typeof(*pos) 的指针由head 指向。它依赖于&pos->member,但比较等于head

虽然它依赖于未定义的行为,但编译器很难确定pos 在技术上是一个无效指针。只要poshead 都指向同一个平面地址空间,Linux 内核就会设法摆脱这种规则的弯曲。

替代方法是#include <linux/list.h> 根本不提供list_for_each_entry(pos, head, member) 宏,而代码使用list_for_each(pos, head)list_entry(ptr, type, member) 宏代替(其中posstruct list_head * 和@987654339 @ 是 type *),但这通常需要代码中的额外变量。

【讨论】:

    【解决方案2】:

    编译内核时的附加断言。这些实际上到处都在使用。

    1. 一个指针可能加载了一个未分配的地址。您会在每个系统调用条目上看到这一点。处理此类指针时必须特别小心,因为取消引用它们可能比崩溃更糟糕。

    2. 不保证取消引用 NULL 指针会崩溃;并且不允许编译器假定取消引用 NULL 的路径不可达。 (这个是在 NULL 指针优化删除安全检查后添加的。)在某些架构上,实际上有一些东西。在其他架构上,它只是另一个用户模式指针。

    编译器选项告诉编译器这些都是真的。 (事实上​​,第一个通常假设在平面模型中是正确的,内核就是。)

    传递给gcc 的标志是-fno-delete-null-pointer-checks。空指针优化改动参考:https://lwn.net/Articles/342420/

    【讨论】:

    • 谢谢,很有趣!如果您提供一些参考/链接到 LKML/GCC 邮件列表讨论上述选项的介绍,我将不胜感激。还提到确切的选项会很好。
    • @Phikimon:来源。你不会找到一个平的。它仍然存在,因为从未尝试将内核移植到不使用平面内存模型的架构。
    猜你喜欢
    • 2021-11-23
    • 1970-01-01
    • 1970-01-01
    • 2022-01-15
    • 1970-01-01
    • 1970-01-01
    • 2015-02-26
    • 1970-01-01
    • 2021-06-11
    相关资源
    最近更新 更多