背景
最近碰到一個問題,有個應用在啟動的時候一直報錯,錯誤信息如下:
java: error while loading shared libraries: libjli.so: cannot open shared object file: No such file or directory
錯誤信息是說 java 應用加載不到 libjli.so 文件,我們使用 java -version
命令,同樣的錯誤又出現了。使用 ldd 命令查看一下 java 應用是否加載了這個 so 文件,發現 java 應用加載的 so 文件中存在 libjli.so。
$ ldd java
linux-vdso.so.1 => (0x00007ffe2a9c7000)
/usr/local/lib/libsysconfcpus.so (0x00002ac503ca8000)
libz.so.1 => /lib64/libz.so.1 (0x00002ac503eaa000)
libjli.so => /apps/svr/jdk-14.0.1/bin/./../lib/libjli.so (0x00002ac5040c0000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00002ac5042d1000)
libdl.so.2 => /lib64/libdl.so.2 (0x00002ac5044ee000)
libc.so.6 => /lib64/libc.so.6 (0x00002ac5046f2000)
/lib64/ld-linux-x86-64.so.2 (0x00002ac503883000)
我們接着查看了 LD_LIBRARY_PATH
和 /etc/ld.so.conf.d/xxx.conf
文件的配置,發現都是正常的。通過對比其他應用的啟動配置,發現該應用使用了 80 端口啟動,但是我們的容器只能使用 apps 權限登錄,所以在啟動前使用 setcap
命令提升了 java 應用的權限,允許其使用 80 端口,會不會是這個操作導致的呢?在查看原因之前,我們需要先理解幾個概念。
Linux 動態庫
動態庫(共享庫)的代碼在可執行程序運行時才載入內存,在編譯過程中僅簡單的引用,不同的應用程序如果調用相同的庫,那么在內存中只需要有一份該動態庫(共享庫)的實例。這類庫的名字一般是libxxx.so,其中so是 Shared Object 的縮寫,即可以共享的目標文件。在鏈接動態庫生成可執行文件時,並不會把動態庫的代碼復制到執行文件中,而是在執行文件中記錄對動態庫的引用。
Linux下生成和使用動態庫的步驟如下:
- 編寫源文件。
- 將一個或幾個源文件編譯鏈接,生成共享庫。
- 通過
-L -lxxx
的gcc選項鏈接生成的libxxx.so。例如gcc -fPIC -shared -o libmax.so max.c
,-fPIC
是編譯選項,PIC是 Position Independent Code 的縮寫,表示要生成位置無關的代碼,這是動態庫需要的特性;-shared
是鏈接選項,告訴gcc生成動態庫而不是可執行文件 - 把libxxx.so放入鏈接庫的標准路徑,或指定
LD_LIBRARY_PATH
,才能運行鏈接了libxxx.so的程序。
Linux是通過 /etc/ld.so.cache
文件搜尋要鏈接的動態庫的。而 /etc/ld.so.cache
是 ldconfig 程序讀取 /etc/ld.so.conf
文件生成的。
(注意, /etc/ld.so.conf
中並不必包含 /lib
和 /usr/lib
,ldconfig
程序會自動搜索這兩個目錄)
我們把要用的 libxx.so 文件所在的路徑添加到 /etc/ld.so.conf
中,再以root權限運行 ldconfig
程序,更新 /etc/ld.so.cache
,程序運行時,就可以找到 libxx.so
。另外就是通過配置 LD_LIBRARY_PATH
的方式來指定通過某些路徑尋找鏈接的動態庫。
ldd 查看程序依賴
理解了動態庫的概念之后,當碰到某個程序報錯缺少某個庫文件時,我們應該怎么查看該程序當前加載了哪些庫文件呢?可以用 ldd
命令。
ldd 命令的作用是用來查看程式運行所需的共享庫,常用來解決程式因缺少某個庫文件而不能運行的一些問題。
例如:查看test程序運行所依賴的庫:
[root@localhost testso]# ldd /etc/alternatives/java
linux-vdso.so.1 => (0x00007ffde15f8000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f03f2f8d000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f03f2d89000)
libc.so.6 => /lib64/libc.so.6 (0x00007f03f29bb000)
/lib64/ld-linux-x86-64.so.2 (0x00007f03f33ab000)
- 第一列:程序需要依賴什么庫
- 第二列: 系統提供的與程序需要的庫所對應的庫
- 第三列:庫加載的開始地址
通過上面的信息,我們可以得到以下幾個信息:
- 通過對比第一列和第二列,我們可以分析程序需要依賴的庫和系統實際提供的,是否相匹配
- 通過觀察第三列,我們可以知道在當前的庫中的符號在對應的進程的地址空間中的開始位置
如果依賴的某個庫找不到,通過這個命令可以迅速定位問題所在.
Linux capability
從內核 2.2 開始,Linux 將傳統上與超級用戶 root 關聯的特權划分為不同的單元,稱為 capabilites。Capabilites 作為線程(Linux 並不真正區分進程和線程)的屬性存在,每個單元可以獨立啟用和禁用。如此一來,權限檢查的過程就變成了:在執行特權操作時,如果進程的有效身份不是 root,就去檢查是否具有該特權操作所對應的 capabilites,並以此決定是否可以進行該特權操作。
下面是從 capabilities man page 中摘取的 capabilites 列表:
capability 名稱 | 描述 |
---|---|
CAP_AUDIT_CONTROL | 啟用和禁用內核審計;改變審計過濾規則;檢索審計狀態和過濾規則 |
CAP_AUDIT_READ | 允許通過 multicast netlink 套接字讀取審計日志 |
CAP_AUDIT_WRITE | 將記錄寫入內核審計日志 |
CAP_BLOCK_SUSPEND | 使用可以阻止系統掛起的特性 |
CAP_CHOWN | 修改文件所有者的權限 |
CAP_DAC_OVERRIDE | 忽略文件的 DAC 訪問限制 |
CAP_DAC_READ_SEARCH | 忽略文件讀及目錄搜索的 DAC 訪問限制 |
CAP_FOWNER | 忽略文件屬主 ID 必須和進程用戶 ID 相匹配的限制 |
CAP_FSETID | 允許設置文件的 setuid 位 |
CAP_IPC_LOCK | 允許鎖定共享內存片段 |
CAP_IPC_OWNER | 忽略 IPC 所有權檢查 |
CAP_KILL | 允許對不屬於自己的進程發送信號 |
CAP_LEASE | 允許修改文件鎖的 FL_LEASE 標志 |
CAP_LINUX_IMMUTABLE | 允許修改文件的 IMMUTABLE 和 APPEND 屬性標志 |
CAP_MAC_ADMIN | 允許 MAC 配置或狀態更改 |
CAP_MAC_OVERRIDE | 覆蓋 MAC(Mandatory Access Control) |
CAP_MKNOD | 允許使用 mknod() 系統調用 |
CAP_NET_ADMIN | 允許執行網絡管理任務 |
CAP_NET_BIND_SERVICE | 允許綁定到小於 1024 的端口 |
CAP_NET_BROADCAST | 允許網絡廣播和多播訪問 |
CAP_NET_RAW | 允許使用原始套接字 |
CAP_SETGID | 允許改變進程的 GID |
CAP_SETFCAP | 允許為文件設置任意的 capabilities |
CAP_SETPCAP | 參考 capabilities man page |
CAP_SETUID | 允許改變進程的 UID |
CAP_SYS_ADMIN | 允許執行系統管理任務,如加載或卸載文件系統、設置磁盤配額等 |
CAP_SYS_BOOT | 允許重新啟動系統 |
CAP_SYS_CHROOT | 允許使用 chroot() 系統調用 |
CAP_SYS_MODULE | 允許插入和刪除內核模塊 |
CAP_SYS_NICE | 允許提升優先級及設置其他進程的優先級 |
CAP_SYS_PACCT | 允許執行進程的 BSD 式審計 |
CAP_SYS_PTRACE | 允許跟蹤任何進程 |
CAP_SYS_RAWIO | 允許直接訪問 /devport、/dev/mem、/dev/kmem 及原始塊設備 |
CAP_SYS_RESOURCE | 忽略資源限制 |
CAP_SYS_TIME | 允許改變系統時鍾 |
CAP_SYS_TTY_CONFIG | 允許配置 TTY 設備 |
CAP_SYSLOG | 允許使用 syslog() 系統調用 |
CAP_WAKE_ALARM | 允許觸發一些能喚醒系統的東西(比如 CLOCK_BOOTTIME_ALARM 計時器) |
getcap 命令和 setcap 命令分別用來查看和設置程序文件的 capabilities 屬性。
例如為 ping 命令文件添加 capabilities
執行 ping 命令所需的 capabilities 為 cap_net_admin 和 cap_net_raw,通過 setcap 命令可以添加它們:
$ sudo setcap cap_net_admin,cap_net_raw+ep /bin/ping
移除添加的 capabilities ,執行下面的命令:
$ sudo setcap cap_net_admin,cap_net_raw-ep /bin/ping
命令中的 ep 分別表示 Effective 和 Permitted 集合(接下來會介紹),+ 號表示把指定的 capabilities 添加到這些集合中,- 號表示從集合中移除(對於 Effective 來說是設置或者清除位)。
解決問題
回到我們開始的問題,由於我們為非 root 用戶賦予了使用 80 端口的權限,調用了如下命令:
setcap cap_net_bind_service=+ep /usr/bin/java
當一個可執行文件提升了權限后,運行時加載程序(rtld)— ld.so,它不會與不受信任路徑中的庫鏈接。Linux 會為使用了 setcap
或 suid
的程序禁用掉 LD_LIBRARY_PATH
。所以就出現了 java 程序加載不到 libjli.so 的情況了,這是 JDK 的一個 bug。
JDK-7157699 : can not run java after granting posix capabilities
那么既然使用 setcap 后不會加載鏈接庫,我們就可以將 libjli.so 所在的路徑添加到 /etc/ld.so.conf/xxx.conf
中,例如:
% cat /etc/ld.so.conf.d/java.conf
/usr/java/jdk1.8.0_261-amd64/lib/amd64/jli
使用 ldconfig
重載 so 文件。
[root@localhost jli]# ldconfig -p | grep libjli
libjli.so (libc6,x86-64) => /usr/java/jdk1.8.0_261-amd64/lib/amd64/jli/libjli.so% ldconfig | grep libjli
libjli.so -> libjli.so
.......
這樣再次測試就可以了。
參考文章
【2】ldd 查看程序依賴庫
【4】capabilities(7) - Linux man page
【6】How to get Oracle java 7 to work with setcap cap_net_bind_service+ep