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_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。