bugfix: check sslhandshake result on immediate OK#532
Conversation
This patch fixes the immediate `FFI_OK` path in `tcpsock:sslhandshake()`.
Currently `sslhandshake()` treats the initial `FFI_OK` return from
`ngx_http_lua_ffi_socket_tcp_sslhandshake()` as the final successful result when
`reused_session == false`:
```lua
rc = ngx_http_lua_ffi_socket_tcp_sslhandshake(...)
if rc == FFI_OK then
if reused_session == false then
return true
end
rc = ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(...)
end
```
This skips `ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result()` for the
`reused_session == false` case.
That is unsafe because the first FFI call result and the final SSL handshake
result are not the same thing.
The first return value tells Lua how the handshake operation progressed:
```text
FFI_ERROR -> immediate call failed
FFI_DONE -> reused session
FFI_OK -> handshake operation completed immediately
FFI_AGAIN -> handshake needs to yield and resume later
```
The final result must still be read from:
```text
ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(...)
```
This matters when the C side completes the handshake path immediately but records
an SSL error in the cosocket state. For example, certificate verification can fail
inside the handshake handler and set:
```text
u->error_ret
u->openssl_error_code_ret
```
The old Lua code can miss that state.
Old flow:
```text
Lua sslhandshake()
|
v
C: ngx_http_lua_ffi_socket_tcp_sslhandshake(...)
|
v
returns FFI_OK
|
v
reused_session == false?
|
v
return true
|
v
final handshake result is never checked
```
So callers may believe TLS succeeded even though the C cosocket state already has
a handshake error. The next socket operation can then fail with a misleading error,
for example a write failure, instead of returning the original SSL handshake error.
The fixed flow always reads the final result for both immediate and async
completion paths:
```text
Lua sslhandshake()
|
v
C: ngx_http_lua_ffi_socket_tcp_sslhandshake(...)
|
+-----------------------------+
| |
v v
FFI_OK FFI_AGAIN
immediate async handshake
completion |
| v
| co_yield()
| |
+------------+-------------+
|
v
get_sslhandshake_result()
|
v
final result
|
+--------+--------+
| |
v v
FFI_ERROR FFI_OK
| |
v v
return SSL error return true/session
```
This patch separates the two meanings by using:
```text
rc -> result of ngx_http_lua_ffi_socket_tcp_sslhandshake()
res -> result of ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result()
```
The new control flow is:
```lua
rc = ngx_http_lua_ffi_socket_tcp_sslhandshake(...)
if rc == FFI_ERROR then
return nil, err
end
if rc == FFI_DONE then
return reused_session
end
local res
if rc == FFI_OK then
res = ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(...)
else
assert(rc == FFI_AGAIN)
co_yield()
res = ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(...)
end
if res == FFI_ERROR then
return nil, ssl_err
end
assert(res == FFI_OK)
return true or session
```
This keeps the existing async behavior, but makes the immediate `FFI_OK` path
follow the same final-result check as the resumed `FFI_AGAIN` path.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Refactors the sslhandshake result handling logic in the cosocket SSL handshake path, replacing a loop with a more linear flow.
Changes:
- Simplified return paths for
FFI_ERRORandFFI_DONE. - Replaced the
while truepolling loop with a single-yield flow and aresvariable. - Centralized handling of
ngx_http_lua_ffi_socket_tcp_get_sslhandshake_resultreturn code.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| local res | ||
|
|
||
| rc = C.ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(r, u, | ||
| session_ptr, errmsg, openssl_error_code) | ||
| if rc == FFI_OK then | ||
| res = C.ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(r, u, | ||
| session_ptr, errmsg, openssl_error_code) | ||
| else | ||
| assert(rc == FFI_AGAIN) | ||
|
|
||
| assert(rc == FFI_OK) | ||
| co_yield() | ||
|
|
||
| if session_ptr[0] == nil then | ||
| return session_ptr[0] | ||
| end | ||
| res = C.ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(r, u, | ||
| session_ptr, errmsg, openssl_error_code) | ||
| end | ||
|
|
||
| return ffi_gc(session_ptr[0], C.ngx_http_lua_ffi_ssl_free_session) | ||
| if res == FFI_ERROR then | ||
| if openssl_error_code[0] ~= 0 then | ||
| return nil, openssl_error_code[0] .. ": " .. ffi_str(errmsg[0]) | ||
| end | ||
| return nil, ffi_str(errmsg[0]) | ||
| end | ||
|
|
||
| assert(rc == FFI_AGAIN) | ||
| assert(res == FFI_OK) |
| if rc == FFI_OK then | ||
| res = C.ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result(r, u, | ||
| session_ptr, errmsg, openssl_error_code) | ||
| else |
|
Another fix way: openresty/lua-nginx-module#2506 |
ReproductionBecause the bug requires the handshake to complete immediately in the kernel, we modify the C module to temporarily force the cosocket fd into blocking mode around C module patch for reproduction (
|
|
Hi @zhuizhuhaomeng PTAL |
|
Closed because the maintainer chose another fix way: openresty/lua-nginx-module#2506 |
This patch fixes the immediate
FFI_OKpath intcpsock:sslhandshake().The bug
Currently
sslhandshake()treats the initialFFI_OKreturn fromngx_http_lua_ffi_socket_tcp_sslhandshake()as the final successful result whenreused_session == false:This skips
ngx_http_lua_ffi_socket_tcp_get_sslhandshake_result()for thereused_session == falsecase.Why this is wrong
The first FFI call result and the final SSL handshake result are not the same
thing.
ngx_http_lua_ffi_socket_tcp_sslhandshake()internally callsngx_ssl_handshake(c), which returnsNGX_OKwhen the TLS protocol handshakecompletes successfully. This means the peer presented a valid certificate that
completed the TLS key exchange. The certificate was correctly formatted, signed
by its issuer, and the TLS record layer handshake finished.
However, after
ngx_ssl_handshake(c)returns, the ngx_lua handler callsSSL_get_verify_result()to check whether the certificate chain is trustedaccording to the configured CA store. If the peer's CA is not in the trusted
store, the handler records the verify error:
But the outer C code only checks the original
rcfromngx_ssl_handshake():The C FFI returns
FFI_OKeven though certificate verification recorded afailure. In Lua, the immediate
FFI_OKpath returnstruedirectly, so thecaller believes TLS succeeded. The next socket operation then fails with a
misleading error (e.g. "closed" on write) instead of reporting the original
certificate verification failure.
Visual comparison
Old flow:
Fixed flow:
The patch
Two changes:
FFI_OKpath now always reads the final result viangx_http_lua_ffi_socket_tcp_get_sslhandshake_result().rcfor the first FFI call state,resfor thefinal handshake result) so the two meanings do not overlap.
This keeps the existing async behavior unchanged, but makes the immediate
FFI_OKpath follow the same final-result check as the resumedFFI_AGAINpath.
#500 has not been completely resolved the #499
I hereby granted the copyright of the changes in this pull request
to the authors of this lua-resty-core project.