长期以来,Linux 用户空间动态链接器(ld.so)中的 $ORIGIN 变量一直是开发者管理共享库依赖的利器:它允许可执行文件或共享库在运行时根据自身所在目录动态定位其他依赖,极大简化了可移植二进制包的部署。然而,这一便利从未进入内核空间。近日,Linux 内核社区合并的一项补丁打破了这一局面——内核将开始理解 $ORIGIN,但仅限于模块加载领域,且带有若干限制。这项变更被部分开发者戏称为“半支持”,但依然有望为内核模块的打包、依赖管理和容器化场景带来实质性改进。
内核为何需要 $ORIGIN?
传统上,内核模块(.ko 文件)的依赖通过 depmod 工具生成的 modules.dep 映射来管理。当模块 A 依赖模块 B 时,必须保证 B 位于标准模块路径(如 /lib/modules/$(uname -r)/)下,且文件名与依赖声明一致。这一模式在单根文件系统的通用发行版中工作良好,但在定制化内核、嵌入式系统或容器化环境中却问题频出——开发者往往需要手动创建符号链接、打包额外依赖,或者修改模块安装路径。
此外,内核固件(firmware)的加载也面临类似痛点。现代驱动经常需要从文件系统获取固件 blob,而固件的默认搜索路径是固件目录,若驱动与固件位于非标准位置(如 /opt/vendor-firmware/),则必须借助内核参数或 udev 规则额外指定。$ORIGIN 的引入有望让模块和固件引用变得“自描述”:模块可以在其软依赖或固件请求中指定 $ORIGIN/relative/path,内核以此动态解析。
补丁如何工作?
该补丁由 Linux 内核模块加载子系统维护者提交,目前仅作用于内核模块的软依赖(MODULE_SOFTDEP 宏)和固件请求(request_firmware 系列函数)。具体机制如下:
- 当内核解析模块的
MODULE_SOFTDEP声明时,若检测到字符串中包含$ORIGIN,则将其替换为该模块文件自身所在的目录路径(绝对路径)。然后内核会在该目录下搜索对应的依赖模块,而不需要依赖modules.dep。 - 对于固件请求,内核在调用
request_firmware时同样会展开$ORIGIN,允许驱动在初始化代码中传递诸如$ORIGIN/../firmware/foo.bin的路径。
补丁特别强调了两点限制:其一,$ORIGIN 仅在模块加载或固件请求过程中由内核展开,不会影响用户空间的任何链接行为;其二,展开后的路径必须位于一个受控的文件系统命名空间中,目前仅支持基于 dentry 的绝对路径解析,且会进行路径遍历检查以防止符号链接攻击。
有限支持背后的考量
标题中的“sort of”恰当地反映了此次变更的谨慎态度。内核开发者明确指出,$ORIGIN 不会成为内核的通用环境变量解释机制,也不会扩展到模块的普通依赖(MODULE_DEPEND)。原因在于,若允许模块在 MODULE_DEPEND 中使用 $ORIGIN,将破坏 depmod 的静态分析能力——内核无法在构建时预知模块的运行时位置,从而无法生成正确的依赖图。此外,安全团队对 $ORIGIN 的用户空间使用历史心有余悸:setuid 二进制文件配合恶意 $ORIGIN 路径可能导致权限提升。内核内部对此类问题更为敏感,因此补丁在代码中显式拒绝任何来自非可信上下文的 $ORIGIN 解释(例如通过 sysfs 用户写入的路径)。
实际影响与未来展望
尽管支持范围有限,该特性已引起发行版维护者和嵌入式开发者的关注。对于 Flatcar、Fedora CoreOS 等不可变操作系统,将驱动固件与内核模块打包在同一目录下并利用 $ORIGIN 引用,可以避免对全局固件目录的写操作,提升安全性。对于 Kubernetes 中的设备插件或 GPU 驱动,模块及其依赖可以随着容器镜像分发,无需挂载宿主的内核模块目录。
未来,社区可能进一步扩展 $ORIGIN 到内核的其他路径请求场景,如 BCachefs 或 Btrfs 的用户空间工具辅助加载、内核回调的用户态帮助程序(如 modprobe 的替代路径)。但无论如何,内核开发者已表态:不会提供像用户空间 ld.so 那样完整的变量展开机制,因为内核应当保持简单和安全。
此次补丁预计将随 Linux 6.12 或 6.13 主线合并。对于苦于模块路径管理的开发者而言,这或许只是“半杯水”,但足够解一时之渴。