Apache coredump 問題發現與解決記錄


Apache coredump 問題發現與解決記錄

背景

組內的開發機原來是 Nginx + Tomcat 環境拓撲,但線上是 Apache + Tomcat,為了與線上環境保持一致,要求將開發機上的 Nginx 替換為 Apache。目前開發機上基於域名的虛擬機有dk.qq.com和dk.oa.com,需要支持 https 協議。利用線上的 Apache,輕松將其部署到開發機上。

發現問題

按照 Nginx 的原有配置,將 Apache 的 http 和 https 相關配置寫完之后,使用 apachectl start 成功啟動了 httpd 服務。於是在 chrome 瀏覽器上嘗試訪問,訪問 http 網址一切正常,但是訪問 https://dk.qq.com/AmarSCFOnline/login.jsp,網頁提示以下錯誤:

無法訪問此網站
dk.qq.com 意外終止了連接。
ERR_CONNECTION_CLOSED

第一件要做的事就是查看 Apache 日志 /usr/local/apache2/log,發現了下面這些日志記錄:

[Sat Aug 19 13:54:40 2017] [notice] child pid 31117 exit signal Segmentation fault (11)
[Sat Aug 19 13:54:40 2017] [notice] child pid 31118 exit signal Segmentation fault (11)
[Sat Aug 19 13:54:40 2017] [notice] child pid 31121 exit signal Segmentation fault (11)
[Sat Aug 19 13:54:40 2017] [notice] child pid 31122 exit signal Segmentation fault (11)
[Sat Aug 19 13:54:40 2017] [notice] child pid 31123 exit signal Segmentation fault (11)
[Sat Aug 19 13:54:40 2017] [notice] child pid 31124 exit signal Segmentation fault (11)
[Sat Aug 19 13:54:40 2017] [notice] child pid 31125 exit signal Segmentation fault (11)

httpd 進程出現段錯誤,每次訪問都這樣,於是使用 gdb 進行調試,以獲取更加詳細的有用信息。

基本思路:將 gdb 附加到其中一個 httpd 子進程,並重新加載,等待崩潰,然后查看函數調用棧。

首先選擇要附加的 httpd 子進程:

ps -ef | grep httpd

nobody   31084 31082  0 13:39 ?        00:00:00 /usr/local/httpd-2.2.27/bin/httpd -k start
nobody   31085 31082  0 13:39 ?        00:00:00 /usr/local/httpd-2.2.27/bin/httpd -k start

現在將 gdb 附加到 PID 為 31084 的 httpd 子進程上:

[root@dev157 /usr/local/apache2/logs]# gdb 
(gdb) attach 31084
Attaching to process 31084
(gdb) c
Continuing.

接下來是重現剛剛的錯誤,這里的做法非常簡單,只需要不斷地刷新網頁直到剛剛指定的進程 core dump 了為止。如果是非常難以重現的錯誤,可以修改 Apache 配置,讓其只使用一個子進程處理請求,添加的配置如下:

StartServers 1
MinSpareServers 1
MaxSpareServers 1

當 gdb 附加的子進程 core dump后:

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x00007f04bc4f94cb in SSL_CTX_ctrl () from /lib64/libssl.so.1.0.0

(gdb) bt
#0  0x00007f04bc4f94cb in SSL_CTX_ctrl () from /lib64/libssl.so.1.0.0
#1  0x000000000047978b in ssl_find_vhost (servername=<optimized out>, c=<optimized out>, s=0x7c2828) at ssl_engine_kernel.c:2106
#2  0x00000000004794d2 in ssl_callback_ServerNameIndication (ssl=<optimized out>, al=<optimized out>, mctx=<optimized out>) at ssl_engine_kernel.c:2022
#3  0x00007f04bc4ea859 in ssl_check_clienthello_tlsext_early () from /lib64/libssl.so.1.0.0
#4  0x00007f04bc4d4ebb in ssl3_get_client_hello () from /lib64/libssl.so.1.0.0
#5  0x00007f04bc4d96fd in ssl3_accept () from /lib64/libssl.so.1.0.0
#6  0x00007f04bc4e73e8 in ssl23_accept () from /lib64/libssl.so.1.0.0
#7  0x0000000000477719 in ssl_io_filter_connect (filter_ctx=0x7fc620) at ssl_engine_io.c:1154
#8  0x0000000000478677 in ssl_io_filter_input (f=0x80f738, bb=0x807228, mode=<optimized out>, block=<optimized out>, readbytes=<optimized out>) at ssl_engine_io.c:1407
#9  0x0000000000435e42 in ap_rgetline_core (s=0x805cb0, n=8192, read=0x7ffd7e933cc0, r=0x805c80, fold=0, bb=0x807228) at protocol.c:231
#10 0x000000000043687e in read_request_line (bb=0x807228, r=0x805c80) at protocol.c:596
#11 ap_read_request (conn=0x7fbe20) at protocol.c:921
#12 0x0000000000485cb0 in ap_process_http_connection (c=0x7fbe20) at http_core.c:183
#13 0x0000000000449bf0 in ap_run_process_connection (c=0x7fbe20) at connection.c:43
#14 0x000000000049ca28 in child_main (child_num_arg=<optimized out>) at prefork.c:667
#15 0x000000000049cd24 in make_child (s=0x734190, slot=0) at prefork.c:768
#16 0x000000000049d02e in startup_children (number_to_start=50) at prefork.c:786
#17 ap_mpm_run (_pconf=<optimized out>, plog=<optimized out>, s=<optimized out>) at prefork.c:1007
#18 0x000000000042eb74 in main (argc=3, argv=0x7ffd7e9341d8) at main.c:753

從上面可以看出 httpd 掛在了握手過程, ssl3_get_client_hello 服務器收到了瀏覽器的請求,ssl_check_clienthello_tlsext_earlyssl_callback_ServerNameIndicationssl_find_vhost 可以知道服務器在向瀏覽器發送服務器證書之前,在進行 TLS SNI 協商,目的是在相同地址支持多個基於域名的虛擬主機的前提下,使服務器更早的切換到正確的虛擬域,並且發送給瀏覽器包含正確名字的數字證書。

根據 Openssl 官方文檔的描述:

The SSL_*_ctrl() family of functions is used to manipulate settings of the SSL_CTX and SSL objects.

看來是 httpd 在使用 SSL_CTX_ctrl 切換 SSL 對象到 SSL_CTX 的時候掛了。

這時問題遇到了難點,SSL_CTX_ctrl ,和 SSL 相關的有很多,SSL 協議版本和包含 SSL_CTX_ctrl 的 libssl.so 版本等等。

偶然情況下,使用 IE 瀏覽器訪問 https://dk.qq.com/AmarSCFOnline/login.jsp,竟然可以正常訪問,覺得是 IE 和 chrome 使用的 SSL 版本不一樣,於是使用 fiddler 進行抓包分析,發現兩者在握手時沒什么區別,唯一的區別就是使用的 SSL 版本不一樣:

抓包數據中發現 IE 使用到的 SSL 版本有很多,

Version: 3.0 (SSL/3.0)
Version: 3.1 (TLS/1.0)
Version: 3.3 (TLS/1.2)

chrome的抓包數據

Version: 3.3 (TLS/1.2)

過程中還發現 chrome 已經默認禁用 SSLv3 支持,而且無法修改使用的 SSL 版本,只能使用 TLS/1.2。通過修改 IE Internet選項-高級-安全使用的 SSL 版本,發現只要使用 TLS/1.2 協議去訪問,后台的 httpd 服務就會掛。於是查看了 Apache 的 SSL 配置,是已經開啟支持 TLS/1.2 的了:

SSLProtocol All -SSLv2 -SSLv3

httpd-2.2.27 支持 TLS/1.2,既然配置已經開啟了支持,還是不行,那應該是 openssl 庫不支持 TLS/1.2 的問題了。

查找了 openssl 的 changelog 文檔,TLS 1.2 是在 OpenSSL 1.0.1 以后版本加入的,而 apache 使用的 libssl.so 是 1.0.0 版本,所以不支持 TLS 1.2 協議。

#0 0x00007f04bc4f94cb in SSL_CTX_ctrl () from /lib64/libssl.so.1.0.0

解決問題

方法 1

apache 使用的 libssl.so 是 1.0.0 版本,不支持 TLS 1.2 協議,所以直接暴力一點:

mv libssl.so.1.0.0 libssl.so.1.0.0.bak
mv libssl.so.1.0.2 libssl.so.1.0.0

重啟 Apache,出現錯誤:

error while loading shared libraries: libcrypto.so.1.0.2: cannot open shared object file: No such file or directory

找不到 libcrypto.so.1.0.2,於是拷貝了一個libcrypto.so.1.0.2 到 /lib64:

cp libcrypto.so.1.0.2 libcrypto.so.1.0.0

這會導致一個問題,就是原來的 libssl.so.1.0.0 被刪除,會導致其他使用 libssl.so.1.0.0 程序的兼容問題,但是問題不是很大,libssl.so.1.0.2 的主版本號和次版本號與原來的一樣,只是發行版本號不一樣而已,應該可以向下兼容 libssl.so.1.0.0

方法 2

重新編譯一個 Apache,但是它使用的 ssl.so 的 soname 必須是 libssl.so.1.0.2,這樣只要將 libssl.so.1.0.2 拷貝到開發機上即可支持 TLS 1.2;

這個方法目前是最好,對開發機的影響最小。

總結

整個過程發現了很多潛在的坑,同時也學到了很多,這里一一總結一下。

Linux 程序編譯鏈接動態庫版本問題

ldd 命令

涉及命令:ldd
ldd 簡介:打印程序或者庫文件所依賴的共享庫列表

涉及選項:

  1. --version:打印指令版本號;
  2. -v:詳細信息模式,打印所有相關信息;
  3. -u:打印未使用的直接依賴;
  4. -d:執行重定位和報告任何丟失的對象;
  5. --r:執行數據對象和函數的重定位,並且報告任何丟失的對象和函數;
  6. --help:顯示幫助信息。
    其他詳細說明請參閱 man 說明。

示例情景:

ldd httpd
	linux-vdso.so.1 =>  (0x00007ffeadb35000)
	/$LIB/libonion.so => /lib64/libonion.so (0x00007fa6b4534000)
	libssl.so.1.0.0 => /lib64/libssl.so.1.0.0 (0x00007fa6b41ae000)
	libcrypto.so.1.0.0 => /lib64/libcrypto.so.1.0.0 (0x00007fa6b3cf4000)
	...

左邊是依賴的動態庫名字,右邊是鏈接指向的文件。

動態庫的編譯和 soname

根據 ldd 的結果,httpd 運行時總會去查找加載 libssl.so.1.0.0 等動態庫文件,這些動態庫文件的名字即 soname,是怎么指定的呢?

動態庫在編譯的時候會通過 -soname 指定動態庫的真正名字,它存在動態庫的二進制數據里面。編譯命令示例如下,這時生成的libhello.so.0.0.1 動態庫的 Library soname 是 libhello.so.0:

gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.1

除了在編譯時指定 soname,我們還可以通過 readelf 命令查看指定動態庫的 Library soname,命令示例如下:

readelf -d libssl.so.1.0.0

Dynamic section at offset 0x6b128 contains 27 entries:
  Tag        Type                         Name/Value
 0x000000000000000e (SONAME)             Library soname: [libssl.so.1.0.2]

我們編譯一個需要動態庫的程序時,需要通過 -l 選項指定動態庫,-L 指定動態庫所在目錄,命令示例如下:

gcc main.c -L. -lhello -o main  

在當前目錄下,需要存在 libhello.so 文件才能編譯過去,也就是說在編譯的時候,鏈接器會去找它依賴的 libxxx.so 這樣的文件,因此必須保證 libxxx.so 的存在。通過 ldd main 和 readelf -d libhello.so 可以發現, main 依賴的 libhello 名字和 libhello.so soname 是一致的,也就是說,main 依賴的動態庫文件名字來自動態庫的 soname。

動態庫版本更新,如果只是小改動,則無需修改 soname,但 so 文件名(.so.a.b.c) 可以增大小版本號,然后再將 soname 軟鏈接到真正的 so 文件。

線上 Apache 坑

開發機使用的 Apache 是在線上直接打包的,通過 ldd 發現其依賴的 ssl.so 的 soname 是 libssl.so.1.0.0,也就是說,線上版本 Apache 編譯時使用的 ssl.so 版本較低,不支持 TLS 1.2,我也不知道線上 Apache 是怎么做到支持 HTTPS 的,ssl.so 版本明明不對。

為了解決剛剛的問題,有兩種方法:

  1. 重新編譯一個 Apache,但是它使用的 ssl.so 的 soname 必須是 libssl.so.1.0.2,這樣只要將 libssl.so.1.0.2 拷貝到開發機上即可支持 TLS 1.2;
  2. 暴力使用 libssl.so.1.0.2 去替換開發機上的 libssl.so.1.0.0,這會導致一個問題,就是原來的 libssl.so.1.0.0 被刪除,會導致其他使用 libssl.so.1.0.0 程序的兼容問題,但是問題不是很大,libssl.so.1.0.2 的主版本號和次版本號與原來的一樣,只是發行版本號不一樣而已,應該可以向下兼容 libssl.so.1.0.0

瀏覽器

IE 10 瀏覽器可以修改 HTTPS 使用的 SSL 協議,包括 SSLv2,SSLv3,TLS 1.0,TLS 1.1,TLS 1.2;而 chrome 是不支持修改使用的 SSL 協議版本的,默認支持 TLS 1.2,Chrome 40 已完全禁用 SSLv3。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM