centos-bootc 是bootc系统的一个发行版,可以基于它做bootc的原子更新系统。
在这个系统里,安装包 kernel与systemd 一起升级,会触发一个bug。
复现
我们先来复现一下,首先创建一个测试的Dockerfile:
FROM quay.io/centos-bootc/centos-bootc@sha256:49befe588c9c49049bcb63919
caf27a48a25842aee5e2083281604da2483b9a4
RUN dnf upgrade -y systemd kernel
执行构建:
podman build
我们看到,在执行kernel 包的post脚本的时候,报了一个错误:
Cleanup : kernel-core-5.14.0-598.el9.x86_64 9/10
Running scriptlet: kernel-core-5.14.0-598.el9.x86_64 9/10
Cleanup : shim-x64-15-15.el8_2.x86_64 10/10
Running scriptlet: kernel-modules-core-5.14.0-661.el9.x86_64 10/10
Running scriptlet: kernel-core-5.14.0-661.el9.x86_64 10/10
grub2-probe: error: failed to get canonical path of `overlay'.
No path or device is specified.
Usage: grub2-probe [OPTION...] [OPTION]... [PATH|DEVICE]
Try 'grub2-probe --help' or 'grub2-probe --usage' for more information.
Running scriptlet: kernel-modules-5.14.0-661.el9.x86_64 10/10
Running scriptlet: shim-x64-15-15.el8_2.x86_64
构建成功以后,注意:构建成功了,我们以为一切正常。
但是,我们进入刚刚构建出来的镜像查看,就会发现/lib/modules 下面有两个目录。
旧内核目录里只有一个initramfs.img,而新内核目录里面没有:
bash-5.1# cd /lib/modules
bash-5.1# ls
5.14.0-598.el9.x86_64 5.14.0-661.el9.x86_64
bash-5.1# ls 5.14.0-598.el9.x86_64/
initramfs.img
bash-5.1#
当我们把这个镜像作为完整系统使用的时候,比如做成iso安装,或者使用bootc switch 切换,就会直接触发内核panic。(因为找不到当前内核的initramfs.img)
触发这个bug 的条件非常简单,就是在旧版的镜像里,同时执行systemd与kernel的升级。
避免
这个bug 如何避免呢?更简单,在升级kernel 这个包之前,不要升级systemd即可。或者如果要升级,就在kernel之后。
比如Dockerfile 写成:
FROM quay.io/centos-bootc/centos-bootc@sha256:49befe588c9c49049bcb63919
caf27a48a25842aee5e2083281604da2483b9a4
RUN dnf upgrade -y kernel
RUN dnf upgrade -y systemd
执行构建,我们看到在同样的位置的输出:
Cleanup : kernel-core-5.14.0-598.el9.x86_64 9/10
Running scriptlet: kernel-core-5.14.0-598.el9.x86_64 9/10
Cleanup : shim-x64-15-15.el8_2.x86_64 10/10
Running scriptlet: kernel-modules-core-5.14.0-661.el9.x86_64 10/10
Running scriptlet: kernel-core-5.14.0-661.el9.x86_64 10/10
Generating initramfs
Running depmod
Running scriptlet: kernel-modules-5.14.0-661.el9.x86_64 10/10
Running scriptlet: shim-x64-15-15.el8_2.x86_64 10/10
即,如果之前没有升级过systemd,则kernel-core 的post脚本会正常创建出来initramfs 。
等构建完成之后,我们再进入系统查看:
bash-5.1# cd /lib/modules
bash-5.1# ls
5.14.0-661.el9.x86_64
bash-5.1# ls 5.14.0-661.el9.x86_64/
System.map modules.builtin modules.modesetting systemtap
build modules.builtin.alias.bin modules.networking updates
config modules.builtin.bin modules.order vdso
initramfs.img modules.builtin.modinfo modules.softdep vmlinuz
kernel modules.dep modules.symbols weak-updates
modules.alias modules.dep.bin modules.symbols.bin
modules.alias.bin modules.devname source
modules.block modules.drm symvers.gz
总结一下就是,如果我们在升级内核之前,升级过systemd,内核生成initramfs的过程中就会报错, 而且报错之后不会退出,而是生成一个错误的initramfs。
原因
我们知道,kernel 安装包里的post脚本,会调用到/usr/bin/kernel-install 这个程序,所以我们给这个脚本打开调试开关,来追踪一下。
即,在/usr/bin/kernel-install开头,加上set -x,然后在不同的两个镜像中(一个先升级了systemd一个没有),执行dnf upgrade -y kernel。
我们发现,在先升级了systemd 的镜像中,执行到这里:
+ KERNEL_INSTALL_PLUGINS='/usr/lib/kernel/install.d/05-rpmostree.instal
l
/usr/lib/kernel/install.d/20-grub.install
/usr/lib/kernel/install.d/20-grubby.install
/usr/lib/kernel/install.d/50-depmod.install
/usr/lib/kernel/install.d/50-dracut.install
/usr/lib/kernel/install.d/60-kdump.install
/usr/lib/kernel/install.d/90-loaderentry.install
/usr/lib/kernel/install.d/90-uki-copy.install
/usr/lib/kernel/install.d/92-crashkernel.install
/usr/lib/kernel/install.d/99-grub-mkconfig.install'
+ '[' 0 -gt 0 ']'
+ case "$COMMAND" in
+ '[' 1 -lt 1 ']'
+ '[' -f /lib/modules/5.14.0-665.el9.x86_64/vmlinuz ']'
+ '[' 1 -eq 0 ']'
+ for f in $KERNEL_INSTALL_PLUGINS
+ log_verbose '+/usr/lib/kernel/install.d/05-rpmostree.install add 5.14
.0-665.el9.x86_64 /boot/1a7e6d7082ed462fa335b4b961d25d09/5.14.0-665.el9
.x86_64' /lib/modules/5.14.0-665.el9.x86_64/vmlinuz
+ :
+ err=0
+ /usr/lib/kernel/install.d/05-rpmostree.install add 5.14.0-665.el9.x86
_64 /boot/1a7e6d7082ed462fa335b4b961d25d09/5.14.0-665.el9.x86_64 /lib/m
odules/5.14.0-665.el9.x86_64/vmlinuz
+ '[' 0 -eq 77 ']'
+ '[' 0 -ne 0 ']'
+ for f in $KERNEL_INSTALL_PLUGINS
+ log_verbose '+/usr/lib/kernel/install.d/20-grub.install add 5.14.0-66
5.el9.x86_64 /boot/1a7e6d7082ed462fa335b4b961d25d09/5.14.0-665.el9.x86_
64' /lib/modules/5.14.0-665.el9.x86_64/vmlinuz
+ :
+ err=0
+ /usr/lib/kernel/install.d/20-grub.install add 5.14.0-665.el9.x86_64 /
boot/1a7e6d7082ed462fa335b4b961d25d09/5.14.0-665.el9.x86_64 /lib/module
s/5.14.0-665.el9.x86_64/vmlinuz
grub2-probe: error: failed to get canonical path of `overlay'.
No path or device is specified.
Usage: grub2-probe [OPTION...] [OPTION]... [PATH|DEVICE]
Try 'grub2-probe --help' or 'grub2-probe --usage' for more information.
+ '[' 0 -eq 77 ']'
+ '[' 0 -ne 0 ']'
+ for f in $KERNEL_INSTALL_PLUGINS
+ log_verbose '+/usr/lib/kernel/install.d/20-grubby.install add 5.14.0-
665.el9.x86_64 /boot/1a7e6d7082ed462fa335b4b961d25d09/5.14.0-665.el9.x8
6_64' /lib/modules/5.14.0-665.el9.x86_64/vmlinuz
+ :
+ err=0
而没有升级systemd 的系统中,则是:
+ KERNEL_INSTALL_PLUGINS='/usr/lib/kernel/install.d/05-rpmostree.instal
l
/usr/lib/kernel/install.d/20-grub.install
/usr/lib/kernel/install.d/20-grubby.install
/usr/lib/kernel/install.d/50-depmod.install
/usr/lib/kernel/install.d/50-dracut.install
/usr/lib/kernel/install.d/60-kdump.install
/usr/lib/kernel/install.d/90-loaderentry.install
/usr/lib/kernel/install.d/90-uki-copy.install
/usr/lib/kernel/install.d/92-crashkernel.install
/usr/lib/kernel/install.d/99-grub-mkconfig.install'
+ '[' 0 -gt 0 ']'
+ case "$COMMAND" in
+ '[' 1 -lt 1 ']'
+ '[' -f /lib/modules/5.14.0-665.el9.x86_64/vmlinuz ']'
+ '[' 1 -eq 0 ']'
+ for f in $KERNEL_INSTALL_PLUGINS
+ log_verbose '+/usr/lib/kernel/install.d/05-rpmostree.install add 5.14
.0-665.el9.x86_64 /boot/bd492504d7314b1b99cf93272e21d572/5.14.0-665.el9
.x86_64' /lib/modules/5.14.0-665.el9.x86_64/vmlinuz
+ :
+ err=0
+ /usr/lib/kernel/install.d/05-rpmostree.install add 5.14.0-665.el9.x86
_64 /boot/bd492504d7314b1b99cf93272e21d572/5.14.0-665.el9.x86_64 /lib/m
odules/5.14.0-665.el9.x86_64/vmlinuz
Generating initramfs
Running depmod
+ err=77
+ '[' 77 -eq 77 ']'
+ break
+ exit 0
即,正常执行应该是到达05-rpmostreee.install 之后就跳出循环结束。但是在升级过systemd 的系统中,会执行完05-rpmostreee.install 之后,继续执行20-grub.install 。
我们看一下05-rpmostree.install 的源码:
#!/usr/bin/bash
# Check if install.conf is missing or does not include layout=ostree
if [ ! -f /usr/lib/kernel/install.conf ] || ! grep -q layout=ostree /us
r/lib/kernel/install.conf; then
exit 0
fi
# This is the hook that has kernel-install call into rpm-ostree kernel-
install
if test -x /usr/bin/rpm-ostree; then
exec /usr/bin/rpm-ostree kernel-install "$@"
fi
它会读取/usr/lib/kernel/install.conf,判断其中layout是否等于ostree。等于ostree的,脚本直接exec到rpm-ostree kernel-install "$@",而没有这个的,则是直接返回0。(当然对于外层循环就会继续执行20-grub.install等......)
我们分别查看升级过systemd 与没有升级过systemd的系统里这个install.conf。
升级过systemd:
bash-5.1# cat /usr/lib/kernel/install.conf
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it u
nder the
# terms of the GNU Lesser General Public License as published by the F
ree
# Software Foundation; either version 2.1 of the License, or (at your
option)
# any later version.
#
# See kernel-install(8) for details.
#layout=bls|other|...
#initrd_generator=dracut|...
没有升级过systemd:
bash-5.1# cat /usr/lib/kernel/install.conf
# kernel-install will not try to run dracut and allow rpm-ostree to
# take over. Rpm-ostree will use this to know that it is responsible
# to run dracut and ensure that there is only one kernel in the image
layout=ostree
我们查询一下,发现这个文件属于systemd-udev。即我们升级systemd的时候,间接升级了systemd-udev,把layout 置空了。
解决
我们改一下Dockerfile:
FROM quay.io/centos-bootc/centos-bootc@sha256:49befe588c9c49049bcb63919
caf27a48a25842aee5e2083281604da2483b9a4
RUN dnf upgrade -y systemd
RUN echo "layout=ostree" >> /usr/lib/kernel/install.conf
RUN dnf upgrade -y kernel
,验证一下:
Running scriptlet: kernel-modules-core-5.14.0-665.el9.x86_64
10/10
Running scriptlet: kernel-core-5.14.0-665.el9.x86_64
10/10
Generating initramfs
Running depmod
Running scriptlet: kernel-modules-5.14.0-665.el9.x86_64
果然正常。
那么,既然systemd中会更改这个值,为什么05-rpmostree.install 却还在依赖这个值呢?
我们查询一下05-rpmostree.install 这个文件,发现它属于rpm-ostree 包。我们升级一下最新的rpm-ostree软件的版本,看看是不是最新的rpm-ostree 已经不再依赖systemd-udev的这个文件:
bash-5.1# dnf upgrade -y rpm-ostree
...
bash-5.1# rpm -qf /usr/lib/kernel/install.d/05-rpmostree.install
rpm-ostree-2025.11-1.el9.x86_64
bash-5.1# cat /usr/lib/kernel/install.d/05-rpmostree.install
#!/usr/bin/bash
# Check if install.conf is missing or does not include layout=ostree
if [ ! -f /usr/lib/kernel/install.conf ] || ! grep -q layout=ostree /us
r/lib/kernel/install.conf; then
exit 0
fi
# This is the hook that has kernel-install call into rpm-ostree kernel-
install
if test -x /usr/bin/rpm-ostree; then
exec /usr/bin/rpm-ostree kernel-install "$@"
fi
看来不行。最新的rpm-ostree 依然是使用systemd-udev 中的layout 的值。rpm-ostree就是需要这个值,但是还避免不了systemd-udev改变这个文件。
总结
所以,这个bug 产生的根本原因是:
rpm-ostree 包依赖了systemd-udev包中的layout值,而升级systemd-udev,会删掉这个值。所以升级systemd-udev之后,就会导致rpm-ostree 这个包构建内核initramfs.img的错误。
所以,bootc (或者systemd)官方可以有两个方向的改进:
一、systemd-udev软件包,把这个install.conf 文件设置为配置类型。这样用户在进行升级的时候,rpm就会把文件命名为install.conf.rpmnew,用户就知道配置文件进行了更新,由用户决定是否同步。
二、bootc 的构建文档应该明确提示,说明在构建镜像过程中,升级systemd等程序会改变这个文件,而系统的关键模块(如内核)会依赖这个文件,需要用户在升级systemd等程序后,手动修正,再进行其它操作。
方案
而对于我们用户来说,为了让程序更加健壮,我们需要在ostree类系统的构建脚本中,明确地在升级systemd之后,修正这个值。避免其它部分也因为这个值的错误,而导致bug。
即,升级后重置:
RUN dnf upgrade -y systemd-udev && echo "layout=ostree" > /usr/lib/kernel/install.conf
或者干脆先保存,再升级,最后再恢复:
RUN cp /usr/lib/kernel/install.conf /usr/lib/kernel/install.conf.bak && dnf upgrade -y systemd-udev && cp /usr/lib/kernel/install.conf.bak /usr/lib/kernel/install.conf