虽然 Linux 内核在快速发展,但它同时依赖着一些非常古老的工具,其中之一就是内核代码仍在使用 1989 年版本的 C 语言标准——此标准在 30 多年前内核项目启动之前就已经编写完成。从讨论结果来看,这一情况有望在 5.18 版本内核中改变。
Jakob Koschel 在向 Linus Torvalds 递交的补丁 (https://lkml.org/lkml/2022/2/17/1032) 中修复了内核链表相关的预测执行漏洞。
起因是 Jakob 发现了一个问题,Linux 内核广泛使用由 struct list_head 定义的双向链表:
struct list_head { struct list_head *next, *prev; };
这种结构体通常被嵌入到其他结构体中,通过这种方式,开发者可以使用任何感兴趣的结构类型制作链表。除此之外,内核还提供了大量可用于遍历和操作链表的函数和宏。其中之一是 list_for_each_entry(),这是一个伪装成控制结构的宏。要了解如何使用此宏,请假设内核包含如下结构:
struct foo { int fooness; struct list_head list; };
list 成员可用于创建 foo 结构体的双向链表,假设我们有一个叫做 foo_list 的结构声明作为此类链表的头,使用以下代码可以遍历此列表:
struct foo *iterator; list_for_each_entry(iterator, &foo_list, list) { do_something_with(iterator); } /* Should not use iterator here */
list 参数告诉宏在 foo 结构中 list_head 结构体的名称。此循环将为列表中的每个元素执行一次,迭代器指向该元素。由此导致了 USB 子系统中的一个 bug:传递给该宏的迭代器在退出宏后还能被使用。
Koschel 通过重新编写有问题的代码,以在循环后停止使用迭代器来解决问题。
不过 Linus 却对补丁修复的问题表示不解,也没有看到它与预测执行漏洞的关系。Koschel 对此进行了进一步解释,对此 Linus 认为这只是一个普通的 bug。但不久之后 Linus 发现了问题的根源所在:传递给列表遍历宏的迭代器,必须在循环本身之外的范围内声明。
随后,Linus 认为也许可以采用更直接的修复如块级变量声明。但 C89 不支持,而 1999 年发布的 C99 标准支持。所以 Linux 内核也许是时候转向使用 C99 标准了。
Linus 说到,内核代码一直停留在 C89 的原因之一是编译器 gcc 的旧版本会出现奇怪的问题,导致初始化程序被破坏。不过现在内核要求的 GCC 最低版本已经提高到了 v5.1,那些 bug 可能不再相关了。
另一位密切关注架构编译器问题的内核开发者 Arnd Bergmann 提议直接升级到 C11 甚至 C2x,尽管他不确定 C11 是否会带来任何对内核有用的新内容。不过如果升级到 C17 或 C2x,会破坏对 gcc-5/6/7 的支持,因此升级到 C11 更容易实现,而且跨越太大内核社区未必接受。
Linus 赞成了这个想法,在 Bergmann 确认应该可以这样做之后,Linus 宣布将在下一个内核版本 v5.18 中尝试使用 C11 标准。如果一切顺利,下一个内核版本使用的 C 语言标准有望升级到 C11。