ngx_lua 问题归档(1)
前段时间在公司引流机器上偶然发现我们的 OpenResty/Nginx 服务存在进程崩溃的情况,错误日志里记录了如下的堆栈:
ngx_http_lua_ssl_cert_handler
tls_post_process_client_hello+0x1be)
ossl_statem_server_post_process_message
read_state_machine
state_machine
ossl_statem_accept
ssl3_read_bytes
ssl3_read_internal
ssl3_read
ssl_read_internal
SSL_read
ngx_ssl_recv
ngx_http_v2_read_handler
ngx_http_v2_idle_handler
ngx_epoll_process_events
ngx_process_events_and_timers
ngx_worker_process_cycle
ngx_spawn_process
ngx_start_worker_processes
ngx_master_process_cycle
main
(在崩溃时打印其堆栈的功能是我们自己加入到 Nginx 中的,官方版本并不支持。)
从堆栈信息来看,当前正在处理的连接,HTTP 协议已经升级到了 HTTP/2 (ngx_http_v2_idle_handler
)。而从偏顶部的函数来看,当前连接似乎又进入到了 SSL/TLS 握手协议的处理,既然堆栈上出现了 HTTP/2 相关的函数,说明当前一定不是第一次 SSL/TLS 握手(否则 HTTP 协议不可能为 HTTP/2),这让我联想到 SSL renegotation,由于没有 coredump 文件的支持,从堆栈信息里看出来的仅限于这么多。
问题至此,便只能带着问题去看源码了。既然问题还和 HTTP/2 有关,那么我猜测问题可能出现在某些地方没有兼容好 HTTP/2 相关逻辑有关,经过查看 ngx_http_lua_ssl_cert_handler
函数(这是实现 ssl_certificate_by_lua
功能的基础函数)和 HTTP/2 相关的逻辑,我发现问题出在 c->data
这个成员上,在协议升级到 HTTP/2 后,函数 ngx_htp_v2_init
会被调用到,这个函数将 c->data
赋值为了 ngx_http_v2_connection_t
的一个实例,而在通常(非 HTTP/2) 的处理下,c->data
则是 ngx_http_connection_t
的实例,ngx_http_lua_ssl_cert_handler
并没有区分这一情况,导致将实际按 ngx_http_v2_connection_t
布局的数据按 ngx_http_connection_t
的结构来操作,从而引发了 segmentation fault。
void
ngx_http_v2_init(ngx_event_t *rev)
{
......
c->data = h2c; /* 赋值为 ngx_http_v2_connection_t 实例 */
rev->handler = ngx_http_v2_read_handler;
c->write->handler = ngx_http_v2_write_handler;
c->idle = 1;
ngx_http_v2_read_handler(rev);
}
int
ngx_http_lua_ssl_cert_handler(ngx_ssl_conn_t *ssl_conn, void *data)
{
......
hc = c->data; /* 此时应该是 h2c (ngx_http_v2_connection_t) = c->data */
......
r->main_conf = hc->conf_ctx->main_conf; /* 可能导致 segmentation fault */
r->srv_conf = hc->conf_ctx->srv_conf; /* 可能导致 segmentation fault */
r->loc_conf = hc->conf_ctx->loc_conf; /* 可能导致 segmentation fault */
......
}
原因似乎找到了,我尝试通过 openssl s_client 这个工具来复现问题,但是没有成功:
echo R | openssl s_client -connect ip:port -alpn h2
(这里键入 “R” 是为了触发 SSL renegotation。)
我想可能遗漏了一些重要细节,通过 gdb 单步跟踪,我发现在执行到 ngx_http_lua_ssl_cert_handler
时,并没有执行到上述逻辑,而是走到了以下的逻辑:
int
ngx_http_lua_ssl_cert_handler(ngx_ssl_conn_t *ssl_conn, void *data)
{
.......
if (cctx && cctx->entered_cert_handler) {
/* not the first time */
if (cctx->done) {
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
"lua_certificate_by_lua: cert cb exit code: %d",
cctx->exit_code);
dd("lua ssl cert done, finally");
return cctx->exit_code;
}
return -1;
}
.......
}
因为早在第一次 SSL/TLS 握手时,cctx->entered_cert_handler
就已经置为 1。所以第二次重新协商时,并没有处理我们期望的逻辑。
反过来说,要出现这个问题,那么第一次 SSL/TLS 握手一定不能走到 ngx_http_lua_ssl_cert_handler
的逻辑,这个函数实际设置在 OpenSSL 内部的 s->cert->cert_cb
上,如果要该函数不被调用,说明第一次握手服务端没有发送证书链到客户端,即触发了SSL Session Reuse 机制。
幸运的是,这仍然可以通过 openssl s_client 来验证猜测:
echo R | openssl s_client -connect ip:port -reconnect -alpn h2
执行后观察错误日志,发现进程果然崩溃,问题已经成功定位到!
我已经将问题反馈到了 OpenResty 官方并提交了 PR 进行修复。值得注意的是,Nginx/1.15.2 已经彻底避免了这个问题,见 dcab86115261;另外,如果 OpenSSL 版本低于 1.1.0,也不会有这个问题。