#1146·openresty

heap-use-after-free in ngx_http_lua_socket_tcp_finalize after receiveuntil userdata GC

Author: BrookPanZMCreated Aug 10, 2026Updated Aug 10, 2026

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        -> u

After 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