From mdounin at mdounin.ru Sun Aug 2 20:07:37 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sun, 02 Aug 2026 23:07:37 +0300 Subject: [nginx] Upstream: avoid side effects on r->args when creating re... Message-ID: details: http://freenginx.org/hg/nginx/rev/42952827f5a9 branches: changeset: 9581:42952827f5a9 user: Maxim Dounin date: Sun Aug 02 23:05:15 2026 +0300 description: Upstream: avoid side effects on r->args when creating requests. Previously, request creation functions in proxy and gRPC proxy assumed that r->args did not change, but it can be changed as a result of proxy_set_header, proxy_set_body, and grpc_set_header variable evaluation, potentially resulting in buffer overrun. In particular, since freenginx 1.31.3 this can be caused by a map where $args is changed by regular expression named captures, and in older versions this could happen due to 3rd party modules. The fix is to copy r->args (and also r->uri, for consistency) into a local variables when creating a request, so length calculations are guaranteed to use the same values as when writing the buffer. Similarly, in gRPC proxy local variables for r->valid_unparsed_uri, r->args, and r->uri are introduced. diffstat: src/http/modules/ngx_http_grpc_module.c | 40 +++++++++++++++++++------------ src/http/modules/ngx_http_proxy_module.c | 31 +++++++++++++++--------- 2 files changed, 43 insertions(+), 28 deletions(-) diffs (176 lines): diff --git a/src/http/modules/ngx_http_grpc_module.c b/src/http/modules/ngx_http_grpc_module.c --- a/src/http/modules/ngx_http_grpc_module.c +++ b/src/http/modules/ngx_http_grpc_module.c @@ -716,7 +716,8 @@ ngx_http_grpc_create_request(ngx_http_re key_len, val_len, uri_len; uintptr_t escape; ngx_buf_t *b; - ngx_uint_t i, next; + ngx_str_t uri, args; + ngx_uint_t i, next, unparsed_uri; ngx_chain_t *cl, *body; ngx_list_part_t *part; ngx_table_elt_t *header; @@ -739,6 +740,12 @@ ngx_http_grpc_create_request(ngx_http_re headers_len = 0; +#if (NGX_SUPPRESS_WARN) + escape = 0; + ngx_str_null(&uri); + ngx_str_null(&args); +#endif + /* :method header */ if (r->method == NGX_HTTP_GET || r->method == NGX_HTTP_POST) { @@ -757,13 +764,15 @@ ngx_http_grpc_create_request(ngx_http_re /* :path header */ if (r->valid_unparsed_uri) { - escape = 0; + unparsed_uri = 1; uri_len = r->unparsed_uri.len; } else { - escape = 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len, - NGX_ESCAPE_URI); - uri_len = r->uri.len + escape + sizeof("?") - 1 + r->args.len; + unparsed_uri = 0; + uri = r->uri; + args = r->args; + escape = 2 * ngx_escape_uri(NULL, uri.data, uri.len, NGX_ESCAPE_URI); + uri_len = uri.len + escape + sizeof("?") - 1 + args.len; } len += 1 + NGX_HTTP_V2_INT_OCTETS + uri_len; @@ -948,7 +957,7 @@ ngx_http_grpc_create_request(ngx_http_re "grpc header: \":scheme: http\""); } - if (r->valid_unparsed_uri) { + if (unparsed_uri) { if (r->unparsed_uri.len > NGX_HTTP_V2_MAX_FIELD) { ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, @@ -970,20 +979,20 @@ ngx_http_grpc_create_request(ngx_http_re ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "grpc header: \":path: %V\"", &r->unparsed_uri); - } else if (escape || r->args.len > 0) { + } else if (escape || args.len > 0) { p = val_tmp; if (escape) { - p = (u_char *) ngx_escape_uri(p, r->uri.data, r->uri.len, + p = (u_char *) ngx_escape_uri(p, uri.data, uri.len, NGX_ESCAPE_URI); } else { - p = ngx_copy(p, r->uri.data, r->uri.len); + p = ngx_copy(p, uri.data, uri.len); } - if (r->args.len > 0) { + if (args.len > 0) { *p++ = '?'; - p = ngx_copy(p, r->args.data, r->args.len); + p = ngx_copy(p, args.data, args.len); } if (p - val_tmp > NGX_HTTP_V2_MAX_FIELD) { @@ -1002,20 +1011,19 @@ ngx_http_grpc_create_request(ngx_http_re } else { - if (r->uri.len > NGX_HTTP_V2_MAX_FIELD) { + if (uri.len > NGX_HTTP_V2_MAX_FIELD) { ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, "too long grpc request header value: " "\":path: %*s...\"", - 256, r->uri.data); + 256, uri.data); return NGX_ERROR; } *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_PATH_INDEX); - b->last = ngx_http_v2_write_value(b->last, r->uri.data, - r->uri.len, tmp); + b->last = ngx_http_v2_write_value(b->last, uri.data, uri.len, tmp); ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "grpc header: \":path: %V\"", &r->uri); + "grpc header: \":path: %V\"", &uri); } if (!glcf->host_set) { diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -1257,7 +1257,7 @@ ngx_http_proxy_create_request(ngx_http_r key_len, val_len; uintptr_t escape; ngx_buf_t *b; - ngx_str_t method; + ngx_str_t method, uri, args; ngx_uint_t i, unparsed_uri; ngx_chain_t *cl, *body; ngx_list_part_t *part; @@ -1310,6 +1310,11 @@ ngx_http_proxy_create_request(ngx_http_r body_len = 0; headers_len = 0; +#if (NGX_SUPPRESS_WARN) + ngx_str_null(&uri); + ngx_str_null(&args); +#endif + if (plcf->proxy_lengths && ctx->vars.uri.len) { uri_len = ctx->vars.uri.len; @@ -1321,13 +1326,16 @@ ngx_http_proxy_create_request(ngx_http_r loc_len = (r->valid_location && ctx->vars.uri.len) ? plcf->location.len : 0; + uri = r->uri; + args = r->args; + if (r->quoted_uri || r->internal) { - escape = 2 * ngx_escape_uri(NULL, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); + escape = 2 * ngx_escape_uri(NULL, uri.data + loc_len, + uri.len - loc_len, NGX_ESCAPE_URI); } - uri_len = ctx->vars.uri.len + r->uri.len - loc_len + escape - + sizeof("?") - 1 + r->args.len; + uri_len = ctx->vars.uri.len + uri.len - loc_len + escape + + sizeof("?") - 1 + args.len; } if (uri_len == 0) { @@ -1452,18 +1460,17 @@ ngx_http_proxy_create_request(ngx_http_r } if (escape) { - ngx_escape_uri(b->last, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); - b->last += r->uri.len - loc_len + escape; + ngx_escape_uri(b->last, uri.data + loc_len, + uri.len - loc_len, NGX_ESCAPE_URI); + b->last += uri.len - loc_len + escape; } else { - b->last = ngx_copy(b->last, r->uri.data + loc_len, - r->uri.len - loc_len); + b->last = ngx_copy(b->last, uri.data + loc_len, uri.len - loc_len); } - if (r->args.len > 0) { + if (args.len > 0) { *b->last++ = '?'; - b->last = ngx_copy(b->last, r->args.data, r->args.len); + b->last = ngx_copy(b->last, args.data, args.len); } } From mdounin at mdounin.ru Sun Aug 2 20:07:54 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sun, 02 Aug 2026 23:07:54 +0300 Subject: [nginx-tests] Tests: fixed map prerequisite in tests. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/1eb66a18000a branches: changeset: 2083:1eb66a18000a user: Maxim Dounin date: Sun Aug 02 20:51:36 2026 +0300 description: Tests: fixed map prerequisite in tests. diffstat: grpc_headers.t | 2 +- rewrite.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diffs (24 lines): diff --git a/grpc_headers.t b/grpc_headers.t --- a/grpc_headers.t +++ b/grpc_headers.t @@ -23,7 +23,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/http http_v2 grpc rewrite/)->plan(7) + ->has(qw/http http_v2 grpc rewrite map/)->plan(7) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% diff --git a/rewrite.t b/rewrite.t --- a/rewrite.t +++ b/rewrite.t @@ -21,7 +21,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite proxy/)->plan(27) +my $t = Test::Nginx->new()->has(qw/http rewrite map proxy/)->plan(27) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% From mdounin at mdounin.ru Sun Aug 2 20:07:54 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sun, 02 Aug 2026 23:07:54 +0300 Subject: [nginx-tests] Tests: reworked and simplified proxy variables tests. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/5a3c867b2f5b branches: changeset: 2084:5a3c867b2f5b user: Maxim Dounin date: Sun Aug 02 23:06:07 2026 +0300 description: Tests: reworked and simplified proxy variables tests. diffstat: proxy.t | 139 +---------------------------- proxy_variables.t | 264 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 193 insertions(+), 210 deletions(-) diffs (506 lines): diff --git a/proxy.t b/proxy.t --- a/proxy.t +++ b/proxy.t @@ -21,7 +21,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(28); +my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(6); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -35,26 +35,14 @@ events { http { %%TEST_GLOBALS_HTTP%% - log_format time '$upstream_connect_time:$upstream_header_time:' - '$upstream_response_time'; - upstream u { server 127.0.0.1:8081; } - upstream u2 { - server 127.0.0.1:8081; - server 127.0.0.1:8081; - } - server { listen 127.0.0.1:8080; server_name localhost; - add_header X-Connect $upstream_connect_time; - add_header X-Header $upstream_header_time; - add_header X-Response $upstream_response_time; - location / { proxy_pass http://127.0.0.1:8081; proxy_read_timeout 2s; @@ -71,25 +59,6 @@ http { proxy_pass http://127.0.0.1:8081; proxy_connect_timeout 2s; } - - location /time/ { - proxy_pass http://127.0.0.1:8081/; - access_log %%TESTDIR%%/time.log time; - } - - location /pnu { - proxy_pass http://u2/bad; - } - - location /vars { - proxy_pass http://127.0.0.1:8080/stub; - - add_header X-Proxy-Host $proxy_host; - add_header X-Proxy-Port $proxy_port; - add_header X-Proxy-Forwarded $proxy_add_x_forwarded_for; - } - - location /stub { } } } @@ -112,77 +81,8 @@ like(http_get('/var?b=u/'), qr/SEE-THIS/ like(http_get('/timeout'), qr/200 OK/, 'proxy connect timeout'); -my $re = qr/(\d\.\d{3})/; -my $p0 = port(8080); -my ($ct, $ht, $rt, $ct2, $ht2, $rt2, $ct3, $ht3, $rt3); - -like(http_get('/vars'), qr/X-Proxy-Host:\s127\.0\.0\.1:$p0/, 'proxy_host'); -like(http_get('/vars'), qr/X-Proxy-Port:\s$p0/, 'proxy_port'); -like(http_xff('/vars', '192.0.2.1'), qr/X-Proxy-Forwarded:.*192\.0\.2\.1/, - 'proxy_add_x_forwarded_for'); - -($ct, $ht) = get('/time/header'); -cmp_ok($ct, '<', 1, 'connect time - slow response header'); -cmp_ok($ht, '>=', 1, 'header time - slow response header'); - -($ct, $ht) = get('/time/body'); -cmp_ok($ct, '<', 1, 'connect time - slow response body'); -cmp_ok($ht, '<', 1, 'header time - slow response body'); - -my $s = http_get('/time/header', start => 1); -select undef, undef, undef, 0.4; -close ($s); - -# expect no header time in 1st (bad) upstream, no (yet) response time in 2nd - -$re = qr/(\d\.\d{3}|-)/; -($ct, $ct2, $ht, $ht2, $rt, $rt2) = get('/pnu', many => 1); - -cmp_ok($ct, '<', 1, 'connect time - next'); -cmp_ok($ct2, '<', 1, 'connect time - next 2'); - -is($ht, '-', 'header time - next'); -cmp_ok($ht2, '<', 1, 'header time - next 2'); - -cmp_ok($rt, '>=', 1, 'response time - next'); -is($rt2, '-', 'response time - next 2'); - -$t->stop(); - -($ct, $ht, $rt, $ct2, $ht2, $rt2, $ct3, $ht3, $rt3) - = $t->read_file('time.log') =~ /^$re:$re:$re\n$re:$re:$re\n$re:$re:$re$/; - -cmp_ok($ct, '<', 1, 'connect time log - slow response header'); -cmp_ok($ct2, '<', 1, 'connect time log - slow response body'); -cmp_ok($ct3, '<', 1, 'connect time log - client close'); - -cmp_ok($ht, '>=', 1, 'header time log - slow response header'); -cmp_ok($ht2, '<', 1, 'header time log - slow response body'); -is($ht3, '-', 'header time log - client close'); - -cmp_ok($rt, '>=', 1, 'response time log - slow response header'); -cmp_ok($rt2, '>=', 1, 'response time log - slow response body'); -cmp_ok($rt3, '>', $ct3, 'response time log - client close'); - ############################################################################### -sub get { - my ($uri, %extra) = @_; - my $re = $extra{many} ? qr/$re, $re?/ : $re; - my $r = http_get($uri); - $r =~ /X-Connect: $re/, $r =~ /X-Header: $re/, $r =~ /X-Response: $re/; -} - -sub http_xff { - my ($uri, $xff) = @_; - return http(<new( @@ -238,43 +138,6 @@ Connection: close EOF - } elsif ($uri eq '/bad') { - - if ($once) { - $once = 0; - select undef, undef, undef, 1.1; - next; - } - - print $client <new()->has(qw/http proxy/)->plan(4) +my $t = Test::Nginx->new()->has(qw/http proxy cache rewrite/)->plan(22) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -35,100 +35,220 @@ events { http { %%TEST_GLOBALS_HTTP%% - log_format u $uri:$upstream_response_length:$upstream_bytes_received: - $upstream_bytes_sent:$upstream_http_x_len; + upstream u { + server 127.0.0.1:8082 max_fails=0; + server 127.0.0.1:8081 backup; + } + + proxy_cache_path cache keys_zone=one:1m; server { listen 127.0.0.1:8080; server_name localhost; location / { - proxy_pass http://127.0.0.1:8081; - access_log %%TESTDIR%%/test.log u; + proxy_pass http://127.0.0.1:8081/stub; + add_header X-Proxy-Host $proxy_host; + add_header X-Proxy-Port $proxy_port; + add_header X-Proxy-Forwarded $proxy_add_x_forwarded_for; + add_header X-Upstream-Addr $upstream_addr; + add_header X-Upstream-Status $upstream_status; + } + + location /time { + proxy_pass http://127.0.0.1:8081/stub; + add_header X-Connect-Time $upstream_connect_time; + add_header X-Header-Time $upstream_header_time; + add_header X-Response-Time $upstream_response_time; + } + + location /next { + proxy_pass http://u/stub; + add_header X-Connect-Time $upstream_connect_time; + add_header X-Header-Time $upstream_header_time; + add_header X-Response-Time $upstream_response_time; + } + + location /length { + proxy_pass http://127.0.0.1:8081/stub_length; + add_trailer X-Response-Length $upstream_response_length; + add_trailer X-Bytes-Received $upstream_bytes_received; + add_trailer X-Bytes-Sent $upstream_bytes_sent; + } + + location /header { + proxy_pass http://127.0.0.1:8081/stub_header; + add_header X-Header $upstream_http_foo; + } + + location /trailer { + proxy_pass http://127.0.0.1:8081/stub_trailer; + proxy_http_version 1.1; + add_header X-Trailer $upstream_trailer_foo; + } + + location /cookie { + proxy_pass http://127.0.0.1:8081/stub_cookie; + add_header X-Cookie $upstream_cookie_foo; } + + location /cache { + proxy_pass http://127.0.0.1:8081/stub; + proxy_cache one; + proxy_cache_key foo; + proxy_cache_valid 200 1m; + add_header X-Cache-Status $upstream_cache_status; + add_header X-Cache-Key $upstream_cache_key; + add_header X-Cache-Age $upstream_cache_age; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + } + + location /stub_length { + add_header X-Length $request_length; + limit_rate 800; + } + + location /stub_header { + add_header Foo foo; + add_header Foo bar; + } + + location /stub_trailer { + add_trailer Foo foo; + add_trailer Foo bar; + } + + location /stub_cookie { + add_header Set-Cookie foo=foo; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + return 444; } } EOF -$t->run_daemon(\&http_daemon, port(8081)); +$t->write_file('stub', ''); +$t->write_file('stub_length', '1234567890' x 100); +$t->write_file('stub_header', ''); +$t->write_file('stub_trailer', ''); +$t->write_file('stub_cookie', ''); $t->run(); -$t->waitforsocket('127.0.0.1:' . port(8081)); - ############################################################################### my $r; -my ($l1) = ($r = http_get('/')) =~ /X-Len: (\d+)/; -like($r, qr/SEE-THIS/, 'proxy request'); +# $proxy_host +# $proxy_port +# $proxy_add_x_forwarded_for + +$r = get('/'); +like($r, qr/X-Proxy-Host: 127\.0\.0\.1:/, '$proxy_host'); +like($r, qr/X-Proxy-Port: \d+/, '$proxy_port'); +like($r, qr/X-Proxy-Forwarded: 127\.0\.0\.1/, '$proxy_add_x_forwarded_for'); + +like(get('/', 'X-Forwarded-For: 192.0.2.1'), + qr/X-Proxy-Forwarded: 192\.0\.2\.1, 127\.0\.0\.1/, + '$proxy_add_x_forwarded_for add'); + +# $upstream_addr +# $upstream_status -my ($l2) = ($r = http_get('/multi')) =~ /X-Len: (\d+)/; -like($r, qr/AND-THIS/, 'proxy request with multiple packets'); +$r = get('/'); +like($r, qr/X-Upstream-Addr: 127\.0\.0\.1:/, '$upstream_addr'); +like($r, qr/X-Upstream-Status: 200/, '$upstream_status'); + +# $upstream_connect_time +# $upstream_header_time +# $upstream_response_time + +# Note that $upstream_response_time is only available after the upstream +# request is finalized. + +$r = get('/time'); +like($r, qr/X-Connect-Time: \d\./, '$upstream_connect_time'); +like($r, qr/X-Header-Time: \d\./, '$upstream_header_time'); +like($r, qr/X-Response-Time: -/, '$upstream_response_time'); + +# Since first request fails before getting a header, $upstream_header_time +# will be only available for the second request, after switching the next +# upstream server. And $upstream_response_time is only available for +# the first request, but not available for the second one, since it is +# not yet finalized. -$t->stop(); +$r = get('/next'); +like($r, qr/X-Connect-Time: \d\.\d+, \d\.\d+/, + '$upstream_connect_time next upstream'); +like($r, qr/X-Header-Time: -, \d\.\d+/, + '$upstream_header_time next upstream'); +like($r, qr/X-Response-Time: \d\.\d+, -/, + '$upstream_response_time next upstream'); + +# $upstream_response_length +# $upstream_bytes_received +# $upstream_bytes_sent + +# Final values are only available after the response is received, so +# we use trailers here. Note that this requires HTTP/1.1 request. + +$r = get('/length'); +like($r, qr/X-Response-Length: 1000/, '$upstream_response_length'); +like($r, qr/X-Bytes-Received: \d+/, '$upstream_bytes_received'); +like($r, qr/X-Length: (\d+).*X-Bytes-Sent: \1/s, '$upstream_bytes_sent'); + +# $upstream_http_ +# $upstream_trailer_ + +like(get('/header'), qr/X-Header: foo, bar/, '$upstream_http_foo'); -my $f = $t->read_file('test.log'); -Test::Nginx::log_core('||', $f); +TODO: { +local $TODO = 'no trailers support in proxy yet'; + +like(get('/trailer'), qr/X-Trailer: foo, bar/, '$upstream_trailer_foo'); + +} + +# $upstream_cookie_ + +like(get('/cookie'), qr/X-Cookie: foo/, '$upstream_cookie_foo'); -like($f, qr!^/:23:68:$l1:$l1!m, 'log - response length'); -like($f, qr!^/multi:32:77:$l2:$l2!m, 'log - response length - multi packets'); +# $upstream_cache_status +# $upstream_cache_key +# $upstream_cache_age + +# Note that the $upstream_cache_last_modified and $upstream_cache_etag +# variables are internal, and therefore not tested here. + +$r = get('/cache'); +like($r, qr/X-Cache-Status: MISS/, '$upstream_cache_status'); +like($r, qr/X-Cache-Key: foo/, '$upstream_cache_key'); + +$r = get('/cache'); +like($r, qr/X-Cache-Status: HIT/, '$upstream_cache_status hit'); +like($r, qr/X-Cache-Age: \d+/, '$upstream_cache_age'); ############################################################################### -sub http_daemon { - my ($port) = @_; - my $server = IO::Socket::INET->new( - Proto => 'tcp', - LocalHost => '127.0.0.1', - LocalPort => $port, - Listen => 5, - Reuse => 1 - ) - or die "Can't create listening socket: $!\n"; - - local $SIG{PIPE} = 'IGNORE'; - - while (my $client = $server->accept()) { - $client->autoflush(1); - - my $headers = ''; - my $uri = ''; - - while (<$client>) { - $headers .= $_; - last if (/^\x0d?\x0a?$/); - } - - $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; - my $len = length($headers); - - if ($uri eq '/') { - print $client <<"EOF"; -HTTP/1.1 200 OK -Connection: close -X-Len: $len - -EOF - print $client "TEST-OK-IF-YOU-SEE-THIS" - unless $headers =~ /^HEAD/i; - - } elsif ($uri eq '/multi') { - - print $client <<"EOF"; -HTTP/1.1 200 OK -Connection: close -X-Len: $len - -TEST-OK-IF-YOU-SEE-THIS -EOF - - select undef, undef, undef, 0.1; - print $client 'AND-THIS'; - } - - close $client; - } +sub get { + my ($url, @headers) = @_; + return http( + "GET $url HTTP/1.1" . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + join(CRLF, @headers) . CRLF . CRLF + ); } ############################################################################### From mdounin at mdounin.ru Sun Aug 2 20:07:54 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sun, 02 Aug 2026 23:07:54 +0300 Subject: [nginx-tests] Tests: tests for proxy_pass with $args changed as ... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/a7fd2e00a815 branches: changeset: 2085:a7fd2e00a815 user: Maxim Dounin date: Sun Aug 02 23:06:34 2026 +0300 description: Tests: tests for proxy_pass with $args changed as a side effect. diffstat: grpc_headers.t | 26 +++++++++++++++++++++++++- proxy_set_body.t | 26 +++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diffs (110 lines): diff --git a/grpc_headers.t b/grpc_headers.t --- a/grpc_headers.t +++ b/grpc_headers.t @@ -23,7 +23,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/http http_v2 grpc rewrite map/)->plan(7) + ->has(qw/http http_v2 grpc rewrite map/)->plan(8) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -40,6 +40,10 @@ http { ~(?.*) $capture; } + map $uri $map_args { + ~(?.*) $args; + } + large_client_header_buffers 2 4m; ignore_invalid_headers off; @@ -84,6 +88,11 @@ http { set $a $a$a$a$a$a$a$a$a$a$a; set $a $a$a$a$a$a$a$a$a$a$a; } + + location /map_args { + grpc_pass 127.0.0.1:8081; + grpc_set_header X-Blah $map_args; + } } server { @@ -156,4 +165,19 @@ like(http_get('/long_set?' . ('~' x 210) } +TODO: { +todo_skip 'might coredump', 1 + unless $t->has_version('1.31.4') + or $ENV{TEST_NGINX_UNSAFE}; +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +# when $args is changed as a side effect of a variable lookup +# during grpc_set_header evaluation, buffer allocated might be +# to small + +like(http_get('/map_args/' . ('x' x 512)), qr!/map_args!, + '$args changed as side effect'); + +} + ############################################################################### diff --git a/proxy_set_body.t b/proxy_set_body.t --- a/proxy_set_body.t +++ b/proxy_set_body.t @@ -21,7 +21,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http proxy rewrite map/)->plan(4) +my $t = Test::Nginx->new()->has(qw/http proxy rewrite map/)->plan(5) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -38,6 +38,10 @@ http { ~(?.*) $capture; } + map $uri $map_args { + ~(?.*) $args; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -72,6 +76,11 @@ http { proxy_set_header X-Header "header $capture $map_capture end"; } + location /map_args { + proxy_pass http://127.0.0.1:8080/body; + proxy_set_header X-Header "header $map_args end"; + } + location /body { add_header X-Body $request_body; add_header X-Header $http_x_header; @@ -107,4 +116,19 @@ like(http_get('/map_header'), qr!X-Heade } +TODO: { +todo_skip 'might coredump', 1 + unless $t->has_version('1.31.4') + or $ENV{TEST_NGINX_UNSAFE}; +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +# when $args is changed as a side effect of a variable lookup +# during proxy_set_header or proxy_set_body evaluation, buffer +# allocated might be to small + +like(http_get('/map_args/' . ('x' x 512)), qr!/map_args!, + '$args changed as side effect'); + +} + ############################################################################### From mdounin at mdounin.ru Sun Aug 2 20:10:49 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 02 Aug 2026 23:10:49 +0300 Subject: [PATCH] Fixed positional captures with cloned subrequests after non-match Message-ID: # HG changeset patch # User Maxim Dounin # Date 1785701313 -10800 # Sun Aug 02 23:08:33 2026 +0300 # Node ID da23d6e6186c5a11b082d894d0107d90761d8079 # Parent 42952827f5a9cc7cf589844bcb6afd1eedd13d50 Fixed positional captures with cloned subrequests after non-match. In 7427:81d49f85afed, the r->realloc_captures flag was introduced, to facilitate use of positional captures with cloned subrequests, as used by proxy_cache_background_update and the slice module. The flag, however, was only used to trigger a new allocation of r->captures, and it contained uninitialized if a regular expression was not matched. As a result, if a regular expression was executed in a cloned subrequest and not matched, and then a positional capture was used, it might refer to arbitrary memory, such as in the following configuration (known as CVE-2026-60005, though security impact is questionable): map $uri $map { ~(not-matched) 1; } location ~ /regex/(foo) { proxy_pass ... proxy_cache ... proxy_cache_background_update on; proxy_set_header Foo $map:$1; } The fix is to actually copy r->captures contents to the new allocation, so positional captures properly refer to the last matched regular expression. See also: https://github.com/nginx/nginx/commit/0cca8e055a2d909f1a00c2071665b502ec2fe94c diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -2657,6 +2657,7 @@ ngx_http_regex_compile(ngx_conf_t *cf, n ngx_int_t ngx_http_regex_exec(ngx_http_request_t *r, ngx_http_regex_t *re, ngx_str_t *s) { + int *captures; ngx_int_t rc, index; ngx_uint_t i, n, len; ngx_http_variable_value_t vv; @@ -2670,10 +2671,16 @@ ngx_http_regex_exec(ngx_http_request_t * if (r->captures == NULL || r->realloc_captures) { r->realloc_captures = 0; - r->captures = ngx_palloc(r->pool, len * sizeof(int)); - if (r->captures == NULL) { + captures = ngx_pcalloc(r->pool, len * sizeof(int)); + if (captures == NULL) { return NGX_ERROR; } + + if (r->captures) { + ngx_memcpy(captures, r->captures, len * sizeof(int)); + } + + r->captures = captures; } } else { From mdounin at mdounin.ru Sun Aug 2 20:12:34 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 02 Aug 2026 23:12:34 +0300 Subject: [PATCH 1 of 2] Tests: proxy_cache_use_stale.t style In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1785701359 -10800 # Sun Aug 02 23:09:19 2026 +0300 # Node ID d17b63340855799260047f83283efa005928023f # Parent a7fd2e00a815914171b8ee7df0b0772e143a6a11 Tests: proxy_cache_use_stale.t style. diff --git a/proxy_cache_use_stale.t b/proxy_cache_use_stale.t --- a/proxy_cache_use_stale.t +++ b/proxy_cache_use_stale.t @@ -24,7 +24,8 @@ use Test::Nginx qw/ :DEFAULT http_end /; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http proxy cache rewrite limit_req ssi/) +my $t = Test::Nginx->new() + ->has(qw/http proxy cache rewrite limit_req ssi/)->plan(35) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -37,9 +38,9 @@ events { http { %%TEST_GLOBALS_HTTP%% - proxy_cache_path %%TESTDIR%%/cache levels=1:2 keys_zone=NAME:1m; + proxy_cache_path cache keys_zone=one:1m; - limit_req_zone $binary_remote_addr zone=one:1m rate=10r/m; + limit_req_zone $binary_remote_addr zone=slow:1m rate=10r/m; server { listen 127.0.0.1:8080; @@ -51,65 +52,54 @@ http { } location /escape { - proxy_pass http://127.0.0.1:8081; - proxy_cache NAME; - proxy_cache_background_update on; + proxy_pass http://127.0.0.1:8081; + proxy_cache one; + proxy_cache_background_update on; add_header X-Cache-Status $upstream_cache_status; } location / { - proxy_pass http://127.0.0.1:8081; - - proxy_cache NAME; + proxy_pass http://127.0.0.1:8081; - proxy_cache_key $uri; - - proxy_cache_revalidate on; - - proxy_cache_background_update on; + proxy_cache one; + proxy_cache_key $uri; + proxy_cache_revalidate on; + proxy_cache_background_update on; add_header X-Cache-Status $upstream_cache_status; location /t4.html { - proxy_pass http://127.0.0.1:8081/t.html; - - proxy_cache_revalidate off; + proxy_pass http://127.0.0.1:8081/t.html; + proxy_cache_revalidate off; } location /t5.html { - proxy_pass http://127.0.0.1:8081/t.html; - - proxy_cache_background_update off; + proxy_pass http://127.0.0.1:8081/t.html; + proxy_cache_background_update off; } location ~ /(reg)(?Pexp).html { - proxy_pass http://127.0.0.1:8081/$1$name.html; - - proxy_cache_background_update on; + proxy_pass http://127.0.0.1:8081/$1$name.html; } location /updating/ { - proxy_pass http://127.0.0.1:8081/; - - proxy_cache_use_stale updating; + proxy_pass http://127.0.0.1:8081/; + proxy_cache_use_stale updating; } location /next/ { - proxy_pass http://127.0.0.1:8081/; - - proxy_next_upstream http_500; + proxy_pass http://127.0.0.1:8081/; + proxy_next_upstream http_500; } location /t7.html { - proxy_pass http://127.0.0.1:8081; - - sendfile_max_chunk 4k; + proxy_pass http://127.0.0.1:8081; + sendfile_max_chunk 4k; } location /t8.html { - proxy_pass http://127.0.0.1:8081/t.html; - - proxy_cache_valid 1s; + proxy_pass http://127.0.0.1:8081/t.html; + proxy_cache_valid 1s; } if ($arg_if) { @@ -117,6 +107,7 @@ http { } } } + server { listen 127.0.0.1:8081; server_name localhost; @@ -134,7 +125,7 @@ http { location / { } location /t6.html { - limit_req zone=one burst=2; + limit_req zone=slow burst=2; } location /t9.html { @@ -156,7 +147,7 @@ EOF $t->write_file('escape.html', 'SEE-THIS'); $t->write_file('regexp.html', 'SEE-THIS'); -$t->run()->plan(35); +$t->run(); ############################################################################### From mdounin at mdounin.ru Sun Aug 2 20:12:35 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 02 Aug 2026 23:12:35 +0300 Subject: [PATCH 2 of 2] Tests: added test for positional captures with cloned subrequests In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1785701374 -10800 # Sun Aug 02 23:09:34 2026 +0300 # Node ID a9bdee1c091f077bd4b220bc07cf1e4b5b87180a # Parent d17b63340855799260047f83283efa005928023f Tests: added test for positional captures with cloned subrequests. diff --git a/proxy_cache_use_stale.t b/proxy_cache_use_stale.t --- a/proxy_cache_use_stale.t +++ b/proxy_cache_use_stale.t @@ -1,5 +1,6 @@ #!/usr/bin/perl +# (C) Maxim Dounin # (C) Sergey Kandaurov # (C) Nginx, Inc. @@ -25,7 +26,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/http proxy cache rewrite limit_req ssi/)->plan(35) + ->has(qw/http proxy cache rewrite limit_req ssi map/)->plan(37) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -42,6 +43,11 @@ http { limit_req_zone $binary_remote_addr zone=slow:1m rate=10r/m; + map $uri $map { + volatile; + ~(nomatch) 1; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -82,6 +88,11 @@ http { proxy_pass http://127.0.0.1:8081/$1$name.html; } + location ~ /map_(regexp.html) { + proxy_pass http://127.0.0.1:8081/$map$1; + add_header X-Cache-Status $upstream_cache_status:$map:$1; + } + location /updating/ { proxy_pass http://127.0.0.1:8081/; proxy_cache_use_stale updating; @@ -172,6 +183,7 @@ get('/next/tt.html', 'max-age=1, stale-i get('/t8.html', 'stale-while-revalidate=20'); get('/escape.htm%6C', 'max-age=1, stale-while-revalidate=20'); get('/regexp.html', 'max-age=1, stale-while-revalidate=20'); +get('/map_regexp.html', 'max-age=1, stale-while-revalidate=20'); sleep 2; @@ -209,6 +221,22 @@ like(http_get('/t5.html'), qr/REVALIDATE like(http_get('/regexp.html'), qr/STALE/, 's-w-r - regexp background update'); like(http_get('/regexp.html'), qr/HIT/, 's-w-r - regexp revalidated'); +TODO: { +todo_skip 'might coredump', 2 + unless $t->has_version('1.31.4') + or $ENV{TEST_NGINX_UNSAFE}; +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +# when r->captures was reallocated but no match happened, positional captures +# used uninitialized offsets from newly allocated r->captures + +like(http_get('/map_regexp.html'), qr/STALE::regexp.html/, + 's-w-r - regexp and map background update'); +like(http_get('/map_regexp.html'), qr/HIT::regexp.html/, + 's-w-r - regexp and map revalidated'); + +} + # UPDATING while s-w-r $t->write_file('t6.html', 'SEE-THAT'); From mdounin at mdounin.ru Wed Aug 5 21:31:01 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 06 Aug 2026 00:31:01 +0300 Subject: [PATCH] Perl: removed zero copy of scalars in $r->print() Message-ID: # HG changeset patch # User Maxim Dounin # Date 1785924079 -10800 # Wed Aug 05 13:01:19 2026 +0300 # Node ID ee54b078e1c17384c272a4f9f64c29bc67974ca9 # Parent da23d6e6186c5a11b082d894d0107d90761d8079 Perl: removed zero copy of scalars in $r->print(). In 9569:7a3dbb7905ad, zero copying of arbitrary single SV values was introduced along with reference counting. It turns out it was an incorrect change though, since non-read-only scalars can be changed after the $r->print() call while the buffer is still needed, leading to corrupted output, such as in the following example: location / { perl 'sub { my $r = shift; $r->send_http_header; my $foo = "it"; $foo .= " works"; $r->print($foo); $foo = " changed"; $r->print($foo); return OK; }'; } Further, checking SvREADONLY() as it was done previously also seems to be incorrect in some edge cases, and might lead to similar issues. In particular, SvREADONLY() can be set and cleared directly by Perl code with Internals::SvREADONLY(): location / { perl 'sub { my $r = shift; $r->send_http_header; my $foo = "it"; $foo .= " works"; Internals::SvREADONLY($foo, 1); $r->print($foo); Internals::SvREADONLY($foo, 0); $foo = " changed"; $r->print($foo); return OK; }'; } The fix is to remove the zero copy optimization in $r->print() completely, similarly to how it was done in ngx_http_perl_sv2str() in 9569:7a3dbb7905ad. An alternative approach would be to create a private copy-on-write SV, thus preserving the buffer until the SV is destroyed, but Perl as of now provides no documented interfaces to create copy-on-write SVs from XS code. See also: https://github.com/nginx/nginx/commit/5e0deb7018b06cdebafab5570b2e9fdf7c3f22de diff --git a/src/http/modules/perl/nginx.xs b/src/http/modules/perl/nginx.xs --- a/src/http/modules/perl/nginx.xs +++ b/src/http/modules/perl/nginx.xs @@ -689,51 +689,6 @@ print(r, ...) croak("print(): header not sent"); } - if (items == 2) { - - /* - * do zero copy for prolate single SV: - * $r->print("some text\n"); - */ - - sv = ST(1); - - if (SvROK(sv) && SvTYPE(SvRV(sv)) == SVt_PV) { - sv = SvRV(sv); - } - - if (SvPOK(sv)) { - - p = (u_char *) SvPV(sv, len); - - if (len == 0) { - XSRETURN_EMPTY; - } - - if (ngx_http_perl_refcount(aTHX_ r, sv) != NGX_OK) { - ctx->error = 1; - croak("ngx_http_perl_refcount() failed"); - } - - b = ngx_calloc_buf(r->pool); - if (b == NULL) { - ctx->error = 1; - croak("ngx_calloc_buf() failed"); - } - - b->memory = 1; - b->pos = p; - b->last = p + len; - b->start = p; - b->end = b->last; - - ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "$r->print: single SV: %z", len); - - goto out; - } - } - size = 0; for (i = 1; i < items; i++) { @@ -773,8 +728,6 @@ print(r, ...) b->last = ngx_cpymem(b->last, p, len); } - out: - rc = ngx_http_perl_output(r, ctx, b); if (rc == NGX_ERROR) { From mdounin at mdounin.ru Wed Aug 5 21:31:52 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 06 Aug 2026 00:31:52 +0300 Subject: [PATCH] Tests: perl tests with scalar changed after $r->print() In-Reply-To: References: Message-ID: <0fb277edb976c00c9150.1785965512@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1785923744 -10800 # Wed Aug 05 12:55:44 2026 +0300 # Node ID 0fb277edb976c00c91500e32d321077d9cf2be7e # Parent a9bdee1c091f077bd4b220bc07cf1e4b5b87180a Tests: perl tests with scalar changed after $r->print(). diff --git a/perl_refcount.t b/perl_refcount.t --- a/perl_refcount.t +++ b/perl_refcount.t @@ -22,7 +22,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http perl/)->plan(6) +my $t = Test::Nginx->new()->has(qw/http perl/)->plan(8) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -106,6 +106,34 @@ http { return OK; }'; } + + location /print_changed { + perl 'sub { + my $r = shift; + $r->send_http_header; + my $foo = "it"; + $foo .= " works"; + $r->print($foo); + $foo = " changed"; + $r->print($foo); + return OK; + }'; + } + + location /print_readonly_changed { + perl 'sub { + my $r = shift; + $r->send_http_header; + my $foo = "it"; + $foo .= " works"; + Internals::SvREADONLY($foo, 1); + $r->print($foo); + Internals::SvREADONLY($foo, 0); + $foo = " changed"; + $r->print($foo); + return OK; + }'; + } } } @@ -162,4 +190,14 @@ like(http_get('/bless'), qr/500 Internal } +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +like(http_get('/print_changed'), qr/works changed/, + 'perl print scalar changed'); +like(http_get('/print_readonly_changed'), qr/works changed/, + 'perl print scalar readonly changed'); + +} + ############################################################################### From mdounin at mdounin.ru Fri Aug 14 06:13:54 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 14 Aug 2026 09:13:54 +0300 Subject: [nginx] Fixed positional captures with cloned subrequests after ... Message-ID: details: http://freenginx.org/hg/nginx/rev/e5cc09d2beb3 branches: changeset: 9582:e5cc09d2beb3 user: Maxim Dounin date: Fri Aug 14 09:05:29 2026 +0300 description: Fixed positional captures with cloned subrequests after non-match. In 7427:81d49f85afed, the r->realloc_captures flag was introduced, to facilitate use of positional captures with cloned subrequests, as used by proxy_cache_background_update and the slice module. The flag, however, was only used to trigger a new allocation of r->captures, and it contained uninitialized if a regular expression was not matched. As a result, if a regular expression was executed in a cloned subrequest and not matched, and then a positional capture was used, it might refer to arbitrary memory, such as in the following configuration (known as CVE-2026-60005, though security impact is questionable): map $uri $map { ~(not-matched) 1; } location ~ /regex/(foo) { proxy_pass ... proxy_cache ... proxy_cache_background_update on; proxy_set_header Foo $map:$1; } The fix is to actually copy r->captures contents to the new allocation, so positional captures properly refer to the last matched regular expression. See also: https://github.com/nginx/nginx/commit/0cca8e055a2d909f1a00c2071665b502ec2fe94c diffstat: src/http/ngx_http_variables.c | 11 +++++++++-- 1 files changed, 9 insertions(+), 2 deletions(-) diffs (30 lines): diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -2657,6 +2657,7 @@ ngx_http_regex_compile(ngx_conf_t *cf, n ngx_int_t ngx_http_regex_exec(ngx_http_request_t *r, ngx_http_regex_t *re, ngx_str_t *s) { + int *captures; ngx_int_t rc, index; ngx_uint_t i, n, len; ngx_http_variable_value_t vv; @@ -2670,10 +2671,16 @@ ngx_http_regex_exec(ngx_http_request_t * if (r->captures == NULL || r->realloc_captures) { r->realloc_captures = 0; - r->captures = ngx_palloc(r->pool, len * sizeof(int)); - if (r->captures == NULL) { + captures = ngx_pcalloc(r->pool, len * sizeof(int)); + if (captures == NULL) { return NGX_ERROR; } + + if (r->captures) { + ngx_memcpy(captures, r->captures, len * sizeof(int)); + } + + r->captures = captures; } } else { From mdounin at mdounin.ru Fri Aug 14 06:13:55 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 14 Aug 2026 09:13:55 +0300 Subject: [nginx] Perl: removed zero copy of scalars in $r->print(). Message-ID: details: http://freenginx.org/hg/nginx/rev/629f14fee33e branches: changeset: 9583:629f14fee33e user: Maxim Dounin date: Fri Aug 14 09:10:44 2026 +0300 description: Perl: removed zero copy of scalars in $r->print(). In 9569:7a3dbb7905ad, zero copying of arbitrary single SV values was introduced along with reference counting. It turns out it was an incorrect change though, since non-read-only scalars can be changed after the $r->print() call while the buffer is still needed, leading to corrupted output, such as in the following example: location / { perl 'sub { my $r = shift; $r->send_http_header; my $foo = "it"; $foo .= " works"; $r->print($foo); $foo = " changed"; $r->print($foo); return OK; }'; } Further, checking SvREADONLY() as it was done previously also seems to be incorrect in some edge cases, and might lead to similar issues. In particular, SvREADONLY() can be set and cleared directly by Perl code with Internals::SvREADONLY(): location / { perl 'sub { my $r = shift; $r->send_http_header; my $foo = "it"; $foo .= " works"; Internals::SvREADONLY($foo, 1); $r->print($foo); Internals::SvREADONLY($foo, 0); $foo = " changed"; $r->print($foo); return OK; }'; } The fix is to remove the zero copy optimization in $r->print() completely, similarly to how it was done in ngx_http_perl_sv2str() in 9569:7a3dbb7905ad. An alternative approach would be to create a private copy-on-write SV, thus preserving the buffer until the SV is destroyed, but Perl as of now provides no documented interfaces to create copy-on-write SVs from XS code. See also: https://github.com/nginx/nginx/commit/5e0deb7018b06cdebafab5570b2e9fdf7c3f22de diffstat: src/http/modules/perl/nginx.xs | 47 ------------------------------------------ 1 files changed, 0 insertions(+), 47 deletions(-) diffs (64 lines): diff --git a/src/http/modules/perl/nginx.xs b/src/http/modules/perl/nginx.xs --- a/src/http/modules/perl/nginx.xs +++ b/src/http/modules/perl/nginx.xs @@ -689,51 +689,6 @@ print(r, ...) croak("print(): header not sent"); } - if (items == 2) { - - /* - * do zero copy for prolate single SV: - * $r->print("some text\n"); - */ - - sv = ST(1); - - if (SvROK(sv) && SvTYPE(SvRV(sv)) == SVt_PV) { - sv = SvRV(sv); - } - - if (SvPOK(sv)) { - - p = (u_char *) SvPV(sv, len); - - if (len == 0) { - XSRETURN_EMPTY; - } - - if (ngx_http_perl_refcount(aTHX_ r, sv) != NGX_OK) { - ctx->error = 1; - croak("ngx_http_perl_refcount() failed"); - } - - b = ngx_calloc_buf(r->pool); - if (b == NULL) { - ctx->error = 1; - croak("ngx_calloc_buf() failed"); - } - - b->memory = 1; - b->pos = p; - b->last = p + len; - b->start = p; - b->end = b->last; - - ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "$r->print: single SV: %z", len); - - goto out; - } - } - size = 0; for (i = 1; i < items; i++) { @@ -773,8 +728,6 @@ print(r, ...) b->last = ngx_cpymem(b->last, p, len); } - out: - rc = ngx_http_perl_output(r, ctx, b); if (rc == NGX_ERROR) { From mdounin at mdounin.ru Fri Aug 14 06:14:04 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 14 Aug 2026 09:14:04 +0300 Subject: [nginx-tests] Tests: proxy_cache_use_stale.t style. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/306b9cae4459 branches: changeset: 2086:306b9cae4459 user: Maxim Dounin date: Fri Aug 14 09:07:10 2026 +0300 description: Tests: proxy_cache_use_stale.t style. diffstat: proxy_cache_use_stale.t | 65 +++++++++++++++++++++--------------------------- 1 files changed, 28 insertions(+), 37 deletions(-) diffs (138 lines): diff --git a/proxy_cache_use_stale.t b/proxy_cache_use_stale.t --- a/proxy_cache_use_stale.t +++ b/proxy_cache_use_stale.t @@ -24,7 +24,8 @@ use Test::Nginx qw/ :DEFAULT http_end /; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http proxy cache rewrite limit_req ssi/) +my $t = Test::Nginx->new() + ->has(qw/http proxy cache rewrite limit_req ssi/)->plan(35) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -37,9 +38,9 @@ events { http { %%TEST_GLOBALS_HTTP%% - proxy_cache_path %%TESTDIR%%/cache levels=1:2 keys_zone=NAME:1m; + proxy_cache_path cache keys_zone=one:1m; - limit_req_zone $binary_remote_addr zone=one:1m rate=10r/m; + limit_req_zone $binary_remote_addr zone=slow:1m rate=10r/m; server { listen 127.0.0.1:8080; @@ -51,65 +52,54 @@ http { } location /escape { - proxy_pass http://127.0.0.1:8081; - proxy_cache NAME; - proxy_cache_background_update on; + proxy_pass http://127.0.0.1:8081; + proxy_cache one; + proxy_cache_background_update on; add_header X-Cache-Status $upstream_cache_status; } location / { - proxy_pass http://127.0.0.1:8081; - - proxy_cache NAME; + proxy_pass http://127.0.0.1:8081; - proxy_cache_key $uri; - - proxy_cache_revalidate on; - - proxy_cache_background_update on; + proxy_cache one; + proxy_cache_key $uri; + proxy_cache_revalidate on; + proxy_cache_background_update on; add_header X-Cache-Status $upstream_cache_status; location /t4.html { - proxy_pass http://127.0.0.1:8081/t.html; - - proxy_cache_revalidate off; + proxy_pass http://127.0.0.1:8081/t.html; + proxy_cache_revalidate off; } location /t5.html { - proxy_pass http://127.0.0.1:8081/t.html; - - proxy_cache_background_update off; + proxy_pass http://127.0.0.1:8081/t.html; + proxy_cache_background_update off; } location ~ /(reg)(?Pexp).html { - proxy_pass http://127.0.0.1:8081/$1$name.html; - - proxy_cache_background_update on; + proxy_pass http://127.0.0.1:8081/$1$name.html; } location /updating/ { - proxy_pass http://127.0.0.1:8081/; - - proxy_cache_use_stale updating; + proxy_pass http://127.0.0.1:8081/; + proxy_cache_use_stale updating; } location /next/ { - proxy_pass http://127.0.0.1:8081/; - - proxy_next_upstream http_500; + proxy_pass http://127.0.0.1:8081/; + proxy_next_upstream http_500; } location /t7.html { - proxy_pass http://127.0.0.1:8081; - - sendfile_max_chunk 4k; + proxy_pass http://127.0.0.1:8081; + sendfile_max_chunk 4k; } location /t8.html { - proxy_pass http://127.0.0.1:8081/t.html; - - proxy_cache_valid 1s; + proxy_pass http://127.0.0.1:8081/t.html; + proxy_cache_valid 1s; } if ($arg_if) { @@ -117,6 +107,7 @@ http { } } } + server { listen 127.0.0.1:8081; server_name localhost; @@ -134,7 +125,7 @@ http { location / { } location /t6.html { - limit_req zone=one burst=2; + limit_req zone=slow burst=2; } location /t9.html { @@ -156,7 +147,7 @@ EOF $t->write_file('escape.html', 'SEE-THIS'); $t->write_file('regexp.html', 'SEE-THIS'); -$t->run()->plan(35); +$t->run(); ############################################################################### From mdounin at mdounin.ru Fri Aug 14 06:14:04 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 14 Aug 2026 09:14:04 +0300 Subject: [nginx-tests] Tests: added test for positional captures with clo... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/7c2b57e66ec2 branches: changeset: 2087:7c2b57e66ec2 user: Maxim Dounin date: Fri Aug 14 09:07:19 2026 +0300 description: Tests: added test for positional captures with cloned subrequests. diffstat: proxy_cache_use_stale.t | 30 +++++++++++++++++++++++++++++- 1 files changed, 29 insertions(+), 1 deletions(-) diffs (74 lines): diff --git a/proxy_cache_use_stale.t b/proxy_cache_use_stale.t --- a/proxy_cache_use_stale.t +++ b/proxy_cache_use_stale.t @@ -1,5 +1,6 @@ #!/usr/bin/perl +# (C) Maxim Dounin # (C) Sergey Kandaurov # (C) Nginx, Inc. @@ -25,7 +26,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/http proxy cache rewrite limit_req ssi/)->plan(35) + ->has(qw/http proxy cache rewrite limit_req ssi map/)->plan(37) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -42,6 +43,11 @@ http { limit_req_zone $binary_remote_addr zone=slow:1m rate=10r/m; + map $uri $map { + volatile; + ~(nomatch) 1; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -82,6 +88,11 @@ http { proxy_pass http://127.0.0.1:8081/$1$name.html; } + location ~ /map_(regexp.html) { + proxy_pass http://127.0.0.1:8081/$map$1; + add_header X-Cache-Status $upstream_cache_status:$map:$1; + } + location /updating/ { proxy_pass http://127.0.0.1:8081/; proxy_cache_use_stale updating; @@ -172,6 +183,7 @@ get('/next/tt.html', 'max-age=1, stale-i get('/t8.html', 'stale-while-revalidate=20'); get('/escape.htm%6C', 'max-age=1, stale-while-revalidate=20'); get('/regexp.html', 'max-age=1, stale-while-revalidate=20'); +get('/map_regexp.html', 'max-age=1, stale-while-revalidate=20'); sleep 2; @@ -209,6 +221,22 @@ like(http_get('/t5.html'), qr/REVALIDATE like(http_get('/regexp.html'), qr/STALE/, 's-w-r - regexp background update'); like(http_get('/regexp.html'), qr/HIT/, 's-w-r - regexp revalidated'); +TODO: { +todo_skip 'might coredump', 2 + unless $t->has_version('1.31.4') + or $ENV{TEST_NGINX_UNSAFE}; +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +# when r->captures was reallocated but no match happened, positional captures +# used uninitialized offsets from newly allocated r->captures + +like(http_get('/map_regexp.html'), qr/STALE::regexp.html/, + 's-w-r - regexp and map background update'); +like(http_get('/map_regexp.html'), qr/HIT::regexp.html/, + 's-w-r - regexp and map revalidated'); + +} + # UPDATING while s-w-r $t->write_file('t6.html', 'SEE-THAT'); From mdounin at mdounin.ru Fri Aug 14 06:14:04 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 14 Aug 2026 09:14:04 +0300 Subject: [nginx-tests] Tests: perl tests with scalar changed after $r->pr... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/b7be6f57f5a9 branches: changeset: 2088:b7be6f57f5a9 user: Maxim Dounin date: Fri Aug 14 09:11:07 2026 +0300 description: Tests: perl tests with scalar changed after $r->print(). diffstat: perl_refcount.t | 40 +++++++++++++++++++++++++++++++++++++++- 1 files changed, 39 insertions(+), 1 deletions(-) diffs (62 lines): diff --git a/perl_refcount.t b/perl_refcount.t --- a/perl_refcount.t +++ b/perl_refcount.t @@ -22,7 +22,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http perl/)->plan(6) +my $t = Test::Nginx->new()->has(qw/http perl/)->plan(8) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -106,6 +106,34 @@ http { return OK; }'; } + + location /print_changed { + perl 'sub { + my $r = shift; + $r->send_http_header; + my $foo = "it"; + $foo .= " works"; + $r->print($foo); + $foo = " changed"; + $r->print($foo); + return OK; + }'; + } + + location /print_readonly_changed { + perl 'sub { + my $r = shift; + $r->send_http_header; + my $foo = "it"; + $foo .= " works"; + Internals::SvREADONLY($foo, 1); + $r->print($foo); + Internals::SvREADONLY($foo, 0); + $foo = " changed"; + $r->print($foo); + return OK; + }'; + } } } @@ -162,4 +190,14 @@ like(http_get('/bless'), qr/500 Internal } +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +like(http_get('/print_changed'), qr/works changed/, + 'perl print scalar changed'); +like(http_get('/print_readonly_changed'), qr/works changed/, + 'perl print scalar readonly changed'); + +} + ###############################################################################