From mdounin at mdounin.ru Thu Jul 2 09:41:52 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 02 Jul 2026 12:41:52 +0300 Subject: [PATCH] Perl: request object validation Message-ID: <9a7e15e971f8759e1771.1782985312@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1782985214 -10800 # Thu Jul 02 12:40:14 2026 +0300 # Node ID 9a7e15e971f8759e177123da2d3217f1a5f621d5 # Parent 3072e5ce18a55cae5d1389d1b396cc62c2e053ab Perl: request object validation. Previously, using stale request objects resulted in accesses to already freed memory, causing segmentation faults: location /stale { perl 'sub { my $r = shift; $prev->log_error(0, "next request arrived") if $prev; $prev = $r; $r->send_http_header; return OK; }'; } Similarly, incorrectly blessed objects might cause segmentation faults, such as in the following configuration: location /bless { perl 'sub { my $v = 10; my $r = bless \$v, "nginx"; $r->send_http_header; return OK; }'; } With this change, active request object is recorded in the ngx_http_perl_call_handler() function, and checked by ngx_http_perl_set_request() to prevent use of unexpected request objects. Reported by Axel Mierczuk, Keith Hoodlet, 1Password?s Off-by-1 Labs. 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 @@ -18,6 +18,9 @@ #define ngx_http_perl_set_request(r, ctx) \ \ ctx = INT2PTR(ngx_http_perl_ctx_t *, SvIV((SV *) SvRV(ST(0)))); \ + if (ctx != ngx_http_perl_active_context || ctx == NULL) { \ + croak("invalid request object"); \ + } \ r = ctx->request diff --git a/src/http/modules/perl/ngx_http_perl_module.c b/src/http/modules/perl/ngx_http_perl_module.c --- a/src/http/modules/perl/ngx_http_perl_module.c +++ b/src/http/modules/perl/ngx_http_perl_module.c @@ -148,6 +148,9 @@ static ngx_http_ssi_command_t ngx_http_ #endif +ngx_http_perl_ctx_t *ngx_http_perl_active_context; + + static ngx_str_t ngx_null_name = ngx_null_string; static HV *nginx_stash; @@ -746,6 +749,8 @@ ngx_http_perl_call_handler(pTHX_ ngx_htt PUSHMARK(sp); + ngx_http_perl_active_context = ctx; + sv = sv_2mortal(sv_bless(newRV_noinc(newSViv(PTR2IV(ctx))), nginx)); XPUSHs(sv); @@ -790,6 +795,8 @@ ngx_http_perl_call_handler(pTHX_ ngx_htt FREETMPS; LEAVE; + ngx_http_perl_active_context = NULL; + if (ctx->error) { ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, diff --git a/src/http/modules/perl/ngx_http_perl_module.h b/src/http/modules/perl/ngx_http_perl_module.h --- a/src/http/modules/perl/ngx_http_perl_module.h +++ b/src/http/modules/perl/ngx_http_perl_module.h @@ -59,6 +59,9 @@ typedef struct { extern ngx_module_t ngx_http_perl_module; +extern ngx_http_perl_ctx_t *ngx_http_perl_active_context; + + /* * workaround for "unused variable `Perl___notused'" warning * when building with perl 5.6.1 From mdounin at mdounin.ru Thu Jul 2 09:43:12 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 02 Jul 2026 12:43:12 +0300 Subject: [PATCH] Tests: perl invalid request object tests In-Reply-To: <9a7e15e971f8759e1771.1782985312@vm-bsd.mdounin.ru> References: <9a7e15e971f8759e1771.1782985312@vm-bsd.mdounin.ru> Message-ID: <722b1568d101a870c324.1782985392@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1782985220 -10800 # Thu Jul 02 12:40:20 2026 +0300 # Node ID 722b1568d101a870c3242f04882be5aa293c2a19 # Parent d2f2ead62ad63ebe74cd21bf9c956625a9d7a70f Tests: perl invalid request object tests. 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(4) +my $t = Test::Nginx->new()->has(qw/http perl/)->plan(6) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -85,6 +85,27 @@ http { return OK; }'; } + + location /stale { + perl 'sub { + my $r = shift; + $prev->log_error(0, "next request arrived") if $prev; + $r->send_http_header; + $prev->print("print to stale request") if $prev; + $prev = $r; + return OK; + }'; + } + + location /bless { + perl 'sub { + my $v = 10; + my $r = bless \$v, "nginx"; + $r->send_http_header; + $r->print("it works"); + return OK; + }'; + } } } @@ -121,4 +142,24 @@ like(http_get('/redirect'), qr/works/, ' } +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); +todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') + or $ENV{TEST_NGINX_UNSAFE}; + +# the $r request object might be preserved by the code, +# and usage of such invalid request objects needs to be prevented; +# note though that the stale request might happen to match the active one + +http_get('/stale'); +like(http_get('/stale'), qr/500 Internal|stale request/, + 'stale request object'); + +# similarly, if the request object is constructed with bless() +# with an incorrect pointer, it should be rejected + +like(http_get('/bless'), qr/500 Internal/, 'invalid request object'); + +} + ############################################################################### From mdounin at mdounin.ru Fri Jul 3 04:06:42 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 03 Jul 2026 07:06:42 +0300 Subject: [nginx] Upstream: fixed missing return in error handling. Message-ID: details: http://freenginx.org/hg/nginx/rev/3072e5ce18a5 branches: changeset: 9570:3072e5ce18a5 user: Maxim Dounin date: Sat Jun 27 01:35:52 2026 +0300 description: Upstream: fixed missing return in error handling. Missed in 9295:c5623963c29e (1.27.2), might cause segmentation faults on memory allocation errors with proxy_no_cache (or if script buffer overrun protections are triggered during evaluation of proxy_no_cache predicates, or on cache node allocation errors if proxy_cache_bypass is also used). Reported by Valentin Bartenev. diffstat: src/http/ngx_http_upstream.c | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diffs (11 lines): diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c --- a/src/http/ngx_http_upstream.c +++ b/src/http/ngx_http_upstream.c @@ -3236,6 +3236,7 @@ ngx_http_upstream_send_response(ngx_http if (ngx_http_upstream_no_cache(r, u) != NGX_OK) { ngx_http_upstream_finalize_request(r, u, NGX_ERROR); + return; } if (u->cacheable) { From mdounin at mdounin.ru Mon Jul 6 04:46:06 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 Jul 2026 07:46:06 +0300 Subject: [nginx] Perl: request object validation. Message-ID: details: http://freenginx.org/hg/nginx/rev/86a2685756ae branches: changeset: 9571:86a2685756ae user: Maxim Dounin date: Mon Jul 06 07:14:28 2026 +0300 description: Perl: request object validation. Previously, using stale request objects resulted in accesses to already freed memory, causing segmentation faults: location /stale { perl 'sub { my $r = shift; $prev->log_error(0, "next request arrived") if $prev; $prev = $r; $r->send_http_header; return OK; }'; } Similarly, incorrectly blessed objects might cause segmentation faults, such as in the following configuration: location /bless { perl 'sub { my $v = 10; my $r = bless \$v, "nginx"; $r->send_http_header; return OK; }'; } With this change, active request object is recorded in the ngx_http_perl_call_handler() function, and checked by ngx_http_perl_set_request() to prevent use of unexpected request objects. Reported by Axel Mierczuk, Keith Hoodlet, 1Password?s Off-by-1 Labs. diffstat: src/http/modules/perl/nginx.xs | 3 +++ src/http/modules/perl/ngx_http_perl_module.c | 7 +++++++ src/http/modules/perl/ngx_http_perl_module.h | 3 +++ 3 files changed, 13 insertions(+), 0 deletions(-) diffs (57 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 @@ -18,6 +18,9 @@ #define ngx_http_perl_set_request(r, ctx) \ \ ctx = INT2PTR(ngx_http_perl_ctx_t *, SvIV((SV *) SvRV(ST(0)))); \ + if (ctx != ngx_http_perl_active_context || ctx == NULL) { \ + croak("invalid request object"); \ + } \ r = ctx->request diff --git a/src/http/modules/perl/ngx_http_perl_module.c b/src/http/modules/perl/ngx_http_perl_module.c --- a/src/http/modules/perl/ngx_http_perl_module.c +++ b/src/http/modules/perl/ngx_http_perl_module.c @@ -148,6 +148,9 @@ static ngx_http_ssi_command_t ngx_http_ #endif +ngx_http_perl_ctx_t *ngx_http_perl_active_context; + + static ngx_str_t ngx_null_name = ngx_null_string; static HV *nginx_stash; @@ -746,6 +749,8 @@ ngx_http_perl_call_handler(pTHX_ ngx_htt PUSHMARK(sp); + ngx_http_perl_active_context = ctx; + sv = sv_2mortal(sv_bless(newRV_noinc(newSViv(PTR2IV(ctx))), nginx)); XPUSHs(sv); @@ -790,6 +795,8 @@ ngx_http_perl_call_handler(pTHX_ ngx_htt FREETMPS; LEAVE; + ngx_http_perl_active_context = NULL; + if (ctx->error) { ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, diff --git a/src/http/modules/perl/ngx_http_perl_module.h b/src/http/modules/perl/ngx_http_perl_module.h --- a/src/http/modules/perl/ngx_http_perl_module.h +++ b/src/http/modules/perl/ngx_http_perl_module.h @@ -59,6 +59,9 @@ typedef struct { extern ngx_module_t ngx_http_perl_module; +extern ngx_http_perl_ctx_t *ngx_http_perl_active_context; + + /* * workaround for "unused variable `Perl___notused'" warning * when building with perl 5.6.1 From mdounin at mdounin.ru Mon Jul 6 04:46:27 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 Jul 2026 07:46:27 +0300 Subject: [nginx-tests] Tests: perl invalid request object tests. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/e35d7a7d58e2 branches: changeset: 2077:e35d7a7d58e2 user: Maxim Dounin date: Mon Jul 06 07:15:12 2026 +0300 description: Tests: perl invalid request object tests. diffstat: perl_refcount.t | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 files changed, 42 insertions(+), 1 deletions(-) diffs (65 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(4) +my $t = Test::Nginx->new()->has(qw/http perl/)->plan(6) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -85,6 +85,27 @@ http { return OK; }'; } + + location /stale { + perl 'sub { + my $r = shift; + $prev->log_error(0, "next request arrived") if $prev; + $r->send_http_header; + $prev->print("print to stale request") if $prev; + $prev = $r; + return OK; + }'; + } + + location /bless { + perl 'sub { + my $v = 10; + my $r = bless \$v, "nginx"; + $r->send_http_header; + $r->print("it works"); + return OK; + }'; + } } } @@ -121,4 +142,24 @@ like(http_get('/redirect'), qr/works/, ' } +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); +todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') + or $ENV{TEST_NGINX_UNSAFE}; + +# the $r request object might be preserved by the code, +# and usage of such invalid request objects needs to be prevented; +# note though that the stale request might happen to match the active one + +http_get('/stale'); +like(http_get('/stale'), qr/500 Internal|stale request/, + 'stale request object'); + +# similarly, if the request object is constructed with bless() +# with an incorrect pointer, it should be rejected + +like(http_get('/bless'), qr/500 Internal/, 'invalid request object'); + +} + ############################################################################### From mdounin at mdounin.ru Tue Jul 7 05:14:22 2026 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 7 Jul 2026 08:14:22 +0300 Subject: freenginx-1.31.3 changes draft Message-ID: Hello! Changes with freenginx 1.31.3 07 Jul 2026 *) Feature: additional checks are now used during variable substitution, including during execution of the ngx_http_rewrite_module directives, to prevent buffer overruns in case of errors in the code. *) Bugfix: if the "auth_request_set" directive or regular expression named captures were used to change prefix variables, such as "$http_...", corresponding variables became empty in other parts of the configuration. *) Bugfix: changing the $limit_rate and $args variables with the "auth_request_set" directive or regular expression named captures worked incorrectly. *) Bugfix: in error handling in the mail proxy module. Thanks to Evan Hellman, Trail of Bits. *) Bugfix: in error handling when using gzipping. Thanks to Evan Hellman, Trail of Bits. *) Bugfix: in HTTP/3. *) Bugfix: a segmentation fault might occur in a worker process when sending very long request header lines to a gRPC backend. Thanks to Evan Hellman, Trail of Bits. *) Bugfix: a segmentation fault might occur in a worker process if the ngx_http_charset_module was used to convert responses from UTF-8. *) Bugfix: in the ngx_http_perl_module. Thanks to Evan Hellman, Trail of Bits and Axel Mierczuk, Keith Hoodlet, 1Password's Off-by-1 Labs. *) Bugfix: in error handling when using the proxy_no_cache directive. Thanks to Valentin Bartenev. ????????? ? freenginx 1.31.3 07.07.2026 *) ??????????: ?????? ??? ??????????? ??????????, ? ??? ????? ??? ?????????? ???????? ?????? ngx_http_rewrite_module, ???????????? ?????????????? ????????, ??????????? ????????????? ????? ?? ??????? ?????? ? ?????? ?????-???? ?????? ? ????. *) ???????????: ??? ????????????? ????????? auth_request_set ??? ??????????? ????????? ? ?????????? ?????????? ??? ????????? ?????????? ??????????, ????? ??? "$http_...", ??????????????? ?????????? ??????????? ??????? ? ?????? ?????? ????????????. *) ??????????: ????????? ?????????? $limit_rate ? $args ? ??????? ????????? auth_request_set ??? ??????????? ????????? ? ?????????? ?????????? ???????? ???????????. *) ???????????: ? ????????? ?????? ? ???????? ??????-???????. ??????? Evan Hellman, Trail of Bits. *) ???????????: ? ????????? ?????? ??? ????????????? ??????. ??????? Evan Hellman, Trail of Bits. *) ???????????: ? HTTP/3. *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault ??? ???????? ?? gRPC-?????? ????? ??????? ????? ????????? ???????. ??????? Evan Hellman, Trail of Bits. *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, ???? ?????? ngx_http_charset_module ????????????? ??? ??????????????? ??????? ?? UTF-8. *) ???????????: ? ?????? ngx_http_perl_module. ??????? Evan Hellman, Trail of Bits ? Axel Mierczuk, Keith Hoodlet, 1Password's Off-by-1 Labs. *) ???????????: ? ????????? ?????? ??? ????????????? ????????? proxy_no_cache. ??????? Valentin Bartenev. -- Maxim Dounin http://mdounin.ru/ From osa at freebsd.org.ru Tue Jul 7 13:03:40 2026 From: osa at freebsd.org.ru (Sergey A. Osokin) Date: Tue, 7 Jul 2026 16:03:40 +0300 Subject: freenginx-1.31.3 changes draft In-Reply-To: References: Message-ID: On Tue, Jul 07, 2026 at 08:14:22AM +0300, Maxim Dounin wrote: > Hello! [...] > *) ??????????: ????????? ?????????? $limit_rate ? $args ? ??????? ????????: "??????????" -> "???????????". -- Sergey A. Osokin From mdounin at mdounin.ru Tue Jul 7 15:00:14 2026 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 7 Jul 2026 18:00:14 +0300 Subject: freenginx-1.31.3 changes draft In-Reply-To: References: Message-ID: Hello! On Tue, Jul 07, 2026 at 04:03:40PM +0300, Sergey A. Osokin wrote: > On Tue, Jul 07, 2026 at 08:14:22AM +0300, Maxim Dounin wrote: > > Hello! > > [...] > > > *) ??????????: ????????? ?????????? $limit_rate ? $args ? ??????? > > ????????: "??????????" -> "???????????". Hmm, this is a generated text prefix (from "type="bugfix"" in changex.xml), so it must be a cut-n-paste issue. Haven't seen such issues before, but it looks like a common problem on macOS due to a small tty buffer size, only 1024 bytes: https://github.com/kovidgoyal/kitty/issues/5869 https://superuser.com/questions/219225/command-limits-when-pasting-into-tcsh-mac-os-x/219304#219304 https://github.com/apple-oss-distributions/xnu/blob/xnu-12377.121.6/bsd/kern/tty.c#L552 https://github.com/apple-oss-distributions/xnu/blob/xnu-12377.121.6/bsd/sys/syslimits.h#L98 -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Tue Jul 7 15:06:43 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 07 Jul 2026 18:06:43 +0300 Subject: [nginx] Updated OpenSSL used for win32 builds. Message-ID: details: http://freenginx.org/hg/nginx/rev/1302742bda64 branches: changeset: 9572:1302742bda64 user: Maxim Dounin date: Tue Jul 07 08:15:42 2026 +0300 description: Updated OpenSSL used for win32 builds. diffstat: misc/GNUmakefile | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (12 lines): diff --git a/misc/GNUmakefile b/misc/GNUmakefile --- a/misc/GNUmakefile +++ b/misc/GNUmakefile @@ -6,7 +6,7 @@ TEMP = tmp CC = cl OBJS = objs.msvc8 -OPENSSL = openssl-3.5.6 +OPENSSL = openssl-3.5.7 ZLIB = zlib-1.3.2 PCRE = pcre2-10.47 From mdounin at mdounin.ru Tue Jul 7 15:06:44 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 07 Jul 2026 18:06:44 +0300 Subject: [nginx] freenginx-1.31.3-RELEASE Message-ID: details: http://freenginx.org/hg/nginx/rev/47e34b225e5f branches: changeset: 9573:47e34b225e5f user: Maxim Dounin date: Tue Jul 07 18:03:30 2026 +0300 description: freenginx-1.31.3-RELEASE diffstat: docs/xml/nginx/changes.xml | 132 +++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 132 insertions(+), 0 deletions(-) diffs (142 lines): diff --git a/docs/xml/nginx/changes.xml b/docs/xml/nginx/changes.xml --- a/docs/xml/nginx/changes.xml +++ b/docs/xml/nginx/changes.xml @@ -7,6 +7,138 @@
+ + + + +?????? ??? ??????????? ??????????, +? ??? ????? ??? ?????????? ???????? ?????? ngx_http_rewrite_module, +???????????? ?????????????? ????????, +??????????? ????????????? ????? ?? ??????? ?????? +? ?????? ?????-???? ?????? ? ????. + + +additional checks are now used during variable substitution, +including during execution of the ngx_http_rewrite_module directives, +to prevent buffer overruns +in case of errors in the code. + + + + + +??? ????????????? ????????? auth_request_set +??? ??????????? ????????? ? ?????????? ?????????? +??? ????????? ?????????? ??????????, ????? ??? "$http_...", +??????????????? ?????????? ??????????? ??????? ? ?????? ?????? ????????????. + + +if the "auth_request_set" directive +or regular expression named captures +were used to change prefix variables, such as "$http_...", +corresponding variables became empty in other parts of the configuration. + + + + + +????????? ?????????? $limit_rate ? $args +? ??????? ????????? auth_request_set +??? ??????????? ????????? ? ?????????? ?????????? +???????? ???????????. + + +changing the $limit_rate and $args variables +with the "auth_request_set" directive +or regular expression named captures +worked incorrectly. + + + + + +? ????????? ?????? ? ???????? ??????-???????.
+??????? Evan Hellman, Trail of Bits. +
+ +in error handling in the mail proxy module.
+Thanks to Evan Hellman, Trail of Bits. +
+
+ + + +? ????????? ?????? ??? ????????????? ??????.
+??????? Evan Hellman, Trail of Bits. +
+ +in error handling when using gzipping.
+Thanks to Evan Hellman, Trail of Bits. +
+
+ + + +? HTTP/3. + + +in HTTP/3. + + + + + +? ??????? ???????? ??? ????????? segmentation fault +??? ???????? ?? gRPC-?????? ????? ??????? ????? ????????? ???????.
+??????? Evan Hellman, Trail of Bits. +
+ +a segmentation fault might occur in a worker process +when sending very long request header lines to a gRPC backend.
+Thanks to Evan Hellman, Trail of Bits. +
+
+ + + +? ??????? ???????? ??? ????????? segmentation fault, +???? ?????? ngx_http_charset_module ????????????? +??? ??????????????? ??????? ?? UTF-8. + + +a segmentation fault might occur in a worker process +if the ngx_http_charset_module was used +to convert responses from UTF-8. + + + + + +? ?????? ngx_http_perl_module.
+??????? Evan Hellman, Trail of Bits +? Axel Mierczuk, Keith Hoodlet, 1Password's Off-by-1 Labs. +
+ +in the ngx_http_perl_module.
+Thanks to Evan Hellman, Trail of Bits +and Axel Mierczuk, Keith Hoodlet, 1Password's Off-by-1 Labs. +
+
+ + + +? ????????? ?????? ??? ????????????? ????????? proxy_no_cache.
+??????? Valentin Bartenev. +
+ +in error handling when using the proxy_no_cache directive.
+Thanks to Valentin Bartenev. +
+
+ +
+ + From mdounin at mdounin.ru Tue Jul 7 15:06:44 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 07 Jul 2026 18:06:44 +0300 Subject: [nginx] release-1.31.3 tag Message-ID: details: http://freenginx.org/hg/nginx/rev/3c6a73c0e927 branches: changeset: 9574:3c6a73c0e927 user: Maxim Dounin date: Tue Jul 07 18:03:31 2026 +0300 description: release-1.31.3 tag diffstat: .hgtags | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diffs (8 lines): diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -496,3 +496,4 @@ cac0fa5721386abbec57dcc2bb317f2531456e19 b1585cfeee5759a1ae3794b5c9c4c541c3c73a35 release-1.31.0 0417363a549372cf4b20c611c6f14944b177e86c release-1.31.1 e3c5f3106d3837629514c0f5cb9aac189c07e432 release-1.31.2 +47e34b225e5f13ceacadba8a37e27ce9a299b2fb release-1.31.3 From mdounin at mdounin.ru Tue Jul 7 15:10:03 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 07 Jul 2026 18:10:03 +0300 Subject: [nginx-site] freenginx-1.31.3 Message-ID: details: http://freenginx.org/hg/nginx-site/rev/7adcd2f1a240 branches: changeset: 3137:7adcd2f1a240 user: Maxim Dounin date: Tue Jul 07 18:08:32 2026 +0300 description: freenginx-1.31.3 diffstat: text/en/CHANGES | 38 ++++++++++++++++++++++++++++++++++++++ text/ru/CHANGES.ru | 41 +++++++++++++++++++++++++++++++++++++++++ xml/index.xml | 7 +++++++ xml/versions.xml | 1 + 4 files changed, 87 insertions(+), 0 deletions(-) diffs (123 lines): diff --git a/text/en/CHANGES b/text/en/CHANGES --- a/text/en/CHANGES +++ b/text/en/CHANGES @@ -1,4 +1,42 @@ +Changes with freenginx 1.31.3 07 Jul 2026 + + *) Feature: additional checks are now used during variable substitution, + including during execution of the ngx_http_rewrite_module directives, + to prevent buffer overruns in case of errors in the code. + + *) Bugfix: if the "auth_request_set" directive or regular expression + named captures were used to change prefix variables, such as + "$http_...", corresponding variables became empty in other parts of + the configuration. + + *) Bugfix: changing the $limit_rate and $args variables with the + "auth_request_set" directive or regular expression named captures + worked incorrectly. + + *) Bugfix: in error handling in the mail proxy module. + Thanks to Evan Hellman, Trail of Bits. + + *) Bugfix: in error handling when using gzipping. + Thanks to Evan Hellman, Trail of Bits. + + *) Bugfix: in HTTP/3. + + *) Bugfix: a segmentation fault might occur in a worker process when + sending very long request header lines to a gRPC backend. + Thanks to Evan Hellman, Trail of Bits. + + *) Bugfix: a segmentation fault might occur in a worker process if the + ngx_http_charset_module was used to convert responses from UTF-8. + + *) Bugfix: in the ngx_http_perl_module. + Thanks to Evan Hellman, Trail of Bits and Axel Mierczuk, Keith + Hoodlet, 1Password's Off-by-1 Labs. + + *) Bugfix: in error handling when using the proxy_no_cache directive. + Thanks to Valentin Bartenev. + + Changes with freenginx 1.31.2 26 May 2026 *) Bugfix: a segmentation fault might occur in a worker process if diff --git a/text/ru/CHANGES.ru b/text/ru/CHANGES.ru --- a/text/ru/CHANGES.ru +++ b/text/ru/CHANGES.ru @@ -1,4 +1,45 @@ +????????? ? freenginx 1.31.3 07.07.2026 + + *) ??????????: ?????? ??? ??????????? ??????????, ? ??? ????? ??? + ?????????? ???????? ?????? ngx_http_rewrite_module, ???????????? + ?????????????? ????????, ??????????? ????????????? ????? ?? ??????? + ?????? ? ?????? ?????-???? ?????? ? ????. + + *) ???????????: ??? ????????????? ????????? auth_request_set ??? + ??????????? ????????? ? ?????????? ?????????? ??? ????????? + ?????????? ??????????, ????? ??? "$http_...", ??????????????? + ?????????? ??????????? ??????? ? ?????? ?????? ????????????. + + *) ???????????: ????????? ?????????? $limit_rate ? $args ? ??????? + ????????? auth_request_set ??? ??????????? ????????? ? ?????????? + ?????????? ???????? ???????????. + + *) ???????????: ? ????????? ?????? ? ???????? ??????-???????. + ??????? Evan Hellman, Trail of Bits. + + *) ???????????: ? ????????? ?????? ??? ????????????? ??????. + ??????? Evan Hellman, Trail of Bits. + + *) ???????????: ? HTTP/3. + + *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault ??? + ???????? ?? gRPC-?????? ????? ??????? ????? ????????? ???????. + ??????? Evan Hellman, Trail of Bits. + + *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, + ???? ?????? ngx_http_charset_module ????????????? ??? ??????????????? + ??????? ?? UTF-8. + + *) ???????????: ? ?????? ngx_http_perl_module. + ??????? Evan Hellman, Trail of Bits ? Axel Mierczuk, Keith Hoodlet, + 1Password's Off-by-1 Labs. + + *) ???????????: ? ????????? ?????? ??? ????????????? ????????? + proxy_no_cache. + ??????? Valentin Bartenev. + + ????????? ? freenginx 1.31.2 26.05.2026 *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, diff --git a/xml/index.xml b/xml/index.xml --- a/xml/index.xml +++ b/xml/index.xml @@ -8,6 +8,13 @@ + + +freenginx-1.31.3 +mainline version has been released. + + + freenginx-1.30.1 diff --git a/xml/versions.xml b/xml/versions.xml --- a/xml/versions.xml +++ b/xml/versions.xml @@ -9,6 +9,7 @@ + From mdounin at mdounin.ru Sun Jul 12 21:49:54 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 13 Jul 2026 00:49:54 +0300 Subject: [PATCH] Tests: style, wrapped to avoid lines longer than 80 chars Message-ID: # HG changeset patch # User Maxim Dounin # Date 1783892966 -10800 # Mon Jul 13 00:49:26 2026 +0300 # Node ID dc84b1a21af6e705131a28dfd2709e29cfc2ccb4 # Parent e35d7a7d58e25daf801a1ba1fc835f7a7a0bee75 Tests: style, wrapped to avoid lines longer than 80 chars. diff --git a/h2.t b/h2.t --- a/h2.t +++ b/h2.t @@ -604,7 +604,8 @@ ok($frame, 'client header timeout - PING # the rest of frame is received after client header timeout $s = Test::Nginx::HTTP2->new(port(8087)); -$sid = $s->new_stream({ path => '/t2.html', split => [20], split_delay => 2.1 }); +$sid = $s->new_stream({ path => '/t2.html', split => [20], + split_delay => 2.1 }); $frames = $s->read(all => [{ type => 'RST_STREAM' }]); ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; diff --git a/h2_request_body.t b/h2_request_body.t --- a/h2_request_body.t +++ b/h2_request_body.t @@ -213,7 +213,8 @@ is($frame->{code}, 0, 'request body disc $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; -is($frame->{headers}->{':status'}, 400, 'request body less than content-length'); +is($frame->{headers}->{':status'}, 400, + 'request body less than content-length'); $sid = $s->new_stream({ body => 'TEST', headers => [ { name => ':method', value => 'GET', mode => 0 }, @@ -224,7 +225,8 @@ is($frame->{headers}->{':status'}, 400, $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; -is($frame->{headers}->{':status'}, 400, 'request body more than content-length'); +is($frame->{headers}->{':status'}, 400, + 'request body more than content-length'); # client_max_body_size diff --git a/h3_headers.t b/h3_headers.t --- a/h3_headers.t +++ b/h3_headers.t @@ -731,7 +731,8 @@ is($frame->{headers}->{':status'}, 400, ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; isnt($frame->{headers}->{'x-referer'}, 'see-this', 'newline in request header'); -is($frame->{headers}->{':status'}, 400, 'newline in request header - bad request'); +is($frame->{headers}->{':status'}, 400, + 'newline in request header - bad request'); # invalid header name as seen with underscore should not lead to ignoring rest diff --git a/http_try_files.t b/http_try_files.t --- a/http_try_files.t +++ b/http_try_files.t @@ -235,27 +235,35 @@ like(http_get('/notfound'), qr!404 Not!, like(http_get('/alias/found.html'), qr!SEE THIS!, 'alias $uri'); like(http_get('/alias/found'), qr!SEE THIS!, 'alias $uri.html'); -like(http_get('/alias/directory'), qr!301 Moved Permanently!, 'alias $uri/ redirect'); +like(http_get('/alias/directory'), qr!301 Moved Permanently!, + 'alias $uri/ redirect'); like(http_get('/alias/directory/'), qr!SEE THIS!, 'alias $uri/ index'); like(http_get('/alias/notfound'), qr!404 Not!, 'alias not found'); like(http_get('/alias-re-add/found.html'), qr!SEE THIS!, 'alias regex ""'); like(http_get('/alias-re-add/found'), qr!SEE THIS!, 'alias regex .html'); -like(http_get('/alias-re-add/directory'), qr!301 Moved Permanently!, 'alias regex / redirect'); -like(http_get('/alias-re-add/directory/'), qr!SEE THIS!, 'alias regex / index'); +like(http_get('/alias-re-add/directory'), qr!301 Moved Permanently!, + 'alias regex / redirect'); +like(http_get('/alias-re-add/directory/'), qr!SEE THIS!, + 'alias regex / index'); like(http_get('/alias-re-add/notfound'), qr!404 Not!, 'alias regex not found'); TODO: { local $TODO = 'not yet' unless $t->has_version('1.31.0'); -like(http_get('/alias-re-prefix/found.html'), qr!SEE THIS!, 'alias regex $uri'); -like(http_get('/alias-re-prefix/found'), qr!SEE THIS!, 'alias regex $uri.html'); -like(http_get('/alias-re-prefix/directory'), qr!301 Moved Permanently!, 'alias regex $uri/ redirect'); -like(http_get('/alias-re-prefix/directory/'), qr!SEE THIS!, 'alias regex $uri/ index'); +like(http_get('/alias-re-prefix/found.html'), qr!SEE THIS!, + 'alias regex $uri'); +like(http_get('/alias-re-prefix/found'), qr!SEE THIS!, + 'alias regex $uri.html'); +like(http_get('/alias-re-prefix/directory'), qr!301 Moved Permanently!, + 'alias regex $uri/ redirect'); +like(http_get('/alias-re-prefix/directory/'), qr!SEE THIS!, + 'alias regex $uri/ index'); } -like(http_get('/alias-re-prefix/notfound'), qr!404 Not!, 'alias regex not found with prefix'); +like(http_get('/alias-re-prefix/notfound'), qr!404 Not!, + 'alias regex not found with prefix'); # various specific tests diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -829,7 +829,8 @@ sub log_core { } $msg =~ s/^/# $prefix/gm; - $msg =~ s/([^\x20-\x7e])/sprintf('\\x%02x', ord($1)) . (($1 eq "\n") ? "\n" : '')/gmxe; + $msg =~ s/([^\x20-\x7e])/sprintf('\\x%02x', ord($1)) + . (($1 eq "\n") ? "\n" : '')/gmxe; $msg .= "\n" unless $msg =~ /\n\Z/; print $msg; } diff --git a/mail_imap.t b/mail_imap.t --- a/mail_imap.t +++ b/mail_imap.t @@ -123,10 +123,12 @@ my $s = Test::Nginx::IMAP->new(); # auth plain -$s->send('1 AUTHENTICATE PLAIN ' . encode_base64("\0test\@example.com\0bad", '')); +$s->send('1 AUTHENTICATE PLAIN ' + . encode_base64("\0test\@example.com\0bad", '')); $s->check(qr/^\S+ NO/, 'auth plain with bad password'); -$s->send('1 AUTHENTICATE PLAIN ' . encode_base64("\0test\@example.com\0secret", '')); +$s->send('1 AUTHENTICATE PLAIN ' + . encode_base64("\0test\@example.com\0secret", '')); $s->ok('auth plain'); # auth login simple diff --git a/proxy_xar.t b/proxy_xar.t --- a/proxy_xar.t +++ b/proxy_xar.t @@ -83,7 +83,8 @@ my $r = http_get('/proxy?xar=/index.html like($r, qr/xar: \/index.html uri: \/index.html/, 'X-Accel-Redirect works'); like($r, qr/^Content-Type: text\/blah/m, 'Content-Type preserved'); like($r, qr/^Set-Cookie: blah=blah/m, 'Set-Cookie preserved'); -like($r, qr/^Content-Disposition: attachment/m, 'Content-Disposition preserved'); +like($r, qr/^Content-Disposition: attachment/m, + 'Content-Disposition preserved'); like($r, qr/^Cache-Control: no-cache/m, 'Cache-Control preserved'); like($r, qr/^Expires: fake/m, 'Expires preserved'); like($r, qr/^Accept-Ranges: parrots/m, 'Accept-Ranges preserved'); diff --git a/ssl.t b/ssl.t --- a/ssl.t +++ b/ssl.t @@ -84,7 +84,8 @@ http { return 200 "body $ssl_client_s_dn:$ssl_client_s_dn_legacy"; } location /time { - return 200 "body $ssl_client_v_start!$ssl_client_v_end!$ssl_client_v_remain"; + return 200 + "body $ssl_client_v_start!$ssl_client_v_end!$ssl_client_v_remain"; } location /body { diff --git a/stream_udp_limit_conn.t b/stream_udp_limit_conn.t --- a/stream_udp_limit_conn.t +++ b/stream_udp_limit_conn.t @@ -97,7 +97,8 @@ is($s->io('1', read_timeout => 0.4), '1' is(dgram('127.0.0.1:' . port(8981))->io('1', read_timeout => 0.1), '', 'rejected new session'); is(dgram('127.0.0.1:' . port(8982))->io('1'), '1', 'passed different zone'); -is(dgram('127.0.0.1:' . port(8983))->io('1'), '1', 'passed same zone unlimited'); +is(dgram('127.0.0.1:' . port(8983))->io('1'), '1', + 'passed same zone unlimited'); sleep 1; # waiting for proxy_timeout to expire diff --git a/sub_filter_perl.t b/sub_filter_perl.t --- a/sub_filter_perl.t +++ b/sub_filter_perl.t @@ -96,11 +96,13 @@ like(http_get('/multi?a=a&b=aaaab'), qr/ like(http_get('/multi?a=aa&b=ab'), qr/^a_replaced$/m, 'aab in aa + ab'); like(http_get('/multi?a=aa&b=aab'), qr/^aa_replaced$/m, 'aab in aa + aab'); like(http_get('/multi?a=aa&b=aaab'), qr/^aaa_replaced$/m, 'aab in aa + aaab'); -like(http_get('/multi?a=aa&b=aaaab'), qr/^aaaa_replaced$/m, 'aab in aa + aaaab'); +like(http_get('/multi?a=aa&b=aaaab'), qr/^aaaa_replaced$/m, + 'aab in aa + aaaab'); # full backtracking -like(http_get('/multi?a=aa&b=xaaab'), qr/^aaxa_replaced$/m, 'aab in aa + xaaab'); +like(http_get('/multi?a=aa&b=xaaab'), qr/^aaxa_replaced$/m, + 'aab in aa + xaaab'); like(http_get('/multi?a=aa&b=axaaab'), qr/^aaaxa_replaced$/m, 'aab in aa + axaaab'); like(http_get('/multi?a=aa&b=aaxaaab'), qr/^aaaaxa_replaced$/m, @@ -118,6 +120,7 @@ like(http_get('/short?a=aa&b=b'), qr/^a_ like(http_get('/short?a=aa&b=ab'), qr/^aa_replaced$/m, 'ab in aa + ab'); like(http_get('/short?a=aa&b=aab'), qr/^aaa_replaced$/m, 'ab in aa + aab'); like(http_get('/short?a=aa&b=aaab'), qr/^aaaa_replaced$/m, 'ab in aa + aaab'); -like(http_get('/short?a=aa&b=aaaab'), qr/^aaaaa_replaced$/m, 'ab in aa + aaaab'); +like(http_get('/short?a=aa&b=aaaab'), qr/^aaaaa_replaced$/m, + 'ab in aa + aaaab'); ############################################################################### From mdounin at mdounin.ru Sun Jul 12 22:12:56 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 13 Jul 2026 01:12:56 +0300 Subject: [PATCH] Index: fixed handling of non-cacheable variables Message-ID: <70007ea1f8a1878f8118.1783894376@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1783887423 -10800 # Sun Jul 12 23:17:03 2026 +0300 # Node ID 70007ea1f8a1878f81189538863dbeb2d80bd2fd # Parent ca8fb89254b3baaa40a421599d4a541c7c02155f Index: fixed handling of non-cacheable variables. Previously, "e.flushed = 1;" was incorrectly used during initial length calculations, and as a result non-cacheable variables were not flushed. The fix is to only set "e.flushed" when evaluating values. A similar issue in try_files was fixed in 2424:46d11bff21ef (0.7.29). diff --git a/src/http/modules/ngx_http_index_module.c b/src/http/modules/ngx_http_index_module.c --- a/src/http/modules/ngx_http_index_module.c +++ b/src/http/modules/ngx_http_index_module.c @@ -149,7 +149,6 @@ ngx_http_index_handler(ngx_http_request_ e.ip = index[i].lengths->elts; e.request = r; - e.flushed = 1; /* 1 is for terminating '\0' as in static names */ len = 1; @@ -186,6 +185,7 @@ ngx_http_index_handler(ngx_http_request_ e.ip = index[i].values->elts; e.pos = name; e.end = name + allocated; + e.flushed = 1; while (*(uintptr_t *) e.ip) { code = *(ngx_http_script_code_pt *) e.ip; From mdounin at mdounin.ru Sun Jul 12 22:15:22 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 13 Jul 2026 01:15:22 +0300 Subject: [PATCH] Tests: index test for flushing non-cacheable variables In-Reply-To: <70007ea1f8a1878f8118.1783894376@vm-bsd.mdounin.ru> References: <70007ea1f8a1878f8118.1783894376@vm-bsd.mdounin.ru> Message-ID: <974aab326913908b0557.1783894522@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1783893091 -10800 # Mon Jul 13 00:51:31 2026 +0300 # Node ID 974aab326913908b055756608c21cae482c4b2ad # Parent dc84b1a21af6e705131a28dfd2709e29cfc2ccb4 Tests: index test for flushing non-cacheable variables. diff --git a/index.t b/index.t --- a/index.t +++ b/index.t @@ -1,5 +1,6 @@ #!/usr/bin/perl +# (C) Maxim Dounin # (C) Sergey Kandaurov # (C) Nginx, Inc. @@ -22,7 +23,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(16) +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(17) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -43,6 +44,11 @@ http { ~(?) ""; } + map $index $map_volatile { + volatile; + ~^ $index; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -99,11 +105,20 @@ http { alias %%TESTDIR%%/; index index.$capture.$map_capture /index.html; } + location /shrink/ { alias %%TESTDIR%%/; set $shrink "some-long-variable-value"; index index$shrink$map_shrink.html /index.html; } + + location /volatile/ { + alias %%TESTDIR%%/; + set $index notfound.html; + set $dummy $map_volatile; + set $index index.html; + index $map_volatile; + } } } @@ -156,6 +171,14 @@ like(http_get('/shrink/'), qr!X-URI: /sh } +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +like(http_get('/volatile/'), qr!X-URI: /volatile/index.html\x0d?($).*body!ms, + 'index flushes non-cacheable variables'); + +} + $t->stop(); like($t->read_file('log_not_found.log'), qr/error/, 'log_not_found'); From mdounin at mdounin.ru Sat Jul 18 17:12:09 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 18 Jul 2026 20:12:09 +0300 Subject: [nginx] Version bump. Message-ID: details: http://freenginx.org/hg/nginx/rev/7b537b801766 branches: changeset: 9575:7b537b801766 user: Maxim Dounin date: Sat Jul 18 19:45:49 2026 +0300 description: Version bump. diffstat: src/core/nginx.h | 4 ++-- 1 files changed, 2 insertions(+), 2 deletions(-) diffs (14 lines): diff --git a/src/core/nginx.h b/src/core/nginx.h --- a/src/core/nginx.h +++ b/src/core/nginx.h @@ -9,8 +9,8 @@ #define _NGINX_H_INCLUDED_ -#define nginx_version 1031003 -#define NGINX_VERSION "1.31.3" +#define nginx_version 1031004 +#define NGINX_VERSION "1.31.4" #define freenginx 1 From mdounin at mdounin.ru Sat Jul 18 17:12:09 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 18 Jul 2026 20:12:09 +0300 Subject: [nginx] Index: fixed handling of non-cacheable variables. Message-ID: details: http://freenginx.org/hg/nginx/rev/511b31fbc525 branches: changeset: 9576:511b31fbc525 user: Maxim Dounin date: Sat Jul 18 19:45:55 2026 +0300 description: Index: fixed handling of non-cacheable variables. Previously, "e.flushed = 1;" was incorrectly used during initial length calculations, and as a result non-cacheable variables were not flushed. The fix is to only set "e.flushed" when evaluating values. A similar issue in try_files was fixed in 2424:46d11bff21ef (0.7.29). diffstat: src/http/modules/ngx_http_index_module.c | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (19 lines): diff --git a/src/http/modules/ngx_http_index_module.c b/src/http/modules/ngx_http_index_module.c --- a/src/http/modules/ngx_http_index_module.c +++ b/src/http/modules/ngx_http_index_module.c @@ -149,7 +149,6 @@ ngx_http_index_handler(ngx_http_request_ e.ip = index[i].lengths->elts; e.request = r; - e.flushed = 1; /* 1 is for terminating '\0' as in static names */ len = 1; @@ -186,6 +185,7 @@ ngx_http_index_handler(ngx_http_request_ e.ip = index[i].values->elts; e.pos = name; e.end = name + allocated; + e.flushed = 1; while (*(uintptr_t *) e.ip) { code = *(ngx_http_script_code_pt *) e.ip; From mdounin at mdounin.ru Sat Jul 18 17:13:26 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 18 Jul 2026 20:13:26 +0300 Subject: [nginx-tests] Tests: style, wrapped to avoid lines longer than 8... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/93e99c000eb9 branches: changeset: 2078:93e99c000eb9 user: Maxim Dounin date: Sat Jul 18 20:10:43 2026 +0300 description: Tests: style, wrapped to avoid lines longer than 80 chars. diffstat: h2.t | 3 ++- h2_request_body.t | 6 ++++-- h3_headers.t | 3 ++- http_try_files.t | 24 ++++++++++++++++-------- lib/Test/Nginx.pm | 3 ++- mail_imap.t | 6 ++++-- proxy_xar.t | 3 ++- ssl.t | 3 ++- stream_udp_limit_conn.t | 3 ++- sub_filter_perl.t | 9 ++++++--- 10 files changed, 42 insertions(+), 21 deletions(-) diffs (194 lines): diff --git a/h2.t b/h2.t --- a/h2.t +++ b/h2.t @@ -604,7 +604,8 @@ ok($frame, 'client header timeout - PING # the rest of frame is received after client header timeout $s = Test::Nginx::HTTP2->new(port(8087)); -$sid = $s->new_stream({ path => '/t2.html', split => [20], split_delay => 2.1 }); +$sid = $s->new_stream({ path => '/t2.html', split => [20], + split_delay => 2.1 }); $frames = $s->read(all => [{ type => 'RST_STREAM' }]); ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; diff --git a/h2_request_body.t b/h2_request_body.t --- a/h2_request_body.t +++ b/h2_request_body.t @@ -213,7 +213,8 @@ is($frame->{code}, 0, 'request body disc $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; -is($frame->{headers}->{':status'}, 400, 'request body less than content-length'); +is($frame->{headers}->{':status'}, 400, + 'request body less than content-length'); $sid = $s->new_stream({ body => 'TEST', headers => [ { name => ':method', value => 'GET', mode => 0 }, @@ -224,7 +225,8 @@ is($frame->{headers}->{':status'}, 400, $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; -is($frame->{headers}->{':status'}, 400, 'request body more than content-length'); +is($frame->{headers}->{':status'}, 400, + 'request body more than content-length'); # client_max_body_size diff --git a/h3_headers.t b/h3_headers.t --- a/h3_headers.t +++ b/h3_headers.t @@ -731,7 +731,8 @@ is($frame->{headers}->{':status'}, 400, ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; isnt($frame->{headers}->{'x-referer'}, 'see-this', 'newline in request header'); -is($frame->{headers}->{':status'}, 400, 'newline in request header - bad request'); +is($frame->{headers}->{':status'}, 400, + 'newline in request header - bad request'); # invalid header name as seen with underscore should not lead to ignoring rest diff --git a/http_try_files.t b/http_try_files.t --- a/http_try_files.t +++ b/http_try_files.t @@ -235,27 +235,35 @@ like(http_get('/notfound'), qr!404 Not!, like(http_get('/alias/found.html'), qr!SEE THIS!, 'alias $uri'); like(http_get('/alias/found'), qr!SEE THIS!, 'alias $uri.html'); -like(http_get('/alias/directory'), qr!301 Moved Permanently!, 'alias $uri/ redirect'); +like(http_get('/alias/directory'), qr!301 Moved Permanently!, + 'alias $uri/ redirect'); like(http_get('/alias/directory/'), qr!SEE THIS!, 'alias $uri/ index'); like(http_get('/alias/notfound'), qr!404 Not!, 'alias not found'); like(http_get('/alias-re-add/found.html'), qr!SEE THIS!, 'alias regex ""'); like(http_get('/alias-re-add/found'), qr!SEE THIS!, 'alias regex .html'); -like(http_get('/alias-re-add/directory'), qr!301 Moved Permanently!, 'alias regex / redirect'); -like(http_get('/alias-re-add/directory/'), qr!SEE THIS!, 'alias regex / index'); +like(http_get('/alias-re-add/directory'), qr!301 Moved Permanently!, + 'alias regex / redirect'); +like(http_get('/alias-re-add/directory/'), qr!SEE THIS!, + 'alias regex / index'); like(http_get('/alias-re-add/notfound'), qr!404 Not!, 'alias regex not found'); TODO: { local $TODO = 'not yet' unless $t->has_version('1.31.0'); -like(http_get('/alias-re-prefix/found.html'), qr!SEE THIS!, 'alias regex $uri'); -like(http_get('/alias-re-prefix/found'), qr!SEE THIS!, 'alias regex $uri.html'); -like(http_get('/alias-re-prefix/directory'), qr!301 Moved Permanently!, 'alias regex $uri/ redirect'); -like(http_get('/alias-re-prefix/directory/'), qr!SEE THIS!, 'alias regex $uri/ index'); +like(http_get('/alias-re-prefix/found.html'), qr!SEE THIS!, + 'alias regex $uri'); +like(http_get('/alias-re-prefix/found'), qr!SEE THIS!, + 'alias regex $uri.html'); +like(http_get('/alias-re-prefix/directory'), qr!301 Moved Permanently!, + 'alias regex $uri/ redirect'); +like(http_get('/alias-re-prefix/directory/'), qr!SEE THIS!, + 'alias regex $uri/ index'); } -like(http_get('/alias-re-prefix/notfound'), qr!404 Not!, 'alias regex not found with prefix'); +like(http_get('/alias-re-prefix/notfound'), qr!404 Not!, + 'alias regex not found with prefix'); # various specific tests diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -829,7 +829,8 @@ sub log_core { } $msg =~ s/^/# $prefix/gm; - $msg =~ s/([^\x20-\x7e])/sprintf('\\x%02x', ord($1)) . (($1 eq "\n") ? "\n" : '')/gmxe; + $msg =~ s/([^\x20-\x7e])/sprintf('\\x%02x', ord($1)) + . (($1 eq "\n") ? "\n" : '')/gmxe; $msg .= "\n" unless $msg =~ /\n\Z/; print $msg; } diff --git a/mail_imap.t b/mail_imap.t --- a/mail_imap.t +++ b/mail_imap.t @@ -123,10 +123,12 @@ my $s = Test::Nginx::IMAP->new(); # auth plain -$s->send('1 AUTHENTICATE PLAIN ' . encode_base64("\0test\@example.com\0bad", '')); +$s->send('1 AUTHENTICATE PLAIN ' + . encode_base64("\0test\@example.com\0bad", '')); $s->check(qr/^\S+ NO/, 'auth plain with bad password'); -$s->send('1 AUTHENTICATE PLAIN ' . encode_base64("\0test\@example.com\0secret", '')); +$s->send('1 AUTHENTICATE PLAIN ' + . encode_base64("\0test\@example.com\0secret", '')); $s->ok('auth plain'); # auth login simple diff --git a/proxy_xar.t b/proxy_xar.t --- a/proxy_xar.t +++ b/proxy_xar.t @@ -83,7 +83,8 @@ my $r = http_get('/proxy?xar=/index.html like($r, qr/xar: \/index.html uri: \/index.html/, 'X-Accel-Redirect works'); like($r, qr/^Content-Type: text\/blah/m, 'Content-Type preserved'); like($r, qr/^Set-Cookie: blah=blah/m, 'Set-Cookie preserved'); -like($r, qr/^Content-Disposition: attachment/m, 'Content-Disposition preserved'); +like($r, qr/^Content-Disposition: attachment/m, + 'Content-Disposition preserved'); like($r, qr/^Cache-Control: no-cache/m, 'Cache-Control preserved'); like($r, qr/^Expires: fake/m, 'Expires preserved'); like($r, qr/^Accept-Ranges: parrots/m, 'Accept-Ranges preserved'); diff --git a/ssl.t b/ssl.t --- a/ssl.t +++ b/ssl.t @@ -84,7 +84,8 @@ http { return 200 "body $ssl_client_s_dn:$ssl_client_s_dn_legacy"; } location /time { - return 200 "body $ssl_client_v_start!$ssl_client_v_end!$ssl_client_v_remain"; + return 200 + "body $ssl_client_v_start!$ssl_client_v_end!$ssl_client_v_remain"; } location /body { diff --git a/stream_udp_limit_conn.t b/stream_udp_limit_conn.t --- a/stream_udp_limit_conn.t +++ b/stream_udp_limit_conn.t @@ -97,7 +97,8 @@ is($s->io('1', read_timeout => 0.4), '1' is(dgram('127.0.0.1:' . port(8981))->io('1', read_timeout => 0.1), '', 'rejected new session'); is(dgram('127.0.0.1:' . port(8982))->io('1'), '1', 'passed different zone'); -is(dgram('127.0.0.1:' . port(8983))->io('1'), '1', 'passed same zone unlimited'); +is(dgram('127.0.0.1:' . port(8983))->io('1'), '1', + 'passed same zone unlimited'); sleep 1; # waiting for proxy_timeout to expire diff --git a/sub_filter_perl.t b/sub_filter_perl.t --- a/sub_filter_perl.t +++ b/sub_filter_perl.t @@ -96,11 +96,13 @@ like(http_get('/multi?a=a&b=aaaab'), qr/ like(http_get('/multi?a=aa&b=ab'), qr/^a_replaced$/m, 'aab in aa + ab'); like(http_get('/multi?a=aa&b=aab'), qr/^aa_replaced$/m, 'aab in aa + aab'); like(http_get('/multi?a=aa&b=aaab'), qr/^aaa_replaced$/m, 'aab in aa + aaab'); -like(http_get('/multi?a=aa&b=aaaab'), qr/^aaaa_replaced$/m, 'aab in aa + aaaab'); +like(http_get('/multi?a=aa&b=aaaab'), qr/^aaaa_replaced$/m, + 'aab in aa + aaaab'); # full backtracking -like(http_get('/multi?a=aa&b=xaaab'), qr/^aaxa_replaced$/m, 'aab in aa + xaaab'); +like(http_get('/multi?a=aa&b=xaaab'), qr/^aaxa_replaced$/m, + 'aab in aa + xaaab'); like(http_get('/multi?a=aa&b=axaaab'), qr/^aaaxa_replaced$/m, 'aab in aa + axaaab'); like(http_get('/multi?a=aa&b=aaxaaab'), qr/^aaaaxa_replaced$/m, @@ -118,6 +120,7 @@ like(http_get('/short?a=aa&b=b'), qr/^a_ like(http_get('/short?a=aa&b=ab'), qr/^aa_replaced$/m, 'ab in aa + ab'); like(http_get('/short?a=aa&b=aab'), qr/^aaa_replaced$/m, 'ab in aa + aab'); like(http_get('/short?a=aa&b=aaab'), qr/^aaaa_replaced$/m, 'ab in aa + aaab'); -like(http_get('/short?a=aa&b=aaaab'), qr/^aaaaa_replaced$/m, 'ab in aa + aaaab'); +like(http_get('/short?a=aa&b=aaaab'), qr/^aaaaa_replaced$/m, + 'ab in aa + aaaab'); ############################################################################### From mdounin at mdounin.ru Sat Jul 18 17:13:27 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 18 Jul 2026 20:13:27 +0300 Subject: [nginx-tests] Tests: index test for flushing non-cacheable varia... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/bd857e1a45f8 branches: changeset: 2079:bd857e1a45f8 user: Maxim Dounin date: Sat Jul 18 20:11:10 2026 +0300 description: Tests: index test for flushing non-cacheable variables. diffstat: index.t | 25 ++++++++++++++++++++++++- 1 files changed, 24 insertions(+), 1 deletions(-) diffs (67 lines): diff --git a/index.t b/index.t --- a/index.t +++ b/index.t @@ -1,5 +1,6 @@ #!/usr/bin/perl +# (C) Maxim Dounin # (C) Sergey Kandaurov # (C) Nginx, Inc. @@ -22,7 +23,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(16) +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(17) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -43,6 +44,11 @@ http { ~(?) ""; } + map $index $map_volatile { + volatile; + ~^ $index; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -99,11 +105,20 @@ http { alias %%TESTDIR%%/; index index.$capture.$map_capture /index.html; } + location /shrink/ { alias %%TESTDIR%%/; set $shrink "some-long-variable-value"; index index$shrink$map_shrink.html /index.html; } + + location /volatile/ { + alias %%TESTDIR%%/; + set $index notfound.html; + set $dummy $map_volatile; + set $index index.html; + index $map_volatile; + } } } @@ -156,6 +171,14 @@ like(http_get('/shrink/'), qr!X-URI: /sh } +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.4'); + +like(http_get('/volatile/'), qr!X-URI: /volatile/index.html\x0d?($).*body!ms, + 'index flushes non-cacheable variables'); + +} + $t->stop(); like($t->read_file('log_not_found.log'), qr/error/, 'log_not_found'); From mdounin at mdounin.ru Sat Jul 18 21:24:38 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 19 Jul 2026 00:24:38 +0300 Subject: [PATCH] Tests: another test with variables which change between accesses Message-ID: <1d795d140c1829c9a3db.1784409878@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1784409820 -10800 # Sun Jul 19 00:23:40 2026 +0300 # Node ID 1d795d140c1829c9a3db45a87069760ff6914afe # Parent bd857e1a45f8b14c01c3091ec4dc7f4c6ddad707 Tests: another test with variables which change between accesses. A test with "return" and map with side effects added, which covers ngx_http_complex_value() usage. 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(26) +my $t = Test::Nginx->new()->has(qw/http rewrite proxy/)->plan(27) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -150,6 +150,10 @@ http { rewrite ^ $capture$map_capture redirect; } + location /map_return/ { + return 200 $capture$map_capture; + } + location /break { rewrite ^ /return200; break; @@ -283,7 +287,7 @@ like(http_get('/capture_nested/%25?a=b') } TODO: { -todo_skip 'might coredump', 1 +todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; local $TODO = 'not yet', $t->todo_alerts(); @@ -291,6 +295,9 @@ local $TODO = 'not yet', $t->todo_alerts like(http_get('/map/test-long-uri'), qr!Location: .*/map/test-long-uri!ms, 'rewrite and map with side effects'); +like(http_get('/map_return/test-long-uri'), qr!.*/map_return/test-long-uri!, + 'return and map with side effects'); + } # break From mdounin at mdounin.ru Sat Jul 18 21:25:14 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 19 Jul 2026 00:25:14 +0300 Subject: [PATCH] Tests: fixed stream_access_log_script.t regex Message-ID: <8af94668705c15b5eceb.1784409914@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1784409894 -10800 # Sun Jul 19 00:24:54 2026 +0300 # Node ID 8af94668705c15b5eceb76e619c38f6a87c9f2c2 # Parent 1d795d140c1829c9a3db45a87069760ff6914afe Tests: fixed stream_access_log_script.t regex. diff --git a/stream_access_log_script.t b/stream_access_log_script.t --- a/stream_access_log_script.t +++ b/stream_access_log_script.t @@ -70,7 +70,7 @@ http_get('/'); my $log = $t->read_file('map.log'); -like($log, qr!start /map /map end!, 'log and map with side effects'); +like($log, qr!start \d+ \d+ end!, 'log and map with side effects'); } From mdounin at mdounin.ru Sun Jul 19 00:01:35 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 19 Jul 2026 03:01:35 +0300 Subject: [PATCH 1 of 4] Script: changed ngx_http_script_run() to avoid flushing all vars Message-ID: <5bfb1aa8b4439ce91668.1784419295@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1784409975 -10800 # Sun Jul 19 00:26:15 2026 +0300 # Node ID 5bfb1aa8b4439ce916685ee6caa18fb7d0cfd68c # Parent 511b31fbc525265eb4318fe520fa3783b07c77ec Script: changed ngx_http_script_run() to avoid flushing all vars. Previously, ngx_http_script_run() flushed all non-cacheable variables. Notably, this made it impossible to ensure that all variables have cached values. With this change, ngx_http_script_run() instead uses an additional length calculation loop without e.flushed set to ensure all relevant non-cacheable variables are flushed, and also looks up all the values. This approach might not be as effective as a separate list of variables to flush, such as used by ngx_http_complex_value(), yet it is expected to be better than flush of all variables as used previously. And this resolves issues observed with non-cacheable variables and variables with side effects when using ngx_http_script_run(), such as in the following configuration: map $uri $map { ~(?.*) $capture; } root html/$capture/$map; Similar changes were made in the stream module. diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c --- a/src/http/ngx_http_script.c +++ b/src/http/ngx_http_script.c @@ -611,25 +611,21 @@ u_char * ngx_http_script_run(ngx_http_request_t *r, ngx_str_t *value, void *code_lengths, size_t len, void *code_values) { - ngx_uint_t i; - ngx_http_script_code_pt code; - ngx_http_script_len_code_pt lcode; - ngx_http_script_engine_t e; - ngx_http_core_main_conf_t *cmcf; - - cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); - - for (i = 0; i < cmcf->variables.nelts; i++) { - if (r->variables[i].no_cacheable) { - r->variables[i].valid = 0; - r->variables[i].not_found = 0; - } - } + ngx_http_script_code_pt code; + ngx_http_script_engine_t e; + ngx_http_script_len_code_pt lcode; ngx_memzero(&e, sizeof(ngx_http_script_engine_t)); e.ip = code_lengths; e.request = r; + + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_http_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = code_lengths; e.flushed = 1; while (*(uintptr_t *) e.ip) { @@ -637,7 +633,6 @@ ngx_http_script_run(ngx_http_request_t * len += lcode(&e); } - value->len = len; value->data = ngx_pnalloc(r->pool, len); if (value->data == NULL) { diff --git a/src/stream/ngx_stream_script.c b/src/stream/ngx_stream_script.c --- a/src/stream/ngx_stream_script.c +++ b/src/stream/ngx_stream_script.c @@ -492,20 +492,9 @@ u_char * ngx_stream_script_run(ngx_stream_session_t *s, ngx_str_t *value, void *code_lengths, size_t len, void *code_values) { - ngx_uint_t i; - ngx_stream_script_code_pt code; - ngx_stream_script_engine_t e; - ngx_stream_core_main_conf_t *cmcf; - ngx_stream_script_len_code_pt lcode; - - cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); - - for (i = 0; i < cmcf->variables.nelts; i++) { - if (s->variables[i].no_cacheable) { - s->variables[i].valid = 0; - s->variables[i].not_found = 0; - } - } + ngx_stream_script_code_pt code; + ngx_stream_script_engine_t e; + ngx_stream_script_len_code_pt lcode; ngx_memzero(&e, sizeof(ngx_stream_script_engine_t)); @@ -515,10 +504,17 @@ ngx_stream_script_run(ngx_stream_session while (*(uintptr_t *) e.ip) { lcode = *(ngx_stream_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = code_lengths; + e.flushed = 1; + + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_stream_script_len_code_pt *) e.ip; len += lcode(&e); } - value->len = len; value->data = ngx_pnalloc(s->connection->pool, len); if (value->data == NULL) { From mdounin at mdounin.ru Sun Jul 19 00:01:36 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 19 Jul 2026 03:01:36 +0300 Subject: [PATCH 2 of 4] Script: flushing now implies lookup of variables In-Reply-To: <5bfb1aa8b4439ce91668.1784419295@vm-bsd.mdounin.ru> References: <5bfb1aa8b4439ce91668.1784419295@vm-bsd.mdounin.ru> Message-ID: # HG changeset patch # User Maxim Dounin # Date 1784411265 -10800 # Sun Jul 19 00:47:45 2026 +0300 # Node ID b9068e5a711c88abe4d6f4ed335467e0977f2956 # Parent 5bfb1aa8b4439ce916685ee6caa18fb7d0cfd68c Script: flushing now implies lookup of variables. Now flushing variables, such as in ngx_http_complex_value() and in ngx_http_script_flush_no_cacheable_variables(), not only clears existing cached values, but also looks up new values by calling ngx_http_get_flushed_variable() for all relevant variables. This ensures that even when there are variables with side effects, proper lengths of all variables are available during length calculations. In particular, this fixes issues observed in the following configuration: map $uri $map { ~(?.*) $capture; } return 200 $capture$map; Note that this requires changes to the proxy module, which does flushing before the actual variable values are known. But the code in the proxy module is incorrect anyway, and using the $proxy_internal_body_length variable in the proxy_set_body will break things. Similar changes were made in the stream module. 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 @@ -1340,10 +1340,9 @@ ngx_http_proxy_create_request(ngx_http_r ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); - ngx_http_script_flush_no_cacheable_variables(r, plcf->body_flushes); - ngx_http_script_flush_no_cacheable_variables(r, headers->flushes); - if (plcf->body_lengths) { + ngx_http_script_flush_no_cacheable_variables(r, plcf->body_flushes); + le.ip = plcf->body_lengths->elts; le.request = r; le.flushed = 1; @@ -1367,6 +1366,8 @@ ngx_http_proxy_create_request(ngx_http_r ctx->internal_body_length = r->headers_in.content_length_n; } + ngx_http_script_flush_no_cacheable_variables(r, headers->flushes); + le.ip = headers->lengths->elts; le.request = r; le.flushed = 1; diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c --- a/src/http/ngx_http_script.c +++ b/src/http/ngx_http_script.c @@ -41,12 +41,7 @@ ngx_http_script_flush_complex_value(ngx_ if (index) { while (*index != (ngx_uint_t) -1) { - - if (r->variables[*index].no_cacheable) { - r->variables[*index].valid = 0; - r->variables[*index].not_found = 0; - } - + (void) ngx_http_get_flushed_variable(r, *index); index++; } } @@ -665,10 +660,7 @@ ngx_http_script_flush_no_cacheable_varia if (indices) { index = indices->elts; for (n = 0; n < indices->nelts; n++) { - if (r->variables[index[n]].no_cacheable) { - r->variables[index[n]].valid = 0; - r->variables[index[n]].not_found = 0; - } + (void) ngx_http_get_flushed_variable(r, index[n]); } } } diff --git a/src/stream/ngx_stream_script.c b/src/stream/ngx_stream_script.c --- a/src/stream/ngx_stream_script.c +++ b/src/stream/ngx_stream_script.c @@ -41,12 +41,7 @@ ngx_stream_script_flush_complex_value(ng if (index) { while (*index != (ngx_uint_t) -1) { - - if (s->variables[*index].no_cacheable) { - s->variables[*index].valid = 0; - s->variables[*index].not_found = 0; - } - + (void) ngx_stream_get_flushed_variable(s, *index); index++; } } @@ -547,10 +542,7 @@ ngx_stream_script_flush_no_cacheable_var if (indices) { index = indices->elts; for (n = 0; n < indices->nelts; n++) { - if (s->variables[index[n]].no_cacheable) { - s->variables[index[n]].valid = 0; - s->variables[index[n]].not_found = 0; - } + (void) ngx_stream_get_flushed_variable(s, index[n]); } } } From mdounin at mdounin.ru Sun Jul 19 00:01:37 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 19 Jul 2026 03:01:37 +0300 Subject: [PATCH 3 of 4] Rewrite: added flushing of variables during length calculations In-Reply-To: <5bfb1aa8b4439ce91668.1784419295@vm-bsd.mdounin.ru> References: <5bfb1aa8b4439ce91668.1784419295@vm-bsd.mdounin.ru> Message-ID: <762271b9b20cbb03e7f7.1784419297@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1784411852 -10800 # Sun Jul 19 00:57:32 2026 +0300 # Node ID 762271b9b20cbb03e7f78c0f4e483930cb1edebc # Parent b9068e5a711c88abe4d6f4ed335467e0977f2956 Rewrite: added flushing of variables during length calculations. In particular, this fixes issues observed in the following configuration: map $uri $map { ~(?.*) $capture; } set $temp $capture$map; Note that this slightly changes meaning of "e->flushed" when used for rewrite-specific codes, notably for ngx_http_script_complex_value_code() and ngx_http_script_regex_start_code(). It is now also used to indicate that relevant code-specific flush arrays should be used. diff --git a/src/http/modules/ngx_http_rewrite_module.c b/src/http/modules/ngx_http_rewrite_module.c --- a/src/http/modules/ngx_http_rewrite_module.c +++ b/src/http/modules/ngx_http_rewrite_module.c @@ -171,6 +171,7 @@ ngx_http_rewrite_handler(ngx_http_reques e->ip = rlcf->codes->elts; e->request = r; + e->flushed = 1; e->quote = 1; e->log = rlcf->log; e->status = NGX_DECLINED; @@ -389,6 +390,7 @@ ngx_http_rewrite(ngx_conf_t *cf, ngx_com sc.cf = cf; sc.source = &value[2]; + sc.flushes = ®ex->flushes; sc.lengths = ®ex->lengths; sc.values = &lcf->codes; sc.variables = ngx_http_script_variables_count(&value[2]); @@ -981,12 +983,14 @@ ngx_http_rewrite_value(ngx_conf_t *cf, n } complex->code = ngx_http_script_complex_value_code; + complex->flushes = NULL; complex->lengths = NULL; ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); sc.cf = cf; sc.source = value; + sc.flushes = &complex->flushes; sc.lengths = &complex->lengths; sc.values = &lcf->codes; sc.variables = n; diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c --- a/src/http/ngx_http_script.c +++ b/src/http/ngx_http_script.c @@ -1154,11 +1154,16 @@ ngx_http_script_regex_start_code(ngx_htt } } + if (e->flushed) { + ngx_http_script_flush_no_cacheable_variables(e->request, code->flushes); + } + ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); le.ip = code->lengths->elts; le.line = e->line; le.request = r; + le.flushed = e->flushed; le.quote = code->redirect; le.is_args = e->is_args; @@ -1771,11 +1776,16 @@ ngx_http_script_complex_value_code(ngx_h ngx_log_debug0(NGX_LOG_DEBUG_HTTP, e->request->connection->log, 0, "http script complex value"); + if (e->flushed) { + ngx_http_script_flush_no_cacheable_variables(e->request, code->flushes); + } + ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); le.ip = code->lengths->elts; le.line = e->line; le.request = e->request; + le.flushed = e->flushed; le.quote = e->quote; le.is_args = e->is_args; diff --git a/src/http/ngx_http_script.h b/src/http/ngx_http_script.h --- a/src/http/ngx_http_script.h +++ b/src/http/ngx_http_script.h @@ -112,6 +112,7 @@ typedef struct { typedef struct { ngx_http_script_code_pt code; ngx_http_regex_t *regex; + ngx_array_t *flushes; ngx_array_t *lengths; uintptr_t size; uintptr_t status; @@ -187,6 +188,7 @@ typedef struct { typedef struct { ngx_http_script_code_pt code; + ngx_array_t *flushes; ngx_array_t *lengths; } ngx_http_script_complex_value_code_t; From mdounin at mdounin.ru Sun Jul 19 00:01:38 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 19 Jul 2026 03:01:38 +0300 Subject: [PATCH 4 of 4] Script: changed index and try_files to evaluate lengths twice In-Reply-To: <5bfb1aa8b4439ce91668.1784419295@vm-bsd.mdounin.ru> References: <5bfb1aa8b4439ce91668.1784419295@vm-bsd.mdounin.ru> Message-ID: # HG changeset patch # User Maxim Dounin # Date 1784411862 -10800 # Sun Jul 19 00:57:42 2026 +0300 # Node ID d6fd0bcea8666d6e0bbd3383d7ab2d28a7f21ce1 # Parent 762271b9b20cbb03e7f78c0f4e483930cb1edebc Script: changed index and try_files to evaluate lengths twice. Similarly to changes introduced to ngx_http_script_run(), direct script evaluation in index and try_files now uses a separate length calculation loop without e.flushed to ensure that all relevant non-cacheable variables are flushed, and also to look up all the values. This resolves issues observed with non-cacheable variables and variables with side effects in the index and try_files directives. diff --git a/src/http/modules/ngx_http_index_module.c b/src/http/modules/ngx_http_index_module.c --- a/src/http/modules/ngx_http_index_module.c +++ b/src/http/modules/ngx_http_index_module.c @@ -150,6 +150,14 @@ ngx_http_index_handler(ngx_http_request_ e.ip = index[i].lengths->elts; e.request = r; + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_http_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = index[i].lengths->elts; + e.flushed = 1; + /* 1 is for terminating '\0' as in static names */ len = 1; diff --git a/src/http/modules/ngx_http_try_files_module.c b/src/http/modules/ngx_http_try_files_module.c --- a/src/http/modules/ngx_http_try_files_module.c +++ b/src/http/modules/ngx_http_try_files_module.c @@ -123,6 +123,14 @@ ngx_http_try_files_handler(ngx_http_requ e.ip = tf->lengths->elts; e.request = r; + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_http_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = tf->lengths->elts; + e.flushed = 1; + /* 1 is for terminating '\0' as in static names */ len = 1; From mdounin at mdounin.ru Sun Jul 19 00:02:23 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 19 Jul 2026 03:02:23 +0300 Subject: [PATCH] Tests: adjusted TODOs for variable evaluation tests Message-ID: <2cbdf8a573d9d8ee60bf.1784419343@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1784416251 -10800 # Sun Jul 19 02:10:51 2026 +0300 # Node ID 2cbdf8a573d9d8ee60bfc8e3258674281e6b0232 # Parent 8af94668705c15b5eceb76e619c38f6a87c9f2c2 Tests: adjusted TODOs for variable evaluation tests. diff --git a/access_log_script.t b/access_log_script.t --- a/access_log_script.t +++ b/access_log_script.t @@ -72,7 +72,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); # map with side effects might result in incorrect buffer size # and buffer overrun diff --git a/fastcgi_header_params.t b/fastcgi_header_params.t --- a/fastcgi_header_params.t +++ b/fastcgi_header_params.t @@ -99,7 +99,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, 'fastcgi params and map with side effects'); diff --git a/grpc_headers.t b/grpc_headers.t --- a/grpc_headers.t +++ b/grpc_headers.t @@ -108,7 +108,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/test-long-uri'), qr!blah .* /test-long-uri end!, 'grpc_set_header and map with side effects'); diff --git a/http_try_files.t b/http_try_files.t --- a/http_try_files.t +++ b/http_try_files.t @@ -416,7 +416,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!404 Not!, 'try_files and map with side effects'); diff --git a/index.t b/index.t --- a/index.t +++ b/index.t @@ -156,7 +156,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri/'), qr!X-URI: /index.html\x0d($).*body!ms, 'index and map with side effects'); diff --git a/proxy_set_body.t b/proxy_set_body.t --- a/proxy_set_body.t +++ b/proxy_set_body.t @@ -97,7 +97,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map'), qr!X-Body: body .* /map end!, 'proxy_set_body and map with side effects'); diff --git a/rewrite.t b/rewrite.t --- a/rewrite.t +++ b/rewrite.t @@ -290,7 +290,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!Location: .*/map/test-long-uri!ms, 'rewrite and map with side effects'); diff --git a/rewrite_set.t b/rewrite_set.t --- a/rewrite_set.t +++ b/rewrite_set.t @@ -201,7 +201,8 @@ TODO: { todo_skip 'might coredump', 5 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); # map can change other variables via named captures, # resulting in invalid buffer length calculations diff --git a/scgi.t b/scgi.t --- a/scgi.t +++ b/scgi.t @@ -117,7 +117,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, 'scgi params and map with side effects'); diff --git a/stream_access_log_script.t b/stream_access_log_script.t --- a/stream_access_log_script.t +++ b/stream_access_log_script.t @@ -59,7 +59,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); # map with side effects might result in incorrect buffer size # and buffer overrun diff --git a/stream_set.t b/stream_set.t --- a/stream_set.t +++ b/stream_set.t @@ -79,7 +79,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); is(stream('127.0.0.1:' . port(8084))->read(), '0 0', 'set and map with side effects'); diff --git a/uwsgi.t b/uwsgi.t --- a/uwsgi.t +++ b/uwsgi.t @@ -139,7 +139,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, 'uwsgi params and map with side effects'); From mdounin at mdounin.ru Sat Jul 25 00:04:55 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 25 Jul 2026 03:04:55 +0300 Subject: [nginx] Script: changed ngx_http_script_run() to avoid flushing ... Message-ID: details: http://freenginx.org/hg/nginx/rev/5bfb1aa8b443 branches: changeset: 9577:5bfb1aa8b443 user: Maxim Dounin date: Sun Jul 19 00:26:15 2026 +0300 description: Script: changed ngx_http_script_run() to avoid flushing all vars. Previously, ngx_http_script_run() flushed all non-cacheable variables. Notably, this made it impossible to ensure that all variables have cached values. With this change, ngx_http_script_run() instead uses an additional length calculation loop without e.flushed set to ensure all relevant non-cacheable variables are flushed, and also looks up all the values. This approach might not be as effective as a separate list of variables to flush, such as used by ngx_http_complex_value(), yet it is expected to be better than flush of all variables as used previously. And this resolves issues observed with non-cacheable variables and variables with side effects when using ngx_http_script_run(), such as in the following configuration: map $uri $map { ~(?.*) $capture; } root html/$capture/$map; Similar changes were made in the stream module. diffstat: src/http/ngx_http_script.c | 25 ++++++++++--------------- src/stream/ngx_stream_script.c | 26 +++++++++++--------------- 2 files changed, 21 insertions(+), 30 deletions(-) diffs (93 lines): diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c --- a/src/http/ngx_http_script.c +++ b/src/http/ngx_http_script.c @@ -611,25 +611,21 @@ u_char * ngx_http_script_run(ngx_http_request_t *r, ngx_str_t *value, void *code_lengths, size_t len, void *code_values) { - ngx_uint_t i; - ngx_http_script_code_pt code; - ngx_http_script_len_code_pt lcode; - ngx_http_script_engine_t e; - ngx_http_core_main_conf_t *cmcf; - - cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); - - for (i = 0; i < cmcf->variables.nelts; i++) { - if (r->variables[i].no_cacheable) { - r->variables[i].valid = 0; - r->variables[i].not_found = 0; - } - } + ngx_http_script_code_pt code; + ngx_http_script_engine_t e; + ngx_http_script_len_code_pt lcode; ngx_memzero(&e, sizeof(ngx_http_script_engine_t)); e.ip = code_lengths; e.request = r; + + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_http_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = code_lengths; e.flushed = 1; while (*(uintptr_t *) e.ip) { @@ -637,7 +633,6 @@ ngx_http_script_run(ngx_http_request_t * len += lcode(&e); } - value->len = len; value->data = ngx_pnalloc(r->pool, len); if (value->data == NULL) { diff --git a/src/stream/ngx_stream_script.c b/src/stream/ngx_stream_script.c --- a/src/stream/ngx_stream_script.c +++ b/src/stream/ngx_stream_script.c @@ -492,20 +492,9 @@ u_char * ngx_stream_script_run(ngx_stream_session_t *s, ngx_str_t *value, void *code_lengths, size_t len, void *code_values) { - ngx_uint_t i; - ngx_stream_script_code_pt code; - ngx_stream_script_engine_t e; - ngx_stream_core_main_conf_t *cmcf; - ngx_stream_script_len_code_pt lcode; - - cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); - - for (i = 0; i < cmcf->variables.nelts; i++) { - if (s->variables[i].no_cacheable) { - s->variables[i].valid = 0; - s->variables[i].not_found = 0; - } - } + ngx_stream_script_code_pt code; + ngx_stream_script_engine_t e; + ngx_stream_script_len_code_pt lcode; ngx_memzero(&e, sizeof(ngx_stream_script_engine_t)); @@ -515,10 +504,17 @@ ngx_stream_script_run(ngx_stream_session while (*(uintptr_t *) e.ip) { lcode = *(ngx_stream_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = code_lengths; + e.flushed = 1; + + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_stream_script_len_code_pt *) e.ip; len += lcode(&e); } - value->len = len; value->data = ngx_pnalloc(s->connection->pool, len); if (value->data == NULL) { From mdounin at mdounin.ru Sat Jul 25 00:04:55 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 25 Jul 2026 03:04:55 +0300 Subject: [nginx] Script: flushing now implies lookup of variables. Message-ID: details: http://freenginx.org/hg/nginx/rev/b9068e5a711c branches: changeset: 9578:b9068e5a711c user: Maxim Dounin date: Sun Jul 19 00:47:45 2026 +0300 description: Script: flushing now implies lookup of variables. Now flushing variables, such as in ngx_http_complex_value() and in ngx_http_script_flush_no_cacheable_variables(), not only clears existing cached values, but also looks up new values by calling ngx_http_get_flushed_variable() for all relevant variables. This ensures that even when there are variables with side effects, proper lengths of all variables are available during length calculations. In particular, this fixes issues observed in the following configuration: map $uri $map { ~(?.*) $capture; } return 200 $capture$map; Note that this requires changes to the proxy module, which does flushing before the actual variable values are known. But the code in the proxy module is incorrect anyway, and using the $proxy_internal_body_length variable in the proxy_set_body will break things. Similar changes were made in the stream module. diffstat: src/http/modules/ngx_http_proxy_module.c | 7 ++++--- src/http/ngx_http_script.c | 12 ++---------- src/stream/ngx_stream_script.c | 12 ++---------- 3 files changed, 8 insertions(+), 23 deletions(-) diffs (83 lines): 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 @@ -1340,10 +1340,9 @@ ngx_http_proxy_create_request(ngx_http_r ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); - ngx_http_script_flush_no_cacheable_variables(r, plcf->body_flushes); - ngx_http_script_flush_no_cacheable_variables(r, headers->flushes); - if (plcf->body_lengths) { + ngx_http_script_flush_no_cacheable_variables(r, plcf->body_flushes); + le.ip = plcf->body_lengths->elts; le.request = r; le.flushed = 1; @@ -1367,6 +1366,8 @@ ngx_http_proxy_create_request(ngx_http_r ctx->internal_body_length = r->headers_in.content_length_n; } + ngx_http_script_flush_no_cacheable_variables(r, headers->flushes); + le.ip = headers->lengths->elts; le.request = r; le.flushed = 1; diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c --- a/src/http/ngx_http_script.c +++ b/src/http/ngx_http_script.c @@ -41,12 +41,7 @@ ngx_http_script_flush_complex_value(ngx_ if (index) { while (*index != (ngx_uint_t) -1) { - - if (r->variables[*index].no_cacheable) { - r->variables[*index].valid = 0; - r->variables[*index].not_found = 0; - } - + (void) ngx_http_get_flushed_variable(r, *index); index++; } } @@ -665,10 +660,7 @@ ngx_http_script_flush_no_cacheable_varia if (indices) { index = indices->elts; for (n = 0; n < indices->nelts; n++) { - if (r->variables[index[n]].no_cacheable) { - r->variables[index[n]].valid = 0; - r->variables[index[n]].not_found = 0; - } + (void) ngx_http_get_flushed_variable(r, index[n]); } } } diff --git a/src/stream/ngx_stream_script.c b/src/stream/ngx_stream_script.c --- a/src/stream/ngx_stream_script.c +++ b/src/stream/ngx_stream_script.c @@ -41,12 +41,7 @@ ngx_stream_script_flush_complex_value(ng if (index) { while (*index != (ngx_uint_t) -1) { - - if (s->variables[*index].no_cacheable) { - s->variables[*index].valid = 0; - s->variables[*index].not_found = 0; - } - + (void) ngx_stream_get_flushed_variable(s, *index); index++; } } @@ -547,10 +542,7 @@ ngx_stream_script_flush_no_cacheable_var if (indices) { index = indices->elts; for (n = 0; n < indices->nelts; n++) { - if (s->variables[index[n]].no_cacheable) { - s->variables[index[n]].valid = 0; - s->variables[index[n]].not_found = 0; - } + (void) ngx_stream_get_flushed_variable(s, index[n]); } } } From mdounin at mdounin.ru Sat Jul 25 00:04:55 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 25 Jul 2026 03:04:55 +0300 Subject: [nginx] Rewrite: added flushing of variables during length calcu... Message-ID: details: http://freenginx.org/hg/nginx/rev/762271b9b20c branches: changeset: 9579:762271b9b20c user: Maxim Dounin date: Sun Jul 19 00:57:32 2026 +0300 description: Rewrite: added flushing of variables during length calculations. In particular, this fixes issues observed in the following configuration: map $uri $map { ~(?.*) $capture; } set $temp $capture$map; Note that this slightly changes meaning of "e->flushed" when used for rewrite-specific codes, notably for ngx_http_script_complex_value_code() and ngx_http_script_regex_start_code(). It is now also used to indicate that relevant code-specific flush arrays should be used. diffstat: src/http/modules/ngx_http_rewrite_module.c | 4 ++++ src/http/ngx_http_script.c | 10 ++++++++++ src/http/ngx_http_script.h | 2 ++ 3 files changed, 16 insertions(+), 0 deletions(-) diffs (90 lines): diff --git a/src/http/modules/ngx_http_rewrite_module.c b/src/http/modules/ngx_http_rewrite_module.c --- a/src/http/modules/ngx_http_rewrite_module.c +++ b/src/http/modules/ngx_http_rewrite_module.c @@ -171,6 +171,7 @@ ngx_http_rewrite_handler(ngx_http_reques e->ip = rlcf->codes->elts; e->request = r; + e->flushed = 1; e->quote = 1; e->log = rlcf->log; e->status = NGX_DECLINED; @@ -389,6 +390,7 @@ ngx_http_rewrite(ngx_conf_t *cf, ngx_com sc.cf = cf; sc.source = &value[2]; + sc.flushes = ®ex->flushes; sc.lengths = ®ex->lengths; sc.values = &lcf->codes; sc.variables = ngx_http_script_variables_count(&value[2]); @@ -981,12 +983,14 @@ ngx_http_rewrite_value(ngx_conf_t *cf, n } complex->code = ngx_http_script_complex_value_code; + complex->flushes = NULL; complex->lengths = NULL; ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); sc.cf = cf; sc.source = value; + sc.flushes = &complex->flushes; sc.lengths = &complex->lengths; sc.values = &lcf->codes; sc.variables = n; diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c --- a/src/http/ngx_http_script.c +++ b/src/http/ngx_http_script.c @@ -1154,11 +1154,16 @@ ngx_http_script_regex_start_code(ngx_htt } } + if (e->flushed) { + ngx_http_script_flush_no_cacheable_variables(e->request, code->flushes); + } + ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); le.ip = code->lengths->elts; le.line = e->line; le.request = r; + le.flushed = e->flushed; le.quote = code->redirect; le.is_args = e->is_args; @@ -1771,11 +1776,16 @@ ngx_http_script_complex_value_code(ngx_h ngx_log_debug0(NGX_LOG_DEBUG_HTTP, e->request->connection->log, 0, "http script complex value"); + if (e->flushed) { + ngx_http_script_flush_no_cacheable_variables(e->request, code->flushes); + } + ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); le.ip = code->lengths->elts; le.line = e->line; le.request = e->request; + le.flushed = e->flushed; le.quote = e->quote; le.is_args = e->is_args; diff --git a/src/http/ngx_http_script.h b/src/http/ngx_http_script.h --- a/src/http/ngx_http_script.h +++ b/src/http/ngx_http_script.h @@ -112,6 +112,7 @@ typedef struct { typedef struct { ngx_http_script_code_pt code; ngx_http_regex_t *regex; + ngx_array_t *flushes; ngx_array_t *lengths; uintptr_t size; uintptr_t status; @@ -187,6 +188,7 @@ typedef struct { typedef struct { ngx_http_script_code_pt code; + ngx_array_t *flushes; ngx_array_t *lengths; } ngx_http_script_complex_value_code_t; From mdounin at mdounin.ru Sat Jul 25 00:04:55 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 25 Jul 2026 03:04:55 +0300 Subject: [nginx] Script: changed index and try_files to evaluate lengths ... Message-ID: details: http://freenginx.org/hg/nginx/rev/1260ebda7574 branches: changeset: 9580:1260ebda7574 user: Maxim Dounin date: Sun Jul 19 03:13:01 2026 +0300 description: Script: changed index and try_files to evaluate lengths twice. Similarly to changes introduced to ngx_http_script_run(), direct script evaluation in index and try_files now uses a separate length calculation loop without e.flushed to ensure that all relevant non-cacheable variables are flushed, and also to look up all the values. This resolves issues observed with non-cacheable variables and variables with side effects in the index and try_files directives. diffstat: src/http/modules/ngx_http_index_module.c | 8 ++++++++ src/http/modules/ngx_http_try_files_module.c | 8 ++++++++ 2 files changed, 16 insertions(+), 0 deletions(-) diffs (36 lines): diff --git a/src/http/modules/ngx_http_index_module.c b/src/http/modules/ngx_http_index_module.c --- a/src/http/modules/ngx_http_index_module.c +++ b/src/http/modules/ngx_http_index_module.c @@ -150,6 +150,14 @@ ngx_http_index_handler(ngx_http_request_ e.ip = index[i].lengths->elts; e.request = r; + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_http_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = index[i].lengths->elts; + e.flushed = 1; + /* 1 is for terminating '\0' as in static names */ len = 1; diff --git a/src/http/modules/ngx_http_try_files_module.c b/src/http/modules/ngx_http_try_files_module.c --- a/src/http/modules/ngx_http_try_files_module.c +++ b/src/http/modules/ngx_http_try_files_module.c @@ -123,6 +123,14 @@ ngx_http_try_files_handler(ngx_http_requ e.ip = tf->lengths->elts; e.request = r; + while (*(uintptr_t *) e.ip) { + lcode = *(ngx_http_script_len_code_pt *) e.ip; + (void) lcode(&e); + } + + e.ip = tf->lengths->elts; + e.flushed = 1; + /* 1 is for terminating '\0' as in static names */ len = 1; From mdounin at mdounin.ru Sat Jul 25 00:05:34 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 25 Jul 2026 03:05:34 +0300 Subject: [nginx-tests] Tests: another test with variables which change be... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/1d795d140c18 branches: changeset: 2080:1d795d140c18 user: Maxim Dounin date: Sun Jul 19 00:23:40 2026 +0300 description: Tests: another test with variables which change between accesses. A test with "return" and map with side effects added, which covers ngx_http_complex_value() usage. diffstat: rewrite.t | 11 +++++++++-- 1 files changed, 9 insertions(+), 2 deletions(-) diffs (42 lines): 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(26) +my $t = Test::Nginx->new()->has(qw/http rewrite proxy/)->plan(27) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -150,6 +150,10 @@ http { rewrite ^ $capture$map_capture redirect; } + location /map_return/ { + return 200 $capture$map_capture; + } + location /break { rewrite ^ /return200; break; @@ -283,7 +287,7 @@ like(http_get('/capture_nested/%25?a=b') } TODO: { -todo_skip 'might coredump', 1 +todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; local $TODO = 'not yet', $t->todo_alerts(); @@ -291,6 +295,9 @@ local $TODO = 'not yet', $t->todo_alerts like(http_get('/map/test-long-uri'), qr!Location: .*/map/test-long-uri!ms, 'rewrite and map with side effects'); +like(http_get('/map_return/test-long-uri'), qr!.*/map_return/test-long-uri!, + 'return and map with side effects'); + } # break From mdounin at mdounin.ru Sat Jul 25 00:05:34 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 25 Jul 2026 03:05:34 +0300 Subject: [nginx-tests] Tests: fixed stream_access_log_script.t regex. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/8af94668705c branches: changeset: 2081:8af94668705c user: Maxim Dounin date: Sun Jul 19 00:24:54 2026 +0300 description: Tests: fixed stream_access_log_script.t regex. diffstat: stream_access_log_script.t | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (12 lines): diff --git a/stream_access_log_script.t b/stream_access_log_script.t --- a/stream_access_log_script.t +++ b/stream_access_log_script.t @@ -70,7 +70,7 @@ http_get('/'); my $log = $t->read_file('map.log'); -like($log, qr!start /map /map end!, 'log and map with side effects'); +like($log, qr!start \d+ \d+ end!, 'log and map with side effects'); } From mdounin at mdounin.ru Sat Jul 25 00:05:34 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 25 Jul 2026 03:05:34 +0300 Subject: [nginx-tests] Tests: adjusted TODOs for variable evaluation tests. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/2cbdf8a573d9 branches: changeset: 2082:2cbdf8a573d9 user: Maxim Dounin date: Sun Jul 19 02:10:51 2026 +0300 description: Tests: adjusted TODOs for variable evaluation tests. diffstat: access_log_script.t | 3 ++- fastcgi_header_params.t | 3 ++- grpc_headers.t | 3 ++- http_try_files.t | 3 ++- index.t | 3 ++- proxy_set_body.t | 3 ++- rewrite.t | 3 ++- rewrite_set.t | 3 ++- scgi.t | 3 ++- stream_access_log_script.t | 3 ++- stream_set.t | 3 ++- uwsgi.t | 3 ++- 12 files changed, 24 insertions(+), 12 deletions(-) diffs (156 lines): diff --git a/access_log_script.t b/access_log_script.t --- a/access_log_script.t +++ b/access_log_script.t @@ -72,7 +72,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); # map with side effects might result in incorrect buffer size # and buffer overrun diff --git a/fastcgi_header_params.t b/fastcgi_header_params.t --- a/fastcgi_header_params.t +++ b/fastcgi_header_params.t @@ -99,7 +99,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, 'fastcgi params and map with side effects'); diff --git a/grpc_headers.t b/grpc_headers.t --- a/grpc_headers.t +++ b/grpc_headers.t @@ -108,7 +108,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/test-long-uri'), qr!blah .* /test-long-uri end!, 'grpc_set_header and map with side effects'); diff --git a/http_try_files.t b/http_try_files.t --- a/http_try_files.t +++ b/http_try_files.t @@ -416,7 +416,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!404 Not!, 'try_files and map with side effects'); diff --git a/index.t b/index.t --- a/index.t +++ b/index.t @@ -156,7 +156,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri/'), qr!X-URI: /index.html\x0d($).*body!ms, 'index and map with side effects'); diff --git a/proxy_set_body.t b/proxy_set_body.t --- a/proxy_set_body.t +++ b/proxy_set_body.t @@ -97,7 +97,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map'), qr!X-Body: body .* /map end!, 'proxy_set_body and map with side effects'); diff --git a/rewrite.t b/rewrite.t --- a/rewrite.t +++ b/rewrite.t @@ -290,7 +290,8 @@ TODO: { todo_skip 'might coredump', 2 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!Location: .*/map/test-long-uri!ms, 'rewrite and map with side effects'); diff --git a/rewrite_set.t b/rewrite_set.t --- a/rewrite_set.t +++ b/rewrite_set.t @@ -201,7 +201,8 @@ TODO: { todo_skip 'might coredump', 5 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); # map can change other variables via named captures, # resulting in invalid buffer length calculations diff --git a/scgi.t b/scgi.t --- a/scgi.t +++ b/scgi.t @@ -117,7 +117,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, 'scgi params and map with side effects'); diff --git a/stream_access_log_script.t b/stream_access_log_script.t --- a/stream_access_log_script.t +++ b/stream_access_log_script.t @@ -59,7 +59,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); # map with side effects might result in incorrect buffer size # and buffer overrun diff --git a/stream_set.t b/stream_set.t --- a/stream_set.t +++ b/stream_set.t @@ -79,7 +79,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); is(stream('127.0.0.1:' . port(8084))->read(), '0 0', 'set and map with side effects'); diff --git a/uwsgi.t b/uwsgi.t --- a/uwsgi.t +++ b/uwsgi.t @@ -139,7 +139,8 @@ TODO: { todo_skip 'might coredump', 1 unless $t->has_version('1.31.3') or $ENV{TEST_NGINX_UNSAFE}; -local $TODO = 'not yet', $t->todo_alerts(); +local $TODO = 'not yet', $t->todo_alerts() + unless $t->has_version('1.31.4'); like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, 'uwsgi params and map with side effects'); From mdounin at mdounin.ru Wed Jul 29 04:29:36 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 29 Jul 2026 07:29:36 +0300 Subject: [PATCH] Tests: reworked and simplified proxy variables tests Message-ID: <5d1b52ccd445392a64eb.1785299376@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1785299249 -10800 # Wed Jul 29 07:27:29 2026 +0300 # Node ID 5d1b52ccd445392a64ebb2a9f416f58e9b537945 # Parent 2cbdf8a573d9d8ee60bfc8e3258674281e6b0232 Tests: reworked and simplified proxy variables tests. 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 Wed Jul 29 04:34:21 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 29 Jul 2026 07:34:21 +0300 Subject: [PATCH] Upstream: avoid side effects on r->args when creating requests Message-ID: # HG changeset patch # User Maxim Dounin # Date 1785299238 -10800 # Wed Jul 29 07:27:18 2026 +0300 # Node ID ca7413416a812fd5c5dbd77a04d649cd71008bbd # Parent 1260ebda75742581472b1b105c9af202a5cc04d1 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. 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 Wed Jul 29 04:35:26 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 29 Jul 2026 07:35:26 +0300 Subject: [PATCH] Tests: tests for proxy_pass with $args changed as a side effect In-Reply-To: References: Message-ID: <682506de6ffbc1627847.1785299726@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1785299402 -10800 # Wed Jul 29 07:30:02 2026 +0300 # Node ID 682506de6ffbc1627847d3fa4c475addf604ea79 # Parent 5d1b52ccd445392a64ebb2a9f416f58e9b537945 Tests: tests for proxy_pass with $args changed as a side effect. 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/)->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 Fri Jul 31 06:32:38 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 Jul 2026 09:32:38 +0300 Subject: [PATCH] Tests: fixed map prerequisite in tests Message-ID: # HG changeset patch # User Maxim Dounin # Date 1785479542 -10800 # Fri Jul 31 09:32:22 2026 +0300 # Node ID bccc315f1725de088bc2fcbebb38f27ba3a9a064 # Parent 2cbdf8a573d9d8ee60bfc8e3258674281e6b0232 Tests: fixed map prerequisite in tests. 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%%