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_early
、ssl_callback_ServerNameIndication
和 ssl_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 簡介:打印程序或者庫文件所依賴的共享庫列表
涉及選項:
- --version:打印指令版本號;
- -v:詳細信息模式,打印所有相關信息;
- -u:打印未使用的直接依賴;
- -d:執行重定位和報告任何丟失的對象;
- --r:執行數據對象和函數的重定位,並且報告任何丟失的對象和函數;
- --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 版本明明不對。
為了解決剛剛的問題,有兩種方法:
- 重新編譯一個 Apache,但是它使用的 ssl.so 的 soname 必須是 libssl.so.1.0.2,這樣只要將 libssl.so.1.0.2 拷貝到開發機上即可支持 TLS 1.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。