Linux 之父終于被勸動:用了 30 年的 Linux 內核 C 語言將升級至 C11
還在使用 89 年版 C 語言的 Linux 內核,現在終于要做出改變了。今天,Linux 開源社區宣布,未來會把內核 C 語言版本升級到 C11,預計 5.18 版之后生效,也就是今年 5 月。
這個決定很突然,從發起問題到官方聲明,不過才一個星期,要知道說服固執的 Linux 之父 Linus Torvalds 可不是件容易的事。事情的原因,說起來還有那么一點偶然的因素。
一個 bug 的連鎖反應
問題的起源是來自上周的一次 Linux 社區討論。
一位名叫 Jakob Koschel 的博士生,在研究阻止與內核鏈表 primitive 相關的預測執行漏洞時,發現了這樣一個問題。
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 提交了一個修復補丁,在循環后停止使用迭代器搞定了 bug。
說服 Linus
但是 Linus Torvalds 本人并不太喜歡這個補丁,也沒有看到它與預測執行漏洞的關系。在 Koschel 詳細解釋后,Linus 承認這只是一個普通的 bug。
然而事情并沒有那么簡單,Linus 不久后意識到了真正的根源:傳遞給鏈表遍歷宏的迭代器,必須在循環本身之外的范圍內聲明。這種非預測性 bug 發生的原因是,C89 中沒有“在循環中聲明變量”。
像 list_for_each_entry () 這樣的宏,從根本上總是將最后一個 HEAD 入口泄漏到循環之外,僅僅是因為我們不能在循環本身中聲明迭代器變量。
如果可以編寫一個可以聲明自己的迭代器列表遍歷宏,那么迭代器在循環之外將不可見,并且不會出現此類問題。但是,由于內核停留在 C89 標準上,因此無法在循環中聲明變量。
Linus 決定,那咱們還是升級吧,也許是時候轉向 C99 標準了。雖然它也有 20 多年的歷史,但至少比 C89 新,可以在循環中聲明變量。
既然 C89 如此陳舊,這么多年還沒做出改變呢?Linus 說,那是因為我們在一些古老的 gcc 編譯器版本中遇到了一些奇怪的問題,不能隨便升級。
但是,現在 Linux 內核已將 gcc 的最低要求提升至 5.1 版,因此過去那些奇怪的 bug 應該不會有了。
而另一位核心開發者 Arnd Bergmann 認為,咱們完全可以升級到 C11 甚至更高版本。但如果升級到 C17 或 C2x,會破壞對 gcc-5/6/7 的支持,因此升級到 C11 更容易實現。
最終,Torvalds 贊成這個想法:“好的,請提醒我,讓我們在 5.18 合并窗口的早期嘗試一下。”接下來遷移到 C11 可能會導致一些意想不到的 bug,但如果一切順利,下一個 Linux 內核版本將正式轉向 C11。
參考鏈接:
[1]https://lwn.net/SubscriberLink/885941/01fdc39df2ecc25f/
[2]https://news.ycombinator.com/item?id=30459634