From 42b406a72c06e11d5edcf70d403fd77c47c04843 Mon Sep 17 00:00:00 2001 From: "Y.Horie" Date: Wed, 16 Sep 2026 07:56:59 +0900 Subject: [PATCH] bugfix: heap-use-after-free in tcp_finalize after receiveuntil iterator GC. finalize_read_part() only cleared cp->upstream and left u->input_filter_ctx pointing to the compiled pattern, which the Lua GC could free before the socket was finalized. Clear both sides. See #2517. --- src/ngx_http_lua_socket_tcp.c | 7 +++- t/066-socket-receiveuntil.t | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/ngx_http_lua_socket_tcp.c b/src/ngx_http_lua_socket_tcp.c index 08c02ae043..13655f100b 100644 --- a/src/ngx_http_lua_socket_tcp.c +++ b/src/ngx_http_lua_socket_tcp.c @@ -4211,10 +4211,15 @@ ngx_http_lua_socket_tcp_finalize_read_part(ngx_http_request_t *r, ngx_memzero(&u->buffer, sizeof(ngx_buf_t)); } - /* mirror tcp_finalize: detach cp so its __gc is safe */ + /* + * mirror tcp_finalize: detach cp so its __gc is safe, and drop our + * reference to cp as well, since cp may be freed by the Lua GC before + * the socket is finalized. + */ 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; } if (u->raw_downstream || u->body_downstream) { diff --git a/t/066-socket-receiveuntil.t b/t/066-socket-receiveuntil.t index 43744a3721..f8dc8e9a2a 100644 --- a/t/066-socket-receiveuntil.t +++ b/t/066-socket-receiveuntil.t @@ -2012,3 +2012,74 @@ close: 1 nil --- no_error_log [error] --- skip_eval: 3:$ENV{TEST_NGINX_USE_HTTP3} + + + +=== TEST 27: close the socket after the iterator got a read error and was GC'ed (GH #2517) +--- config + server_tokens off; + location /t { + content_by_lua_block { + -- JIT traces may keep the iterator closure alive + jit.off() + jit.flush() + + local sock = ngx.socket.tcp() + local port = ngx.var.server_port + + local ok, err = sock:connect("127.0.0.1", port) + if not ok then + ngx.say("failed to connect: ", err) + return + end + + local req = "GET /foo HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n" + + local bytes, err = sock:send(req) + if not bytes then + ngx.say("failed to send request: ", err) + return + end + + local weak = setmetatable({}, { __mode = "v" }) + + local function read_until_error() + local reader = sock:receiveuntil("--no-such-boundary") + weak[1] = reader + + while true do + local data, err = reader(1) + if not data then + ngx.say("failed to read: ", err) + return + end + end + end + + read_until_error() + + for _ = 1, 4 do + collectgarbage("collect") + end + + ngx.say("reader collected: ", weak[1] == nil) + + ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + } + } + + location /foo { + content_by_lua_block { + ngx.print("hello world") + } + } +--- request +GET /t +--- response_body +failed to read: closed +reader collected: true +close: 1 nil +--- no_error_log +[error] +--- skip_eval: 3:$ENV{TEST_NGINX_USE_HTTP3}