Skip to content

Commit cac9cbc

Browse files
authored
bugfix: prevent use-after-free crash in ngx_http_lua_pipe by ensuring connections are closed before pool destruction in quic connection close path.
1 parent a44c67f commit cac9cbc

File tree

2 files changed

+165
-8
lines changed

2 files changed

+165
-8
lines changed

src/ngx_http_lua_pipe.c

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,40 @@ ngx_http_lua_ffi_pipe_proc_destroy(ngx_http_lua_ffi_pipe_proc_t *proc)
12001200
}
12011201

12021202
ngx_http_lua_pipe_proc_finalize(proc);
1203+
1204+
/*
1205+
* pipe_proc_finalize -> ngx_http_lua_pipe_close_helper may leave pipe
1206+
* connections open with active timers/posted events when there are
1207+
* pending I/O operations (handler != dummy_handler). Close them now
1208+
* before destroying the pool.
1209+
*
1210+
* Without this, when pool cleanup LIFO ordering causes pipe_proc_destroy
1211+
* to run before request_cleanup_handler (e.g. QUIC connection close path:
1212+
* ngx_quic_close_streams -> ngx_http_free_request -> ngx_destroy_pool),
1213+
* request_cleanup sees proc->pipe == NULL and returns early, leaving the
1214+
* timer live. The timer then fires after the request pool is freed,
1215+
* accessing a dangling wait_co_ctx pointer -> SIGSEGV.
1216+
*
1217+
* ngx_close_connection handles everything: timers (ngx_del_timer),
1218+
* posted events (ngx_delete_posted_event), epoll removal, fd close,
1219+
* and connection recycling.
1220+
*/
1221+
1222+
if (pipe->stdout_ctx && pipe->stdout_ctx->c) {
1223+
ngx_close_connection(pipe->stdout_ctx->c);
1224+
pipe->stdout_ctx->c = NULL;
1225+
}
1226+
1227+
if (pipe->stderr_ctx && pipe->stderr_ctx->c) {
1228+
ngx_close_connection(pipe->stderr_ctx->c);
1229+
pipe->stderr_ctx->c = NULL;
1230+
}
1231+
1232+
if (pipe->stdin_ctx && pipe->stdin_ctx->c) {
1233+
ngx_close_connection(pipe->stdin_ctx->c);
1234+
pipe->stdin_ctx->c = NULL;
1235+
}
1236+
12031237
ngx_destroy_pool(pipe->pool);
12041238
proc->pipe = NULL;
12051239
}
@@ -2495,13 +2529,20 @@ ngx_http_lua_pipe_proc_read_stdout_cleanup(void *data)
24952529
"lua pipe proc read stdout cleanup");
24962530

24972531
proc = wait_co_ctx->data;
2532+
2533+
wait_co_ctx->cleanup = NULL;
2534+
2535+
if (proc->pipe == NULL) {
2536+
/* pipe_proc_destroy already ran (LIFO pool cleanup) and cancelled
2537+
* timers/connections; nothing left to clean up here. */
2538+
return;
2539+
}
2540+
24982541
c = proc->pipe->stdout_ctx->c;
24992542
if (c) {
25002543
rev = c->read;
25012544
ngx_http_lua_pipe_clear_event(rev);
25022545
}
2503-
2504-
wait_co_ctx->cleanup = NULL;
25052546
}
25062547

25072548

@@ -2517,13 +2558,18 @@ ngx_http_lua_pipe_proc_read_stderr_cleanup(void *data)
25172558
"lua pipe proc read stderr cleanup");
25182559

25192560
proc = wait_co_ctx->data;
2561+
2562+
wait_co_ctx->cleanup = NULL;
2563+
2564+
if (proc->pipe == NULL) {
2565+
return;
2566+
}
2567+
25202568
c = proc->pipe->stderr_ctx->c;
25212569
if (c) {
25222570
rev = c->read;
25232571
ngx_http_lua_pipe_clear_event(rev);
25242572
}
2525-
2526-
wait_co_ctx->cleanup = NULL;
25272573
}
25282574

25292575

@@ -2539,13 +2585,18 @@ ngx_http_lua_pipe_proc_write_cleanup(void *data)
25392585
"lua pipe proc write cleanup");
25402586

25412587
proc = wait_co_ctx->data;
2588+
2589+
wait_co_ctx->cleanup = NULL;
2590+
2591+
if (proc->pipe == NULL) {
2592+
return;
2593+
}
2594+
25422595
c = proc->pipe->stdin_ctx->c;
25432596
if (c) {
25442597
wev = c->write;
25452598
ngx_http_lua_pipe_clear_event(wev);
25462599
}
2547-
2548-
wait_co_ctx->cleanup = NULL;
25492600
}
25502601

25512602

@@ -2561,13 +2612,18 @@ ngx_http_lua_pipe_proc_wait_cleanup(void *data)
25612612
"lua pipe proc wait cleanup");
25622613

25632614
proc = wait_co_ctx->data;
2615+
2616+
wait_co_ctx->cleanup = NULL;
2617+
2618+
if (proc->pipe == NULL) {
2619+
return;
2620+
}
2621+
25642622
node = proc->pipe->node;
25652623
pipe_node = (ngx_http_lua_pipe_node_t *) &node->color;
25662624
pipe_node->wait_co_ctx = NULL;
25672625

25682626
ngx_http_lua_pipe_clear_event(&wait_co_ctx->sleep);
2569-
2570-
wait_co_ctx->cleanup = NULL;
25712627
}
25722628

25732629

t/191-pipe-proc-quic-close-crash.t

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# vim:set ft= ts=4 sw=4 et fdm=marker:
2+
#
3+
# Regression test for use-after-free crash in ngx_http_lua_pipe_resume_read_stdout_handler.
4+
#
5+
# Root cause: pool cleanup runs in LIFO order. pipe_proc_destroy (registered
6+
# when the pipe is spawned, i.e. *later*) therefore runs *before*
7+
# request_cleanup_handler (registered at Lua handler init time).
8+
# pipe_proc_destroy calls pipe_proc_finalize → ngx_http_lua_pipe_close_helper,
9+
# which, when a read is in progress, posts the event rather than calling
10+
# ngx_close_connection, leaving the read-timeout timer live. It then sets
11+
# proc->pipe = NULL. When request_cleanup_handler runs next it sees
12+
# proc->pipe == NULL and returns early, so the timer is never cancelled.
13+
# After ngx_destroy_pool(r->pool) frees wait_co_ctx, the timer fires and
14+
# dereferences the dangling pointer → SIGSEGV.
15+
#
16+
# Trigger path (QUIC-specific):
17+
# QUIC connection close
18+
# → ngx_quic_close_streams → sc->read->handler (no-op without lua_check_client_abort)
19+
# → ngx_http_free_request → ngx_destroy_pool(r->pool)
20+
# → LIFO pool cleanups: pipe_proc_destroy first, request_cleanup_handler second
21+
# → timer remains live; pool freed; timer fires → SIGSEGV
22+
#
23+
# Fix (ngx_http_lua_ffi_pipe_proc_destroy): after pipe_proc_finalize, call
24+
# ngx_close_connection on any pipe ctx whose connection is still open.
25+
# ngx_close_connection removes both the read-timeout timer (ngx_del_timer)
26+
# and any already-posted event (ngx_delete_posted_event), preventing the UAF.
27+
#
28+
# Timing:
29+
# pipe stdout read timeout : 1 s (timer armed 1 s after request lands)
30+
# curl --max-time : 0.5 s (QUIC connection closed while timer live)
31+
# --- wait : 2 s (covers remaining ~0.5 s + safety margin)
32+
33+
34+
BEGIN {
35+
if (!$ENV{TEST_NGINX_USE_HTTP3}) {
36+
$SkipReason = "TEST_NGINX_USE_HTTP3 is not set";
37+
} else {
38+
my $nginx = $ENV{TEST_NGINX_BINARY} || 'nginx';
39+
my $v = eval { `$nginx -V 2>&1` } // '';
40+
41+
if ($v !~ /--with-http_v3_module/) {
42+
$SkipReason = "requires nginx built with --with-http_v3_module";
43+
}
44+
}
45+
}
46+
47+
use Test::Nginx::Socket::Lua $SkipReason ? (skip_all => $SkipReason) : ();
48+
49+
master_on(); # master process needed to detect/survive worker crash
50+
workers(1);
51+
52+
repeat_each(1);
53+
54+
# 1 subtest per block: the --- no_error_log [alert] check.
55+
# (--- ignore_response suppresses the default status-code subtest.)
56+
plan tests => repeat_each() * blocks();
57+
58+
log_level('info');
59+
no_long_string();
60+
61+
add_block_preprocessor(sub {
62+
my $block = shift;
63+
64+
my $http_config = $block->http_config || '';
65+
$http_config .= <<'_EOC_';
66+
lua_package_path "../lua-resty-core/lib/?.lua;../lua-resty-lrucache/lib/?.lua;;";
67+
68+
init_by_lua_block {
69+
require "resty.core"
70+
}
71+
_EOC_
72+
$block->set_value("http_config", $http_config);
73+
});
74+
75+
run_tests();
76+
77+
__DATA__
78+
79+
=== TEST 1: pipe read timer must not fire after pool is freed on QUIC connection close
80+
--- config
81+
location = /t {
82+
content_by_lua_block {
83+
-- Spawn a long-lived child; stdout will never produce output.
84+
-- set_timeouts(write_timeout, stdout_timeout, stderr_timeout, wait_timeout)
85+
local proc = require("ngx.pipe").spawn({"sleep", "100"})
86+
proc:set_timeouts(nil, 1000) -- 1 s stdout read timeout
87+
88+
-- This call yields and arms a 1 s timer.
89+
-- The test client closes the QUIC connection before the timer fires,
90+
-- triggering the LIFO pool-cleanup use-after-free (without the fix).
91+
proc:stdout_read_line()
92+
}
93+
}
94+
--- request
95+
GET /t
96+
--- timeout: 0.5
97+
--- wait: 2
98+
--- ignore_response
99+
--- curl_error eval: qr/\(28\)/
100+
--- no_error_log
101+
[alert]

0 commit comments

Comments
 (0)