LWN:Ubuntu内核bug导致容器崩溃!

Linux News搬运工

共 6151字,需浏览 13分钟

 · 2022-07-24

关注了就能看到更多这么棒的文章哦~

An Ubuntu kernel bug causes container crashes

July 5, 2022
This article was contributed by Jordan Webb
DeepL assisted translation
https://lwn.net/Articles/899420/

一些运行 Ubuntu 20.04 的系统管理员在 6 月 8 日的时候度日如年,当时 Ubuntu 发布了包含一个特别让人讨厌的 bug 的内核 package,这个 bug 是由一个 Ubuntu 专用的内核 patch 引起的。这个错误导致在启动 Docker 容器时出现 kernel panic。相关的 fix 包于 6 月 10 日发布,但人们对处理该 patch 时出现的问题疑惑;具体来说,人们都没有想到,已经超过使用寿命好几个月的的内核 5.13 竟然出现在运行 Ubuntu 20.04 的机器上,而 20.04 本应是一个长期支持版本。

Ubuntu's kernel release lifecycle

除非是采用滚动发布模式,否则 Linux 发行项目通常都会选择固定在一个内核分支上,并在发行版的生命周期内坚持使用它。例如,像 Ubuntu 20.04 那样搭载 5.4 内核的发行版,可能会收到来自后续 5.4.x 内核的更新,但在发行版重新发布新的主要版本之前,不太可能会升级到 5.15。出于这个原因,这类项目通常喜欢甚至是迫切需要由 stable kernel 团队来指定为长期维护的一个 branch。对发行版的维护者来说,能确定他们所发布的软件版本在上游是有支持的,那么晚上就更容易入睡。

Debian 和 Red Hat 在为他们的发行版挑选内核时都遵守了这些规则。Ubuntu 声称也是如此,至少在其 LTS(长期支持)版本中是这样做的。这些版本每两年发布一次,并会支持五年。这些版本都使用一个 long-term stable kernel;Ubuntu 版本在有效期内会提供更新。

在 Ubuntu 的两个 LTS 版本之间,Ubuntu 也会以每六个月一次的时间间隔来发布非 LTS 版本。与 LTS 版本相比,这些版本只会支持 6-9 个月,并在新版本发布后的 3 个月内就会被宣布终止使用(EOL)。由于它们的保质期相对较短,Ubuntu 并不会确保在这些版本中使用 long-term kernel。最近发布的非 LTS 版本的 Ubuntu 21.10 中就搭载了 Linux 5.13,这不是一个长期分支。事实上,5.13 分支在 2021 年 9 月 18 日被宣布为 EOL,甚至比 Ubuntu 21.10 在 10 月 14 日发布的日子还要早一个月。

优先考虑稳定性的用户会高度重视 LTS 版本带来的这个长期支持窗口期,但在硬件世界中,五年都快相当于是永恒了,尤其是在像 graphics 这样快速发展的领域。为了支持更加新的硬件,Ubuntu 定期为其 LTS 版本发布新的硬件支持(HWE, hardware enablement)软件栈。这些都是从最新(可能是非 LTS)版本中移植回来的软件包所组成的。HWE 栈包括更新过的内核包,也可能包括更新了的 Xorg 和 Mesa 包。

根据 Ubuntu 的说法,在 Ubuntu 的新桌面版本中,HWE 栈是默认启用的,但在服务器安装中则需要明确选择才会开。这种选择政策似乎也只适用于从 ISO 镜像安装的用户;亚马逊 AWS、Azure 和谷歌云上的默认 Ubuntu 20.04 镜像都预装了 HWE 内核。许多系统管理员(包括我)也为他们的服务器选择了 HWE 栈,要么是因为渴望用那些只有较新内核才有的功能,要么是需要一个能与他们的硬件配合工作的内核。

如果单独考虑这些决策时,在非 LTS 版本中不要求使用 long-term kernel 的决定以及发布 HWE 内核来扩展 LTS 版本的硬件支持的决定都显得很合理。但是,这两个决定结合起来会导致一个有点令人吃惊的情况;运行 "长期支持 "发行版的用户可能最终运行一个被内核开发者认为生命周期已经终止的 Linux 版本。

截至目前,在 Ubuntu 20.04 上运行 HWE 内核的用户会开始使用一个从 21.10 backport 过来的 5.13 内核。Ubuntu 22.04,也就是下一个 LTS 版本,包括了 5.15 内核,这是一个长期稳定的分支。目前 20.04 的用户可以使用这个名字 "hwe-20.04-edge"。据推测,它将在 7 月 14 日之前的某个时候取代 21.10 的内核,即 hwe-20.04,届时 Ubuntu 21.10 本身就会超出服务期了。不过现在,以及在过去的几个月,任何在 20.04 上运行 HWE 内核的人都在运行一个基于 5.13 的内核。由于 HWE 内核是所有三个主要云服务商默认安装的内核,那么它的问题会影响到 Ubuntu 的一大部分用户。

A tale of four filesystems

HWE 内核允许在 Ubuntu LTS 上使用较新的硬件和内核功能,但似乎这可能会在稳定性方面带来一些代价。内核 crash 的根本原因在于有四种文件系统之间的共同作用,尽管它们都不是传统意义上的文件系统(即把数据写入持久性存储的东西)。

第一个是 overlayfs。顾名思义,overlayfs 允许将一个目录(用 overlayfs 的说法是 "upper" 目录)中的文件来覆盖在另一个目录("lower" 目录)中的文件之上。这导致这个 mount 点其实包含了上层和下层目录中所有文件;如果两个目录都包含一个同名的文件,overlayfs 会显示上层目录中的这个版本。对 overlayfs mount 进行改变都会反映在上层目录中。overlayfs 提供的功能对 Docker 这样的容器运行时系统(container runtime)特别有价值,它将容器镜像文件存储为一系列的 layer;overlayfs 提供了一种有效的方法,可以从这些 layer 中来构建出 container 的根目录。从 2014 年的 3.18 版本起,这个功能已经成为内核的一部分。

第二个文件系统是 AUFS,它做了 overlayfs 所做的一切,甚至更多,但它的实现明显更复杂。AUFS 的代码量约为 35000 行,而 overlayfs 约为 12000 行。AUFS 在 2008 年首次被提交到内核中,但从未被合并进来;从那时起就一直在 kernel tree 外维护着。Ubuntu 在其 20.10 版本的内核中包含了 AUFS,但在 21.04 中放弃了它。

第三个文件系统是 shiftfs,它最初是由 James Bottomley 在 2018 年创建的,允许重新映射 mount 文件系统中的用户和组 ID,虽然它从未被合并到上游,但自 5.0 内核系列以来,它一直被包含在 Ubuntu 的代码中。Canonical 的 LXD 项目可以使用 shiftfs 来加速非特权容器的创建动作,容器内的 root 用户被映射到容器外的 root 用户;否则,文件系统需要改变其用户和组 ID 才能实现这个效果。不过 shiftfs 不太可能出现在 Linus Torvalds 的代码中,因为它的功能完全被 5.12 版本的内核中加入的 ID-mapped mounts 替代了。LXD 后来也被修改为使用 ID-mapped mounts。

我们故事中的第四个也是最后一个文件系统是 procfs。众所周知,在 Linux 系统上运行的每个进程在/proc 中有一个相应的目录。这些目录中的每一个都包含了一个名为 map_files 的子目录,它有一系列的符号链接。每个链接都对应了进程地址空间中被映射到一个文件的地址范围;每个链接的名称表示被映射的地址范围,而目标是被映射到该范围的文件。比如说

$ ls -l /proc/$$/map_files/
total 0lr-------- 1 jordan everybody 64 Jun 22 16:21 55e0cc120000-55e0cc14d000 -> /usr/bin/bash
lr-------- 1 jordan everybody 64 Jun 22 16:21 55e0cc14d000-55e0cc1fe000 -> /usr/bin/bash
...

map_files 子目录最主要的使用者可能是 Checkpoint/Restore In Userspace(CRIU)工具,它可以通过将进程的整个状态序列化存储到磁盘来作为 "checkpoint",然后从这些序列化的状态来重新创建进程来 "恢复 "它。

What does this patch 多?

在创建 Docker 容器时引起内核 panic 的补丁是为了纠正在一起使用 overlayfs 和 shiftfs 时的一个问题。如果一个进程从这样的 mount 中 map 一个文件,map_files 中的符号链接会指向该文件的原始 "unshifted" 版本,而不是 shiftfs mount 中的路径。这就破坏了 checkpoint 和 restore Docker 容器的功能,因为 map_files 中的符号链接所指向的文件是在容器内没有安装的文件系统。

这个问题在 2020 年初被发现,并在 Ubuntu 20.04 发布后不久被修复。当时,AUFS 被包含在 Ubuntu 的内核中。AUFS 的开发者还面临着一个挑战,就是如何区分文件的真实名称以及 AUFS mount 内的别名。为了解决这个问题,AUFS 补丁在内核的 vm_area_struct 中引入了一个叫做 vm_prfile 的额外字段,该字段被填写为 AUFS 的文件名。为了解决 overlayfs 和 shiftfs 的问题,Ubuntu 的开发者需要在这种复合 mount 中跟踪文件的别名,而且,由于 AUFS 已经为类似的目的添加了 vm_prfile,他们就选择重新使这个字段,而不是再加一个。他们知道他们的 fix 方法依赖于 AUFS 的启用,所以他们也选择在#ifdef 块中保护一下,如果 AUFS 没有被配置到内核中,那么这个 patch 就没有用了。

How things went wrong

当 Ubuntu 的开发者将 5.8 内核分支的 shiftfs 相关 patch 移植到 5.13 和 5.15 内核时,修正 map_files 和 shiftfs 问题的补丁被遗漏了,因为它依赖于 AUFS,而 AUFS 已经从 Ubuntu 的内核中删除。当这些 kernel 被移植回 Ubuntu 20.04 时,AUFS 继续被支持,缺失的 patch 就被注意到了,同时也就用在了 Ubuntu 的 5.13 和 5.15 树上。

不幸的是,overlayfs 的内部结构随着时间的推移发生了变化,最终导致该 patch 不再正确了。结果,当 overlayfs 上的文件被映射到内存中时,patch 添加的函数试图用 fput() 来释放对结构文件的引用,但由于先前的 fput()调用使得该结构已经被释放了。这导致了内核的 panic。

在 Ubuntu 21.10 上,5.13 是默认内核,这并没有引起任何问题。由于 AUFS 没有被启用,patch 引入的代码周围的 #ifdef 阻止了它被编译进内核。这个问题在为 Ubuntu 20.04 重建 5.13 和 5.15 时出现。由于 HWE 内核需要支持它所取代的内核所支持的所有功能,所以在这些构建中启用了 AUFS,包含了多余的 fput()的代码就被编译进去了。

这个问题是在 5 月份被注意到的,几乎是在 patch 被重新加入后就立即发现了。然而,5.13 似乎被忽略了;该 patch 在 Ubuntu 的 5.15 branch 中被 revert 掉了,并被替换为不调用 fput() 的版本,但这个不正确的版本仍留在 5.13 branch 中,并进入了 5.13 HWE 内核。

根据 changelog,有问题的内核包是在 6 月 3 日构建的,尽管它在之后的一段时间内可能没有被发布到 Ubuntu 的软件包库中。这个问题在 6 月 8 日被报告出来。在 6 月 10 日提供更新的软件包之前,受影响的用户唯一可用的解决办法是手动回滚到以前的内核。

Conclusion

无论在什么时候,只要是维护一个树外的内核 patch 都是一项艰巨的任务。在 Linux 对用户空间的兼容性提供钢铁一般的保证的同时,它对不同版本的内部内核接口稳定方面是完全不提供保证的。由于要跟上内核中其他地方的变动,那些没有被合并的代码往往很快就被淘汰了。

当 Ubuntu 在其 LTS 版本中发布树外补丁时,就意味着它的内核开发者要对这些 patch 进行至少五年的维护,而且通常是需要在内核的多个分支中同时进行的。有时,这些赌注会得到回报;Ubuntu 在合并前就将 overlayfs 纳入了自己的内核,现在它被上游维护了。另一方面,尽管 Ubuntu 在 2021 年放弃了对 AUFS 的支持,但由于该发行版在 20.04 中提供了 AUFS,所以他们在 2025 年之前都要继续支持 AUFS。他们最新的 LTS 版本,22.04,仍然包含了对 shiftfs 的支持;这些 patch 将在 Ubuntu 的代码树中持续存在至少到 2027 年。正如这个 patch 的问题所表明的,保持这些 patch 的更新不是简单的工作;内核其他部分的变化可能而且必定会导致问题,这些需要非常仔细地砍进去。

根据这些时间表,对于 Ubuntu 的内核开发者来说,事情似乎不会在短时间内变得简单。事实上,注定会变得更难;由于内核现在提供了类似的功能,人们对这些 kernel tree 外的替代品的兴趣可能会减弱,这将把维护的负担更多地放在 Ubuntu 的肩上。没有回报的赌注就会变成债务,并产生复利,造成持续的损失。

最后,Ubuntu 似乎至少在某种程度上成为了自己造成的复杂性的受害者。Ubuntu 的开发者很快发现并 fix 了这个问题,但只是在受影响的一个 branch 中完成了 fix。不幸的是,被遗漏的那个 branch 就是被发布给用户的那个。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~



浏览 12
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报