1. 程式人生 > 實用技巧 >解決setcap導致Java載入libjli.so 失敗問題

解決setcap導致Java載入libjli.so 失敗問題

背景

最近碰到一個問題,有個應用在啟動的時候一直報錯,錯誤資訊如下:

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下生成和使用動態庫的步驟如下:

  1. 編寫原始檔。
  2. 將一個或幾個原始檔編譯連結,生成共享庫。
  3. 通過 -L -lxxx 的gcc選項鍊接生成的libxxx.so。例如gcc -fPIC -shared -o libmax.so max.c , -fPIC 是編譯選項,PIC是 Position Independent Code 的縮寫,表示要生成位置無關的程式碼,這是動態庫需要的特性; -shared 是連結選項,告訴gcc生成動態庫而不是可執行檔案
  4. 把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/libldconfig程式會自動搜尋這兩個目錄)

我們把要用的 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)
  • 第一列:程式需要依賴什麼庫
  • 第二列: 系統提供的與程式需要的庫所對應的庫
  • 第三列:庫載入的開始地址

通過上面的資訊,我們可以得到以下幾個資訊:

  1. 通過對比第一列和第二列,我們可以分析程式需要依賴的庫和系統實際提供的,是否相匹配
  2. 通過觀察第三列,我們可以知道在當前的庫中的符號在對應的程序的地址空間中的開始位置

如果依賴的某個庫找不到,通過這個命令可以迅速定位問題所在.

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 會為使用了 setcapsuid 的程式禁用掉 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
.......

這樣再次測試就可以了。

參考文章

【1】Linux動態庫生成與使用指南

【2】ldd 檢視程式依賴庫

【3】Linux Capabiliites 簡介

【4】capabilities(7) - Linux man page

【5】如何允許非 root 程序繫結低位埠

【6】[How to get Oracle java 7 to work with setcap cap_net_bind_service+ep](https://unix.stackexchange.com/questions/87978/how-to-get-oracle-java-7-to-work-with-setcap-cap-net-bind-serviceep)