首页 / 资讯中心 / 文章详情

snapd 安装致 yum 崩溃:OpenSSL 符号冲突与依赖修复

snapd 安装致 yum 崩溃:OpenSSL 符号冲突与依赖修复 ★ FEATURED ARTICLE
装 snap 环境本来是件很轻的事几条命令的事结果我在一台业务机上执行完sudo yum install snapd还没等到装完的提示符回落随手再敲一个yum就见到了这么一行东西libk5crypto.so.3: undefined symbol: EVP_KDF_ctrl, version OPENSSL_1_1_1。那一瞬间的感觉熟悉的人都懂——SSH 还连着命令还能敲但整台机器的包管理器已经废了yum起不来rpm之外几乎没有趁手的工具想装什么都装不了。这个报错表面上看是 OpenSSL 的符号问题根子上却是依赖链上的一次错位某个库被换成了另一套 ABI而调用方还按老规矩要符号。这篇内容我打算把这条链路从头拆到尾包括我是怎么在没有yum的情况下把系统救回来的、每一步为什么这么选、以及装带第三方依赖的包时哪些操作我后来再也不敢图省事。适合手里管着 7 系企业发行版、经常要跟 EPEL 之类扩展源打交道的人看也适合刚接手老机器、还没被依赖地狱教育过的新手。1. 先把报错读薄一行错误信息里的四个独立信息点1.1 EVP_KDF_ctrl 是 OpenSSL 1.1.1 才有的符号EVP_KDF_ctrl这个函数不是 OpenSSL 的元老它是 1.1.1 这条线上才正式暴露出来的 KDF密钥派生函数控制接口。1.0.2 时代的 OpenSSL 做密钥派生基本靠PKCS5_PBKDF2_HMAC这类散装函数没有统一的 EVP_KDF 抽象到了 3.0这套接口又被重构掉了EVP_KDF_ctrl被EVP_KDF_CTX_set_params和EVP_KDF_derive取代等于说这个符号的生命周期就压在 1.1.1 这一个版本带上。这一点非常关键因为它决定了排查方向报错说这个符号找不到说明libk5crypto.so.3在链接时是照着 1.1.1 的头文件和符号表编出来的但运行时真正加载进来的那份libcrypto并不提供它。可能是加载到了一份 1.0.2 改名过来的库也可能是一份编译时裁掉了相关特性的 1.1.x还可能是路径污染导致载入了错误的一份文件。三种情况表现一致成因完全不同后面的定位手段也不一样。我见过有人一上来就怀疑是openssl 装了两份然后去find / -name libcrypto*满盘搜搜出一堆结果反而更乱。正确的顺序是先确认调用方要什么再去确认提供方给了什么而不是先去找文件。1.2 libk5crypto 为什么会跑去向 libcrypto 要符号libk5crypto.so.3是 krb5 这套 Kerberos 实现里的加密支撑库负责 AES、DES 之类的算法封装和密钥派生。krb5 从 1.17 往后逐步把内部的加密实现往 OpenSSL 上靠一部分密钥派生逻辑直接改用了 OpenSSL 的 EVP_KDF 接口于是libk5crypto对libcrypto的符号依赖就从一个软依赖变成了硬绑定——版本对不上直接起不来。普通用户平时感知不到这一层是因为 Kerberos 只在ssh、curl、yum这些工具需要做 GSSAPI 认证时才会被拉起来。而 yum 恰好走的是pycurl→libcurl→libkrb5这条链libcurl在编译时通常带上了 GSSAPI 支持所以每次启动 yum动态链接器都会把libkrb5.so.3和libk5crypto.so.3一起载入。krb5 一崩yum 跟着崩。这也解释了一个很反直觉的现象明明报错跟 Kerberos 八竿子打不着你也没配过任何 KDC为什么 yum 会被 Kerberos 的库拖死。因为这不是用到才加载而是链接了就加载。1.3 version OPENSSL_1_1_1 是符号版本不是文件名标题里那一串被截断成了version OPENSSL_1实际完整的通常是version OPENSSL_1_1_1或OPENSSL_1_1_0。这个version后面跟的东西是 ELF 的符号版本标记是 GNU 工具链在做符号版本化时写进去的跟你机器上装了几个 openssl 文件没关系。很多人卡在这一步以为报错是在说你要 OPENSSL_1 这个文件然后去搜find / -name OPENSSL_1*当然什么都找不到。这里要建立的直觉是ELF 的符号版本是一张表.gnu.version_r段里记着我需要 libcrypto.so.1.1 这个对象提供版本为 OPENSSL_1_1_1 的那批符号。动态链接器会拿着这张表去目标库里核对版本对不上就报 undefined symbol。想看这张表长什么样readelf -V /usr/lib64/libk5crypto.so.3就能看到完整的版本需求列表里面会明确写着需要哪个库、哪个版本节点、涉及哪些符号。这条命令在后面的定位过程中是我用得最多的一条。1.4 为什么偏偏是 yum 先倒下而不是 ssh这里有个容易被忽略的细节ssh 也用 GSSAPI但 ssh 通常只在真正发起 GSSAPI 认证时才去dlopen相关模块而且很多发行版的 ssh 编译时把 GSSAPI 做成了可选项。yum 不一样它是个 Python 脚本跑起来第一步就是 import 一堆 C 扩展pycurl.so是其中之一链接器在进程启动阶段就把整条依赖链拉起来了没有任何延迟加载的余地。我当时的实际验证方法是ldd /usr/lib64/python2.7/site-packages/pycurl.so7 系发行版的 yum 是 Python 2 写的输出里能清清楚楚看到libcurl.so.4→libkrb5.so.3→libk5crypto.so.3→libcrypto.so.1.1这条链路。顺带说一句head -1 $(which yum)能看到 shebang 指向/usr/bin/python确认它确实是 Python 2 脚本这样你就知道该去哪个 site-packages 目录下找扩展模块。2. 装 snapd 这一步到底动了哪块地基2.1 依赖解析的真实链条snapd → krb5-libs → openssl-libssnapd 本身并不是个轻量包它要管理 squashfs 镜像、要挂 FUSE、要跟 systemd 打交道从扩展源装的时候往往会顺带拉一堆依赖。在 7 系发行版上一次yum install snapd的依赖树里krb5-libs被提升版本是相当常见的结果——不是因为 snapd 直接依赖 Kerberos而是依赖树里某个中间包认证、远程拉取、策略相关声明了更高版本的 krb5。问题就出在这个顺带提升上。老机器的openssl-libs常年停在 1.0.2 那条线上很多业务组件是照着这个版本编译的运维出于稳定考虑也不敢随便动它。于是形成了一种很别扭的状态krb5-libs被拉到了一个新版本这个新版本是照着 OpenSSL 1.1.1 编译的而openssl-libs还老老实实待在 1.0.2。两边要求的 ABI 不一致符号自然对不上。用一条命令就能把这件事看清楚rpm -qp --requires krb5-libs-*.rpm | grep -i -E openssl|libcrypto看它到底声明了对哪个 soname 的依赖。如果输出里出现libcrypto.so.1.1那就说明这版 krb5 是奔着 1.1.1 去的跟 1.0.2 系统的libcrypto.so.10完全不是一个世界。2.2 两种归因库被覆盖了还是需求被拉高了同样是undefined symbol底层成因其实分两类处理方式差别很大。第一类是需求被拉高调用方这里是libk5crypto换成了新版本它要新符号而系统里根本没有对应版本的库。这种情况下ldd能正常解析到文件readelf -V也能看到版本需求只是目标库里那个版本节点不存在或者没有这个符号。我遇到的多数是这一类。第二类是提供方被覆盖系统里本来有一份正确的libcrypto.so.1.1结果被某个第三方包用另一份文件顶掉了路径没变、文件名没变内容变了。这种更阴险因为rpm -q查出来的包名看起来没问题但文件的实际内容已经被替换。区分方法很直接先rpm -qf /usr/lib64/libcrypto.so.1.1看归属再用rpm -V校验这个包的文件完整性。如果rpm -V openssl-libs输出的那几行里libcrypto.so.1.1前面出现大小写或 MD5 不一致的标记那就是文件被动了手脚提供方被覆盖这条归因就坐实了。我当时这台机器的情况是前者krb5-libs从 1.15 被拉到了 1.18而openssl-libs纹丝没动。所以修复方向不是找回被覆盖的库而是把 krb5 拉回跟系统 openssl 匹配的版本。2.3 用三条证据把归因钉死排查这类问题我最怕的就是猜所以固定跑三条命令跑完基本不会有第二种解释。第一条rpm -qa | grep -E krb5|openssl | sort把当前装着的所有相关包和版本一次性列清楚。别小看这一步我遇到过装了openssl11-libs这种并行包的情况名字里带 openssl 但提供的是另一套路径下的库不列出来根本想不到。第二条readelf -V /usr/lib64/libk5crypto.so.3 | head -40看它需要的版本节点。如果这里写的是OPENSSL_1_1_1而系统openssl-libs是 1.0.2那结论已经出来了不用再往下猜。第三条ls -l /usr/lib64/libcrypto*加readelf -V --dyn-syms /usr/lib64/libcrypto.so.1.1 | grep -i kdf确认这个文件到底存不存在、导不导出目标符号。这里有个坑要注意如果文件根本不存在报错会是cannot open shared object file而不是undefined symbol。既然报的是后者说明文件在只是内容不对——这条推理能帮你省掉很多弯路。把这三条的输出放在一起看归因基本就是唯一的了。我在实际排查里还会再加一条ls -l --time-stylefull-iso /usr/lib64/libcrypto*看文件修改时间是不是跟那次yum install的时间吻合吻合的话因果关系就更明确了。3. 应急与修复三条路线的取舍3.1 为什么我不建议第一时间升级 openssl-libs看到符号缺失很多人的第一反应是那就把 openssl 升上去。在开发机上这么干顶多折腾半天在生产机上我强烈不建议尤其当这台机器跑着一堆老业务组件的时候。原因是 OpenSSL 从 1.0.2 到 1.1.1 不是打个补丁那么简单大量老 API 被标记废弃甚至行为变更结构体变成了不透明指针很多照着 1.0.2 编译的二进制直接跑不起来。一旦你把系统openssl-libs从 1.0.2 换成 1.1.1受影响的不只是 krb5openssh、Python的 ssl 模块、各种语言的加密扩展、数据库客户端全都会跟着抖一遍。更麻烦的是这种破坏是弥散性的不会一次性全暴露可能你今天修好了 yum明天备份脚本跑不起来后天某个监控探针连不上。还有一个隐藏风险系统里如果同时存在openssl-libs和另外装的并行版本比如某些仓库提供的openssl11-libs两者会争抢/usr/lib64下的软链接和ld.so.cache谁最后被ldconfig扫到就用谁。这种不确定性在排障阶段是最要命的。我的判断标准很朴素当系统的根基库版本和业务组件的编译基线一致时不要为了让一个新装的软件跑起来去动根基库应该反过来把那个新软件依赖的东西降回基线。3.2 路线一把 krb5-libs 降回与系统匹配的版本这是我最终选的方案也是在 7 系老机器上最稳妥的一条。核心思路是让krb5-libs回到那个照着 1.0.2 编译的版本符号需求自然就消失了。具体操作要连带着把同版本族的包一起降krb5-libs、krb5-workstation、libkadm5这三个通常是同一次构建出来的版本必须一致只降一个会引发新的依赖冲突。命令形如rpm -Uvh --oldpackage \ krb5-libs-1.15.1-50.el7.x86_64.rpm \ krb5-workstation-1.15.1-50.el7.x86_64.rpm \ libkadm5-1.15.1-50.el7.x86_64.rpm--oldpackage这个参数是必须的不加的话 rpm 会认为你在降级而拒绝执行报package ... is already installed之类的错。这个参数的意思就是我知道我在往回退别拦我。降级之前一定先rpm -q --whatrequires krb5-libs看一眼有谁依赖它确认没有哪个业务包硬性要求 1.18 以上。这一步不能省我见过有人降完 krb5 发现某个自研的认证组件起不来了因为它编译时用了 1.18 的新接口。3.3 路线二从本地介质补回正确的 openssl-libs如果评估下来确实需要 1.1.1比如业务侧已经有一批组件在用它那就走这条路从发行版的安装介质或者内部镜像源里取出对应版本的openssl-libs用rpm -Uvh --replacepkgs覆盖安装。这里要特别注意--replacepkgs和--force的区别。--replacepkgs是即使已安装也重新装一遍行为可控--force是--replacepkgs加--replacefiles加--oldpackage的组合它会无视文件冲突直接覆盖副作用大得多。我在生产环境基本只用前者后者留到实在没辙的时候。还有一个细节覆盖安装之后必须跑一次ldconfig让/etc/ld.so.cache重新生成。很多人装完发现没生效就是因为缓存里还指着旧的 inode。判断方法很简单ldconfig -p | grep libcrypto看看列出来的路径和实际文件对不对得上。3.4 路线三先把 snapd 拆干净再谈恢复有时候最省事的做法是釜底抽薪把 snapd 及它带进来的一串依赖整个卸掉让系统回到装之前的状态。问题在于yum已经不能用了只能用rpm -e硬卸。而rpm -e snapd会因为依赖关系被拒绝得加--nodeps跳过依赖检查。这一步要格外小心因为--nodeps卸掉的可能不只是 snapd还可能把某个被别的东西依赖的库一起带走导致更多命令失效。我的习惯是先rpm -e --test snapd做一次演练看 rpm 打算连带卸掉哪些包把清单打印出来逐条确认。确认没问题再真正执行。--test这个参数救过我不少次它只做依赖推演不实际动手成本极低但价值很高。卸完之后别急着装别的先rpm -Va跑一遍全量校验看看有没有别的包的文件在这次折腾里被改坏了。这个命令输出可能会很长重点看那些标记为missing或者MD5 differ的行。4. 完整修复实录从工具崩掉到 yum 复活4.1 开工前的三分钟备份在动任何系统库之前我会做三件事加起来不超过三分钟但这三分钟能救命。第一件把当前所有相关包的版本信息落盘rpm -qa | grep -E krb5|openssl|curl|python /root/pkg-baseline-$(date %F).txt。这份文件是回滚时的对照表比凭记忆靠谱一百倍。第二件把要被覆盖的库文件复制一份到/root/libbackup/包括libcrypto.so.1.1、libssl.so.1.1、libk5crypto.so.3以及它们的软链接。注意要复制软链接指向的真实文件不能只复制符号链接本身否则回滚时链接指向的旧 inode 早就没了。第三件确认手上还有一条能进机器的通道。SSH 会话别关另开一个tmux或screen会话挂着。因为一旦你降级 krb5 失败sshd有可能因为 PAM 或 GSSAPI 相关的库变动而拒绝新连接——老连接还活着新连接进不来这时候你会非常感谢那个没关掉的会话。4.2 用 rpm 做降级或覆盖安装的完整命令我在那台机器上的实际操作顺序是这样的。先确认本地有没有可用的 rpm 包没有的话从内部镜像源取取包用的是wget而不是curl——这个选择有讲究后面第 5 节会细说。拿到包之后先做一次安装演练rpm -Uvh --test --oldpackage krb5-libs-1.15.1-50.el7.x86_64.rpm--test会告诉你这次操作会不会引发文件冲突、会不会破坏依赖但不会真的写盘。输出里如果有file /usr/lib64/xxx from install conflicts with file from package yyy那就是有冲突得先把冲突的包处理掉。演练通过后正式执行然后把同族的另外两个包一起补上。整套动作做完立刻验证ldd /usr/lib64/libk5crypto.so.3看它现在链接的是libcrypto.so.10还是libcrypto.so.1.1。如果变成了libcrypto.so.10说明降级生效了。最后一步是ldconfig然后yum --version试探一下。我第一次跑的时候还是报错因为yum的 Python 字节码缓存里还留着旧的模块引用rm -rf /var/cache/yum/*加上yum clean all如果能跑起来的话之后就正常了。4.3 ldconfig 和缓存清理里最容易踩的两个坑第一个坑是顺序。必须先装包、再ldconfig不能反过来。我见过有人装完包忘了跑ldconfig然后反复排查为什么文件明明换了还是报错折腾了半小时。判断缓存是否更新很简单ldconfig -p | grep -E libcrypto|libk5crypto看输出的路径和时间戳。第二个坑是**ld.so.conf.d里的残留**。如果之前有人为了用某个并行版本的库在/etc/ld.so.conf.d/下加过一条路径比如指向某个第三方目录那么ldconfig重扫的时候会把这个目录里的库也纳入搜索范围优先级可能压过/usr/lib64。这种情况下你换多少次包都没用因为加载的根本不是你以为的那个文件。检查方法是cat /etc/ld.so.conf.d/*.conf逐条看有可疑的就注释掉再ldconfig。还有一个更隐蔽的情况环境变量LD_LIBRARY_PATH被写进了某个 profile 文件里。这个变量优先级比ld.so.cache高一旦被设上全局生效。排查时echo $LD_LIBRARY_PATH加grep -r LD_LIBRARY_PATH /etc/profile.d/两下就能查清。4.4 修完之后必须跑的验证清单yum 能跑起来不等于修好了。我固定跑下面这几条全绿才算收工。验证项命令预期结果依赖链完整性ldd $(which python) | grep not found无输出krb5 库链接版本ldd /usr/lib64/libk5crypto.so.3指向libcrypto.so.10包文件完整性rpm -Va | grep -E krb5|openssl无输出或仅配置文件差异包管理器可用性yum repolist正常列出仓库Kerberos 工具链klist -V正常打印版本SSH 新连接另开会话登录能正常登录这里重点说两条。rpm -Va的输出里如果某些行的标记只有c配置文件变更那是正常的如果出现missing或者5MD5 不符说明包文件确实被改坏了需要yum reinstall那个包。klist -V是个很多人不知道的小工具它能验证 krb5 整套库是否自洽比单纯看ldd更有说服力。5. 工具链崩了怎么自救断网状态下的应急下载5.1 wget 为什么常常还能用这是我在这次事故里最有价值的发现。yum挂了、curl挂了因为curl也走libcurl→libkrb5这条链但wget往往还能正常工作。原因在于实现路径不同curl是基于libcurl这个共享库的而libcurl又链接了libkrb5wget则是一个自包含的二进制直接用 OpenSSL 或 GnuTLS 做 TLS不经过libcurl自然也就不会被 krb5 的符号问题牵连。这个差别平时看不出来出事的时候就是救命稻草。我当时就是靠wget从内部镜像把krb5-libs的降级包拉下来的全程没有碰curl和yum。后来我专门验证过ldd $(which wget) | grep -E krb5|curl确实没有任何输出。同样的道理python3如果没链接 GSSAPI 相关模块也能用来做简单的 HTTP 下载。python3 -c import urllib.request; urllib.request.urlretrieve(http://内部源/xxx.rpm,xxx.rpm)这一行在关键时刻同样管用。但要注意某些发行版的 Python 是带着 ssl 模块链接libssl的ldd一下确认更稳妥。5.2 从本地源或安装介质取包如果wget也不行那就只能走离线路线。发行版的完整安装介质里通常带着全套基础包挂载之后可以直接从Packages目录取。mount -o loop /path/to/install.iso /mnt ls /mnt/Packages | grep -E ^krb5-libs-1.15 rpm -Uvh --oldpackage /mnt/Packages/krb5-libs-1.15.1-50.el7.x86_64.rpm这里有个经验介质里的包版本要跟你系统的基线版本对得上。7 系的发行版迭代了很多个小版本1.15.1-37和1.15.1-50虽然都叫 1.15.1但打过的补丁不一样。降级的目标应该是跟当前系统其他组件兼容的那个版本不是随便一个 1.15.1。判断办法是对照/etc/redhat-release或者rpm -q --qf %{VERSION}-%{RELEASE}\n看系统基线。5.3 一个可复用的临时救急思路除了换工具还有一个思路值得记住用LD_PRELOAD临时指定库。比如系统里有两份libcrypto一份在/usr/lib64一份在/opt/xxx/lib你可以这样临时让某个命令用指定的那份LD_PRELOAD/opt/xxx/lib/libcrypto.so.1.1 yum repolist但这条路我把它明确定义为临时验证手段不是修复方案。原因有三第一LD_PRELOAD会把指定的库强制注入进程如果这个库本身又有依赖可能引发更细的连锁问题第二它只对当前命令有效不改系统状态重启或者换个 shell 就失效第三它容易掩盖真正的问题让人误以为修好了。真正有价值的用法是用它快速验证换一份正确的库是不是就好了验证通过之后再按正规流程去装包、改配置、跑ldconfig。诊断和修复分开这个习惯在系统层面排障里特别重要。6. 事后复盘几条被这次事故教育出来的硬习惯6.1 装带扩展源的包之前先看依赖树这次事故之后我给自己定了条规矩凡是装来自扩展源或第三方的包先跑yum install --assumeno 包名或者yum deplist 包名把依赖树看一遍再决定。--assumeno会在解析完依赖、打印出完整的将要安装/升级/降级的包列表之后停下来问你要不要继续你答 no 就什么都不会发生。这个参数的价值在于它把依赖解析和实际写入分开了让你有机会在动手之前看到全貌。那次如果我先跑了这一步就会看到krb5-libs从 1.15 升到 1.18 这一行然后立刻反应过来这个升级会牵动 openssl 的 ABI后面的麻烦全都可以避免。代价只是多看几行输出这笔账怎么算都划算。对于特别关键的生产机我还会加一层保险先在测试机上跑一遍完整的安装rpm -qa | sort after.txt跟安装前的before.txt做 diff看看除了预期的包之外还有哪些被顺带动了。这种版本漂移往往是事故的源头。6.2 rpm -Va 和 rpm -qf 应该成为日常命令rpm -qf是这个文件属于哪个包rpm -Va是所有包的文件有没有被改过。这两条我现在的使用频率比yum install还高。rpm -Va的输出格式是一串标记位加文件名标记位的含义是S大小变了、M权限变了、5MD5 变了、D设备号变了、L链接变了、U用户变了、G组变了、T时间戳变了、P能力变了c表示这是配置文件这类变更通常正常missing表示文件丢了。日常巡检时重点看两类一是非c标记的5二进制文件被改可疑二是missing文件被删可能是被人手动清理过。我在一台跑了三年的老机器上就靠这个发现过一个被误删的libssl软链接当时没人报故障但某天重启后某个服务起不来就是这个原因。rpm -qf的用法更直接遇到不认识的文件rpm -qf /usr/lib64/xxx.so一看就知道来路。如果返回file ... is not owned by any package那这个文件就是野的要么是手工编译装的要么是别的工具写进去的两种情况都值得深究。6.3 把系统的版本基线记录下来我现在管的所有机器都有一个/root/.baseline/目录里面放着几个文件pkg-list.txtrpm -qa的完整输出、repo-list.txt各仓库的配置文件备份、ld-conf.txt/etc/ld.so.conf.d/的全部内容。这些文件在系统初始化时生成一次之后每次计划内的变更都更新一次。这东西的价值在于出事时的对照。diff baseline/pkg-list.txt (rpm -qa | sort)一条命令就能看出从基线到现在到底有哪些包被动了比靠yum history靠谱得多因为yum history只记录 yum 操作的用 rpm 直接装的它看不到。说到yum history它确实是个好工具yum history list能看到历史事务yum history info ID能看到某次事务的完整明细yum history undo ID甚至能回滚。但它有一个前提yum 得能跑。而这次事故里 yum 恰恰是挂掉的所以不能把希望全寄托在它身上。6.4 一个我现在的操作习惯最后分享一个我现在养成的习惯在任何一台机器上装会引入新依赖树的软件之前先开两个终端一个用来操作一个用来随时准备救场。救场那个终端什么都不干就挂着tmux里面提前cd /root/libbackup并把关键库的备份路径准备好。听起来有点小题大做但真出问题的时候这个准备好的第二终端能省掉大量慌乱。因为一旦主终端里的包管理器崩了你最需要的其实是冷静——而冷静的前提是知道退路在哪。我这次能在二十分钟内修好很大程度上是因为备份和会话都是提前准备好的剩下的只是执行。至于 snapd 本身那次修好之后我还是装上了但换了种方式先在测试机上完整走一遍依赖解析确认它不会再动krb5和openssl的版本才在生产机上执行。装完之后立刻跑一遍第 4.4 节那张验证表全绿才算完事。多花十分钟换来的是一整晚的安稳。
阅读完成 · 觉得有帮助?
咨询建站