运维的抢修笔记·升级系统与Coreutils
背景引入
在Ubuntu 26.04 LTS中,引入了以Rust重构的uutils作为传统的GNUcoreutils的替代,同时似乎这个被配置成为了默认的coreutils提供者。
说到coreutils,这个名字可能会有些陌生,但如果说是ls、rm、cat这些耳熟能详的系统级基础命令,那就不奇怪了。coreutils就是在通常的Linux发行版中提供这些最基本命令的软件包。
一些相关链接
事故起因
最近出于某些原因,把压箱底吃灰了很久的香橙派5Pro又重新拿了出来,用作一些用途。这块板子搭载了RK3588S芯片,性能在ARM开发板中算是第一梯队,综合算力还是很给力的,但生态就一言难尽了。在配置系统的过程中,自然而言地准备滚一下系统(应该算是Arch用户的后遗症),结果发现原本配置的镜像站(清华源IPv6 only)访问不上,主要的原因是路由器v6部分有些问题,但没怎么想着从路由器端解决,打算直接修改apt配置文件换自动解析版本的清华源。
参考了清华源的配置文档,但是我当时看都没看,直接无脑地将清华源给出的适用于最新的Ubuntu 26 LTS的apt源粘贴到了我的/etc/apt/sources.list。于是故事从这里开始。 (注:此前设备上跑得是Ubuntu jammy,即Ubuntu 22 LTS。一个两代的大版本跨越,我居然浑然不觉。)
其实如果在这里,我执行的下面这一组指令,那么这个过程其实算是一个没那么正规的升级流程,倒也没啥问题
1 | sudo apt update |
因为full-upgrade会处理依赖关系和需要移除的冲突包,起码不会出现半路把核心工具给删了的情况。然而,我也没意识到自己在干什么,尤其是没意识到大版本变了,一如既往的执行了
1 | sudo apt upgrade |
upgrade只会更新已有包的版本,不会处理需要“安装新包同时移除旧包”的变更。而Ubuntu 26.04恰恰把coreutils这个Essential包从coreutils-from-gnu替换成了coreutils-from-uutils——这属于“替换式”变更,apt upgrade只会傻乎乎地把coreutils-from-gnu标记为“不再需要”然后删除,却不会自动安装新的替代者。结果就是:包管理器先卸了旧的GNU coreutils,然后在安装新的uutils coreutils的过程中,需要执行post-install脚本,而这些脚本要用到rm、cp、chmod等命令——可是它们已经被删了啊!经典的“你卸掉了梯子,却想让我下楼取快递”的困境。
其实当时我还纳闷了一下:我有这么久没更新吗,居然有这么多包要更新。屏幕上滚过了上百个包的下载和解包信息,直到这样一条报错显示到了终端中
1 | dpkg: warning: 'rm' not found in PATH or not executable |
等等,你告诉我什么没了?rm?
事故分析
似乎是因为我不正确的“更新”方式,原本的coreutils(在这个版本里面被称作coreutils-from-gnu)卸掉了,但是似乎coreutils-from-uutils的安装过程也需要用到rm等命令。这造成了一个死锁:包管理器想装新coreutils,但装新coreutils需要用到老coreutils的命令(通过postinst脚本)。更糟糕的是,dpkg自身的一些辅助脚本(比如配置触发器和post-invoke)也依赖这些基本命令。系统现在处于一个“缺胳膊少腿”但还能勉强维持呼吸的状态——SSH还连着,bash还能用,Python和Perl等解释器还在,glibc也在,但任何试图调用外部命令的行为都会失败。
核心问题:/bin、/usr/bin下除了shell内建命令和极少数静态编译的二进制,大部分都是coreutils提供的软链接。而/usr/bin/dpkg本身虽然还在,但它的子进程执行rm时,会在PATH中搜索——找不到,直接报错退出。
尝试解决
apt自救(失败)
1 | sudo apt remove coreutils-from-uutils --allow-remove-essential |
毫无疑问,失败了,原因是没有coreutils提供的rm等命令,dpkg自己的工具脚本无法展开。
本机编译coreutils(失败)
通过观察设备环境发现,git、make、gcc等这些工具还在,那么就还有希望(大概吧)
从清华源弄来一份coreutils源代码,打算现场编译,尝试自救。
1 | wget https://mirrors.tuna.tsinghua.edu.cn/gnu/coreutils/coreutils-9.11.tar.xz |
然而,问题就出在这里:configure是依赖于coreutils的。一个脚本文件怎么说都要或多或少用上几条命令吧,倒也可以理解。但具体到我这个场合就不能这么理解了。
复制来一份busybox (还是失败了)
这里简单提两句,busybox是一个更小型化的coreutils实现(甚至不止coreutils,比如bash,也可以由其提供),它把很多命令打包成一个二进制文件,通过busybox ls、busybox rm的方式调用,或者通过软链接来模拟各个命令。很多嵌入式系统和紧急救援环境都会预装busybox,因为它静态编译、依赖少、功能全。
GitHub上有热心人已经把各平台编译好的busybox二进制文件上传了。只需要下载适用于我们平台(香橙派,aarch64)的即可。
1 | wget https://raw.githubusercontent.com/xerta555/Busybox-Binaries/master/busybox-arm64 |
等等,发现问题了吗?
chmod也是coreutils提供的
这意味着直接复制、下载二进制文件的路都走不通了。因为下载下来的文件默认是没有可执行权限的(umask通常为644),而没有可执行权限就无法运行它,也就无法用busybox内部的chmod给自己加权限——又是一个鸡生蛋蛋生鸡的问题。我甚至尝试了另一种方式:指望动态链接器绕过可执行权限检查
1 | sudo /lib/ld-linux-aarch64.so.1 ./busybox-arm64 |
1 | ld-linux-aarch64.so.1: cannot execute ./busybox-arm64: EACCES |
内核的execve系统调用在加载ELF文件之前就会检查文件的执行权限,动态链接器只是解释ELF的程序头,它绕不过去。完蛋。
万能的Python救场
就在一筹莫展之际,设备上的Python3还能正常工作。它是由python3包提供的,那个包没有因为coreutils的缺失而被波及。而且Python的标准库中有os.chmod,这个函数直接调用C库的chmod(2)系统调用,根本不需要/usr/bin/chmod命令。
1 | python3 -c "import os; os.chmod('./busybox-arm64', 0o755);" |
救场成功!
事后分析:Python的os库的这些POSIX命令封装走的应该是Linux系统调用,或者是libc库的封装,而不是通过命令实现的,因此不受coreutils影响。
可喜可贺。
后续修复
有了 busybox 提供的临时 coreutils 之后,系统终于能喘口气了。/bin/rm、/bin/cp、/bin/chmod 这些软链接已经指向 busybox,虽然功能上不如 GNU 完整,但执行基本的文件操作绰绰有余。
接下来需要解决两个问题:
- 修复 dpkg 的半拉子状态 —— 之前
coreutils-from-gnu被卸载而coreutils-from-uutils安装失败,导致大量软件包的 postinst 脚本残留,dpkg 数据库处于不一致状态。 - 完成到 Ubuntu 26.04 LTS 的正经升级 —— 既然源已经指向了 26.04,而且系统已经有部分包被更新了,不如将错就错,彻底执行一次完整的
full-upgrade。
1 | sudo apt remove coreutils-from-uutils coreutils-from-gnu coreutils --allow-remove-essential |
这次没有再出现 rm not found 的恐怖提示。屏幕上滚过大量升级信息,包括内核、systemd、GNOME 组件等。算是升级到了Ubuntu 26 LTS(resolute)
事后我检查了 /etc/os-release,确认 VERSION_ID="26.04"。折腾一圈,居然从 jammy 误打误撞升级到 resolute ,而且全程没重装系统。虽然过程惊险,但结局不算太糟。
事后诸葛亮:手搓一个 chmod
这次事故中,一个很抽象的点在于下载了busybox却没法给它加执行权限。其实在千钧一发之际,有经验的运维老手可能会想到一个更底层的办法:绕过所有用户态命令,直接通过系统调用修改文件权限。
在 Linux 上,修改文件权限的系统调用是 chmod(const char *pathname, mode_t mode)。C 语言标准库(glibc)提供了封装,但只要写一个极简的 C 程序,调用 syscall(SYS_chmod, path, mode) 就行,编译出的二进制不依赖任何外部命令(当然,前提是系统还有 gcc 和 glibc 头文件)。具体代码可以像这样:
1 |
|
保存为 chmod.c,然后用 gcc chmod.c -o chmod 编译。如果幸运,gcc 还在且能工作(实际上一开始我确认过 gcc 没挂),就能得到一个可以给 busybox 加执行权限的救命稻草。有了它,无需 Python,直接 ./chmod 然后 ./busybox-arm64 --install -s,一样能反败为胜。
为什么当时没想到? 因为大脑在紧急状态下更倾向于「已知的、用过的方法」(比如 Python 的 os.chmod),而不是从零写代码。另外,大多数运维人员的肌肉记忆是「命令缺了就找 busybox / static binary」,极少会想到现场编译一个系统调用胶水程序。这其实是个思维盲区:我们太依赖现成的工具链,而忘记了操作系统内核就在那里,系统调用是永远可用的最终保障。
当然,这个方案也有前提:gcc 能跑、glibc 头文件还在、系统调用表没被阉割。在本次事故中,这些条件全部满足。如果连 gcc 都没了,那就真的只能靠 Python 或者手写汇编了 —— 不过那就属于另一个层级的灾难了。
本文根据2026年6月14日下午的真实运维事故改编,以此来记住这次教训。