Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ install:
#- if [ -n "$OPENSSL_VER" ] && [ ! -f download-cache/openssl-$OPENSSL_VER.tar.gz ]; then wget -P download-cache https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VER/openssl-$OPENSSL_VER.tar.gz || wget -P download-cache https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz || wget -P download-cache https://www.openssl.org/source/old/${OPENSSL_VER//[a-z]/}/openssl-$OPENSSL_VER.tar.gz; fi
- wget https://github.com/openresty/openresty-deps-prebuild/releases/download/v20230902/boringssl-20230902-x64-focal.tar.gz
- wget https://github.com/openresty/openresty-deps-prebuild/releases/download/v20230902/curl-h3-x64-focal.tar.gz
- git clone https://github.com/openresty/test-nginx.git
- git clone --revision=efb42ee915b079b6b5e7fb24ee2afd7be44fa2bf --depth=1 https://github.com/openresty/test-nginx.git
- git clone https://github.com/openresty/openresty.git ../openresty
- git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx
- git clone https://github.com/openresty/openresty-devel-utils.git
Expand Down
19 changes: 12 additions & 7 deletions src/ngx_http_lua_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -1425,8 +1425,10 @@ ngx_http_lua_run_thread(lua_State *L, ngx_http_request_t *r,
return NGX_AGAIN;
}

ngx_http_lua_del_thread(r, L, ctx, ctx->cur_co_ctx);
ctx->uthreads--;
if (ctx->cur_co_ctx->co_ref != LUA_NOREF) {
ngx_http_lua_del_thread(r, L, ctx, ctx->cur_co_ctx);
ctx->uthreads--;
}

if (ctx->uthreads == 0) {
if (ngx_http_lua_entry_thread_alive(ctx)) {
Expand Down Expand Up @@ -1464,9 +1466,10 @@ ngx_http_lua_run_thread(lua_State *L, ngx_http_request_t *r,
lua_xmove(ctx->cur_co_ctx->co, next_co, nrets);
}

if (ctx->cur_co_ctx->is_uthread) {
ngx_http_lua_del_thread(r, L, ctx, ctx->cur_co_ctx);
ctx->uthreads--;
if (ctx->cur_co_ctx->is_uthread
&& ctx->cur_co_ctx->co_ref != LUA_NOREF) {
ngx_http_lua_del_thread(r, L, ctx, ctx->cur_co_ctx);
ctx->uthreads--;
}

if (!ctx->cur_co_ctx->is_wrap) {
Expand Down Expand Up @@ -1575,8 +1578,10 @@ ngx_http_lua_run_thread(lua_State *L, ngx_http_request_t *r,
return NGX_AGAIN;
}

ngx_http_lua_del_thread(r, L, ctx, ctx->cur_co_ctx);
ctx->uthreads--;
if (ctx->cur_co_ctx->co_ref != LUA_NOREF) {
ngx_http_lua_del_thread(r, L, ctx, ctx->cur_co_ctx);
ctx->uthreads--;
}

if (ctx->uthreads == 0) {
if (ngx_http_lua_entry_thread_alive(ctx)) {
Expand Down
114 changes: 113 additions & 1 deletion t/127-uthread-kill.t
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ our $StapScript = $t::StapThread::StapScript;

repeat_each(2);

plan tests => repeat_each() * (blocks() * 5 + 1);
plan tests => repeat_each() * (blocks() * 5 + 1) -2;

$ENV{TEST_NGINX_RESOLVER} ||= '8.8.8.8';
$ENV{TEST_NGINX_MEMCACHED_PORT} ||= '11211';
$ENV{TEST_NGINX_REDIS_PORT} ||= '6379';

#no_shuffle();
no_long_string();
Expand Down Expand Up @@ -505,3 +506,114 @@ thread created: zombie
[alert]
lua tcp socket abort resolver
--- error_log



=== TEST 10: phantom uthreads-- via killed uthread with live child coroutine
When a user thread internally uses coroutine.resume() to run a child
coroutine that does cosocket I/O, and the parent uthread is then killed
via ngx.thread.kill(), the child's socket I/O is NOT cancelled (because
cleanup_pending_operation on the parent's co_ctx is a no-op -- the parent
yielded for coroutine.resume, not for I/O, so cleanup is NULL).

When the child later completes, user_co_done walks up to the killed
parent's co_ctx, finds is_uthread=1, and does uthreads-- even though
del_thread returns early (co_ref already LUA_NOREF). This phantom
decrement causes uthreads to underflow when finalize_threads runs
during the access->content phase transition.
--- config
location = /t {
rewrite_by_lua_block {
ngx.ctx._rewrite = true
}

access_by_lua_block {
local redis_port = $TEST_NGINX_REDIS_PORT

-- Phase 1: simulate DNS resolution with thread.spawn + wait + kill
-- (like thread_process_dns_query in resolver.lua)
local dns_threads = {}

-- T1: fast DNS query (PING, completes immediately)
dns_threads[1] = ngx.thread.spawn(function()
local sock = ngx.socket.tcp()
sock:settimeout(2000)
local ok, err = sock:connect("127.0.0.1", redis_port)
if not ok then return nil, err end
sock:send("PING\r\n")
local line = sock:receive()
sock:setkeepalive()
return line
end)

-- T2: uses coroutine.resume internally with FAST I/O.
-- The child coroutine does a fast Redis PING so its response
-- arrives in the same epoll batch as T1's response.
-- When T2 is killed, the child's socket is NOT cleaned up
-- because T2 yielded for coroutine.resume (cleanup=NULL),
-- not for socket I/O. The child's event then fires AFTER
-- T2 is killed but BEFORE finalize_threads runs.
dns_threads[2] = ngx.thread.spawn(function()
local child = coroutine.create(function()
local sock = ngx.socket.tcp()
sock:settimeout(2000)
local ok, err = sock:connect("127.0.0.1", redis_port)
if not ok then return nil, err end
sock:send("PING\r\n")
local line = sock:receive()
sock:setkeepalive()
return line
end)

local ok, res = coroutine.resume(child)
return res
end)

-- wait_any pattern: wait for first result
local ok, res = ngx.thread.wait(dns_threads[1], dns_threads[2])

-- kill remaining (T2's child socket is still alive!)
for _, t in ipairs(dns_threads) do
ngx.thread.kill(t)
end

-- Phase 2: simulate probe thread spawning
-- (like run_batch_probe_targets in edge_probe)
local probe_threads = {}
for i = 1, 10 do
probe_threads[i] = ngx.thread.spawn(function(idx)
local sock = ngx.socket.tcp()
sock:settimeout(2000)
local ok, err = sock:connect("127.0.0.1", redis_port)
if not ok then return nil, err end
sock:send("PING\r\n")
local line = sock:receive()
sock:setkeepalive()
return line
end, i)
end

local ok_count = 0
for i = 1, #probe_threads do
local ok, res, err = ngx.thread.wait(probe_threads[i])
if ok and res then
ok_count = ok_count + 1
end
end

ngx.ctx.ok_count = ok_count
}

content_by_lua_block {
-- reset_ctx -> finalize_threads runs here.
-- Without the fix, the phantom uthreads-- from the killed T2's
-- child completing causes uthreads underflow.
ngx.say("ok_count=", ngx.ctx.ok_count)
}
}
--- request
GET /t
--- response_body
ok_count=10
--- no_error_log
[error]