heap-use-after-free in ngx_http_lua_socket_tcp_finalize after receiveuntil userdata GC
A heap-use-after-free can occur after a receiveuntil() read error, followed by LuaJIT garbage collection and sock:close(). ASan reports:
ERROR: AddressSanitizer: heap-use-after-free
WRITE of size 8
#0 ngx_http_lua_socket_tcp_finalize
src/ngx_http_lua_socket_tcp.c:4650
#1 ngx_http_lua_socket_tcp_close
src/ngx_http_lua_socket_tcp.c:3535
The invalid write is:
((ngx_http_lua_socket_compiled_pattern_t *)
u->input_filter_ctx)->upstream = NULL;The root cause appears to be incomplete cleanup of the relationship between the socket upstream and the receiveuntil() compiled-pattern userdata:
u->input_filter_ctx -> cp
cp->upstream -> uAfter a read error, ngx_http_lua_socket_tcp_finalize_read_part() clears only:
cp->upstream = NULL;
However, u->input_filter_ctx is not cleared. When the iterator becomes unreachable, LuaJIT GC frees cp. Because cp->upstream is already NULL, the userdata’s __gc handler does not clear u->input_filter_ctx. As a result, u->input_filter_ctx becomes a dangling pointer. A later call to sock:close() dereferences it in ngx_http_lua_socket_tcp_finalize(), causing the heap-use-after-free. This appears related to the fix introduced by #2504: https://github.com/openresty/lua-nginx-module/commit/48d5628 The minimal fix is to clear both sides of the relationship in ngx_http_lua_socket_tcp_finalize_read_part():
if (u->input_filter_ctx != NULL && u->input_filter_ctx != u) {
((ngx_http_lua_socket_compiled_pattern_t *)
u->input_filter_ctx)->upstream = NULL;
+ u->input_filter_ctx = NULL;
}Environment: ngx_lua: 0.10.31rc5 LuaJIT: 2.1-20260415 AddressSanitizer enabled
Source: openresty/openresty