From mdounin at mdounin.ru Tue Jun 2 16:11:49 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:49 +0300 Subject: [nginx] Version bump. Message-ID: details: http://freenginx.org/hg/nginx/rev/0af8d622f0b6 branches: stable-1.30 changeset: 9536:0af8d622f0b6 user: Maxim Dounin date: Tue Jun 02 04:17:45 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 1030000 -#define NGINX_VERSION "1.30.0" +#define nginx_version 1030001 +#define NGINX_VERSION "1.30.1" #define freenginx 1 From mdounin at mdounin.ru Tue Jun 2 16:11:49 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:49 +0300 Subject: [nginx] SSL: logging levels of various errors reported with tlsf... Message-ID: details: http://freenginx.org/hg/nginx/rev/a2d7e910b26e branches: stable-1.30 changeset: 9537:a2d7e910b26e user: Maxim Dounin date: Thu Apr 30 07:21:42 2026 +0300 description: SSL: logging levels of various errors reported with tlsfuzzer. The following errors were observed during tlsfuzzer runs with OpenSSL 3.0.20, and indicate invalid data got from the client: SSL_do_handshake() failed (SSL: error:0A000104:SSL routines::invalid ccs message) SSL_read() failed (SSL: error:0A0000B6:SSL routines::not on record boundary) And the following error was observed during tlsfuzzer runs with OpenSSL 3.4.5: SSL_do_handshake() failed (SSL: error:0A000156:SSL routines::required compression algorithm missing) Accordingly, the SSL_R_INVALID_CCS_MESSAGE ("invalid ccs message"), SSL_R_NOT_ON_RECORD_BOUNDARY ("not on record boundary"), and SSL_R_REQUIRED_COMPRESSION_ALGORITHM_MISSING ("required compression algorithm missing") errors are now logged at the "info" level. Also, starting with OpenSSL 3.2.0, many errors observed in previous versions are additionally followed by the SSL_R_RECORD_LAYER_FAILURE ("record layer failure") error, for example: SSL_do_handshake() failed (SSL: error:0A000119:SSL routines::decryption failed or bad record mac error:0A000139:SSL routines::record layer failure) SSL_read() failed (SSL: error:0A000119:SSL routines::decryption failed or bad record mac error:0A000139:SSL routines::record layer failure) SSL_do_handshake() failed (SSL: error:0A000092:SSL routines::data length too long error:0A000139:SSL routines::record layer failure) SSL_do_handshake() failed (SSL: error:0A00010B:SSL routines::wrong version number error:0A000139:SSL routines::record layer failure) SSL_do_handshake() failed (SSL: error:0A0001BB:SSL routines::bad record type error:0A000139:SSL routines::record layer failure) SSL_read() failed (SSL: error:0A0001BB:SSL routines::bad record type error:0A000139:SSL routines::record layer failure) The SSL_R_RECORD_LAYER_FAILURE ("record layer failure") error, however, might be generated for a number of reasons, including memory allocation failures, and therefore it is wrong to log all such errors at the "info" level. Instead, we now try to fallback to ERR_peek_error() in order to decide which logging level should be used. With these changes, no additional errors were observed with OpenSSL 3.0.20, OpenSSL 3.2.6, OpenSSL 3.3.7, OpenSSL 3.4.5, OpenSSL 3.5.6, OpenSSL 3.6.2, and OpenSSL 4.0.0. diffstat: src/event/ngx_event_openssl.c | 25 +++++++++++++++++++++++++ 1 files changed, 25 insertions(+), 0 deletions(-) diffs (56 lines): diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -4048,6 +4048,22 @@ ngx_ssl_connection_error(ngx_connection_ n = ERR_GET_REASON(ERR_peek_last_error()); +#ifdef SSL_R_RECORD_LAYER_FAILURE + + if (n == SSL_R_RECORD_LAYER_FAILURE + && ERR_GET_LIB(ERR_peek_error()) == ERR_LIB_SSL) + { + /* + * OpenSSL 3.2.0+ returns SSL_R_RECORD_LAYER_FAILURE in the + * error queue after many different errors, including memory + * allocation failures, so fallback to ERR_peek_error() + */ + + n = ERR_GET_REASON(ERR_peek_error()); + } + +#endif + /* handshake failures */ if (n == SSL_R_BAD_CHANGE_CIPHER_SPEC /* 103 */ #ifdef SSL_R_NO_SUITABLE_KEY_SHARE @@ -4101,6 +4117,9 @@ ngx_ssl_connection_error(ngx_connection_ #ifdef SSL_R_NO_CIPHERS_PASSED || n == SSL_R_NO_CIPHERS_PASSED /* 182 */ #endif +#ifdef SSL_R_NOT_ON_RECORD_BOUNDARY + || n == SSL_R_NOT_ON_RECORD_BOUNDARY /* 182 */ +#endif || n == SSL_R_NO_CIPHERS_SPECIFIED /* 183 */ #ifdef SSL_R_BAD_CIPHER || n == SSL_R_BAD_CIPHER /* 186 */ @@ -4146,6 +4165,9 @@ ngx_ssl_connection_error(ngx_connection_ || n == SSL_R_MISSING_KEY_SHARE /* 258 */ #endif || n == SSL_R_UNSUPPORTED_PROTOCOL /* 258 */ +#ifdef SSL_R_INVALID_CCS_MESSAGE + || n == SSL_R_INVALID_CCS_MESSAGE /* 260 */ +#endif #ifdef SSL_R_NO_SHARED_GROUP || n == SSL_R_NO_SHARED_GROUP /* 266 */ #endif @@ -4184,6 +4206,9 @@ ngx_ssl_connection_error(ngx_connection_ #ifdef SSL_R_UNSAFE_LEGACY_RENEGOTIATION_DISABLED || n == SSL_R_UNSAFE_LEGACY_RENEGOTIATION_DISABLED /* 338 */ #endif +#ifdef SSL_R_REQUIRED_COMPRESSION_ALGORITHM_MISSING + || n == SSL_R_REQUIRED_COMPRESSION_ALGORITHM_MISSING /* 342 */ +#endif #ifdef SSL_R_SCSV_RECEIVED_WHEN_RENEGOTIATING || n == SSL_R_SCSV_RECEIVED_WHEN_RENEGOTIATING /* 345 */ #endif From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] SSL: logging levels of errors observed with BoringSSL. Message-ID: details: http://freenginx.org/hg/nginx/rev/95a766c8af7f branches: stable-1.30 changeset: 9538:95a766c8af7f user: Maxim Dounin date: Thu Apr 30 07:25:52 2026 +0300 description: SSL: logging levels of errors observed with BoringSSL. The following client-related errors were observed during tlsfuzzer runs with BoringSSL: SSL_do_handshake() failed (SSL: error:100000f3:SSL routines:OPENSSL_internal:WRONG_CURVE) SSL_do_handshake() failed (SSL: error:10000083:SSL routines:OPENSSL_internal:CLIENTHELLO_PARSE_FAILED) Accordingly, the SSL_R_WRONG_CURVE and SSL_R_CLIENTHELLO_PARSE_FAILED errors are now logged at the "info" level. diffstat: src/event/ngx_event_openssl.c | 6 ++++++ 1 files changed, 6 insertions(+), 0 deletions(-) diffs (23 lines): diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -4090,6 +4090,9 @@ ngx_ssl_connection_error(ngx_connection_ || n == SSL_R_BAD_KEY_UPDATE /* 122 */ #endif || n == SSL_R_BLOCK_CIPHER_PAD_IS_WRONG /* 129 */ +#ifdef SSL_R_CLIENTHELLO_PARSE_FAILED + || n == SSL_R_CLIENTHELLO_PARSE_FAILED /* 131 */ +#endif || n == SSL_R_CCS_RECEIVED_EARLY /* 133 */ #ifdef SSL_R_DECODE_ERROR || n == SSL_R_DECODE_ERROR /* 137 */ @@ -4151,6 +4154,9 @@ ngx_ssl_connection_error(ngx_connection_ #ifdef SSL_R_NO_APPLICATION_PROTOCOL || n == SSL_R_NO_APPLICATION_PROTOCOL /* 235 */ #endif +#ifdef SSL_R_WRONG_CURVE + || n == SSL_R_WRONG_CURVE /* 243 */ +#endif || n == SSL_R_UNEXPECTED_MESSAGE /* 244 */ || n == SSL_R_UNEXPECTED_RECORD /* 245 */ || n == SSL_R_UNKNOWN_ALERT_TYPE /* 246 */ From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] Updated OpenSSL used for win32 builds. Message-ID: details: http://freenginx.org/hg/nginx/rev/d3eec5fc152e branches: stable-1.30 changeset: 9539:d3eec5fc152e user: Maxim Dounin date: Tue May 05 14:05:36 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.0.20 +OPENSSL = openssl-3.5.6 ZLIB = zlib-1.3.2 PCRE = pcre2-10.47 From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] Rewrite: fixed incorrect escaping and possible segfault. Message-ID: details: http://freenginx.org/hg/nginx/rev/d8a7c50badce branches: stable-1.30 changeset: 9540:d8a7c50badce user: Maxim Dounin date: Tue May 19 01:56:22 2026 +0300 description: Rewrite: fixed incorrect escaping and possible segfault. Similarly to 4617:972642646f06, the following code resulted in incorrect escaping of the $temp variable and possible segfault: location / { rewrite ^(.*) /uri?args; set $temp $1; return 200 "$temp"; } If there were arguments in rewrite's replacement string, the is_args flag was set and never cleared. This resulted in escaping being incorrectly applied to positional captures evaluated after the rewrite in the same script engine, notably in "set", "if", and "rewrite" directives. Additionally, in "set", "if", and "rewrite" with duplicate captures or additional variables, the buffer was allocated without escaping expected, so this also resulted in a buffer overrun and a possible segfault (CVE-2026-42945). The fix is to clear the is_args flag after rewrite evaluation in ngx_http_script_regex_end_code(), similarly to how we clear e->quote and e->args. Additionally, to ensure that buffer allocation stays correct even if the is_args flag is somehow set, e->is_args is now propagated to length calculations in ngx_http_script_regex_start_code() and in ngx_http_script_complex_value_code(). See also: https://github.com/nginx/nginx/commit/2046b45aa0c6e712c216b9075886f3f26e9b4ca9 diffstat: src/http/ngx_http_script.c | 3 +++ 1 files changed, 3 insertions(+), 0 deletions(-) diffs (27 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 @@ -1161,6 +1161,7 @@ ngx_http_script_regex_start_code(ngx_htt le.line = e->line; le.request = r; le.quote = code->redirect; + le.is_args = e->is_args; len = 0; @@ -1203,6 +1204,7 @@ ngx_http_script_regex_end_code(ngx_http_ r = e->request; e->quote = 0; + e->is_args = 0; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http script regex end"); @@ -1769,6 +1771,7 @@ ngx_http_script_complex_value_code(ngx_h le.line = e->line; le.request = e->request; le.quote = e->quote; + le.is_args = e->is_args; for (len = 0; *(uintptr_t *) le.ip; len += lcode(&le)) { lcode = *(ngx_http_script_len_code_pt *) le.ip; From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] Charset: fixed handling of incomplete UTF-8 characters. Message-ID: details: http://freenginx.org/hg/nginx/rev/fbdfd610a7bb branches: stable-1.30 changeset: 9541:fbdfd610a7bb user: Maxim Dounin date: Tue May 19 01:56:27 2026 +0300 description: Charset: fixed handling of incomplete UTF-8 characters. Previously, if a UTF-8 character was split across multiple buffers, the second and subsequent buffers were handled incorrectly: ngx_decode_utf8() was called with the wrong size if there are fewer bytes in the buffer than ctx->saved can hold, the following code called ngx_memcpy() with the wrong size, potentially reading past the supplied buffer, and ctx->saved_len was set to an incorrect value, which could later result in reading before the buffer (CVE-2026-42934). The fix is to adjust the code to make sure that the "i" value properly represents the number of bytes available in ctx->saved in all cases, remove the unneeded ngx_memcpy() call, and set ctx->saved_len to the correct value. See also: https://github.com/nginx/nginx/commit/696a7f1b9198d576e6a59c1655b746fbf06561cf diffstat: src/http/modules/ngx_http_charset_filter_module.c | 7 +++---- 1 files changed, 3 insertions(+), 4 deletions(-) diffs (24 lines): diff --git a/src/http/modules/ngx_http_charset_filter_module.c b/src/http/modules/ngx_http_charset_filter_module.c --- a/src/http/modules/ngx_http_charset_filter_module.c +++ b/src/http/modules/ngx_http_charset_filter_module.c @@ -788,8 +788,8 @@ ngx_http_charset_recode_from_utf8(ngx_po p = src; - for (i = ctx->saved_len; i < NGX_UTF_LEN; i++) { - ctx->saved[i] = *p++; + for (i = ctx->saved_len; i < NGX_UTF_LEN; /* void */) { + ctx->saved[i++] = *p++; if (p == buf->last) { break; @@ -826,8 +826,7 @@ ngx_http_charset_recode_from_utf8(ngx_po b->sync = 1; b->shadow = buf; - ngx_memcpy(&ctx->saved[ctx->saved_len], src, i); - ctx->saved_len += i; + ctx->saved_len = i; return out; } From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] Updated copyright year for upcoming patch imports. Message-ID: details: http://freenginx.org/hg/nginx/rev/792196f176f4 branches: stable-1.30 changeset: 9542:792196f176f4 user: Maxim Dounin date: Tue May 19 01:56:34 2026 +0300 description: Updated copyright year for upcoming patch imports. diffstat: docs/text/LICENSE | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (11 lines): diff --git a/docs/text/LICENSE b/docs/text/LICENSE --- a/docs/text/LICENSE +++ b/docs/text/LICENSE @@ -1,6 +1,6 @@ /* * Copyright (C) 2002-2021 Igor Sysoev - * Copyright (C) 2011-2024 Nginx, Inc. + * Copyright (C) 2011-2026 Nginx, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] OCSP: resolve cleanup on connection close. Message-ID: details: http://freenginx.org/hg/nginx/rev/cf866da03b6d branches: stable-1.30 changeset: 9543:cf866da03b6d user: Roman Arutyunyan date: Tue Apr 21 14:51:41 2026 +0400 description: OCSP: resolve cleanup on connection close. Previously, when a client SSL connection was terminated (typically due to a timeout) while resolving an OCSP responder, the OCSP context was freed, but the resolve context was not. This resulted in use-after-free on resolve completion. Reported by Leo Lin. Obtained from: https://github.com/nginx/nginx/commit/71841dcedfdf46048ef5e25413fdf97a66957913 diffstat: src/event/ngx_event_openssl_stapling.c | 11 +++++++++++ 1 files changed, 11 insertions(+), 0 deletions(-) diffs (50 lines): diff --git a/src/event/ngx_event_openssl_stapling.c b/src/event/ngx_event_openssl_stapling.c --- a/src/event/ngx_event_openssl_stapling.c +++ b/src/event/ngx_event_openssl_stapling.c @@ -111,6 +111,7 @@ struct ngx_ssl_ocsp_ctx_s { ngx_resolver_t *resolver; ngx_msec_t resolver_timeout; + ngx_resolver_ctx_t *resolve; ngx_msec_t timeout; @@ -1303,6 +1304,10 @@ ngx_ssl_ocsp_done(ngx_ssl_ocsp_ctx_t *ct ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ctx->log, 0, "ssl ocsp done"); + if (ctx->resolve) { + ngx_resolve_name_done(ctx->resolve); + } + if (ctx->peer.connection) { ngx_close_connection(ctx->peer.connection); } @@ -1395,7 +1400,10 @@ ngx_ssl_ocsp_request(ngx_ssl_ocsp_ctx_t resolve->data = ctx; resolve->timeout = ctx->resolver_timeout; + ctx->resolve = resolve; + if (ngx_resolve_name(resolve) != NGX_OK) { + ctx->resolve = NULL; ngx_ssl_ocsp_error(ctx); return; } @@ -1484,6 +1492,7 @@ ngx_ssl_ocsp_resolve_handler(ngx_resolve } ngx_resolve_name_done(resolve); + ctx->resolve = NULL; ngx_ssl_ocsp_connect(ctx); return; @@ -1491,6 +1500,8 @@ ngx_ssl_ocsp_resolve_handler(ngx_resolve failed: ngx_resolve_name_done(resolve); + ctx->resolve = NULL; + ngx_ssl_ocsp_error(ctx); } From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] QUIC: avoid assigning unvalidated address to new streams. Message-ID: details: http://freenginx.org/hg/nginx/rev/212e717f6878 branches: stable-1.30 changeset: 9544:212e717f6878 user: Roman Arutyunyan date: Thu Apr 30 17:15:53 2026 +0400 description: QUIC: avoid assigning unvalidated address to new streams. Previously, when a client migrated to a new address, new QUIC streams received this address before validation. This allowed an attacker to create QUIC streams with a spoofed address. Reported by Rodrigo Laneth. Obtained from: https://github.com/nginx/nginx/commit/f37ec3e5d4f527e52ed5b25951ad8aa7d1ff6266 diffstat: src/event/quic/ngx_event_quic_migration.c | 9 +++++---- 1 files changed, 5 insertions(+), 4 deletions(-) diffs (34 lines): diff --git a/src/event/quic/ngx_event_quic_migration.c b/src/event/quic/ngx_event_quic_migration.c --- a/src/event/quic/ngx_event_quic_migration.c +++ b/src/event/quic/ngx_event_quic_migration.c @@ -193,6 +193,8 @@ valid: path->validated = 1; + ngx_quic_set_connection_path(c, path); + if (path->mtu_unvalidated) { path->mtu_unvalidated = 0; return ngx_quic_validate_path(c, path); @@ -510,9 +512,10 @@ ngx_quic_handle_migration(ngx_connection qc->path = next; qc->path->tag = NGX_QUIC_PATH_ACTIVE; - ngx_quic_set_connection_path(c, next); + if (next->validated) { + ngx_quic_set_connection_path(c, next); - if (!next->validated && next->state != NGX_QUIC_PATH_VALIDATING) { + } else if (next->state != NGX_QUIC_PATH_VALIDATING) { if (ngx_quic_validate_path(c, next) != NGX_OK) { return NGX_ERROR; } @@ -806,8 +809,6 @@ ngx_quic_expire_path_validation(ngx_conn qc->path = bkp; qc->path->tag = NGX_QUIC_PATH_ACTIVE; - ngx_quic_set_connection_path(c, qc->path); - ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic path seq:%uL addr:%V is restored from backup", qc->path->seqnum, &qc->path->addr_text); From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] Upstream: reset parsing state after invalid status line. Message-ID: details: http://freenginx.org/hg/nginx/rev/fb85e780ef42 branches: stable-1.30 changeset: 9545:fb85e780ef42 user: Sergey Kandaurov date: Wed Apr 29 21:56:51 2026 +0400 description: Upstream: reset parsing state after invalid status line. Previously, it was possible to start parsing headers with a wrong parsing state after status line was not recognized, as a fallback used in the scgi and uwsgi modules. Reported by Leo Lin. Obtained from: https://github.com/nginx/nginx/commit/f79c286b34d3b708bd4856a56e27529e11386098 diffstat: src/http/modules/ngx_http_scgi_module.c | 1 + src/http/modules/ngx_http_uwsgi_module.c | 1 + 2 files changed, 2 insertions(+), 0 deletions(-) diffs (22 lines): diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c --- a/src/http/modules/ngx_http_scgi_module.c +++ b/src/http/modules/ngx_http_scgi_module.c @@ -1016,6 +1016,7 @@ ngx_http_scgi_process_status_line(ngx_ht if (rc == NGX_ERROR) { u->process_header = ngx_http_scgi_process_header; + r->state = 0; return ngx_http_scgi_process_header(r); } diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -1245,6 +1245,7 @@ ngx_http_uwsgi_process_status_line(ngx_h if (rc == NGX_ERROR) { u->process_header = ngx_http_uwsgi_process_header; + r->state = 0; return ngx_http_uwsgi_process_header(r); } From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] Upstream: fixed parsing of split status lines. Message-ID: details: http://freenginx.org/hg/nginx/rev/373222b8c7b3 branches: stable-1.30 changeset: 9546:373222b8c7b3 user: Sergey Kandaurov date: Wed Apr 29 23:02:20 2026 +0400 description: Upstream: fixed parsing of split status lines. If the first response line was split across reads and it didn't appear a status line, the portion already processed was lost. The change introduces a new field for proper backtracking on status line fallback. Obtained from (with minor changes): https://github.com/nginx/nginx/commit/5f86648ef8c969e98aa2f7b938472296b12055be diffstat: src/http/modules/ngx_http_proxy_module.c | 2 ++ src/http/modules/ngx_http_scgi_module.c | 1 + src/http/modules/ngx_http_uwsgi_module.c | 1 + src/http/ngx_http.h | 1 + src/http/ngx_http_parse.c | 2 ++ 5 files changed, 7 insertions(+), 0 deletions(-) diffs (57 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 @@ -1862,6 +1862,8 @@ ngx_http_proxy_process_status_line(ngx_h u->headers_in.status_n = 200; u->headers_in.connection_close = 1; + u->buffer.pos = ctx->status.line_start; + return NGX_OK; } diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c --- a/src/http/modules/ngx_http_scgi_module.c +++ b/src/http/modules/ngx_http_scgi_module.c @@ -1016,6 +1016,7 @@ ngx_http_scgi_process_status_line(ngx_ht if (rc == NGX_ERROR) { u->process_header = ngx_http_scgi_process_header; + u->buffer.pos = status->line_start; r->state = 0; return ngx_http_scgi_process_header(r); } diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -1245,6 +1245,7 @@ ngx_http_uwsgi_process_status_line(ngx_h if (rc == NGX_ERROR) { u->process_header = ngx_http_uwsgi_process_header; + u->buffer.pos = status->line_start; r->state = 0; return ngx_http_uwsgi_process_header(r); } diff --git a/src/http/ngx_http.h b/src/http/ngx_http.h --- a/src/http/ngx_http.h +++ b/src/http/ngx_http.h @@ -72,6 +72,7 @@ struct ngx_http_chunked_s { typedef struct { ngx_uint_t http_version; ngx_uint_t code; + u_char *line_start; u_char *start; u_char *end; } ngx_http_status_t; diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -1663,6 +1663,8 @@ ngx_http_parse_status_line(ngx_http_requ /* "HTTP/" */ case sw_start: + status->line_start = p; + switch (ch) { case 'H': state = sw_H; From mdounin at mdounin.ru Tue Jun 2 16:11:50 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:50 +0300 Subject: [nginx] Rewrite: removed optimized length calculations. Message-ID: details: http://freenginx.org/hg/nginx/rev/dbf061c1f4f2 branches: stable-1.30 changeset: 9547:dbf061c1f4f2 user: Maxim Dounin date: Tue May 26 03:18:34 2026 +0300 description: Rewrite: removed optimized length calculations. Previously, ngx_http_script_regex_start_code() tried to use optimized buffer length calculations when possible, without length codes evaluation. Originally the code assumed that allocating space for all the captures and escaping required for full URI is enough if variables are not used. In 641:5e8fb59c18c1 (0.3.42) this was further refined to require that duplicate captures are not used. However, length calculations can be wrong when nested captures are used, since the same URI character can appear in multiple captures and might require escaping in all of them, leading to a buffer overflow, for example (CVE-2026-9256): rewrite ^/((.*)) /?c=$1&d=$2; While it is possible to preserve and further refine optimized length calculations, it is believed that a better solution would be to remove them altogether, and this is what this change does. See also: https://github.com/nginx/nginx/commit/ca4f92a27464ae6c2082245e4f67048c633aa032 diffstat: src/http/modules/ngx_http_rewrite_module.c | 4 -- src/http/ngx_http_script.c | 47 +++++++---------------------- src/http/ngx_http_script.h | 2 - 3 files changed, 12 insertions(+), 41 deletions(-) diffs (108 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 @@ -405,10 +405,6 @@ ngx_http_rewrite(ngx_conf_t *cf, ngx_com regex->size = sc.size; regex->args = sc.args; - if (sc.variables == 0 && !sc.dup_capture) { - regex->lengths = NULL; - } - regex_end = ngx_http_script_add_code(lcf->codes, sizeof(ngx_http_script_regex_end_code_t), ®ex); 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 @@ -483,12 +483,6 @@ ngx_http_script_compile(ngx_http_script_ n = sc->source->data[i] - '0'; - if (sc->captures_mask & ((ngx_uint_t) 1 << n)) { - sc->dup_capture = 1; - } - - sc->captures_mask |= (ngx_uint_t) 1 << n; - if (ngx_http_script_add_capture_code(sc, n) != NGX_OK) { return NGX_ERROR; } @@ -1039,7 +1033,6 @@ ngx_http_script_regex_start_code(ngx_htt { size_t len; ngx_int_t rc; - ngx_uint_t n; ngx_http_request_t *r; ngx_http_script_engine_t le; ngx_http_script_len_code_pt lcode; @@ -1140,38 +1133,22 @@ ngx_http_script_regex_start_code(ngx_htt } } - if (code->lengths == NULL) { - e->buf.len = code->size; + ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); - if (code->uri) { - if (r->ncaptures && (r->quoted_uri || r->plus_in_uri)) { - e->buf.len += 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len, - NGX_ESCAPE_ARGS); - } - } - - for (n = 2; n < r->ncaptures; n += 2) { - e->buf.len += r->captures[n + 1] - r->captures[n]; - } + le.ip = code->lengths->elts; + le.line = e->line; + le.request = r; + le.quote = code->redirect; + le.is_args = e->is_args; - } else { - ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); - - le.ip = code->lengths->elts; - le.line = e->line; - le.request = r; - le.quote = code->redirect; - le.is_args = e->is_args; + len = 0; - len = 0; + while (*(uintptr_t *) le.ip) { + lcode = *(ngx_http_script_len_code_pt *) le.ip; + len += lcode(&le); + } - while (*(uintptr_t *) le.ip) { - lcode = *(ngx_http_script_len_code_pt *) le.ip; - len += lcode(&le); - } - - e->buf.len = len; - } + e->buf.len = len; if (code->add_args && r->args.len) { e->buf.len += r->args.len + 1; 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 @@ -46,7 +46,6 @@ typedef struct { ngx_uint_t variables; ngx_uint_t ncaptures; - ngx_uint_t captures_mask; ngx_uint_t size; void *main; @@ -58,7 +57,6 @@ typedef struct { unsigned conf_prefix:1; unsigned root_prefix:1; - unsigned dup_capture:1; unsigned args:1; } ngx_http_script_compile_t; From mdounin at mdounin.ru Tue Jun 2 16:11:51 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:51 +0300 Subject: [nginx] freenginx-1.30.1-RELEASE Message-ID: details: http://freenginx.org/hg/nginx/rev/3945b69e1c42 branches: stable-1.30 changeset: 9548:3945b69e1c42 user: Maxim Dounin date: Tue Jun 02 18:25:15 2026 +0300 description: freenginx-1.30.1-RELEASE diffstat: docs/xml/nginx/changes.xml | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 90 insertions(+), 0 deletions(-) diffs (100 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,96 @@
+ + + + +??????? ???????????? ?????? SSL +"invalid ccs message", "not on record boundary", +"required compression algorithm missing" +? ????????? ?????? "record layer failure" +??????? ? ?????? crit ?? info. + + +the logging level of the +"invalid ccs message", "not on record boundary", +"required compression algorithm missing", +and some "record layer failure" SSL errors +has been lowered from "crit" to "info". + + + + + +? ??????? ???????? ??? ????????? segmentation fault, +???? ????????? rewrite ?????????????? ??? ????????? ?????????? ??????? +? ????? ??? ??????????? ?????? ????????? ?????? ngx_http_rewrite_module. + + +a segmentation fault might occur in a worker process +if the "rewrite" directive was used to change request arguments +and other directives of the ngx_http_rewrite_module were executed afterwards. + + + + + +? ??????? ???????? ??? ????????? segmentation fault, +???? ? ????????? rewrite ?????????????? ????????? ?????????. + + +a segmentation fault might occur in a worker process +if nested captures were used in the "rewrite" directive. + + + + + +? ??????? ???????? ??? ????????? 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. + + + + + +? ??????? ???????? ??? ????????? segmentation fault, +???? ?????????????? ????????? ssl_ocsp. + + +a segmentation fault might occur in a worker process +if the "ssl_ocsp" directive was used. + + + + + +? ??????? ???????? ??? ????????? segmentation fault, +???? ?????????????? ????????? scgi_pass ??? uwsgi_pass. + + +a segmentation fault might occur in a worker process +if the "scgi_pass" or "uwsgi_pass" directives were used. + + + + + +? HTTP/3. + + +in HTTP/3. + + + + + + From mdounin at mdounin.ru Tue Jun 2 16:11:51 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:11:51 +0300 Subject: [nginx] release-1.30.1 tag Message-ID: details: http://freenginx.org/hg/nginx/rev/9726d5b82240 branches: stable-1.30 changeset: 9549:9726d5b82240 user: Maxim Dounin date: Tue Jun 02 18:25:16 2026 +0300 description: release-1.30.1 tag diffstat: .hgtags | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diffs (8 lines): diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -494,3 +494,4 @@ 4f4280557d20bc46ebbdc240ffd365f5ca6ce939 e4207f631186855d37ac286799c8cd4c9477d166 release-1.29.6 cac0fa5721386abbec57dcc2bb317f2531456e19 release-1.29.7 f126ef64a014db167927741688c74ffdf1b0fc26 release-1.30.0 +3945b69e1c4218e98a85a7a0e152e77c9d4fc83b release-1.30.1 From mdounin at mdounin.ru Tue Jun 2 16:12:24 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:12:24 +0300 Subject: [nginx-tests] Tests: adjusted TODOs. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/6e9d814c49fd branches: changeset: 2063:6e9d814c49fd user: Maxim Dounin date: Tue Jun 02 05:54:47 2026 +0300 description: Tests: adjusted TODOs. diffstat: charset_perl.t | 4 ++-- proxy_status.t | 3 ++- rewrite.t | 8 ++++---- rewrite_set.t | 2 +- scgi_status.t | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diffs (82 lines): diff --git a/charset_perl.t b/charset_perl.t --- a/charset_perl.t +++ b/charset_perl.t @@ -89,9 +89,9 @@ EOF TODO: { local $TODO = 'not yet' - unless $t->has_version('1.31.1'); + unless $t->has_version('1.31.1') or $t->has_version('1.30.1'); todo_skip 'might coredump', 1 - unless $t->has_version('1.31.1') + unless $t->has_version('1.31.1') or $t->has_version('1.30.1') or $ENV{TEST_NGINX_UNSAFE}; like(http_get('/multi'), qr/^CCTT𐀀𐀀$/m, 'multiple buffers'); diff --git a/proxy_status.t b/proxy_status.t --- a/proxy_status.t +++ b/proxy_status.t @@ -80,7 +80,8 @@ like(http_get('/allow09/http09'), qr!^HT 'http 0.9 allowed'); TODO: { -local $TODO = 'not yet' unless $t->has_version('1.31.1'); +local $TODO = 'not yet' unless $t->has_version('1.31.1') + or $t->has_version('1.30.1'); like(http_get('/allow09/split'), qr!^HTTP/1.1 200 OK.*HTTP/0.9!s, 'http 0.9 split between packets'); diff --git a/rewrite.t b/rewrite.t --- a/rewrite.t +++ b/rewrite.t @@ -250,9 +250,9 @@ like(http_get('/capture_dup/%25?a=b'), TODO: { local $TODO = 'not yet' - unless $t->has_version('1.31.1'); + unless $t->has_version('1.31.1') or $t->has_version('1.30.1'); todo_skip 'might coredump', 1 - unless $t->has_version('1.31.1') + unless $t->has_version('1.31.1') or $t->has_version('1.30.1') or $ENV{TEST_NGINX_UNSAFE}; like(http_get('/capture_another/%25?a=b'), @@ -263,9 +263,9 @@ like(http_get('/capture_another/%25?a=b' TODO: { local $TODO = 'not yet' - unless $t->has_version('1.31.2'); + unless $t->has_version('1.31.2') or $t->has_version('1.30.1'); todo_skip 'might coredump', 1 - unless $t->has_version('1.31.2') + unless $t->has_version('1.31.2') or $t->has_version('1.30.1') or $ENV{TEST_NGINX_UNSAFE}; like(http_get('/capture_nested/%25?a=b'), diff --git a/rewrite_set.t b/rewrite_set.t --- a/rewrite_set.t +++ b/rewrite_set.t @@ -135,7 +135,7 @@ TODO: { local $TODO = 'not yet' unless $t->has_version('1.31.1'); todo_skip 'might coredump', 1 - unless $t->has_version('1.31.1') + unless $t->has_version('1.31.1') or $t->has_version('1.30.1') or $ENV{TEST_NGINX_UNSAFE}; # set after a rewrite with arguments, diff --git a/scgi_status.t b/scgi_status.t --- a/scgi_status.t +++ b/scgi_status.t @@ -91,9 +91,9 @@ like(http_get('/001'), qr!^HTTP/1.1 502 TODO: { local $TODO = 'not yet' - unless $t->has_version('1.31.1'); + unless $t->has_version('1.31.1') or $t->has_version('1.30.1'); todo_skip 'leaves coredump', 1 - unless $t->has_version('1.31.1') + unless $t->has_version('1.31.1') or $t->has_version('1.30.1') or $ENV{TEST_NGINX_UNSAFE}; like(http_get('/split'), qr!^HTTP/1.1 200 .*HTTP-Header: foo!s, From mdounin at mdounin.ru Tue Jun 2 16:17:13 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 02 Jun 2026 19:17:13 +0300 Subject: [nginx-site] freenginx-1.30.1 Message-ID: details: http://freenginx.org/hg/nginx-site/rev/d3091f56593c branches: changeset: 3136:d3091f56593c user: Maxim Dounin date: Tue Jun 02 19:13:39 2026 +0300 description: freenginx-1.30.1 diffstat: text/en/CHANGES-1.30 | 26 ++++++++++++++++++++++++++ text/ru/CHANGES.ru-1.30 | 28 ++++++++++++++++++++++++++++ xml/index.xml | 8 ++++++++ xml/versions.xml | 1 + 4 files changed, 63 insertions(+), 0 deletions(-) diffs (99 lines): diff --git a/text/en/CHANGES-1.30 b/text/en/CHANGES-1.30 --- a/text/en/CHANGES-1.30 +++ b/text/en/CHANGES-1.30 @@ -1,4 +1,30 @@ +Changes with freenginx 1.30.1 02 Jun 2026 + + *) Change: the logging level of the "invalid ccs message", "not on + record boundary", "required compression algorithm missing", and some + "record layer failure" SSL errors has been lowered from "crit" to + "info". + + *) Bugfix: a segmentation fault might occur in a worker process if the + "rewrite" directive was used to change request arguments and other + directives of the ngx_http_rewrite_module were executed afterwards. + + *) Bugfix: a segmentation fault might occur in a worker process if + nested captures were used in the "rewrite" directive. + + *) 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: a segmentation fault might occur in a worker process if the + "ssl_ocsp" directive was used. + + *) Bugfix: a segmentation fault might occur in a worker process if the + "scgi_pass" or "uwsgi_pass" directives were used. + + *) Bugfix: in HTTP/3. + + Changes with freenginx 1.30.0 14 Apr 2026 *) 1.30.x stable branch. diff --git a/text/ru/CHANGES.ru-1.30 b/text/ru/CHANGES.ru-1.30 --- a/text/ru/CHANGES.ru-1.30 +++ b/text/ru/CHANGES.ru-1.30 @@ -1,4 +1,32 @@ +????????? ? freenginx 1.30.1 02.06.2026 + + *) ?????????: ??????? ???????????? ?????? SSL "invalid ccs message", + "not on record boundary", "required compression algorithm missing" ? + ????????? ?????? "record layer failure" ??????? ? ?????? crit ?? + info. + + *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, + ???? ????????? rewrite ?????????????? ??? ????????? ?????????? + ??????? ? ????? ??? ??????????? ?????? ????????? ?????? + ngx_http_rewrite_module. + + *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, + ???? ? ????????? rewrite ?????????????? ????????? ?????????. + + *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, + ???? ?????? ngx_http_charset_module ????????????? ??? ??????????????? + ??????? ?? UTF-8. + + *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, + ???? ?????????????? ????????? ssl_ocsp. + + *) ???????????: ? ??????? ???????? ??? ????????? segmentation fault, + ???? ?????????????? ????????? scgi_pass ??? uwsgi_pass. + + *) ???????????: ? HTTP/3. + + ????????? ? freenginx 1.30.0 14.04.2026 *) ?????????? ????? 1.30.x. diff --git a/xml/index.xml b/xml/index.xml --- a/xml/index.xml +++ b/xml/index.xml @@ -8,6 +8,14 @@ + + +freenginx-1.30.1 +stable version has been released, +with bug fixes merged from the 1.31.x mainline branch. + + + freenginx-1.31.2 diff --git a/xml/versions.xml b/xml/versions.xml --- a/xml/versions.xml +++ b/xml/versions.xml @@ -18,6 +18,7 @@ + From mdounin at mdounin.ru Wed Jun 3 15:46:49 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:46:49 +0300 Subject: [PATCH 1 of 6] Version bump Message-ID: # HG changeset patch # User Maxim Dounin # Date 1780500563 -10800 # Wed Jun 03 18:29:23 2026 +0300 # Node ID bb7f4e268d757385eca7908c14e0c5134cf8b14d # Parent f7971cdfa50f0db26a9cb132e0cac6854dfcb4f6 Version bump. 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 1031002 -#define NGINX_VERSION "1.31.2" +#define nginx_version 1031003 +#define NGINX_VERSION "1.31.3" #define freenginx 1 From mdounin at mdounin.ru Wed Jun 3 15:46:50 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:46:50 +0300 Subject: [PATCH 2 of 6] Auth request: avoid assigning v->data In-Reply-To: References: Message-ID: <3ea72346b4743b474771.1780501610@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780500565 -10800 # Wed Jun 03 18:29:25 2026 +0300 # Node ID 3ea72346b4743b474771bb60a435223f67b83dec # Parent bb7f4e268d757385eca7908c14e0c5134cf8b14d Auth request: avoid assigning v->data. The ngx_http_auth_request_variable() handler does not use v->data, and there is no need to assign anything to it. diff --git a/src/http/modules/ngx_http_auth_request_module.c b/src/http/modules/ngx_http_auth_request_module.c --- a/src/http/modules/ngx_http_auth_request_module.c +++ b/src/http/modules/ngx_http_auth_request_module.c @@ -431,7 +431,6 @@ ngx_http_auth_request_set(ngx_conf_t *cf if (v->get_handler == NULL) { v->get_handler = ngx_http_auth_request_variable; - v->data = (uintptr_t) av; } av->set_handler = v->set_handler; From mdounin at mdounin.ru Wed Jun 3 15:46:51 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:46:51 +0300 Subject: [PATCH 3 of 6] Auth request: fixed prefix variables handling In-Reply-To: References: Message-ID: <9fc7ceee87c6fb6f96be.1780501611@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780500567 -10800 # Wed Jun 03 18:29:27 2026 +0300 # Node ID 9fc7ceee87c6fb6f96be73450d715200e3174a11 # Parent 3ea72346b4743b474771bb60a435223f67b83dec Auth request: fixed prefix variables handling. Previously, auth_request_set did not use the NGX_HTTP_VAR_WEAK flag, and using auth_request_set with a prefix variable anywhere in the configuration resulted in an empty value of the variable in other contexts. For example, in the following configuration the $http_foo variable was always empty in requests to "/", even if the "Foo" header was present in requests: location / { return 200 $http_foo; } location /protected/ { auth_request /auth; auth_request_set $http_foo "set"; ... } The fix is to use the NGX_HTTP_VAR_WEAK flag, much like the "set" directive does. diff --git a/src/http/modules/ngx_http_auth_request_module.c b/src/http/modules/ngx_http_auth_request_module.c --- a/src/http/modules/ngx_http_auth_request_module.c +++ b/src/http/modules/ngx_http_auth_request_module.c @@ -419,7 +419,8 @@ ngx_http_auth_request_set(ngx_conf_t *cf return NGX_CONF_ERROR; } - v = ngx_http_add_variable(cf, &value[1], NGX_HTTP_VAR_CHANGEABLE); + v = ngx_http_add_variable(cf, &value[1], + NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_WEAK); if (v == NULL) { return NGX_CONF_ERROR; } From mdounin at mdounin.ru Wed Jun 3 15:46:52 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:46:52 +0300 Subject: [PATCH 4 of 6] Fixed named captures to don't redefine get handler In-Reply-To: References: Message-ID: <54ab4e1a0d1f0d51e98b.1780501612@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780500569 -10800 # Wed Jun 03 18:29:29 2026 +0300 # Node ID 54ab4e1a0d1f0d51e98b1b3b998fee4641d00492 # Parent 9fc7ceee87c6fb6f96be73450d715200e3174a11 Fixed named captures to don't redefine get handler. Previously, named captures unconditionally changed v->get_handler to ngx_http_variable_not_found(), so merely defining a regular expression with a named captures was enough to hide an existing variable. For example, in the following configuration the $foo variable was always empty in requests to "/": map $uri $foo { default "a value from map"; } location / { return 200 "value: $foo"; } location ~ /re/(?.*) { return 200 "value: $foo"; } Similarly, for variables within an existing prefix, such as for $http_foo, using a regular expression with a named captures was enough to hide the original prefix variable. For example, in the following configuration the $http_foo variable was always empty in requests to "/", even if the "Foo" header was present in requests: location / { return 200 "value: $http_foo"; } location ~ /re/(?.*) { return 200 "value: $http_foo"; } The fix is to avoid changing v->get_handler if it's already present, and to use the NGX_HTTP_VAR_WEAK flag to avoid redefining get handler for prefix variables, similarly to what the "set" directive does. diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -2589,7 +2589,8 @@ ngx_http_regex_compile(ngx_conf_t *cf, n name.data = &p[2]; name.len = ngx_strlen(name.data); - v = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE); + v = ngx_http_add_variable(cf, &name, + NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_WEAK); if (v == NULL) { return NULL; } @@ -2599,7 +2600,9 @@ ngx_http_regex_compile(ngx_conf_t *cf, n return NULL; } - v->get_handler = ngx_http_variable_not_found; + if (v->get_handler == NULL) { + v->get_handler = ngx_http_variable_not_found; + } p += size; } diff --git a/src/stream/ngx_stream_variables.c b/src/stream/ngx_stream_variables.c --- a/src/stream/ngx_stream_variables.c +++ b/src/stream/ngx_stream_variables.c @@ -1072,7 +1072,8 @@ ngx_stream_regex_compile(ngx_conf_t *cf, name.data = &p[2]; name.len = ngx_strlen(name.data); - v = ngx_stream_add_variable(cf, &name, NGX_STREAM_VAR_CHANGEABLE); + v = ngx_stream_add_variable(cf, &name, + NGX_STREAM_VAR_CHANGEABLE|NGX_STREAM_VAR_WEAK); if (v == NULL) { return NULL; } @@ -1082,7 +1083,9 @@ ngx_stream_regex_compile(ngx_conf_t *cf, return NULL; } - v->get_handler = ngx_stream_variable_not_found; + if (v->get_handler == NULL) { + v->get_handler = ngx_stream_variable_not_found; + } p += size; } From mdounin at mdounin.ru Wed Jun 3 15:46:53 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:46:53 +0300 Subject: [PATCH 5 of 6] Added helper function to set indexed variables In-Reply-To: References: Message-ID: <8baa7a14576b2c9d7b7e.1780501613@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780500572 -10800 # Wed Jun 03 18:29:32 2026 +0300 # Node ID 8baa7a14576b2c9d7b7ef8c649544a5e54b0c12b # Parent 54ab4e1a0d1f0d51e98b1b3b998fee4641d00492 Added helper function to set indexed variables. The ngx_http_set_indexed_variable() function correctly stores the variable in r->variables[], and calls v->set_handler() if it is set (which is now propagated to cmcf->variables). This reduces amount of code needed in various places which set variables (currently "set", "auth_request_set", and named captures), and also unifies the behaviour across these places. Notable changes include: - named captures now properly handle variables with v->set_handler(), notably $limit_rate and $args, much like "set" currently does; - the "auth_request_set" directive now does not update the value in the r->variables cache if there is a set handler, similarly to how "set" does, so invalid $limit_rate values are not returned by subsequent variable lookups. Similar changes were made in the stream module. diff --git a/src/http/modules/ngx_http_auth_request_module.c b/src/http/modules/ngx_http_auth_request_module.c --- a/src/http/modules/ngx_http_auth_request_module.c +++ b/src/http/modules/ngx_http_auth_request_module.c @@ -26,7 +26,6 @@ typedef struct { typedef struct { ngx_int_t index; ngx_http_complex_value_t value; - ngx_http_set_variable_pt set_handler; } ngx_http_auth_request_variable_t; @@ -239,10 +238,8 @@ ngx_http_auth_request_set_variables(ngx_ ngx_http_auth_request_conf_t *arcf, ngx_http_auth_request_ctx_t *ctx) { ngx_str_t val; - ngx_http_variable_t *v; - ngx_http_variable_value_t *vv; + ngx_http_variable_value_t vv; ngx_http_auth_request_variable_t *av, *last; - ngx_http_core_main_conf_t *cmcf; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "auth request set variables"); @@ -251,9 +248,6 @@ ngx_http_auth_request_set_variables(ngx_ return NGX_OK; } - cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); - v = cmcf->variables.elts; - av = arcf->vars->elts; last = av + arcf->vars->nelts; @@ -263,27 +257,16 @@ ngx_http_auth_request_set_variables(ngx_ * internal redirects */ - vv = &r->variables[av->index]; - if (ngx_http_complex_value(ctx->subrequest, &av->value, &val) != NGX_OK) { return NGX_ERROR; } - vv->valid = 1; - vv->not_found = 0; - vv->data = val.data; - vv->len = val.len; + vv.data = val.data; + vv.len = val.len; - if (av->set_handler) { - /* - * set_handler only available in cmcf->variables_keys, so we store - * it explicitly - */ - - av->set_handler(r, vv, v[av->index].data); - } + ngx_http_set_indexed_variable(r, av->index, &vv); av++; } @@ -434,8 +417,6 @@ ngx_http_auth_request_set(ngx_conf_t *cf v->get_handler = ngx_http_auth_request_variable; } - av->set_handler = v->set_handler; - ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; 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 @@ -893,11 +893,10 @@ ngx_http_rewrite_set(ngx_conf_t *cf, ngx { ngx_http_rewrite_loc_conf_t *lcf = conf; - ngx_int_t index; - ngx_str_t *value; - ngx_http_variable_t *v; - ngx_http_script_var_code_t *vcode; - ngx_http_script_var_handler_code_t *vhcode; + ngx_int_t index; + ngx_str_t *value; + ngx_http_variable_t *v; + ngx_http_script_var_code_t *vcode; value = cf->args->elts; @@ -930,20 +929,6 @@ ngx_http_rewrite_set(ngx_conf_t *cf, ngx return NGX_CONF_ERROR; } - if (v->set_handler) { - vhcode = ngx_http_script_start_code(cf->pool, &lcf->codes, - sizeof(ngx_http_script_var_handler_code_t)); - if (vhcode == NULL) { - return NGX_CONF_ERROR; - } - - vhcode->code = ngx_http_script_var_set_handler_code; - vhcode->handler = v->set_handler; - vhcode->data = v->data; - - return NGX_CONF_OK; - } - vcode = ngx_http_script_start_code(cf->pool, &lcf->codes, sizeof(ngx_http_script_var_code_t)); if (vcode == NULL) { 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 @@ -1781,43 +1781,7 @@ ngx_http_script_set_var_code(ngx_http_sc e->sp--; - r->variables[code->index].len = e->sp->len; - r->variables[code->index].valid = 1; - r->variables[code->index].no_cacheable = 0; - r->variables[code->index].not_found = 0; - r->variables[code->index].data = e->sp->data; - -#if (NGX_DEBUG) - { - ngx_http_variable_t *v; - ngx_http_core_main_conf_t *cmcf; - - cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); - - v = cmcf->variables.elts; - - ngx_log_debug1(NGX_LOG_DEBUG_HTTP, e->request->connection->log, 0, - "http script set $%V", &v[code->index].name); - } -#endif -} - - -void -ngx_http_script_var_set_handler_code(ngx_http_script_engine_t *e) -{ - ngx_http_script_var_handler_code_t *code; - - ngx_log_debug0(NGX_LOG_DEBUG_HTTP, e->request->connection->log, 0, - "http script set var handler"); - - code = (ngx_http_script_var_handler_code_t *) e->ip; - - e->ip += sizeof(ngx_http_script_var_handler_code_t); - - e->sp--; - - code->handler(e->request, e->sp, code->data); + ngx_http_set_indexed_variable(r, code->index, e->sp); } 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 @@ -102,13 +102,6 @@ typedef struct { typedef struct { ngx_http_script_code_pt code; - ngx_http_set_variable_pt handler; - uintptr_t data; -} ngx_http_script_var_handler_code_t; - - -typedef struct { - ngx_http_script_code_pt code; uintptr_t n; } ngx_http_script_copy_capture_code_t; @@ -259,7 +252,6 @@ void ngx_http_script_file_code(ngx_http_ void ngx_http_script_complex_value_code(ngx_http_script_engine_t *e); void ngx_http_script_value_code(ngx_http_script_engine_t *e); void ngx_http_script_set_var_code(ngx_http_script_engine_t *e); -void ngx_http_script_var_set_handler_code(ngx_http_script_engine_t *e); void ngx_http_script_var_code(ngx_http_script_engine_t *e); void ngx_http_script_nop_code(ngx_http_script_engine_t *e); diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -745,6 +745,43 @@ ngx_http_get_variable(ngx_http_request_t } +void +ngx_http_set_indexed_variable(ngx_http_request_t *r, ngx_uint_t index, + ngx_http_variable_value_t *value) +{ + ngx_http_variable_t *v; + ngx_http_variable_value_t *vv; + ngx_http_core_main_conf_t *cmcf; + + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); + + if (cmcf->variables.nelts <= index) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "unknown variable index: %ui", index); + return; + } + + v = cmcf->variables.elts; + + if (v[index].set_handler) { + v[index].set_handler(r, value, v[index].data); + + } else { + vv = &r->variables[index]; + + vv->len = value->len; + vv->valid = 1; + vv->no_cacheable = 0; + vv->not_found = 0; + vv->escape = 0; + vv->data = value->data; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http set $%V to \"%v\"", &v[index].name, value); +} + + static ngx_int_t ngx_http_variable_request(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) @@ -2616,7 +2653,7 @@ ngx_http_regex_exec(ngx_http_request_t * { ngx_int_t rc, index; ngx_uint_t i, n, len; - ngx_http_variable_value_t *vv; + ngx_http_variable_value_t vv; ngx_http_core_main_conf_t *cmcf; cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); @@ -2654,24 +2691,11 @@ ngx_http_regex_exec(ngx_http_request_t * n = re->variables[i].capture; index = re->variables[i].index; - vv = &r->variables[index]; - - vv->len = r->captures[n + 1] - r->captures[n]; - vv->valid = 1; - vv->no_cacheable = 0; - vv->not_found = 0; - vv->data = &s->data[r->captures[n]]; - -#if (NGX_DEBUG) - { - ngx_http_variable_t *v; - - v = cmcf->variables.elts; - - ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "http regex set $%V to \"%v\"", &v[index].name, vv); - } -#endif + + vv.len = r->captures[n + 1] - r->captures[n]; + vv.data = &s->data[r->captures[n]]; + + ngx_http_set_indexed_variable(r, index, &vv); } r->ncaptures = rc * 2; @@ -2754,6 +2778,7 @@ ngx_http_variables_init_vars(ngx_conf_t && ngx_strncmp(v[i].name.data, key[n].key.data, v[i].name.len) == 0) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = av->data; @@ -2786,6 +2811,7 @@ ngx_http_variables_init_vars(ngx_conf_t } if (av) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = (uintptr_t) &v[i].name; v[i].flags = av->flags; diff --git a/src/http/ngx_http_variables.h b/src/http/ngx_http_variables.h --- a/src/http/ngx_http_variables.h +++ b/src/http/ngx_http_variables.h @@ -57,6 +57,9 @@ ngx_http_variable_value_t *ngx_http_get_ ngx_http_variable_value_t *ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key); +void ngx_http_set_indexed_variable(ngx_http_request_t *r, ngx_uint_t index, + ngx_http_variable_value_t *value); + ngx_int_t ngx_http_variable_unknown_header(ngx_http_request_t *r, ngx_http_variable_value_t *v, ngx_str_t *var, ngx_list_part_t *part, size_t prefix); diff --git a/src/stream/ngx_stream_set_module.c b/src/stream/ngx_stream_set_module.c --- a/src/stream/ngx_stream_set_module.c +++ b/src/stream/ngx_stream_set_module.c @@ -12,8 +12,6 @@ typedef struct { ngx_int_t index; - ngx_stream_set_variable_pt set_handler; - uintptr_t data; ngx_stream_complex_value_t value; } ngx_stream_set_cmd_t; @@ -90,18 +88,10 @@ ngx_stream_set_handler(ngx_stream_sessio return NGX_ERROR; } - if (cmds[i].set_handler != NULL) { - vv.len = str.len; - vv.data = str.data; - cmds[i].set_handler(s, &vv, cmds[i].data); + vv.len = str.len; + vv.data = str.data; - } else { - s->variables[cmds[i].index].len = str.len; - s->variables[cmds[i].index].valid = 1; - s->variables[cmds[i].index].no_cacheable = 0; - s->variables[cmds[i].index].not_found = 0; - s->variables[cmds[i].index].data = str.data; - } + ngx_stream_set_indexed_variable(s, cmds[i].index, &vv); } return NGX_DECLINED; @@ -209,8 +199,6 @@ ngx_stream_set(ngx_conf_t *cf, ngx_comma } set_cmd->index = index; - set_cmd->set_handler = v->set_handler; - set_cmd->data = v->data; ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t)); diff --git a/src/stream/ngx_stream_variables.c b/src/stream/ngx_stream_variables.c --- a/src/stream/ngx_stream_variables.c +++ b/src/stream/ngx_stream_variables.c @@ -477,6 +477,43 @@ ngx_stream_get_variable(ngx_stream_sessi } +void +ngx_stream_set_indexed_variable(ngx_stream_session_t *s, ngx_uint_t index, + ngx_stream_variable_value_t *value) +{ + ngx_stream_variable_t *v; + ngx_stream_variable_value_t *vv; + ngx_stream_core_main_conf_t *cmcf; + + cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); + + if (cmcf->variables.nelts <= index) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, 0, + "unknown variable index: %ui", index); + return; + } + + v = cmcf->variables.elts; + + if (v[index].set_handler) { + v[index].set_handler(s, value, v[index].data); + + } else { + vv = &s->variables[index]; + + vv->len = value->len; + vv->valid = 1; + vv->no_cacheable = 0; + vv->not_found = 0; + vv->escape = 0; + vv->data = value->data; + } + + ngx_log_debug2(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, + "stream set $%V to \"%v\"", &v[index].name, value); +} + + static ngx_int_t ngx_stream_variable_binary_remote_addr(ngx_stream_session_t *s, ngx_stream_variable_value_t *v, uintptr_t data) @@ -1100,7 +1137,7 @@ ngx_stream_regex_exec(ngx_stream_session { ngx_int_t rc, index; ngx_uint_t i, n, len; - ngx_stream_variable_value_t *vv; + ngx_stream_variable_value_t vv; ngx_stream_core_main_conf_t *cmcf; cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); @@ -1136,24 +1173,11 @@ ngx_stream_regex_exec(ngx_stream_session n = re->variables[i].capture; index = re->variables[i].index; - vv = &s->variables[index]; - - vv->len = s->captures[n + 1] - s->captures[n]; - vv->valid = 1; - vv->no_cacheable = 0; - vv->not_found = 0; - vv->data = &str->data[s->captures[n]]; -#if (NGX_DEBUG) - { - ngx_stream_variable_t *v; + vv.len = s->captures[n + 1] - s->captures[n]; + vv.data = &str->data[s->captures[n]]; - v = cmcf->variables.elts; - - ngx_log_debug2(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, - "stream regex set $%V to \"%v\"", &v[index].name, vv); - } -#endif + ngx_stream_set_indexed_variable(s, index, &vv); } s->ncaptures = rc * 2; @@ -1236,6 +1260,7 @@ ngx_stream_variables_init_vars(ngx_conf_ && ngx_strncmp(v[i].name.data, key[n].key.data, v[i].name.len) == 0) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = av->data; @@ -1268,6 +1293,7 @@ ngx_stream_variables_init_vars(ngx_conf_ } if (av) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = (uintptr_t) &v[i].name; v[i].flags = av->flags; diff --git a/src/stream/ngx_stream_variables.h b/src/stream/ngx_stream_variables.h --- a/src/stream/ngx_stream_variables.h +++ b/src/stream/ngx_stream_variables.h @@ -57,6 +57,9 @@ ngx_stream_variable_value_t *ngx_stream_ ngx_stream_variable_value_t *ngx_stream_get_variable(ngx_stream_session_t *s, ngx_str_t *name, ngx_uint_t key); +void ngx_stream_set_indexed_variable(ngx_stream_session_t *s, ngx_uint_t index, + ngx_stream_variable_value_t *value); + #if (NGX_PCRE) From mdounin at mdounin.ru Wed Jun 3 15:46:54 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:46:54 +0300 Subject: [PATCH 6 of 6] Disabled redefinition of variables with v->set_handler In-Reply-To: References: Message-ID: <2425956404250ba696b0.1780501614@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780500654 -10800 # Wed Jun 03 18:30:54 2026 +0300 # Node ID 2425956404250ba696b066cab707b797813cef65 # Parent 8baa7a14576b2c9d7b7ef8c649544a5e54b0c12b Disabled redefinition of variables with v->set_handler. Changeable variables can be updated by the "set" directive, and to support this ngx_http_add_variable() does not generate an error when a changeable variable is added again. This, however, makes it possible to redefine variables to completely different use, such as from a builtin variable to a map{}. And this in turn might cause unexpected issues, such as when a variable with set_handler is redefined to a map, which changes v->get_handler and v->data, but not v->set_handler. Most notably, this caused segmentation faults when trying to define a map to the $limit_rate variable before 7504:c19ca381b2e6 (https://trac.nginx.org/nginx/ticket/1238). With this change, redefinition of variables with v->set_handler is not allowed unless the NGX_HTTP_VAR_WEAK flag is also provided, which is currently used by "set" and other similar directives. Note that this change changes the meaning of the NGX_HTTP_VAR_WEAK flag. Previously, it was used solely to preserve get handler from prefix variables. Now the meaning of the flag becomes somewhat broader, and now it means that we are defining a fallback variable to be used in "set" or similar directives, and the caller is not going to redefine the variable if it's already present. Similar changes were made in the stream module. diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -448,6 +448,12 @@ ngx_http_add_variable(ngx_conf_t *cf, ng return NULL; } + if (!(flags & NGX_HTTP_VAR_WEAK) && v->set_handler) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the duplicate \"%V\" variable", name); + return NULL; + } + if (!(flags & NGX_HTTP_VAR_WEAK)) { v->flags &= ~NGX_HTTP_VAR_WEAK; } diff --git a/src/stream/ngx_stream_variables.c b/src/stream/ngx_stream_variables.c --- a/src/stream/ngx_stream_variables.c +++ b/src/stream/ngx_stream_variables.c @@ -177,6 +177,12 @@ ngx_stream_add_variable(ngx_conf_t *cf, return NULL; } + if (!(flags & NGX_STREAM_VAR_WEAK) && v->set_handler) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the duplicate \"%V\" variable", name); + return NULL; + } + if (!(flags & NGX_STREAM_VAR_WEAK)) { v->flags &= ~NGX_STREAM_VAR_WEAK; } From mdounin at mdounin.ru Wed Jun 3 15:48:22 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:48:22 +0300 Subject: [PATCH] Tests: fixed gzip and pcre prerequisites in stream tests Message-ID: # HG changeset patch # User Maxim Dounin # Date 1780501688 -10800 # Wed Jun 03 18:48:08 2026 +0300 # Node ID fdb816d8d2ab966cc183e5f97252bd1aea3e6369 # Parent 6e9d814c49fd49893267c316ad191db2c1528c33 Tests: fixed gzip and pcre prerequisites in stream tests. Simply requiring gzip or rewrite is not enough, since these won't be actually present if http is not compiled in at all. The fix is to require http explicitly. diff --git a/stream_access_log.t b/stream_access_log.t --- a/stream_access_log.t +++ b/stream_access_log.t @@ -25,7 +25,7 @@ use Test::Nginx::Stream qw/ stream /; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/stream stream_map gzip/); +my $t = Test::Nginx->new()->has(qw/stream stream_map http gzip/); $t->write_file_expand('nginx.conf', <<'EOF'); diff --git a/stream_map.t b/stream_map.t --- a/stream_map.t +++ b/stream_map.t @@ -23,8 +23,8 @@ use Test::Nginx::Stream qw/ stream /; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/stream stream_return stream_map rewrite/) - ->has(qw/http rewrite/); +my $t = Test::Nginx->new() + ->has(qw/stream stream_return stream_map http rewrite/); $t->write_file_expand('nginx.conf', <<'EOF'); diff --git a/stream_proxy_protocol2_tlv.t b/stream_proxy_protocol2_tlv.t --- a/stream_proxy_protocol2_tlv.t +++ b/stream_proxy_protocol2_tlv.t @@ -25,7 +25,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/stream stream_return stream_map rewrite/)->plan(14) + ->has(qw/stream stream_return stream_map http rewrite/)->plan(14) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% From mdounin at mdounin.ru Wed Jun 3 15:49:16 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:49:16 +0300 Subject: [PATCH] Tests: renamed $proxy_port in mail_oauth.t to avoid conflicts Message-ID: <26a9c0862ac79ff6efd7.1780501756@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780501739 -10800 # Wed Jun 03 18:48:59 2026 +0300 # Node ID 26a9c0862ac79ff6efd78999e4773ef4059cc600 # Parent fdb816d8d2ab966cc183e5f97252bd1aea3e6369 Tests: renamed $proxy_port in mail_oauth.t to avoid conflicts. The $proxy_port variable is a standard variable provided by the proxy module, and redefining it to something different might not be a good idea. diff --git a/mail_oauth.t b/mail_oauth.t --- a/mail_oauth.t +++ b/mail_oauth.t @@ -67,7 +67,7 @@ http { map_hash_bucket_size 64; - map $http_auth_protocol $proxy_port { + map $http_auth_protocol $port { imap %%PORT_8144%%; pop3 %%PORT_8111%%; smtp %%PORT_8026%%; @@ -94,7 +94,7 @@ http { location = /mail/auth { add_header Auth-Status $reply; add_header Auth-Server 127.0.0.1; - add_header Auth-Port $proxy_port; + add_header Auth-Port $port; add_header Auth-Pass $passw; add_header Auth-Wait 1; add_header Auth-Error-SASL $sasl; From mdounin at mdounin.ru Wed Jun 3 15:50:35 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:50:35 +0300 Subject: [PATCH] Tests: added auth_request_set tests with prefix variables In-Reply-To: <9fc7ceee87c6fb6f96be.1780501611@vm-bsd.mdounin.ru> References: <9fc7ceee87c6fb6f96be.1780501611@vm-bsd.mdounin.ru> Message-ID: # HG changeset patch # User Maxim Dounin # Date 1780501789 -10800 # Wed Jun 03 18:49:49 2026 +0300 # Node ID fd936830a8ea642b9b9a32c771fc23f71d9f8149 # Parent 26a9c0862ac79ff6efd78999e4773ef4059cc600 Tests: added auth_request_set tests with prefix variables. diff --git a/auth_request_set.t b/auth_request_set.t --- a/auth_request_set.t +++ b/auth_request_set.t @@ -22,7 +22,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http rewrite proxy auth_request/) - ->plan(6); + ->plan(8); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -88,6 +88,17 @@ http { return 204; } + location = /prefix { + add_header X-Foo $arg_foo; + return 204; + } + + location = /prefix_set { + auth_request /auth; + auth_request_set $arg_foo "set"; + add_header X-Foo $arg_foo; + } + location = /auth { proxy_pass http://127.0.0.1:8081; } @@ -121,6 +132,7 @@ EOF $t->write_file('t1.html', ''); $t->write_file('t4-fallback.html', ''); +$t->write_file('prefix_set', ''); $t->run(); ############################################################################### @@ -141,4 +153,16 @@ like(http_get('/t5.html'), qr/X-Args: se like(http_get('/t6.html'), qr/X-Unset-Username: xx/, 'unset variable'); +# variables introduced by auth_request_set did not use the NGX_HTTP_VAR_WEAK +# flag, thus overriding corresponding prefix variables in unrelated contexts + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/prefix?foo=arg'), qr/X-Foo: arg/, 'prefix variable'); + +} + +like(http_get('/prefix_set?foo=arg'), qr/X-Foo: set/, 'set prefix variable'); + ############################################################################### From mdounin at mdounin.ru Wed Jun 3 15:53:39 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:53:39 +0300 Subject: [PATCH] Tests: added tests for named captures In-Reply-To: <54ab4e1a0d1f0d51e98b.1780501612@vm-bsd.mdounin.ru> References: <54ab4e1a0d1f0d51e98b.1780501612@vm-bsd.mdounin.ru> Message-ID: <57f8b3db13319b6f1977.1780502019@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780501916 -10800 # Wed Jun 03 18:51:56 2026 +0300 # Node ID 57f8b3db13319b6f1977e825a72689e76e7d7e2e # Parent fd936830a8ea642b9b9a32c771fc23f71d9f8149 Tests: added tests for named captures. diff --git a/rewrite_named_captures.t b/rewrite_named_captures.t new file mode 100644 --- /dev/null +++ b/rewrite_named_captures.t @@ -0,0 +1,113 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for rewrite module, named captures. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(6) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $uri $map { + default map; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /map { + return 200 "value: $map\n"; + } + + location /map/rewrite { + rewrite ^(?.*) /; + return 200 "value: $map\n"; + } + + location /later { + return 200 "value: $late\n"; + } + + location /later/rewrite { + rewrite ^(?.*) /; + return 200 "value: $late\n"; + } + + location /prefix { + return 200 "value: $arg_foo\n"; + } + + location /prefix/rewrite { + rewrite ^(?.*) /; + return 200 "value: $arg_foo\n"; + } + } + + map $uri $late { + default map; + } +} + +EOF + +$t->run(); + +############################################################################### + +# named captures used to override the v->get_handler if it was previously +# set by a changeable variable, such as provided by map, and did not use +# the NGX_HTTP_VAR_WEAK flag, thus overriding corresponding prefix variables + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/map'), qr!value: map!, 'value from map'); + +} + +like(http_get('/map/rewrite'), qr!value: /map/rewrite!, + 'value from capture overrides map'); + +like(http_get('/later'), qr!value: map!, 'value from later map'); +like(http_get('/later/rewrite'), qr!value: /later/rewrite!, + 'value from capture overrides later map'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/prefix?foo=arg'), qr!value: arg!, 'value from arg'); + +} + +like(http_get('/prefix/rewrite?foo=arg'), qr!value: /prefix/rewrite!, + 'value from capture overrides arg'); + +############################################################################### diff --git a/stream_named_captures.t b/stream_named_captures.t new file mode 100644 --- /dev/null +++ b/stream_named_captures.t @@ -0,0 +1,135 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for stream named captures. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/stream stream_return stream_map http rewrite/) + ->plan(6); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + map 0 $map { + default map; + } + + map 0 $capture { + ~(?.*) $map; + } + + map 0 $capture_late { + ~(?.*) $late; + } + + map 0 $late { + default map; + } + + map 0 $capture_prefix { + ~(?.*) $proxy_protocol_tlv_0x01; + } + + server { + listen 127.0.0.1:8090; + return $map; + } + + server { + listen 127.0.0.1:8091; + return $capture; + } + + server { + listen 127.0.0.1:8092; + return $late; + } + + server { + listen 127.0.0.1:8093; + return $capture_late; + } + + server { + listen 127.0.0.1:8094 proxy_protocol; + return $proxy_protocol_tlv_0x01; + } + + server { + listen 127.0.0.1:8095 proxy_protocol; + return $capture_prefix; + } +} + +EOF + +$t->run(); + +############################################################################### + +# named captures used to override the v->get_handler if it was previously +# set by a changeable variable, such as provided by map, and did not use +# the NGX_HTTP_VAR_WEAK flag, thus overriding corresponding prefix variables + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +is(stream('127.0.0.1:' . port(8090))->read(), 'map', 'value from map'); + +} + +is(stream('127.0.0.1:' . port(8091))->read(), '0', + 'value from capture overrides map'); + +is(stream('127.0.0.1:' . port(8092))->read(), 'map', 'value from later map'); +is(stream('127.0.0.1:' . port(8093))->read(), '0', + 'value from capture overrides later map'); + +my $pp = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A" + . "\x21" + . "\x11" + . "\x00\x12" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x01\x00\x03tlv"; + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +is(stream('127.0.0.1:' . port(8094))->io($pp), 'tlv', 'value from tlv'); + +} + +is(stream('127.0.0.1:' . port(8095))->io($pp), '0', + 'value from capture overrides tlv'); + +############################################################################### From mdounin at mdounin.ru Wed Jun 3 15:55:49 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 03 Jun 2026 18:55:49 +0300 Subject: [PATCH] Tests: added tests for named captures with v->set_handler In-Reply-To: <8baa7a14576b2c9d7b7e.1780501613@vm-bsd.mdounin.ru> References: <8baa7a14576b2c9d7b7e.1780501613@vm-bsd.mdounin.ru> Message-ID: <00e132c32aae2f22581b.1780502149@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780502078 -10800 # Wed Jun 03 18:54:38 2026 +0300 # Node ID 00e132c32aae2f22581bdfffb5db846f99d13c71 # Parent 57f8b3db13319b6f1977e825a72689e76e7d7e2e Tests: added tests for named captures with v->set_handler. diff --git a/rewrite_named_captures.t b/rewrite_named_captures.t --- a/rewrite_named_captures.t +++ b/rewrite_named_captures.t @@ -22,7 +22,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(6) +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(7) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -69,6 +69,17 @@ http { rewrite ^(?.*) /; return 200 "value: $arg_foo\n"; } + + location /set_args { + if ($arg_foo ~ "(?.*)") {} + proxy_pass http://127.0.0.1:8081; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + return 200 "value: $args"; } map $uri $late { @@ -110,4 +121,15 @@ like(http_get('/prefix?foo=arg'), qr!val like(http_get('/prefix/rewrite?foo=arg'), qr!value: /prefix/rewrite!, 'value from capture overrides arg'); +# when a named capture changes a variable with v->set_handler, notably $args +# or $limit_rate, the handler should be called + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/set_args?foo=arg'), qr!value: arg!, + 'variable with set_handler'); + +} + ############################################################################### From mdounin at mdounin.ru Mon Jun 8 14:56:05 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:56:05 +0300 Subject: [nginx] Version bump. Message-ID: details: http://freenginx.org/hg/nginx/rev/91212eff66c8 branches: changeset: 9550:91212eff66c8 user: Maxim Dounin date: Mon Jun 08 17:53:26 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 1031002 -#define NGINX_VERSION "1.31.2" +#define nginx_version 1031003 +#define NGINX_VERSION "1.31.3" #define freenginx 1 From mdounin at mdounin.ru Mon Jun 8 14:56:05 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:56:05 +0300 Subject: [nginx] Auth request: avoid assigning v->data. Message-ID: details: http://freenginx.org/hg/nginx/rev/7be51873bdb1 branches: changeset: 9551:7be51873bdb1 user: Maxim Dounin date: Mon Jun 08 17:53:31 2026 +0300 description: Auth request: avoid assigning v->data. The ngx_http_auth_request_variable() handler does not use v->data, and there is no need to assign anything to it. diffstat: src/http/modules/ngx_http_auth_request_module.c | 1 - 1 files changed, 0 insertions(+), 1 deletions(-) diffs (11 lines): diff --git a/src/http/modules/ngx_http_auth_request_module.c b/src/http/modules/ngx_http_auth_request_module.c --- a/src/http/modules/ngx_http_auth_request_module.c +++ b/src/http/modules/ngx_http_auth_request_module.c @@ -431,7 +431,6 @@ ngx_http_auth_request_set(ngx_conf_t *cf if (v->get_handler == NULL) { v->get_handler = ngx_http_auth_request_variable; - v->data = (uintptr_t) av; } av->set_handler = v->set_handler; From mdounin at mdounin.ru Mon Jun 8 14:56:05 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:56:05 +0300 Subject: [nginx] Auth request: fixed prefix variables handling. Message-ID: details: http://freenginx.org/hg/nginx/rev/bb638269f1e3 branches: changeset: 9552:bb638269f1e3 user: Maxim Dounin date: Mon Jun 08 17:53:35 2026 +0300 description: Auth request: fixed prefix variables handling. Previously, auth_request_set did not use the NGX_HTTP_VAR_WEAK flag, and using auth_request_set with a prefix variable anywhere in the configuration resulted in an empty value of the variable in other contexts. For example, in the following configuration the $http_foo variable was always empty in requests to "/", even if the "Foo" header was present in requests: location / { return 200 $http_foo; } location /protected/ { auth_request /auth; auth_request_set $http_foo "set"; ... } The fix is to use the NGX_HTTP_VAR_WEAK flag, much like the "set" directive does. diffstat: src/http/modules/ngx_http_auth_request_module.c | 3 ++- 1 files changed, 2 insertions(+), 1 deletions(-) diffs (13 lines): diff --git a/src/http/modules/ngx_http_auth_request_module.c b/src/http/modules/ngx_http_auth_request_module.c --- a/src/http/modules/ngx_http_auth_request_module.c +++ b/src/http/modules/ngx_http_auth_request_module.c @@ -419,7 +419,8 @@ ngx_http_auth_request_set(ngx_conf_t *cf return NGX_CONF_ERROR; } - v = ngx_http_add_variable(cf, &value[1], NGX_HTTP_VAR_CHANGEABLE); + v = ngx_http_add_variable(cf, &value[1], + NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_WEAK); if (v == NULL) { return NGX_CONF_ERROR; } From mdounin at mdounin.ru Mon Jun 8 14:56:05 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:56:05 +0300 Subject: [nginx] Fixed named captures to don't redefine get handler. Message-ID: details: http://freenginx.org/hg/nginx/rev/623210753d47 branches: changeset: 9553:623210753d47 user: Maxim Dounin date: Mon Jun 08 17:53:50 2026 +0300 description: Fixed named captures to don't redefine get handler. Previously, named captures unconditionally changed v->get_handler to ngx_http_variable_not_found(), so merely defining a regular expression with a named captures was enough to hide an existing variable. For example, in the following configuration the $foo variable was always empty in requests to "/": map $uri $foo { default "a value from map"; } location / { return 200 "value: $foo"; } location ~ /re/(?.*) { return 200 "value: $foo"; } Similarly, for variables within an existing prefix, such as for $http_foo, using a regular expression with a named captures was enough to hide the original prefix variable. For example, in the following configuration the $http_foo variable was always empty in requests to "/", even if the "Foo" header was present in requests: location / { return 200 "value: $http_foo"; } location ~ /re/(?.*) { return 200 "value: $http_foo"; } The fix is to avoid changing v->get_handler if it's already present, and to use the NGX_HTTP_VAR_WEAK flag to avoid redefining get handler for prefix variables, similarly to what the "set" directive does. diffstat: src/http/ngx_http_variables.c | 7 +++++-- src/stream/ngx_stream_variables.c | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diffs (48 lines): diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -2589,7 +2589,8 @@ ngx_http_regex_compile(ngx_conf_t *cf, n name.data = &p[2]; name.len = ngx_strlen(name.data); - v = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE); + v = ngx_http_add_variable(cf, &name, + NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_WEAK); if (v == NULL) { return NULL; } @@ -2599,7 +2600,9 @@ ngx_http_regex_compile(ngx_conf_t *cf, n return NULL; } - v->get_handler = ngx_http_variable_not_found; + if (v->get_handler == NULL) { + v->get_handler = ngx_http_variable_not_found; + } p += size; } diff --git a/src/stream/ngx_stream_variables.c b/src/stream/ngx_stream_variables.c --- a/src/stream/ngx_stream_variables.c +++ b/src/stream/ngx_stream_variables.c @@ -1072,7 +1072,8 @@ ngx_stream_regex_compile(ngx_conf_t *cf, name.data = &p[2]; name.len = ngx_strlen(name.data); - v = ngx_stream_add_variable(cf, &name, NGX_STREAM_VAR_CHANGEABLE); + v = ngx_stream_add_variable(cf, &name, + NGX_STREAM_VAR_CHANGEABLE|NGX_STREAM_VAR_WEAK); if (v == NULL) { return NULL; } @@ -1082,7 +1083,9 @@ ngx_stream_regex_compile(ngx_conf_t *cf, return NULL; } - v->get_handler = ngx_stream_variable_not_found; + if (v->get_handler == NULL) { + v->get_handler = ngx_stream_variable_not_found; + } p += size; } From mdounin at mdounin.ru Mon Jun 8 14:56:05 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:56:05 +0300 Subject: [nginx] Added helper function to set indexed variables. Message-ID: details: http://freenginx.org/hg/nginx/rev/7d3b1378f3aa branches: changeset: 9554:7d3b1378f3aa user: Maxim Dounin date: Mon Jun 08 17:54:00 2026 +0300 description: Added helper function to set indexed variables. The ngx_http_set_indexed_variable() function correctly stores the variable in r->variables[], and calls v->set_handler() if it is set (which is now propagated to cmcf->variables). This reduces amount of code needed in various places which set variables (currently "set", "auth_request_set", and named captures), and also unifies the behaviour across these places. Notable changes include: - named captures now properly handle variables with v->set_handler(), notably $limit_rate and $args, much like "set" currently does; - the "auth_request_set" directive now does not update the value in the r->variables cache if there is a set handler, similarly to how "set" does, so invalid $limit_rate values are not returned by subsequent variable lookups. Similar changes were made in the stream module. diffstat: src/http/modules/ngx_http_auth_request_module.c | 27 +-------- src/http/modules/ngx_http_rewrite_module.c | 23 +------- src/http/ngx_http_script.c | 38 +-------------- src/http/ngx_http_script.h | 8 --- src/http/ngx_http_variables.c | 64 +++++++++++++++++------- src/http/ngx_http_variables.h | 3 + src/stream/ngx_stream_set_module.c | 18 +----- src/stream/ngx_stream_variables.c | 60 ++++++++++++++++------ src/stream/ngx_stream_variables.h | 3 + 9 files changed, 106 insertions(+), 138 deletions(-) diffs (457 lines): diff --git a/src/http/modules/ngx_http_auth_request_module.c b/src/http/modules/ngx_http_auth_request_module.c --- a/src/http/modules/ngx_http_auth_request_module.c +++ b/src/http/modules/ngx_http_auth_request_module.c @@ -26,7 +26,6 @@ typedef struct { typedef struct { ngx_int_t index; ngx_http_complex_value_t value; - ngx_http_set_variable_pt set_handler; } ngx_http_auth_request_variable_t; @@ -239,10 +238,8 @@ ngx_http_auth_request_set_variables(ngx_ ngx_http_auth_request_conf_t *arcf, ngx_http_auth_request_ctx_t *ctx) { ngx_str_t val; - ngx_http_variable_t *v; - ngx_http_variable_value_t *vv; + ngx_http_variable_value_t vv; ngx_http_auth_request_variable_t *av, *last; - ngx_http_core_main_conf_t *cmcf; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "auth request set variables"); @@ -251,9 +248,6 @@ ngx_http_auth_request_set_variables(ngx_ return NGX_OK; } - cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); - v = cmcf->variables.elts; - av = arcf->vars->elts; last = av + arcf->vars->nelts; @@ -263,27 +257,16 @@ ngx_http_auth_request_set_variables(ngx_ * internal redirects */ - vv = &r->variables[av->index]; - if (ngx_http_complex_value(ctx->subrequest, &av->value, &val) != NGX_OK) { return NGX_ERROR; } - vv->valid = 1; - vv->not_found = 0; - vv->data = val.data; - vv->len = val.len; + vv.data = val.data; + vv.len = val.len; - if (av->set_handler) { - /* - * set_handler only available in cmcf->variables_keys, so we store - * it explicitly - */ - - av->set_handler(r, vv, v[av->index].data); - } + ngx_http_set_indexed_variable(r, av->index, &vv); av++; } @@ -434,8 +417,6 @@ ngx_http_auth_request_set(ngx_conf_t *cf v->get_handler = ngx_http_auth_request_variable; } - av->set_handler = v->set_handler; - ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; 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 @@ -893,11 +893,10 @@ ngx_http_rewrite_set(ngx_conf_t *cf, ngx { ngx_http_rewrite_loc_conf_t *lcf = conf; - ngx_int_t index; - ngx_str_t *value; - ngx_http_variable_t *v; - ngx_http_script_var_code_t *vcode; - ngx_http_script_var_handler_code_t *vhcode; + ngx_int_t index; + ngx_str_t *value; + ngx_http_variable_t *v; + ngx_http_script_var_code_t *vcode; value = cf->args->elts; @@ -930,20 +929,6 @@ ngx_http_rewrite_set(ngx_conf_t *cf, ngx return NGX_CONF_ERROR; } - if (v->set_handler) { - vhcode = ngx_http_script_start_code(cf->pool, &lcf->codes, - sizeof(ngx_http_script_var_handler_code_t)); - if (vhcode == NULL) { - return NGX_CONF_ERROR; - } - - vhcode->code = ngx_http_script_var_set_handler_code; - vhcode->handler = v->set_handler; - vhcode->data = v->data; - - return NGX_CONF_OK; - } - vcode = ngx_http_script_start_code(cf->pool, &lcf->codes, sizeof(ngx_http_script_var_code_t)); if (vcode == NULL) { 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 @@ -1781,43 +1781,7 @@ ngx_http_script_set_var_code(ngx_http_sc e->sp--; - r->variables[code->index].len = e->sp->len; - r->variables[code->index].valid = 1; - r->variables[code->index].no_cacheable = 0; - r->variables[code->index].not_found = 0; - r->variables[code->index].data = e->sp->data; - -#if (NGX_DEBUG) - { - ngx_http_variable_t *v; - ngx_http_core_main_conf_t *cmcf; - - cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); - - v = cmcf->variables.elts; - - ngx_log_debug1(NGX_LOG_DEBUG_HTTP, e->request->connection->log, 0, - "http script set $%V", &v[code->index].name); - } -#endif -} - - -void -ngx_http_script_var_set_handler_code(ngx_http_script_engine_t *e) -{ - ngx_http_script_var_handler_code_t *code; - - ngx_log_debug0(NGX_LOG_DEBUG_HTTP, e->request->connection->log, 0, - "http script set var handler"); - - code = (ngx_http_script_var_handler_code_t *) e->ip; - - e->ip += sizeof(ngx_http_script_var_handler_code_t); - - e->sp--; - - code->handler(e->request, e->sp, code->data); + ngx_http_set_indexed_variable(r, code->index, e->sp); } 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 @@ -102,13 +102,6 @@ typedef struct { typedef struct { ngx_http_script_code_pt code; - ngx_http_set_variable_pt handler; - uintptr_t data; -} ngx_http_script_var_handler_code_t; - - -typedef struct { - ngx_http_script_code_pt code; uintptr_t n; } ngx_http_script_copy_capture_code_t; @@ -259,7 +252,6 @@ void ngx_http_script_file_code(ngx_http_ void ngx_http_script_complex_value_code(ngx_http_script_engine_t *e); void ngx_http_script_value_code(ngx_http_script_engine_t *e); void ngx_http_script_set_var_code(ngx_http_script_engine_t *e); -void ngx_http_script_var_set_handler_code(ngx_http_script_engine_t *e); void ngx_http_script_var_code(ngx_http_script_engine_t *e); void ngx_http_script_nop_code(ngx_http_script_engine_t *e); diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -745,6 +745,43 @@ ngx_http_get_variable(ngx_http_request_t } +void +ngx_http_set_indexed_variable(ngx_http_request_t *r, ngx_uint_t index, + ngx_http_variable_value_t *value) +{ + ngx_http_variable_t *v; + ngx_http_variable_value_t *vv; + ngx_http_core_main_conf_t *cmcf; + + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); + + if (cmcf->variables.nelts <= index) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "unknown variable index: %ui", index); + return; + } + + v = cmcf->variables.elts; + + if (v[index].set_handler) { + v[index].set_handler(r, value, v[index].data); + + } else { + vv = &r->variables[index]; + + vv->len = value->len; + vv->valid = 1; + vv->no_cacheable = 0; + vv->not_found = 0; + vv->escape = 0; + vv->data = value->data; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http set $%V to \"%v\"", &v[index].name, value); +} + + static ngx_int_t ngx_http_variable_request(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) @@ -2616,7 +2653,7 @@ ngx_http_regex_exec(ngx_http_request_t * { ngx_int_t rc, index; ngx_uint_t i, n, len; - ngx_http_variable_value_t *vv; + ngx_http_variable_value_t vv; ngx_http_core_main_conf_t *cmcf; cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); @@ -2654,24 +2691,11 @@ ngx_http_regex_exec(ngx_http_request_t * n = re->variables[i].capture; index = re->variables[i].index; - vv = &r->variables[index]; - - vv->len = r->captures[n + 1] - r->captures[n]; - vv->valid = 1; - vv->no_cacheable = 0; - vv->not_found = 0; - vv->data = &s->data[r->captures[n]]; - -#if (NGX_DEBUG) - { - ngx_http_variable_t *v; - - v = cmcf->variables.elts; - - ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "http regex set $%V to \"%v\"", &v[index].name, vv); - } -#endif + + vv.len = r->captures[n + 1] - r->captures[n]; + vv.data = &s->data[r->captures[n]]; + + ngx_http_set_indexed_variable(r, index, &vv); } r->ncaptures = rc * 2; @@ -2754,6 +2778,7 @@ ngx_http_variables_init_vars(ngx_conf_t && ngx_strncmp(v[i].name.data, key[n].key.data, v[i].name.len) == 0) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = av->data; @@ -2786,6 +2811,7 @@ ngx_http_variables_init_vars(ngx_conf_t } if (av) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = (uintptr_t) &v[i].name; v[i].flags = av->flags; diff --git a/src/http/ngx_http_variables.h b/src/http/ngx_http_variables.h --- a/src/http/ngx_http_variables.h +++ b/src/http/ngx_http_variables.h @@ -57,6 +57,9 @@ ngx_http_variable_value_t *ngx_http_get_ ngx_http_variable_value_t *ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key); +void ngx_http_set_indexed_variable(ngx_http_request_t *r, ngx_uint_t index, + ngx_http_variable_value_t *value); + ngx_int_t ngx_http_variable_unknown_header(ngx_http_request_t *r, ngx_http_variable_value_t *v, ngx_str_t *var, ngx_list_part_t *part, size_t prefix); diff --git a/src/stream/ngx_stream_set_module.c b/src/stream/ngx_stream_set_module.c --- a/src/stream/ngx_stream_set_module.c +++ b/src/stream/ngx_stream_set_module.c @@ -12,8 +12,6 @@ typedef struct { ngx_int_t index; - ngx_stream_set_variable_pt set_handler; - uintptr_t data; ngx_stream_complex_value_t value; } ngx_stream_set_cmd_t; @@ -90,18 +88,10 @@ ngx_stream_set_handler(ngx_stream_sessio return NGX_ERROR; } - if (cmds[i].set_handler != NULL) { - vv.len = str.len; - vv.data = str.data; - cmds[i].set_handler(s, &vv, cmds[i].data); + vv.len = str.len; + vv.data = str.data; - } else { - s->variables[cmds[i].index].len = str.len; - s->variables[cmds[i].index].valid = 1; - s->variables[cmds[i].index].no_cacheable = 0; - s->variables[cmds[i].index].not_found = 0; - s->variables[cmds[i].index].data = str.data; - } + ngx_stream_set_indexed_variable(s, cmds[i].index, &vv); } return NGX_DECLINED; @@ -209,8 +199,6 @@ ngx_stream_set(ngx_conf_t *cf, ngx_comma } set_cmd->index = index; - set_cmd->set_handler = v->set_handler; - set_cmd->data = v->data; ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t)); diff --git a/src/stream/ngx_stream_variables.c b/src/stream/ngx_stream_variables.c --- a/src/stream/ngx_stream_variables.c +++ b/src/stream/ngx_stream_variables.c @@ -477,6 +477,43 @@ ngx_stream_get_variable(ngx_stream_sessi } +void +ngx_stream_set_indexed_variable(ngx_stream_session_t *s, ngx_uint_t index, + ngx_stream_variable_value_t *value) +{ + ngx_stream_variable_t *v; + ngx_stream_variable_value_t *vv; + ngx_stream_core_main_conf_t *cmcf; + + cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); + + if (cmcf->variables.nelts <= index) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, 0, + "unknown variable index: %ui", index); + return; + } + + v = cmcf->variables.elts; + + if (v[index].set_handler) { + v[index].set_handler(s, value, v[index].data); + + } else { + vv = &s->variables[index]; + + vv->len = value->len; + vv->valid = 1; + vv->no_cacheable = 0; + vv->not_found = 0; + vv->escape = 0; + vv->data = value->data; + } + + ngx_log_debug2(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, + "stream set $%V to \"%v\"", &v[index].name, value); +} + + static ngx_int_t ngx_stream_variable_binary_remote_addr(ngx_stream_session_t *s, ngx_stream_variable_value_t *v, uintptr_t data) @@ -1100,7 +1137,7 @@ ngx_stream_regex_exec(ngx_stream_session { ngx_int_t rc, index; ngx_uint_t i, n, len; - ngx_stream_variable_value_t *vv; + ngx_stream_variable_value_t vv; ngx_stream_core_main_conf_t *cmcf; cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); @@ -1136,24 +1173,11 @@ ngx_stream_regex_exec(ngx_stream_session n = re->variables[i].capture; index = re->variables[i].index; - vv = &s->variables[index]; - - vv->len = s->captures[n + 1] - s->captures[n]; - vv->valid = 1; - vv->no_cacheable = 0; - vv->not_found = 0; - vv->data = &str->data[s->captures[n]]; -#if (NGX_DEBUG) - { - ngx_stream_variable_t *v; + vv.len = s->captures[n + 1] - s->captures[n]; + vv.data = &str->data[s->captures[n]]; - v = cmcf->variables.elts; - - ngx_log_debug2(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, - "stream regex set $%V to \"%v\"", &v[index].name, vv); - } -#endif + ngx_stream_set_indexed_variable(s, index, &vv); } s->ncaptures = rc * 2; @@ -1236,6 +1260,7 @@ ngx_stream_variables_init_vars(ngx_conf_ && ngx_strncmp(v[i].name.data, key[n].key.data, v[i].name.len) == 0) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = av->data; @@ -1268,6 +1293,7 @@ ngx_stream_variables_init_vars(ngx_conf_ } if (av) { + v[i].set_handler = av->set_handler; v[i].get_handler = av->get_handler; v[i].data = (uintptr_t) &v[i].name; v[i].flags = av->flags; diff --git a/src/stream/ngx_stream_variables.h b/src/stream/ngx_stream_variables.h --- a/src/stream/ngx_stream_variables.h +++ b/src/stream/ngx_stream_variables.h @@ -57,6 +57,9 @@ ngx_stream_variable_value_t *ngx_stream_ ngx_stream_variable_value_t *ngx_stream_get_variable(ngx_stream_session_t *s, ngx_str_t *name, ngx_uint_t key); +void ngx_stream_set_indexed_variable(ngx_stream_session_t *s, ngx_uint_t index, + ngx_stream_variable_value_t *value); + #if (NGX_PCRE) From mdounin at mdounin.ru Mon Jun 8 14:56:05 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:56:05 +0300 Subject: [nginx] Disabled redefinition of variables with v->set_handler. Message-ID: details: http://freenginx.org/hg/nginx/rev/5930e96ebd5a branches: changeset: 9555:5930e96ebd5a user: Maxim Dounin date: Mon Jun 08 17:54:20 2026 +0300 description: Disabled redefinition of variables with v->set_handler. Changeable variables can be updated by the "set" directive, and to support this ngx_http_add_variable() does not generate an error when a changeable variable is added again. This, however, makes it possible to redefine variables to completely different use, such as from a builtin variable to a map{}. And this in turn might cause unexpected issues, such as when a variable with set_handler is redefined to a map, which changes v->get_handler and v->data, but not v->set_handler. Most notably, this caused segmentation faults when trying to define a map to the $limit_rate variable before 7504:c19ca381b2e6 (https://trac.nginx.org/nginx/ticket/1238). With this change, redefinition of variables with v->set_handler is not allowed unless the NGX_HTTP_VAR_WEAK flag is also provided, which is currently used by "set" and other similar directives. Note that this change changes the meaning of the NGX_HTTP_VAR_WEAK flag. Previously, it was used solely to preserve get handler from prefix variables. Now the meaning of the flag becomes somewhat broader, and now it means that we are defining a fallback variable to be used in "set" or similar directives, and the caller is not going to redefine the variable if it's already present. Similar changes were made in the stream module. diffstat: src/http/ngx_http_variables.c | 6 ++++++ src/stream/ngx_stream_variables.c | 6 ++++++ 2 files changed, 12 insertions(+), 0 deletions(-) diffs (32 lines): diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -448,6 +448,12 @@ ngx_http_add_variable(ngx_conf_t *cf, ng return NULL; } + if (!(flags & NGX_HTTP_VAR_WEAK) && v->set_handler) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the duplicate \"%V\" variable", name); + return NULL; + } + if (!(flags & NGX_HTTP_VAR_WEAK)) { v->flags &= ~NGX_HTTP_VAR_WEAK; } diff --git a/src/stream/ngx_stream_variables.c b/src/stream/ngx_stream_variables.c --- a/src/stream/ngx_stream_variables.c +++ b/src/stream/ngx_stream_variables.c @@ -177,6 +177,12 @@ ngx_stream_add_variable(ngx_conf_t *cf, return NULL; } + if (!(flags & NGX_STREAM_VAR_WEAK) && v->set_handler) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the duplicate \"%V\" variable", name); + return NULL; + } + if (!(flags & NGX_STREAM_VAR_WEAK)) { v->flags &= ~NGX_STREAM_VAR_WEAK; } From mdounin at mdounin.ru Mon Jun 8 14:57:06 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:57:06 +0300 Subject: [nginx-tests] Tests: fixed gzip and pcre prerequisites in stream... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/70244b716f01 branches: changeset: 2064:70244b716f01 user: Maxim Dounin date: Mon Jun 08 17:56:13 2026 +0300 description: Tests: fixed gzip and pcre prerequisites in stream tests. Simply requiring gzip or rewrite is not enough, since these won't be actually present if http is not compiled in at all. The fix is to require http explicitly. diffstat: stream_access_log.t | 2 +- stream_map.t | 4 ++-- stream_proxy_protocol2_tlv.t | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diffs (38 lines): diff --git a/stream_access_log.t b/stream_access_log.t --- a/stream_access_log.t +++ b/stream_access_log.t @@ -25,7 +25,7 @@ use Test::Nginx::Stream qw/ stream /; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/stream stream_map gzip/); +my $t = Test::Nginx->new()->has(qw/stream stream_map http gzip/); $t->write_file_expand('nginx.conf', <<'EOF'); diff --git a/stream_map.t b/stream_map.t --- a/stream_map.t +++ b/stream_map.t @@ -23,8 +23,8 @@ use Test::Nginx::Stream qw/ stream /; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/stream stream_return stream_map rewrite/) - ->has(qw/http rewrite/); +my $t = Test::Nginx->new() + ->has(qw/stream stream_return stream_map http rewrite/); $t->write_file_expand('nginx.conf', <<'EOF'); diff --git a/stream_proxy_protocol2_tlv.t b/stream_proxy_protocol2_tlv.t --- a/stream_proxy_protocol2_tlv.t +++ b/stream_proxy_protocol2_tlv.t @@ -25,7 +25,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/stream stream_return stream_map rewrite/)->plan(14) + ->has(qw/stream stream_return stream_map http rewrite/)->plan(14) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% From mdounin at mdounin.ru Mon Jun 8 14:57:47 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:57:47 +0300 Subject: [nginx-tests] Tests: renamed $proxy_port in mail_oauth.t to avoi... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/26ac2f9b63f4 branches: changeset: 2065:26ac2f9b63f4 user: Maxim Dounin date: Mon Jun 08 17:57:08 2026 +0300 description: Tests: renamed $proxy_port in mail_oauth.t to avoid conflicts. The $proxy_port variable is a standard variable provided by the proxy module, and redefining it to something different might not be a good idea. diffstat: mail_oauth.t | 4 ++-- 1 files changed, 2 insertions(+), 2 deletions(-) diffs (21 lines): diff --git a/mail_oauth.t b/mail_oauth.t --- a/mail_oauth.t +++ b/mail_oauth.t @@ -67,7 +67,7 @@ http { map_hash_bucket_size 64; - map $http_auth_protocol $proxy_port { + map $http_auth_protocol $port { imap %%PORT_8144%%; pop3 %%PORT_8111%%; smtp %%PORT_8026%%; @@ -94,7 +94,7 @@ http { location = /mail/auth { add_header Auth-Status $reply; add_header Auth-Server 127.0.0.1; - add_header Auth-Port $proxy_port; + add_header Auth-Port $port; add_header Auth-Pass $passw; add_header Auth-Wait 1; add_header Auth-Error-SASL $sasl; From mdounin at mdounin.ru Mon Jun 8 14:59:29 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:59:29 +0300 Subject: [nginx-tests] Tests: added auth_request_set tests with prefix va... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/30114671bcf3 branches: changeset: 2066:30114671bcf3 user: Maxim Dounin date: Mon Jun 08 17:57:56 2026 +0300 description: Tests: added auth_request_set tests with prefix variables. diffstat: auth_request_set.t | 26 +++++++++++++++++++++++++- 1 files changed, 25 insertions(+), 1 deletions(-) diffs (55 lines): diff --git a/auth_request_set.t b/auth_request_set.t --- a/auth_request_set.t +++ b/auth_request_set.t @@ -22,7 +22,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http rewrite proxy auth_request/) - ->plan(6); + ->plan(8); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -88,6 +88,17 @@ http { return 204; } + location = /prefix { + add_header X-Foo $arg_foo; + return 204; + } + + location = /prefix_set { + auth_request /auth; + auth_request_set $arg_foo "set"; + add_header X-Foo $arg_foo; + } + location = /auth { proxy_pass http://127.0.0.1:8081; } @@ -121,6 +132,7 @@ EOF $t->write_file('t1.html', ''); $t->write_file('t4-fallback.html', ''); +$t->write_file('prefix_set', ''); $t->run(); ############################################################################### @@ -141,4 +153,16 @@ like(http_get('/t5.html'), qr/X-Args: se like(http_get('/t6.html'), qr/X-Unset-Username: xx/, 'unset variable'); +# variables introduced by auth_request_set did not use the NGX_HTTP_VAR_WEAK +# flag, thus overriding corresponding prefix variables in unrelated contexts + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/prefix?foo=arg'), qr/X-Foo: arg/, 'prefix variable'); + +} + +like(http_get('/prefix_set?foo=arg'), qr/X-Foo: set/, 'set prefix variable'); + ############################################################################### From mdounin at mdounin.ru Mon Jun 8 14:59:29 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:59:29 +0300 Subject: [nginx-tests] Tests: added tests for named captures. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/66fec8afee89 branches: changeset: 2067:66fec8afee89 user: Maxim Dounin date: Mon Jun 08 17:58:06 2026 +0300 description: Tests: added tests for named captures. diffstat: rewrite_named_captures.t | 113 +++++++++++++++++++++++++++++++++++++++ stream_named_captures.t | 135 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 0 deletions(-) diffs (258 lines): diff --git a/rewrite_named_captures.t b/rewrite_named_captures.t new file mode 100644 --- /dev/null +++ b/rewrite_named_captures.t @@ -0,0 +1,113 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for rewrite module, named captures. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(6) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $uri $map { + default map; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /map { + return 200 "value: $map\n"; + } + + location /map/rewrite { + rewrite ^(?.*) /; + return 200 "value: $map\n"; + } + + location /later { + return 200 "value: $late\n"; + } + + location /later/rewrite { + rewrite ^(?.*) /; + return 200 "value: $late\n"; + } + + location /prefix { + return 200 "value: $arg_foo\n"; + } + + location /prefix/rewrite { + rewrite ^(?.*) /; + return 200 "value: $arg_foo\n"; + } + } + + map $uri $late { + default map; + } +} + +EOF + +$t->run(); + +############################################################################### + +# named captures used to override the v->get_handler if it was previously +# set by a changeable variable, such as provided by map, and did not use +# the NGX_HTTP_VAR_WEAK flag, thus overriding corresponding prefix variables + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/map'), qr!value: map!, 'value from map'); + +} + +like(http_get('/map/rewrite'), qr!value: /map/rewrite!, + 'value from capture overrides map'); + +like(http_get('/later'), qr!value: map!, 'value from later map'); +like(http_get('/later/rewrite'), qr!value: /later/rewrite!, + 'value from capture overrides later map'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/prefix?foo=arg'), qr!value: arg!, 'value from arg'); + +} + +like(http_get('/prefix/rewrite?foo=arg'), qr!value: /prefix/rewrite!, + 'value from capture overrides arg'); + +############################################################################### diff --git a/stream_named_captures.t b/stream_named_captures.t new file mode 100644 --- /dev/null +++ b/stream_named_captures.t @@ -0,0 +1,135 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for stream named captures. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/stream stream_return stream_map http rewrite/) + ->plan(6); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + map 0 $map { + default map; + } + + map 0 $capture { + ~(?.*) $map; + } + + map 0 $capture_late { + ~(?.*) $late; + } + + map 0 $late { + default map; + } + + map 0 $capture_prefix { + ~(?.*) $proxy_protocol_tlv_0x01; + } + + server { + listen 127.0.0.1:8090; + return $map; + } + + server { + listen 127.0.0.1:8091; + return $capture; + } + + server { + listen 127.0.0.1:8092; + return $late; + } + + server { + listen 127.0.0.1:8093; + return $capture_late; + } + + server { + listen 127.0.0.1:8094 proxy_protocol; + return $proxy_protocol_tlv_0x01; + } + + server { + listen 127.0.0.1:8095 proxy_protocol; + return $capture_prefix; + } +} + +EOF + +$t->run(); + +############################################################################### + +# named captures used to override the v->get_handler if it was previously +# set by a changeable variable, such as provided by map, and did not use +# the NGX_HTTP_VAR_WEAK flag, thus overriding corresponding prefix variables + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +is(stream('127.0.0.1:' . port(8090))->read(), 'map', 'value from map'); + +} + +is(stream('127.0.0.1:' . port(8091))->read(), '0', + 'value from capture overrides map'); + +is(stream('127.0.0.1:' . port(8092))->read(), 'map', 'value from later map'); +is(stream('127.0.0.1:' . port(8093))->read(), '0', + 'value from capture overrides later map'); + +my $pp = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A" + . "\x21" + . "\x11" + . "\x00\x12" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x01\x00\x03tlv"; + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +is(stream('127.0.0.1:' . port(8094))->io($pp), 'tlv', 'value from tlv'); + +} + +is(stream('127.0.0.1:' . port(8095))->io($pp), '0', + 'value from capture overrides tlv'); + +############################################################################### From mdounin at mdounin.ru Mon Jun 8 14:59:29 2026 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 17:59:29 +0300 Subject: [nginx-tests] Tests: added tests for named captures with v->set_... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/bad0e9744aa3 branches: changeset: 2068:bad0e9744aa3 user: Maxim Dounin date: Mon Jun 08 17:58:14 2026 +0300 description: Tests: added tests for named captures with v->set_handler. diffstat: rewrite_named_captures.t | 24 +++++++++++++++++++++++- 1 files changed, 23 insertions(+), 1 deletions(-) diffs (46 lines): diff --git a/rewrite_named_captures.t b/rewrite_named_captures.t --- a/rewrite_named_captures.t +++ b/rewrite_named_captures.t @@ -22,7 +22,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(6) +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(7) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -69,6 +69,17 @@ http { rewrite ^(?.*) /; return 200 "value: $arg_foo\n"; } + + location /set_args { + if ($arg_foo ~ "(?.*)") {} + proxy_pass http://127.0.0.1:8081; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + return 200 "value: $args"; } map $uri $late { @@ -110,4 +121,15 @@ like(http_get('/prefix?foo=arg'), qr!val like(http_get('/prefix/rewrite?foo=arg'), qr!value: /prefix/rewrite!, 'value from capture overrides arg'); +# when a named capture changes a variable with v->set_handler, notably $args +# or $limit_rate, the handler should be called + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/set_args?foo=arg'), qr!value: arg!, + 'variable with set_handler'); + +} + ############################################################################### From mdounin at mdounin.ru Mon Jun 8 17:37:20 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:37:20 +0300 Subject: [PATCH 0 of 3] script buffer overrun protection Message-ID: Hello! The following patch series introduces script buffer overrun protection, preventing script evaluation from writing outside of the provided buffer. Review and testing appreciated. -- Maxim Dounin From mdounin at mdounin.ru Mon Jun 8 17:37:21 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:37:21 +0300 Subject: [PATCH 1 of 3] Script: simplified copy capture codes In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1780936492 -10800 # Mon Jun 08 19:34:52 2026 +0300 # Node ID ee56583e9b7e2d80bb5d6efc18147cdb13295b43 # Parent 5930e96ebd5a3d93bd719210e837f7281fc4f6d9 Script: simplified copy capture codes. 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 @@ -1310,6 +1310,7 @@ ngx_http_script_copy_capture_len_code(ng { int *cap; u_char *p; + size_t len; ngx_uint_t n; ngx_http_request_t *r; ngx_http_script_copy_capture_code_t *code; @@ -1325,17 +1326,17 @@ ngx_http_script_copy_capture_len_code(ng if (n < r->ncaptures) { cap = r->captures; + len = cap[n + 1] - cap[n]; if ((e->is_args || e->quote) && (e->request->quoted_uri || e->request->plus_in_uri)) { - p = r->captures_data; + p = r->captures_data + cap[n]; - return cap[n + 1] - cap[n] - + 2 * ngx_escape_uri(NULL, &p[cap[n]], cap[n + 1] - cap[n], - NGX_ESCAPE_ARGS); + return len + 2 * ngx_escape_uri(NULL, p, len, NGX_ESCAPE_ARGS); + } else { - return cap[n + 1] - cap[n]; + return len; } } @@ -1348,6 +1349,7 @@ ngx_http_script_copy_capture_code(ngx_ht { int *cap; u_char *p, *pos; + size_t len; ngx_uint_t n; ngx_http_request_t *r; ngx_http_script_copy_capture_code_t *code; @@ -1365,16 +1367,16 @@ ngx_http_script_copy_capture_code(ngx_ht if (n < r->ncaptures) { cap = r->captures; - p = r->captures_data; + len = cap[n + 1] - cap[n]; + p = r->captures_data + cap[n]; if ((e->is_args || e->quote) && (e->request->quoted_uri || e->request->plus_in_uri)) { - e->pos = (u_char *) ngx_escape_uri(pos, &p[cap[n]], - cap[n + 1] - cap[n], - NGX_ESCAPE_ARGS); + e->pos = (u_char *) ngx_escape_uri(pos, p, len, NGX_ESCAPE_ARGS); + } else { - e->pos = ngx_copy(pos, &p[cap[n]], cap[n + 1] - cap[n]); + e->pos = ngx_copy(pos, p, len); } } 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 @@ -888,6 +888,7 @@ size_t ngx_stream_script_copy_capture_len_code(ngx_stream_script_engine_t *e) { int *cap; + size_t len; ngx_uint_t n; ngx_stream_session_t *s; ngx_stream_script_copy_capture_code_t *code; @@ -902,7 +903,8 @@ ngx_stream_script_copy_capture_len_code( if (n < s->ncaptures) { cap = s->captures; - return cap[n + 1] - cap[n]; + len = cap[n + 1] - cap[n]; + return len; } return 0; @@ -914,6 +916,7 @@ ngx_stream_script_copy_capture_code(ngx_ { int *cap; u_char *p, *pos; + size_t len; ngx_uint_t n; ngx_stream_session_t *s; ngx_stream_script_copy_capture_code_t *code; @@ -930,8 +933,9 @@ ngx_stream_script_copy_capture_code(ngx_ if (n < s->ncaptures) { cap = s->captures; - p = s->captures_data; - e->pos = ngx_copy(pos, &p[cap[n]], cap[n + 1] - cap[n]); + len = cap[n + 1] - cap[n]; + p = s->captures_data + cap[n]; + e->pos = ngx_copy(pos, p, len); } ngx_log_debug2(NGX_LOG_DEBUG_STREAM, e->session->connection->log, 0, From mdounin at mdounin.ru Mon Jun 8 17:37:22 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:37:22 +0300 Subject: [PATCH 2 of 3] Script: buffer overrun protection In-Reply-To: References: Message-ID: <1794ecca0620d0e54f56.1780940242@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780936496 -10800 # Mon Jun 08 19:34:56 2026 +0300 # Node ID 1794ecca0620d0e54f5649c3405ea010e8abce2e # Parent ee56583e9b7e2d80bb5d6efc18147cdb13295b43 Script: buffer overrun protection. With this change, all script copy operations now check if there is enough room in the buffer. To do so, the script engine now provides the e->end pointer, which specifies expected buffer end, and each copy operation is checked against it with the ngx_http_script_check_length() function. The e->end pointer is optional and only checked when set, thus introducing no incompatible API changes. All standard functions were updated to use it, notably ngx_http_complex_value(), ngx_http_script_run(), ngx_http_script_regex_start_code(), ngx_http_script_complex_value_code(). Direct script evaluation in the proxy, fastcgi, scgi, uwsgi, grpc proxy, index, and try_files modules will be updated by a separate patch. In particular, this catches issues as observed when evaluating variables with side effects, such as in the following configuration: map $uri $map { ~(?.*) $capture; } set $capture ""; set $temp "$capture $map"; As well as when evaluating non-cacheable variables, where length of a variable might change between length and copy codes, such as in the following configuration: map prefix:$capture $map_volatile { volatile; ~(?.*) $capture; } set $capture ""; set $temp "$map_volatile"; 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 @@ -91,12 +91,17 @@ ngx_http_complex_value(ngx_http_request_ e.ip = val->values; e.pos = value->data; e.buf = *value; + e.end = value->data + len; while (*(uintptr_t *) e.ip) { code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); } + if (e.status) { + return NGX_ERROR; + } + *value = e.buf; return NGX_OK; @@ -641,12 +646,17 @@ ngx_http_script_run(ngx_http_request_t * e.ip = code_values; e.pos = value->data; + e.end = value->data + len; while (*(uintptr_t *) e.ip) { code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); } + if (e.status) { + return NULL; + } + return e.pos; } @@ -794,6 +804,25 @@ ngx_http_script_add_code(ngx_array_t *co } +ngx_int_t +ngx_http_script_check_length(ngx_http_script_engine_t *e, size_t len) +{ + if (e->end == NULL) { + return NGX_OK; + } + + if (e->end - e->pos < (ssize_t) len) { + ngx_log_error(NGX_LOG_ALERT, e->request->connection->log, 0, + "no buffer space in script copy"); + e->ip = ngx_http_script_exit; + e->status = NGX_HTTP_INTERNAL_SERVER_ERROR; + return NGX_ERROR; + } + + return NGX_OK; +} + + static ngx_int_t ngx_http_script_add_copy_code(ngx_http_script_compile_t *sc, ngx_str_t *value, ngx_uint_t last) @@ -862,6 +891,11 @@ ngx_http_script_copy_code(ngx_http_scrip p = e->pos; if (!e->skip) { + + if (ngx_http_script_check_length(e, code->len) != NGX_OK) { + return; + } + e->pos = ngx_copy(p, e->ip + sizeof(ngx_http_script_copy_code_t), code->len); } @@ -965,6 +999,11 @@ ngx_http_script_copy_var_code(ngx_http_s } if (value && !value->not_found) { + + if (ngx_http_script_check_length(e, value->len) != NGX_OK) { + return; + } + p = e->pos; e->pos = ngx_copy(p, value->data, value->len); @@ -1159,6 +1198,7 @@ ngx_http_script_regex_start_code(ngx_htt e->quote = code->redirect; e->pos = e->buf.data; + e->end = e->buf.data + e->buf.len; e->ip += sizeof(ngx_http_script_regex_code_t); } @@ -1196,6 +1236,11 @@ ngx_http_script_regex_end_code(ngx_http_ e->pos = dst; if (code->add_args && r->args.len) { + + if (ngx_http_script_check_length(e, r->args.len + 1) != NGX_OK) { + return; + } + *e->pos++ = (u_char) (code->args ? '&' : '?'); e->pos = ngx_copy(e->pos, r->args.data, r->args.len); } @@ -1229,6 +1274,11 @@ ngx_http_script_regex_end_code(ngx_http_ e->buf.len = e->args - e->buf.data; if (code->add_args && r->args.len) { + + if (ngx_http_script_check_length(e, r->args.len + 1) != NGX_OK) { + return; + } + *e->pos++ = '&'; e->pos = ngx_copy(e->pos, r->args.data, r->args.len); } @@ -1350,6 +1400,7 @@ ngx_http_script_copy_capture_code(ngx_ht int *cap; u_char *p, *pos; size_t len; + uintptr_t escape; ngx_uint_t n; ngx_http_request_t *r; ngx_http_script_copy_capture_code_t *code; @@ -1373,9 +1424,20 @@ ngx_http_script_copy_capture_code(ngx_ht if ((e->is_args || e->quote) && (e->request->quoted_uri || e->request->plus_in_uri)) { + escape = 2 * ngx_escape_uri(NULL, p, len, NGX_ESCAPE_ARGS); + + if (ngx_http_script_check_length(e, len + escape) != NGX_OK) { + return; + } + e->pos = (u_char *) ngx_escape_uri(pos, p, len, NGX_ESCAPE_ARGS); } else { + + if (ngx_http_script_check_length(e, len) != NGX_OK) { + return; + } + e->pos = ngx_copy(pos, p, len); } } @@ -1743,6 +1805,7 @@ ngx_http_script_complex_value_code(ngx_h } e->pos = e->buf.data; + e->end = e->buf.data + len; e->sp->len = e->buf.len; e->sp->data = e->buf.data; 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 @@ -17,6 +17,7 @@ typedef struct { u_char *ip; u_char *pos; + u_char *end; ngx_http_variable_value_t *sp; ngx_str_t buf; @@ -231,6 +232,9 @@ void *ngx_http_script_start_code(ngx_poo size_t size); void *ngx_http_script_add_code(ngx_array_t *codes, size_t size, void *code); +ngx_int_t ngx_http_script_check_length(ngx_http_script_engine_t *e, + size_t len); + size_t ngx_http_script_copy_len_code(ngx_http_script_engine_t *e); void ngx_http_script_copy_code(ngx_http_script_engine_t *e); size_t ngx_http_script_copy_var_len_code(ngx_http_script_engine_t *e); 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 @@ -90,6 +90,7 @@ ngx_stream_complex_value(ngx_stream_sess e.ip = val->values; e.pos = value->data; + e.end = value->data + len; e.buf = *value; while (*(uintptr_t *) e.ip) { @@ -97,6 +98,10 @@ ngx_stream_complex_value(ngx_stream_sess code((ngx_stream_script_engine_t *) &e); } + if (e.status) { + return NGX_ERROR; + } + *value = e.buf; return NGX_OK; @@ -522,12 +527,17 @@ ngx_stream_script_run(ngx_stream_session e.ip = code_values; e.pos = value->data; + e.end = value->data + len; while (*(uintptr_t *) e.ip) { code = *(ngx_stream_script_code_pt *) e.ip; code((ngx_stream_script_engine_t *) &e); } + if (e.status) { + return NULL; + } + return e.pos; } @@ -662,6 +672,25 @@ ngx_stream_script_add_code(ngx_array_t * } +ngx_int_t +ngx_stream_script_check_length(ngx_stream_script_engine_t *e, size_t len) +{ + if (e->end == NULL) { + return NGX_OK; + } + + if (e->end - e->pos < (ssize_t) len) { + ngx_log_error(NGX_LOG_ALERT, e->session->connection->log, 0, + "no buffer space in script copy"); + e->ip = ngx_stream_script_exit; + e->status = NGX_STREAM_INTERNAL_SERVER_ERROR; + return NGX_ERROR; + } + + return NGX_OK; +} + + static ngx_int_t ngx_stream_script_add_copy_code(ngx_stream_script_compile_t *sc, ngx_str_t *value, ngx_uint_t last) @@ -731,6 +760,11 @@ ngx_stream_script_copy_code(ngx_stream_s p = e->pos; if (!e->skip) { + + if (ngx_stream_script_check_length(e, code->len) != NGX_OK) { + return; + } + e->pos = ngx_copy(p, e->ip + sizeof(ngx_stream_script_copy_code_t), code->len); } @@ -835,6 +869,11 @@ ngx_stream_script_copy_var_code(ngx_stre } if (value && !value->not_found) { + + if (ngx_stream_script_check_length(e, value->len) != NGX_OK) { + return; + } + p = e->pos; e->pos = ngx_copy(p, value->data, value->len); @@ -935,6 +974,11 @@ ngx_stream_script_copy_capture_code(ngx_ cap = s->captures; len = cap[n + 1] - cap[n]; p = s->captures_data + cap[n]; + + if (ngx_stream_script_check_length(e, len) != NGX_OK) { + return; + } + e->pos = ngx_copy(pos, p, len); } diff --git a/src/stream/ngx_stream_script.h b/src/stream/ngx_stream_script.h --- a/src/stream/ngx_stream_script.h +++ b/src/stream/ngx_stream_script.h @@ -17,6 +17,7 @@ typedef struct { u_char *ip; u_char *pos; + u_char *end; ngx_stream_variable_value_t *sp; ngx_str_t buf; @@ -25,6 +26,7 @@ typedef struct { unsigned flushed:1; unsigned skip:1; + ngx_int_t status; ngx_stream_session_t *session; } ngx_stream_script_engine_t; @@ -127,6 +129,9 @@ void ngx_stream_script_flush_no_cacheabl void *ngx_stream_script_add_code(ngx_array_t *codes, size_t size, void *code); +ngx_int_t ngx_stream_script_check_length(ngx_stream_script_engine_t *e, + size_t len); + size_t ngx_stream_script_copy_len_code(ngx_stream_script_engine_t *e); void ngx_stream_script_copy_code(ngx_stream_script_engine_t *e); size_t ngx_stream_script_copy_var_len_code(ngx_stream_script_engine_t *e); From mdounin at mdounin.ru Mon Jun 8 17:37:23 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:37:23 +0300 Subject: [PATCH 3 of 3] Script: buffer overrun protection in direct script usage In-Reply-To: References: Message-ID: <67059a7429f113a0af1e.1780940243@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780936500 -10800 # Mon Jun 08 19:35:00 2026 +0300 # Node ID 67059a7429f113a0af1e0b7d21ff8bf7ef4ac9f7 # Parent 1794ecca0620d0e54f5649c3405ea010e8abce2e Script: buffer overrun protection in direct script usage. Following the previous change, this change adds script overrun protection to direct script evaluation in the proxy, fastcgi, scgi, uwsgi, grpc proxy, index, and try_files modules. diff --git a/src/http/modules/ngx_http_fastcgi_module.c b/src/http/modules/ngx_http_fastcgi_module.c --- a/src/http/modules/ngx_http_fastcgi_module.c +++ b/src/http/modules/ngx_http_fastcgi_module.c @@ -836,8 +836,8 @@ ngx_http_fastcgi_create_request(ngx_http { off_t file_pos; u_char ch, sep, *pos, *lowcase_key; - size_t size, len, key_len, val_len, padding, - allocated; + size_t size, len, params_len, + key_len, val_len, padding, allocated; ngx_uint_t i, n, next, hash, skip_empty, header_params; ngx_buf_t *b; ngx_chain_t *cl, *body; @@ -852,6 +852,7 @@ ngx_http_fastcgi_create_request(ngx_http ngx_http_script_len_code_pt lcode; len = 0; + params_len = 0; header_params = 0; ignored = NULL; @@ -891,8 +892,10 @@ ngx_http_fastcgi_create_request(ngx_http continue; } - len += 1 + key_len + ((val_len > 127) ? 4 : 1) + val_len; + params_len += 1 + key_len + ((val_len > 127) ? 4 : 1) + val_len; } + + len += params_len; } if (flcf->upstream.pass_request_headers) { @@ -1048,6 +1051,7 @@ ngx_http_fastcgi_create_request(ngx_http e.ip = params->values->elts; e.pos = b->last; + e.end = b->last + params_len; e.request = r; e.flushed = 1; @@ -1080,6 +1084,12 @@ ngx_http_fastcgi_create_request(ngx_http continue; } + if (ngx_http_script_check_length(&e, 1 + ((val_len > 127) ? 4 : 1)) + != NGX_OK) + { + return NGX_ERROR; + } + *e.pos++ = (u_char) key_len; if (val_len > 127) { @@ -1098,6 +1108,10 @@ ngx_http_fastcgi_create_request(ngx_http } e.ip += sizeof(uintptr_t); + if (e.status) { + return NGX_ERROR; + } + ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "fastcgi param: \"%*s: %*s\"", key_len, e.pos - (key_len + val_len), 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 @@ -710,8 +710,10 @@ ngx_http_grpc_eval(ngx_http_request_t *r static ngx_int_t ngx_http_grpc_create_request(ngx_http_request_t *r) { - u_char *p, *tmp, *key_tmp, *val_tmp, *headers_frame; - size_t len, tmp_len, key_len, val_len, uri_len; + u_char *p, *tmp, *key_tmp, *val_tmp, *headers_frame, + *headers_end; + size_t len, headers_len, tmp_len, + key_len, val_len, uri_len; uintptr_t escape; ngx_buf_t *b; ngx_uint_t i, next; @@ -735,6 +737,8 @@ ngx_http_grpc_create_request(ngx_http_re len = sizeof(ngx_http_grpc_connection_start) - 1 + sizeof(ngx_http_grpc_frame_t); /* headers frame */ + headers_len = 0; + /* :method header */ if (r->method == NGX_HTTP_GET || r->method == NGX_HTTP_POST) { @@ -801,8 +805,8 @@ ngx_http_grpc_create_request(ngx_http_re continue; } - len += 1 + NGX_HTTP_V2_INT_OCTETS + key_len - + NGX_HTTP_V2_INT_OCTETS + val_len; + headers_len += 1 + NGX_HTTP_V2_INT_OCTETS + key_len + + NGX_HTTP_V2_INT_OCTETS + val_len; if (tmp_len < key_len) { tmp_len = key_len; @@ -813,6 +817,8 @@ ngx_http_grpc_create_request(ngx_http_re } } + len += headers_len; + if (glcf->upstream.pass_request_headers) { part = &r->headers_in.headers.part; header = part->elts; @@ -995,6 +1001,8 @@ ngx_http_grpc_create_request(ngx_http_re le.ip = glcf->headers.lengths->elts; + headers_end = b->last + headers_len; + while (*(uintptr_t *) le.ip) { lcode = *(ngx_http_script_len_code_pt *) le.ip; @@ -1022,13 +1030,25 @@ ngx_http_grpc_create_request(ngx_http_re *b->last++ = 0; e.pos = key_tmp; + e.end = key_tmp + tmp_len; code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); + if (e.status) { + return NGX_ERROR; + } + + if (headers_end - b->last < (ssize_t) key_len) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "no buffer space in grpc create request"); + return NGX_ERROR; + } + b->last = ngx_http_v2_write_name(b->last, key_tmp, key_len, tmp); e.pos = val_tmp; + e.end = val_tmp + tmp_len; while (*(uintptr_t *) e.ip) { code = *(ngx_http_script_code_pt *) e.ip; @@ -1036,6 +1056,16 @@ ngx_http_grpc_create_request(ngx_http_re } e.ip += sizeof(uintptr_t); + if (e.status) { + return NGX_ERROR; + } + + if (headers_end - b->last < (ssize_t) val_len) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "no buffer space in grpc create request"); + return NGX_ERROR; + } + b->last = ngx_http_v2_write_value(b->last, val_tmp, val_len, tmp); #if (NGX_DEBUG) 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 @@ -130,6 +130,7 @@ ngx_http_index_handler(ngx_http_request_ name = NULL; /* suppress MSVC warning */ path.data = NULL; + e.status = 0; index = ilcf->indices->elts; for (i = 0; i < ilcf->indices->nelts; i++) { @@ -184,18 +185,28 @@ ngx_http_index_handler(ngx_http_request_ } else { e.ip = index[i].values->elts; e.pos = name; + e.end = name + allocated; while (*(uintptr_t *) e.ip) { code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); } + if (e.status) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (ngx_http_script_check_length(&e, 1) != NGX_OK) { + return NGX_ERROR; + } + if (*name == '/') { - uri.len = len - 1; + uri.len = e.pos - name; uri.data = name; return ngx_http_internal_redirect(r, &uri, &r->args); } + len = e.pos - name + 1; path.len = e.pos - path.data; *e.pos = '\0'; 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 @@ -1253,7 +1253,7 @@ static ngx_int_t ngx_http_proxy_create_request(ngx_http_request_t *r) { u_char *key; - size_t len, uri_len, loc_len, body_len, + size_t len, uri_len, loc_len, body_len, headers_len, key_len, val_len; uintptr_t escape; ngx_buf_t *b; @@ -1307,6 +1307,8 @@ ngx_http_proxy_create_request(ngx_http_r escape = 0; loc_len = 0; unparsed_uri = 0; + body_len = 0; + headers_len = 0; if (plcf->proxy_lengths && ctx->vars.uri.len) { uri_len = ctx->vars.uri.len; @@ -1345,7 +1347,6 @@ ngx_http_proxy_create_request(ngx_http_r le.ip = plcf->body_lengths->elts; le.request = r; le.flushed = 1; - body_len = 0; while (*(uintptr_t *) le.ip) { lcode = *(ngx_http_script_len_code_pt *) le.ip; @@ -1384,8 +1385,10 @@ ngx_http_proxy_create_request(ngx_http_r continue; } - len += key_len + sizeof(": ") - 1 + val_len + sizeof(CRLF) - 1; - } + headers_len += key_len + sizeof(": ") - 1 + val_len + sizeof(CRLF) - 1; + } + + len += headers_len; if (plcf->upstream.pass_request_headers) { @@ -1478,6 +1481,7 @@ ngx_http_proxy_create_request(ngx_http_r e.ip = headers->values->elts; e.pos = b->last; + e.end = b->last + headers_len; e.request = r; e.flushed = 1; @@ -1518,6 +1522,14 @@ ngx_http_proxy_create_request(ngx_http_r ctx->upgrade = 1; } + if (e.status) { + return NGX_ERROR; + } + + if (ngx_http_script_check_length(&e, 2) != NGX_OK) { + return NGX_ERROR; + } + *e.pos++ = ':'; *e.pos++ = ' '; while (*(uintptr_t *) e.ip) { @@ -1526,6 +1538,14 @@ ngx_http_proxy_create_request(ngx_http_r } e.ip += sizeof(uintptr_t); + if (e.status) { + return NGX_ERROR; + } + + if (ngx_http_script_check_length(&e, 2) != NGX_OK) { + return NGX_ERROR; + } + *e.pos++ = CR; *e.pos++ = LF; } @@ -1576,6 +1596,7 @@ ngx_http_proxy_create_request(ngx_http_r if (plcf->body_values) { e.ip = plcf->body_values->elts; e.pos = b->last; + e.end = b->last + body_len; e.skip = 0; while (*(uintptr_t *) e.ip) { @@ -1583,6 +1604,10 @@ ngx_http_proxy_create_request(ngx_http_r code((ngx_http_script_engine_t *) &e); } + if (e.status) { + return NGX_ERROR; + } + b->last = e.pos; } diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c --- a/src/http/modules/ngx_http_scgi_module.c +++ b/src/http/modules/ngx_http_scgi_module.c @@ -634,7 +634,7 @@ ngx_http_scgi_create_request(ngx_http_re { off_t content_length_n; u_char ch, sep, *key, *val, *lowcase_key; - size_t len, key_len, val_len, allocated; + size_t len, params_len, key_len, val_len, allocated; ngx_buf_t *b; ngx_str_t content_length; ngx_uint_t i, n, hash, skip_empty, header_params; @@ -659,6 +659,7 @@ ngx_http_scgi_create_request(ngx_http_re len = sizeof("CONTENT_LENGTH") + content_length.len + 1; + params_len = 0; header_params = 0; ignored = NULL; @@ -696,8 +697,10 @@ ngx_http_scgi_create_request(ngx_http_re continue; } - len += key_len + val_len + 1; + params_len += key_len + val_len + 1; } + + len += params_len; } if (scf->upstream.pass_request_headers) { @@ -812,6 +815,7 @@ ngx_http_scgi_create_request(ngx_http_re e.ip = params->values->elts; e.pos = b->last; + e.end = b->last + params_len; e.request = r; e.flushed = 1; @@ -850,6 +854,10 @@ ngx_http_scgi_create_request(ngx_http_re code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); + if (e.status) { + return NGX_ERROR; + } + #if (NGX_DEBUG) val = e.pos; #endif @@ -857,6 +865,15 @@ ngx_http_scgi_create_request(ngx_http_re code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); } + + if (e.status) { + return NGX_ERROR; + } + + if (ngx_http_script_check_length(&e, 1) != NGX_OK) { + return NGX_ERROR; + } + *e.pos++ = '\0'; e.ip += sizeof(uintptr_t); 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 @@ -165,6 +165,7 @@ ngx_http_try_files_handler(ngx_http_requ } else { e.ip = tf->values->elts; e.pos = name; + e.end = name + (r->uri.len - alias) + allocated; e.flushed = 1; while (*(uintptr_t *) e.ip) { @@ -172,6 +173,14 @@ ngx_http_try_files_handler(ngx_http_requ code((ngx_http_script_engine_t *) &e); } + if (e.status) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (ngx_http_script_check_length(&e, 1) != NGX_OK) { + return NGX_ERROR; + } + path.len = e.pos - path.data; *e.pos = '\0'; diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -849,7 +849,7 @@ static ngx_int_t ngx_http_uwsgi_create_request(ngx_http_request_t *r) { u_char ch, sep, *lowcase_key; - size_t key_len, val_len, len, allocated; + size_t key_len, val_len, len, params_len, allocated; ngx_uint_t i, n, hash, skip_empty, header_params; ngx_buf_t *b; ngx_chain_t *cl, *body; @@ -862,6 +862,7 @@ ngx_http_uwsgi_create_request(ngx_http_r ngx_http_script_len_code_pt lcode; len = 0; + params_len = 0; header_params = 0; ignored = NULL; @@ -899,8 +900,10 @@ ngx_http_uwsgi_create_request(ngx_http_r continue; } - len += 2 + key_len + 2 + val_len; + params_len += 2 + key_len + 2 + val_len; } + + len += params_len; } if (uwcf->upstream.pass_request_headers) { @@ -1032,6 +1035,7 @@ ngx_http_uwsgi_create_request(ngx_http_r e.ip = params->values->elts; e.pos = b->last; + e.end = b->last + params_len; e.request = r; e.flushed = 1; @@ -1064,12 +1068,24 @@ ngx_http_uwsgi_create_request(ngx_http_r continue; } + if (ngx_http_script_check_length(&e, 2) != NGX_OK) { + return NGX_ERROR; + } + *e.pos++ = (u_char) (key_len & 0xff); *e.pos++ = (u_char) ((key_len >> 8) & 0xff); code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); + if (e.status) { + return NGX_ERROR; + } + + if (ngx_http_script_check_length(&e, 2) != NGX_OK) { + return NGX_ERROR; + } + *e.pos++ = (u_char) (val_len & 0xff); *e.pos++ = (u_char) ((val_len >> 8) & 0xff); @@ -1078,6 +1094,10 @@ ngx_http_uwsgi_create_request(ngx_http_r code((ngx_http_script_engine_t *) &e); } + if (e.status) { + return NGX_ERROR; + } + e.ip += sizeof(uintptr_t); ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, From mdounin at mdounin.ru Mon Jun 8 17:46:39 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:46:39 +0300 Subject: [PATCH 1 of 4] Tests: slightly improved set with captures test In-Reply-To: References: Message-ID: <29d3b09bcce5c912235a.1780940799@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780936526 -10800 # Mon Jun 08 19:35:26 2026 +0300 # Node ID 29d3b09bcce5c912235a0552db2cb1a3c1996881 # Parent bad0e9744aa37ad6c217b8970e4037624b7514a0 Tests: slightly improved set with captures test. diff --git a/rewrite_set.t b/rewrite_set.t --- a/rewrite_set.t +++ b/rewrite_set.t @@ -56,8 +56,8 @@ http { } location /if/ { - if ($uri ~ "(.*)") { - set $temp "set_$1"; + if ($uri ~ "(/if/)(.*)") { + set $temp "set_$1$2"; } return 200 "X${temp}X"; } From mdounin at mdounin.ru Mon Jun 8 17:46:40 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:46:40 +0300 Subject: [PATCH 2 of 4] Tests: tests with variables which change between accesses In-Reply-To: References: Message-ID: <4af2fea518e2389a44c4.1780940800@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780936528 -10800 # Mon Jun 08 19:35:28 2026 +0300 # Node ID 4af2fea518e2389a44c483ace19f7b4e645989a7 # Parent 29d3b09bcce5c912235a0552db2cb1a3c1996881 Tests: tests with variables which change between accesses. One example is a named capture, which is changed by a side effect of a map. Another example is a non-cacheable (volatile) map which uses captures from its previous evaluation as input, and therefore becomes longer on each evaluation. 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(25) +my $t = Test::Nginx->new()->has(qw/http rewrite proxy/)->plan(26) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -34,6 +34,10 @@ events { http { %%TEST_GLOBALS_HTTP%% + map $uri $map_capture { + ~(?.*) $capture; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -142,6 +146,10 @@ http { return 200 "uri:$uri args:$args"; } + location /map/ { + rewrite ^ $capture$map_capture redirect; + } + location /break { rewrite ^ /return200; break; @@ -274,6 +282,17 @@ like(http_get('/capture_nested/%25?a=b') } +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(); + +like(http_get('/map/test-long-uri'), qr!Location: .*/map/test-long-uri!ms, + 'rewrite and map with side effects'); + +} + # break like(http_get('/break'), qr/200/, 'valid_location reset'); diff --git a/rewrite_set.t b/rewrite_set.t --- a/rewrite_set.t +++ b/rewrite_set.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 ssi/)->plan(10); +my $t = Test::Nginx->new()->has(qw/http rewrite ssi map/)->plan(16); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -36,6 +37,21 @@ events { http { %%TEST_GLOBALS_HTTP%% + map $uri $map_capture { + ~(?.*) $capture; + } + + map prefix:$capture $map_volatile { + volatile; + ~(?.*) $capture; + } + + map $args $map_flush { + volatile; + default wrong; + secret good; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -78,6 +94,40 @@ http { return 200 "X${temp}X"; } + location /map { + set $temp "$capture $map_capture"; + return 200 "X${temp}X"; + } + + location /map_volatile { + set $temp "$map_volatile"; + return 200 "X${temp}X"; + } + + location /map_root { + root html/$pid; + return 200 "X${map_volatile}X${document_root}X"; + } + + location /map_root_root { + root html/$pid; + return 200 "X${map_volatile}X${document_root}${realpath_root}X"; + } + + location /map_root_overflow { + root html/$capture/$map_capture; + set $temp "$document_root"; + return 200 "X${temp}X"; + } + + location /map_flush { + set $args "wrong"; + set $temp "$map_flush"; + set $args "secret"; + set $temp "$temp:$map_flush"; + return 200 "X${temp}X"; + } + location /t1 { set $http_foo "set_foo"; ssi on; @@ -147,6 +197,48 @@ like(http_get('/args/%20x'), qr!Xset_/ar } +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(); + +# map can change other variables via named captures, +# resulting in invalid buffer length calculations + +like(http_get('/map'), qr!X.*/mapX!, 'set and map side effects'); + +# non-cacheable variable can change its length on each evaluation, +# resulting in invalid buffer length calculations + +like(http_get('/map_volatile'), qr!Xprefix:.*X!, + 'set and volatile map'); + +# even if a separate flush step is used, such as with return, +# which uses ngx_http_complex_value(), an additional flush might happen +# as a side effect of a variable lookup (notably $document_root and +# $realpath_root when using root with variables) + +like(http_get('/map_root'), qr!Xprefix:X.*X!, + 'return and volatile map with $document_root'); + +like(http_get('/map_root_root'), qr!Xprefix:X.*X!, + 'return and volatile map with $document_root and $realpath_root'); + +# similarly, map with side effects can cause invalid buffer length +# during evaluation of $document_root, which uses ngx_http_script_run() + +like(http_get('/map_root_overflow'), qr!X.*/map_root_overflowX!, + '$document_root with map side effects'); + +} + +# non-cacheable map can be derived from a non-cacheable variable, +# which also needs to be flushed before getting the map value + +like(http_get('/map_flush'), qr!Xwrong:goodX!, + 'set and volatile map source flush'); + # non-indexed access of prefixed variables like(http_get_extra('/t1.html', 'Foo: http_foo'), qr/Xset_fooX/, diff --git a/stream_set.t b/stream_set.t --- a/stream_set.t +++ b/stream_set.t @@ -24,7 +24,8 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/stream stream_return stream_map stream_set/); + ->has(qw/stream stream_return stream_map stream_set http rewrite/) + ->plan(3); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -42,6 +43,10 @@ stream { default "original"; } + map 0 $map_capture { + ~(?.*) $capture; + } + server { listen 127.0.0.1:8082; return $map_var:$set_var; @@ -54,15 +59,31 @@ stream { listen 127.0.0.1:8083; return $set_var; } + + server { + listen 127.0.0.1:8084; + return "$capture $map_capture"; + } } EOF -$t->run()->plan(2); +$t->run(); ############################################################################### is(stream('127.0.0.1:' . port(8082))->read(), 'new:original', 'set'); is(stream('127.0.0.1:' . port(8083))->read(), '', 'uninitialized variable'); +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(); + +is(stream('127.0.0.1:' . port(8084))->read(), '0 0', + 'set and map with side effects'); + +} + ############################################################################### From mdounin at mdounin.ru Mon Jun 8 17:46:41 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:46:41 +0300 Subject: [PATCH 3 of 4] Tests: more tests with variables which change between accesses In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1780936532 -10800 # Mon Jun 08 19:35:32 2026 +0300 # Node ID e2ccdc5e3d031950a1a833b372791812902cc365 # Parent 4af2fea518e2389a44c483ace19f7b4e645989a7 Tests: more tests with variables which change between accesses. These tests are designed to test all cases where script evaluation is done directly by a module (proxy, grpc, fastcgi, scgi, uwsgi, try_files, index), rather than thru standard functions (ngx_http_script_run(), ngx_http_complex_value()), and use a capture and a map which changes the capture to trigger buffer overrun. diff --git a/fastcgi_header_params.t b/fastcgi_header_params.t --- a/fastcgi_header_params.t +++ b/fastcgi_header_params.t @@ -25,7 +25,7 @@ eval { require FCGI; }; plan(skip_all => 'FCGI not installed') if $@; plan(skip_all => 'win32') if $^O eq 'MSWin32'; -my $t = Test::Nginx->new()->has(qw/http fastcgi/)->plan(4) +my $t = Test::Nginx->new()->has(qw/http fastcgi rewrite map/)->plan(5) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -38,6 +38,10 @@ events { http { %%TEST_GLOBALS_HTTP%% + map $uri $map_capture { + ~(?.*) $capture; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -46,6 +50,12 @@ http { fastcgi_pass 127.0.0.1:8081; fastcgi_param HTTP_X_BLAH "blah"; } + + location /map/ { + fastcgi_pass 127.0.0.1:8081; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param HTTP_FOO "foo $capture $map_capture end"; + } } } @@ -85,6 +95,17 @@ like($r, qr/X-Cookie: foo; bar; bazz/, like($r, qr/X-Foo: foo, bar, bazz/, 'fastcgi with multiple unknown headers'); +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(); + +like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, + 'fastcgi params and map with side effects'); + +} + ############################################################################### sub http_get_headers { diff --git a/grpc_headers.t b/grpc_headers.t new file mode 100644 --- /dev/null +++ b/grpc_headers.t @@ -0,0 +1,95 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for grpc module, grpc_set_header directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_v2 grpc rewrite/)->plan(2) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $uri $map_capture { + ~(?.*) $capture; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + grpc_pass 127.0.0.1:8081; + grpc_set_header X-Blah "blah $capture $map_capture end"; + } + + location /multi { + grpc_pass 127.0.0.1:8081; + grpc_set_header X-Blah1 "$capture"; + grpc_set_header X-Blah2 "$capture"; + grpc_set_header X-Blah3 "$capture"; + grpc_set_header X-Blah4 "$capture"; + grpc_set_header X-Blah5 "$capture"; + grpc_set_header X-Blah6 "$capture"; + grpc_set_header X-Blah "blah $map_capture end"; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + http2 on; + + location / { + return 200 "$http_x_blah\n"; + } + } +} + +EOF + +$t->run(); + +############################################################################### + +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(); + +like(http_get('/test-long-uri'), qr!blah .* /test-long-uri end!, + 'grpc_set_header and map with side effects'); + +like(http_get('/multi'), qr!blah /multi end!, + 'grpc_set_header and map with side effects, multiple headers'); + +} + +############################################################################### diff --git a/http_try_files.t b/http_try_files.t --- a/http_try_files.t +++ b/http_try_files.t @@ -21,7 +21,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http proxy rewrite/)->plan(48) +my $t = Test::Nginx->new()->has(qw/http proxy rewrite map/)->plan(49) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -34,6 +34,10 @@ events { http { %%TEST_GLOBALS_HTTP%% + map $uri $map_capture { + ~(?.*) $capture; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -189,6 +193,10 @@ http { location = /uri-after-alias-add-redirect { try_files /notfound /uri-after-alias-add/found; } + + location /map/ { + try_files /$capture/$map_capture =404; + } } server { @@ -396,4 +404,15 @@ like(http_get('/uri-after-alias-add-redi } +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(); + +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 @@ -22,7 +22,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http/)->plan(14) +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(16) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -35,6 +35,14 @@ events { http { %%TEST_GLOBALS_HTTP%% + map $uri $map_capture { + ~(?.*) $capture; + } + + map $uri $map_shrink { + ~(?) ""; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -86,6 +94,16 @@ http { log_not_found off; } } + + location /map/test-long-uri/ { + 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; + } } } @@ -119,6 +137,25 @@ like(http_get('/not_found/off/'), qr/404 like(http_get('/forbidden/'), qr/403 Forbidden/, 'directory access denied'); like(http_get('/index.html/'), qr/404 Not Found/, 'not a directory'); +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(); + +like(http_get('/map/test-long-uri/'), qr!X-URI: /index.html\x0d($).*body!ms, + 'index and map with side effects'); + +} + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +like(http_get('/shrink/'), qr!X-URI: /shrink/index.html\x0d?($).*body!ms, + 'index and map with shrink side effect'); + +} + $t->stop(); like($t->read_file('log_not_found.log'), qr/error/, 'log_not_found'); 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/)->plan(2) +my $t = Test::Nginx->new()->has(qw/http proxy rewrite map/)->plan(4) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -34,6 +34,10 @@ events { http { %%TEST_GLOBALS_HTTP%% + map $uri $map_capture { + ~(?.*) $capture; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -58,8 +62,19 @@ http { return 204; } + location /map { + proxy_pass http://127.0.0.1:8080/body; + proxy_set_body "body $capture $map_capture end"; + } + + location /map_header { + proxy_pass http://127.0.0.1:8080/body; + proxy_set_header X-Header "header $capture $map_capture end"; + } + location /body { add_header X-Body $request_body; + add_header X-Header $http_x_header; proxy_pass http://127.0.0.1:8080/empty; } @@ -78,4 +93,17 @@ EOF like(http_get('/'), qr/X-Body: body/, 'proxy_set_body'); like(http_get('/p1'), qr/X-Body: body two/, 'proxy_set_body twice'); +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(); + +like(http_get('/map'), qr!X-Body: body .* /map end!, + 'proxy_set_body and map with side effects'); +like(http_get('/map_header'), qr!X-Header: header .* /map_header end!, + 'proxy_set_header and map with side effects'); + +} + ############################################################################### diff --git a/scgi.t b/scgi.t --- a/scgi.t +++ b/scgi.t @@ -24,7 +24,7 @@ select STDOUT; $| = 1; eval { require SCGI; }; plan(skip_all => 'SCGI not installed') if $@; -my $t = Test::Nginx->new()->has(qw/http scgi/)->plan(10) +my $t = Test::Nginx->new()->has(qw/http scgi rewrite map/)->plan(11) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -41,6 +41,10 @@ http { server 127.0.0.1:8081; } + map $uri $map_capture { + ~(?.*) $capture; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -58,6 +62,12 @@ http { scgi_param REQUEST_URI $request_uri; } + location /map/ { + scgi_pass 127.0.0.1:8081; + scgi_param SCGI 1; + scgi_param REQUEST_URI $request_uri; + scgi_param HTTP_FOO "foo $capture $map_capture end"; + } } } @@ -103,6 +113,17 @@ like($r, qr/X-Cookie: foo; bar; bazz/, like($r, qr/X-Foo: foo, bar, bazz/, 'scgi with multiple unknown headers'); +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(); + +like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, + 'scgi params and map with side effects'); + +} + ############################################################################### sub http_get_headers { diff --git a/uwsgi.t b/uwsgi.t --- a/uwsgi.t +++ b/uwsgi.t @@ -21,7 +21,8 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http uwsgi/)->has_daemon('uwsgi')->plan(8) +my $t = Test::Nginx->new() + ->has(qw/http uwsgi rewrite map/)->has_daemon('uwsgi')->plan(9) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -38,6 +39,10 @@ http { server 127.0.0.1:8081; } + map $uri $map_capture { + ~(?.*) $capture; + } + server { listen 127.0.0.1:8080; server_name localhost; @@ -52,6 +57,12 @@ http { uwsgi_pass $arg_b; uwsgi_param SERVER_PROTOCOL $server_protocol; } + + location /map/ { + uwsgi_pass 127.0.0.1:8081; + uwsgi_param SERVER_PROTOCOL $server_protocol; + uwsgi_param HTTP_FOO "foo $capture $map_capture end"; + } } } @@ -124,6 +135,17 @@ like($r, qr/X-Cookie: foo; bar; bazz/, like($r, qr/X-Foo: foo, bar, bazz/, 'uwsgi with multiple unknown headers'); +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(); + +like(http_get('/map/test-long-uri'), qr!foo .* /map/test-long-uri end!, + 'uwsgi params and map with side effects'); + +} + ############################################################################### sub http_get_headers { From mdounin at mdounin.ru Mon Jun 8 17:46:42 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 08 Jun 2026 20:46:42 +0300 Subject: [PATCH 4 of 4] Tests: access_log variable evaluation tests In-Reply-To: References: Message-ID: <2cbaa4b88c1dc8a7c3b3.1780940802@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1780936534 -10800 # Mon Jun 08 19:35:34 2026 +0300 # Node ID 2cbaa4b88c1dc8a7c3b329c220daa4e5a9c92b18 # Parent e2ccdc5e3d031950a1a833b372791812902cc365 Tests: access_log variable evaluation tests. The access log module uses its own script engine, which, however, suffers from similar issues when trying to use variables with side effects, or non-cacheable variables along with variables which use ngx_http_script_run() and therefore flush all other non-cacheable variables ($document_root, $realpath_root). diff --git a/access_log_script.t b/access_log_script.t new file mode 100644 --- /dev/null +++ b/access_log_script.t @@ -0,0 +1,95 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for access_log, script execution. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(2) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + log_format map_capture "start $capture $map_capture end"; + log_format map_volatile + "start $map_volatile $document_root $realpath_root end"; + + map $uri $map_capture { + ~(?.*) $capture; + } + + map prefix:$capture $map_volatile { + volatile; + ~(?.*) $capture; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /map { + access_log %%TESTDIR%%/map.log map_capture; + } + + location /map_volatile { + root html/$pid; + access_log %%TESTDIR%%/map.log map_volatile; + } + } +} + +EOF + +$t->run(); + +############################################################################### + +TODO: { +todo_skip 'might coredump', 2 + unless $ENV{TEST_NGINX_UNSAFE}; +local $TODO = 'not yet'; + +# map with side effects might result in incorrect buffer size +# and buffer overrun + +http_get('/map'); + +# using a non-cacheable variable might result in incorrect buffer +# size and buffer overrun + +http_get('/map_volatile'); + +$t->stop(); + +my $log = $t->read_file('map.log'); + +like($log, qr!start /map /map end!, 'log and map with side effects'); +like($log, qr!start prefix: .* end!, 'log and volatile map'); + +} + +############################################################################### From mdounin at mdounin.ru Wed Jun 10 10:36:36 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 10 Jun 2026 13:36:36 +0300 Subject: [PATCH] Mail: updated ngx_mail_send() to set handlers on s->quit Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781087610 -10800 # Wed Jun 10 13:33:30 2026 +0300 # Node ID e6ef0412a95a5e9e2bd7243978750475f07f0377 # Parent 67059a7429f113a0af1e0b7d21ff8bf7ef4ac9f7 Mail: updated ngx_mail_send() to set handlers on s->quit. Most notably, this fixes potential issues when a fatal error message is sent with ngx_mail_send() with s->quit set, but the read handler is not updated. Despite s->quit, ngx_mail_send() might not be able to send the error message immediately, and the read handler might be called on read events. In particular, in ngx_mail_auth_http_block_read() and in ngx_mail_proxy_block_read() this could potentially cause accesses to freed memory or already closed connections in ngx_handle_read_event() error handling code paths. Reported by Evan Hellman, https://github.com/freenginx/nginx/issues/23 https://github.com/freenginx/nginx/issues/24 diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -25,6 +25,7 @@ static ngx_int_t ngx_mail_verify_cert(ng static void ngx_mail_lingering_close(ngx_connection_t *c); static void ngx_mail_lingering_close_handler(ngx_event_t *rev); static void ngx_mail_empty_handler(ngx_event_t *wev); +static void ngx_mail_block_read(ngx_event_t *rev); void @@ -419,8 +420,6 @@ ngx_mail_verify_cert(ngx_mail_session_t s->out = cscf->protocol->cert_error; s->quit = 1; - c->write->handler = ngx_mail_send; - ngx_mail_send(s->connection->write); return NGX_ERROR; } @@ -440,8 +439,6 @@ ngx_mail_verify_cert(ngx_mail_session_t s->out = cscf->protocol->no_cert; s->quit = 1; - c->write->handler = ngx_mail_send; - ngx_mail_send(s->connection->write); return NGX_ERROR; } @@ -1088,6 +1085,11 @@ ngx_mail_send(ngx_event_t *wev) delay = (ngx_msec_t) (excess * 1000 / cscf->limit_rate + 1); ngx_add_timer(wev, delay); + if (s->quit) { + c->read->handler = ngx_mail_block_read; + c->write->handler = ngx_mail_send; + } + if (ngx_handle_write_event(wev, 0) != NGX_OK) { ngx_mail_close_connection(c); } @@ -1141,6 +1143,11 @@ again: ngx_add_timer(wev, cscf->timeout); } + if (s->quit) { + c->read->handler = ngx_mail_block_read; + c->write->handler = ngx_mail_send; + } + if (ngx_handle_write_event(wev, 0) != NGX_OK) { ngx_mail_close_connection(c); return; @@ -1422,6 +1429,17 @@ ngx_mail_empty_handler(ngx_event_t *wev) } +static void +ngx_mail_block_read(ngx_event_t *rev) +{ + ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0, "mail block read"); + + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_mail_close_connection(rev->data); + } +} + + void ngx_mail_close_connection(ngx_connection_t *c) { From mdounin at mdounin.ru Wed Jun 10 10:37:33 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 10 Jun 2026 13:37:33 +0300 Subject: [PATCH] Mail: fixed missing returns in error handling Message-ID: <24b7d1e523493753729c.1781087853@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1781087835 -10800 # Wed Jun 10 13:37:15 2026 +0300 # Node ID 24b7d1e523493753729c6f77601f96ae22240404 # Parent e6ef0412a95a5e9e2bd7243978750475f07f0377 Mail: fixed missing returns in error handling. diff --git a/src/mail/ngx_mail_imap_handler.c b/src/mail/ngx_mail_imap_handler.c --- a/src/mail/ngx_mail_imap_handler.c +++ b/src/mail/ngx_mail_imap_handler.c @@ -48,6 +48,7 @@ ngx_mail_imap_init_session(ngx_mail_sess if (ngx_handle_read_event(c->read, 0) != NGX_OK) { ngx_mail_close_connection(c); + return; } ngx_mail_send(c->write); diff --git a/src/mail/ngx_mail_pop3_handler.c b/src/mail/ngx_mail_pop3_handler.c --- a/src/mail/ngx_mail_pop3_handler.c +++ b/src/mail/ngx_mail_pop3_handler.c @@ -69,6 +69,7 @@ ngx_mail_pop3_init_session(ngx_mail_sess if (ngx_handle_read_event(c->read, 0) != NGX_OK) { ngx_mail_close_connection(c); + return; } ngx_mail_send(c->write); From mdounin at mdounin.ru Wed Jun 10 16:32:56 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 10 Jun 2026 19:32:56 +0300 Subject: [PATCH] Gzip: fixed duplicate ngx_pfree() on ctx->preallocated Message-ID: <9647f416bbae2c122d3a.1781109176@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1781106967 -10800 # Wed Jun 10 18:56:07 2026 +0300 # Node ID 9647f416bbae2c122d3a68976914b7228a8cf15f # Parent 24b7d1e523493753729c6f77601f96ae22240404 Gzip: fixed duplicate ngx_pfree() on ctx->preallocated. Previously, the gzip filter called ngx_pfree() on the ctx->preallocated pointer in ngx_http_gzip_filter_deflate_end() when compression is finished, but did not clear it. As a result, ngx_pfree() might be called again in the ngx_http_gzip_body_filter() error handling code path if an error happened either in ngx_http_gzip_filter_deflate_end() during allocation of a chain link, or anywhere in the next body filters when sending the last part of the response. Potentially, this might cause issues if the same address is used by a large pool allocation in the next body filters (unlikely in practice though, especially given that standard body filters running after the gzip filter don't do any large pool allocations). The fix is to clear ctx->preallocated after it is freed in ngx_http_gzip_filter_deflate_end(). Reported by Evan Hellman, https://github.com/freenginx/nginx/issues/25 diff --git a/src/http/modules/ngx_http_gzip_filter_module.c b/src/http/modules/ngx_http_gzip_filter_module.c --- a/src/http/modules/ngx_http_gzip_filter_module.c +++ b/src/http/modules/ngx_http_gzip_filter_module.c @@ -902,6 +902,7 @@ ngx_http_gzip_filter_deflate_end(ngx_htt } ngx_pfree(r->pool, ctx->preallocated); + ctx->preallocated = NULL; cl = ngx_alloc_chain_link(r->pool); if (cl == NULL) { From mdounin at mdounin.ru Thu Jun 11 13:17:44 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 11 Jun 2026 16:17:44 +0300 Subject: [PATCH] HTTP/3: fixed NGX_HTTP_V3_VARLEN_INT_LEN value Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781172660 -10800 # Thu Jun 11 13:11:00 2026 +0300 # Node ID ba68cb5eeb1a46c189a1baef5f4b3b083a2f77e0 # Parent 9647f416bbae2c122d3a68976914b7228a8cf15f HTTP/3: fixed NGX_HTTP_V3_VARLEN_INT_LEN value. This change was missed in 8349:b13176e717ba, and in theory might cause 1-byte buffer overwrite in ngx_http_v3_body_filter() if memory buffers larger than 1G are used (which is unlikely in practice). See also: https://github.com/freenginx/nginx/issues/21 https://github.com/nginx/nginx/commit/0f9f43b79eed64ab1a876be76ff0f49d499784fc diff --git a/src/http/v3/ngx_http_v3.h b/src/http/v3/ngx_http_v3.h --- a/src/http/v3/ngx_http_v3.h +++ b/src/http/v3/ngx_http_v3.h @@ -23,7 +23,7 @@ #define NGX_HTTP_V3_HQ_ALPN_PROTO "\x0Ahq-interop" #define NGX_HTTP_V3_HQ_PROTO "hq-interop" -#define NGX_HTTP_V3_VARLEN_INT_LEN 4 +#define NGX_HTTP_V3_VARLEN_INT_LEN 8 #define NGX_HTTP_V3_PREFIX_INT_LEN 11 #define NGX_HTTP_V3_STREAM_CONTROL 0x00 From mdounin at mdounin.ru Sat Jun 13 10:41:26 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 13 Jun 2026 13:41:26 +0300 Subject: [PATCH 1 of 3] HTTP/2: improved error messages about too long headers Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781346963 -10800 # Sat Jun 13 13:36:03 2026 +0300 # Node ID e83db11197c688c132055e5ff0fef582890a848e # Parent ba68cb5eeb1a46c189a1baef5f4b3b083a2f77e0 HTTP/2: improved error messages about too long headers. Error logging is limited to NGX_MAX_ERROR_STR (2048 bytes), and trying to fully log headers which are longer than NGX_HTTP_V2_MAX_FIELD (~2 megabytes) doesn't make sense and will hide important details about the request if the error will indeed happen. Instead, we now log only first 256 bytes of the too long name or value. diff --git a/src/http/v2/ngx_http_v2_filter_module.c b/src/http/v2/ngx_http_v2_filter_module.c --- a/src/http/v2/ngx_http_v2_filter_module.c +++ b/src/http/v2/ngx_http_v2_filter_module.c @@ -370,15 +370,15 @@ ngx_http_v2_header_filter(ngx_http_reque if (header[i].key.len > NGX_HTTP_V2_MAX_FIELD) { ngx_log_error(NGX_LOG_CRIT, fc->log, 0, - "too long response header name: \"%V\"", - &header[i].key); + "too long response header name: \"%*s...\"", + 256, header[i].key.data); return NGX_ERROR; } if (header[i].value.len > NGX_HTTP_V2_MAX_FIELD) { ngx_log_error(NGX_LOG_CRIT, fc->log, 0, - "too long response header value: \"%V: %V\"", - &header[i].key, &header[i].value); + "too long response header value: \"%V: %*s...\"", + &header[i].key, 256, header[i].value.data); return NGX_ERROR; } @@ -778,15 +778,15 @@ ngx_http_v2_create_trailers_frame(ngx_ht if (header[i].key.len > NGX_HTTP_V2_MAX_FIELD) { ngx_log_error(NGX_LOG_CRIT, fc->log, 0, - "too long response trailer name: \"%V\"", - &header[i].key); + "too long response trailer name: \"%*s...\"", + 256, header[i].key.data); return NULL; } if (header[i].value.len > NGX_HTTP_V2_MAX_FIELD) { ngx_log_error(NGX_LOG_CRIT, fc->log, 0, - "too long response trailer value: \"%V: %V\"", - &header[i].key, &header[i].value); + "too long response trailer value: \"%V: %*s...\"", + &header[i].key, 256, header[i].value.data); return NULL; } From mdounin at mdounin.ru Sat Jun 13 10:41:27 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 13 Jun 2026 13:41:27 +0300 Subject: [PATCH 2 of 3] HTTP/2: length checking of Content-Type and Location headers In-Reply-To: References: Message-ID: <95c3ef836da866dcdd63.1781347287@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1781347019 -10800 # Sat Jun 13 13:36:59 2026 +0300 # Node ID 95c3ef836da866dcdd63c611732e2abccd9fc935 # Parent e83db11197c688c132055e5ff0fef582890a848e HTTP/2: length checking of Content-Type and Location headers. When serializing response headers, HTTP/2 reserves NGX_HTTP_V2_INT_OCTETS (4 bytes) for string lengths, and this implies that strings up to NGX_HTTP_V2_MAX_FIELD (~2 megabytes) can be used. Serializing a longer string results in additional bytes being used for the string length, potentially resulting in buffer overrun (though unlikely with reasonable buffer sizes, as it uses only 1 extra byte for lengths up to ~256 megabytes). Longer strings are properly rejected when serializing headers from the r->headers_out.headers list and trailers from the r->headers_out.trailers list, but Content-Type and Location values weren't checked. With this change, these are checked as well. See also: https://github.com/freenginx/nginx/issues/28 https://github.com/nginx/nginx/commit/58a7bc3406ac8b9dc0e0afafc69ba42df56009e3 diff --git a/src/http/v2/ngx_http_v2_filter_module.c b/src/http/v2/ngx_http_v2_filter_module.c --- a/src/http/v2/ngx_http_v2_filter_module.c +++ b/src/http/v2/ngx_http_v2_filter_module.c @@ -237,6 +237,15 @@ ngx_http_v2_header_filter(ngx_http_reque } if (r->headers_out.content_type.len) { + + if (r->headers_out.content_type.len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, fc->log, 0, + "too long response header value: " + "\"Content-Type: %*s...\"", + 256, r->headers_out.content_type.data); + return NGX_ERROR; + } + len += 1 + NGX_HTTP_V2_INT_OCTETS + r->headers_out.content_type.len; if (r->headers_out.content_type_len == r->headers_out.content_type.len @@ -333,6 +342,14 @@ ngx_http_v2_header_filter(ngx_http_reque r->headers_out.location->hash = 0; + if (r->headers_out.location->value.len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, fc->log, 0, + "too long response header value: " + "\"Location: %*s...\"", + 256, r->headers_out.location->value.data); + return NGX_ERROR; + } + len += 1 + NGX_HTTP_V2_INT_OCTETS + r->headers_out.location->value.len; } From mdounin at mdounin.ru Sat Jun 13 10:41:28 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 13 Jun 2026 13:41:28 +0300 Subject: [PATCH 3 of 3] gRPC: length checking of request headers In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781347140 -10800 # Sat Jun 13 13:39:00 2026 +0300 # Node ID fe5ef747dc2e471967f816f36360013c5a7aa215 # Parent 95c3ef836da866dcdd63c611732e2abccd9fc935 gRPC: length checking of request headers. Similarly to HTTP/2, when serializing request headers, gRPC module reserves NGX_HTTP_V2_INT_OCTETS (4 bytes) for string lengths, and this implies that strings up to NGX_HTTP_V2_MAX_FIELD (~2 megabytes) can be used. Serializing a longer string results in additional bytes being used for the string length, potentially resulting in buffer overrun (though unlikely with reasonable buffer sizes, as it uses only 1 extra byte for lengths up to ~256 megabytes). Previously, request headers weren't checked in gRPC. With this change, all headers are properly checked. Reported by Evan Hellman, https://github.com/freenginx/nginx/issues/28 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 @@ -916,6 +916,15 @@ ngx_http_grpc_create_request(ngx_http_re "grpc header: \":method: POST\""); } else { + + if (r->method_name.len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "too long grpc request header value: " + "\":method: %*s...\"", + 256, r->method_name.data); + return NGX_ERROR; + } + *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_METHOD_INDEX); b->last = ngx_http_v2_write_value(b->last, r->method_name.data, r->method_name.len, tmp); @@ -941,6 +950,14 @@ ngx_http_grpc_create_request(ngx_http_re if (r->valid_unparsed_uri) { + if (r->unparsed_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->unparsed_uri.data); + return NGX_ERROR; + } + if (r->unparsed_uri.len == 1 && r->unparsed_uri.data[0] == '/') { *b->last++ = ngx_http_v2_indexed(NGX_HTTP_V2_PATH_ROOT_INDEX); @@ -969,6 +986,14 @@ ngx_http_grpc_create_request(ngx_http_re p = ngx_copy(p, r->args.data, r->args.len); } + if (p - val_tmp > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "too long grpc request header value: " + "\":path: %*s...\"", + 256, val_tmp); + return NGX_ERROR; + } + *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_PATH_INDEX); b->last = ngx_http_v2_write_value(b->last, val_tmp, p - val_tmp, tmp); @@ -976,6 +1001,15 @@ ngx_http_grpc_create_request(ngx_http_re "grpc header: \":path: %*s\"", p - val_tmp, val_tmp); } else { + + if (r->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); + 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); @@ -985,6 +1019,15 @@ ngx_http_grpc_create_request(ngx_http_re } if (!glcf->host_set) { + + if (ctx->host.len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "too long grpc request header value: " + "\":authority: %*s...\"", + 256, ctx->host.data); + return NGX_ERROR; + } + *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_AUTHORITY_INDEX); b->last = ngx_http_v2_write_value(b->last, ctx->host.data, ctx->host.len, tmp); @@ -1045,6 +1088,14 @@ ngx_http_grpc_create_request(ngx_http_re return NGX_ERROR; } + if (key_len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "too long grpc request header name: " + "\"%*s...\"", + 256, key_tmp); + return NGX_ERROR; + } + b->last = ngx_http_v2_write_name(b->last, key_tmp, key_len, tmp); e.pos = val_tmp; @@ -1066,6 +1117,14 @@ ngx_http_grpc_create_request(ngx_http_re return NGX_ERROR; } + if (val_len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "too long grpc request header value: " + "\"%*s: %*s...\"", + key_len, key_tmp, 256, val_tmp); + return NGX_ERROR; + } + b->last = ngx_http_v2_write_value(b->last, val_tmp, val_len, tmp); #if (NGX_DEBUG) @@ -1103,6 +1162,22 @@ ngx_http_grpc_create_request(ngx_http_re *b->last++ = 0; + if (header[i].key.len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "too long grpc request header name: " + "\"%*s...\"", + 256, header[i].key.data); + return NGX_ERROR; + } + + if (header[i].value.len > NGX_HTTP_V2_MAX_FIELD) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "too long grpc request header value: " + "\"%V: %*s...\"", + &header[i].key, 256, header[i].value.data); + return NGX_ERROR; + } + b->last = ngx_http_v2_write_name(b->last, header[i].key.data, header[i].key.len, tmp); From mdounin at mdounin.ru Sat Jun 13 10:43:48 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 13 Jun 2026 13:43:48 +0300 Subject: [PATCH] Tests: HTTP/2 response header length checking tests In-Reply-To: <95c3ef836da866dcdd63.1781347287@vm-bsd.mdounin.ru> References: <95c3ef836da866dcdd63.1781347287@vm-bsd.mdounin.ru> Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781347331 -10800 # Sat Jun 13 13:42:11 2026 +0300 # Node ID e0642736380fdbce13153863dc0c41faf026bdc2 # Parent 2cbaa4b88c1dc8a7c3b329c220daa4e5a9c92b18 Tests: HTTP/2 response header length checking tests. diff --git a/h2_headers.t b/h2_headers.t --- a/h2_headers.t +++ b/h2_headers.t @@ -23,7 +23,7 @@ use Test::Nginx::HTTP2; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http http_v2 proxy rewrite/)->plan(103) +my $t = Test::Nginx->new()->has(qw/http http_v2 proxy rewrite/)->plan(105) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -87,6 +87,23 @@ http { add_header X-Cookie-c $cookie_c; return 200; } + location /long_value { + set $a $args; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + add_header X-Long $a; + return 200; + } + location /long_location { + set $a $args; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + return 302 $a; + } } server { @@ -624,6 +641,29 @@ is($frame->{headers}->{'x-uc-a'}, 'b', is($frame->{headers}->{'x-uc-c'}, 'd', 'multiple response header proxied - upstream cookie 2'); +# too long response header value + +$s = Test::Nginx::HTTP2->new(); +$sid = $s->new_stream({ path => '/long_value?' . ('~' x 210) }); +$frames = $s->read(all => [{ type => 'RST_STREAM' }], wait => 0.5); + +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; +is($frame->{code}, 2, 'too long response header value'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +# too long Location response header value + +$s = Test::Nginx::HTTP2->new(); +$sid = $s->new_stream({ path => '/long_location?' . ('~' x 210) }); +$frames = $s->read(all => [{ type => 'RST_STREAM' }], wait => 0.5); + +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; +is($frame->{code}, 2, 'too long response location'); + +} + # CONTINUATION in response # put three long header fields (not less than SETTINGS_MAX_FRAME_SIZE/2) # to break header block into separate frames, one such field per frame From mdounin at mdounin.ru Sat Jun 13 10:45:53 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 13 Jun 2026 13:45:53 +0300 Subject: [PATCH] Tests: gRPC header length checking tests In-Reply-To: References: Message-ID: <2a4ea0fc3e05cb238036.1781347553@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1781347442 -10800 # Sat Jun 13 13:44:02 2026 +0300 # Node ID 2a4ea0fc3e05cb238036959e6d473d453d61c1ba # Parent e0642736380fdbce13153863dc0c41faf026bdc2 Tests: gRPC header length checking tests. diff --git a/grpc_headers.t b/grpc_headers.t --- a/grpc_headers.t +++ b/grpc_headers.t @@ -10,6 +10,7 @@ use warnings; use strict; use Test::More; +use Socket qw/ CRLF /; BEGIN { use FindBin; chdir($FindBin::Bin); } @@ -22,7 +23,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new() - ->has(qw/http http_v2 grpc rewrite/)->plan(2) + ->has(qw/http http_v2 grpc rewrite/)->plan(7) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -39,6 +40,9 @@ http { ~(?.*) $capture; } + large_client_header_buffers 2 4m; + ignore_invalid_headers off; + server { listen 127.0.0.1:8080; server_name localhost; @@ -58,6 +62,28 @@ http { grpc_set_header X-Blah6 "$capture"; grpc_set_header X-Blah "blah $map_capture end"; } + + location /long { + grpc_pass 127.0.0.1:8081; + grpc_set_header Host ""; + grpc_set_header TE ""; + grpc_set_header Content-Length ""; + grpc_set_header Connection ""; + } + + location /long_set { + grpc_pass 127.0.0.1:8081; + grpc_set_header Host ""; + grpc_set_header TE ""; + grpc_set_header Content-Length ""; + grpc_set_header Connection ""; + grpc_set_header X-Long $a; + set $a $args; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + set $a $a$a$a$a$a$a$a$a$a$a; + } } server { @@ -92,4 +118,41 @@ like(http_get('/multi'), qr!blah /multi } +TODO: { +todo_skip 'might coredump', 4 + unless $t->has_version('1.31.3') + or $ENV{TEST_NGINX_UNSAFE}; +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +# gRPC module reserves NGX_HTTP_V2_INT_OCTETS (4 bytes) for string lengths, +# and strings longer than NGX_HTTP_V2_MAX_FIELD (~2 megabytes) will use +# one more byte for string length + +like(http_get('/long?' . ('~' x 2097279)), qr!HTTP/1.!, 'too long :path'); + +like(http('GE' . ('X' x 2097279) . ' /long?' . ('~' x 16513) + . ' HTTP/1.0' . CRLF . CRLF), + qr!HTTP/1.!, 'too long :method'); + +like(http('GET /long?' . ('~' x 16513) . ' HTTP/1.0' . CRLF + . 'F' . ('~' x 2097279) . ': ' . ('~' x 16513) . CRLF . CRLF), + qr!HTTP/1.!, 'too long header name'); + +like(http('GET /long?' . ('~' x 16513) . ' HTTP/1.0' . CRLF + . 'F' . ('~' x 16513) . ': ' . ('~' x 2097279) . CRLF . CRLF), + qr!HTTP/1.!, 'too long header value'); + +} + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +# this cannot overflow, since header name is short, but still has +# to be rejected + +like(http_get('/long_set?' . ('~' x 210)), qr!500 Internal!, + 'too long set header value'); + +} + ############################################################################### From mdounin at mdounin.ru Sun Jun 14 20:59:12 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 14 Jun 2026 23:59:12 +0300 Subject: [PATCH] Access log: buffer overrun protection Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781464439 -10800 # Sun Jun 14 22:13:59 2026 +0300 # Node ID ec34a87ad20c74ac8ed95ddff647fb03aaf48beb # Parent fe5ef747dc2e471967f816f36360013c5a7aa215 Access log: buffer overrun protection. Similarly to generic script operations, access log script copy operations now check if there is enough room in the buffer. diff --git a/src/http/modules/ngx_http_log_module.c b/src/http/modules/ngx_http_log_module.c --- a/src/http/modules/ngx_http_log_module.c +++ b/src/http/modules/ngx_http_log_module.c @@ -17,7 +17,7 @@ typedef struct ngx_http_log_op_s ngx_http_log_op_t; typedef u_char *(*ngx_http_log_op_run_pt) (ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); typedef size_t (*ngx_http_log_op_getlen_pt) (ngx_http_request_t *r, uintptr_t data); @@ -112,39 +112,42 @@ static void ngx_http_log_flush(ngx_open_ static void ngx_http_log_flush_handler(ngx_event_t *ev); static u_char *ngx_http_log_pipe(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static u_char *ngx_http_log_time(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static u_char *ngx_http_log_iso8601(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static u_char *ngx_http_log_msec(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static u_char *ngx_http_log_request_time(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static u_char *ngx_http_log_status(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static u_char *ngx_http_log_bytes_sent(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); -static u_char *ngx_http_log_body_bytes_sent(ngx_http_request_t *r, - u_char *buf, ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); +static u_char *ngx_http_log_body_bytes_sent(ngx_http_request_t *r, u_char *buf, + u_char *end, ngx_http_log_op_t *op); static u_char *ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static ngx_int_t ngx_http_log_variable_compile(ngx_conf_t *cf, ngx_http_log_op_t *op, ngx_str_t *value, ngx_uint_t escape); static size_t ngx_http_log_variable_getlen(ngx_http_request_t *r, uintptr_t data); static u_char *ngx_http_log_variable(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static uintptr_t ngx_http_log_escape(u_char *dst, u_char *src, size_t size); static size_t ngx_http_log_json_variable_getlen(ngx_http_request_t *r, uintptr_t data); static u_char *ngx_http_log_json_variable(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); + u_char *end, ngx_http_log_op_t *op); static size_t ngx_http_log_unescaped_variable_getlen(ngx_http_request_t *r, uintptr_t data); static u_char *ngx_http_log_unescaped_variable(ngx_http_request_t *r, - u_char *buf, ngx_http_log_op_t *op); + u_char *buf, u_char *end, ngx_http_log_op_t *op); + +static ngx_int_t ngx_http_log_check_length(ngx_http_request_t *r, + u_char *buf, u_char *end, size_t len); static void *ngx_http_log_create_main_conf(ngx_conf_t *cf); @@ -253,7 +256,7 @@ static ngx_http_log_var_t ngx_http_log_ static ngx_int_t ngx_http_log_handler(ngx_http_request_t *r) { - u_char *line, *p; + u_char *line, *p, *end; size_t len; ngx_str_t val; ngx_uint_t i, l; @@ -308,6 +311,8 @@ ngx_http_log_handler(ngx_http_request_t } } + len += NGX_LINEFEED_SIZE; + if (log[l].syslog_peer) { /* length of syslog's PRI and HEADER message parts */ @@ -318,8 +323,6 @@ ngx_http_log_handler(ngx_http_request_t goto alloc_line; } - len += NGX_LINEFEED_SIZE; - buffer = log[l].file ? log[l].file->data : NULL; if (buffer) { @@ -335,13 +338,18 @@ ngx_http_log_handler(ngx_http_request_t if (len <= (size_t) (buffer->last - buffer->pos)) { p = buffer->pos; + end = p + len - NGX_LINEFEED_SIZE; if (buffer->event && p == buffer->start) { ngx_add_timer(buffer->event, buffer->flush); } - for (i = 0; i < log[l].format->ops->nelts; i++) { - p = op[i].run(r, p, &op[i]); + for (i = 0; i < log[l].format->ops->nelts && p; i++) { + p = op[i].run(r, p, end, &op[i]); + } + + if (p == NULL) { + return NGX_ERROR; } ngx_linefeed(p); @@ -364,13 +372,18 @@ ngx_http_log_handler(ngx_http_request_t } p = line; + end = line + len - NGX_LINEFEED_SIZE; if (log[l].syslog_peer) { p = ngx_syslog_add_header(log[l].syslog_peer, line); } - for (i = 0; i < log[l].format->ops->nelts; i++) { - p = op[i].run(r, p, &op[i]); + for (i = 0; i < log[l].format->ops->nelts && p; i++) { + p = op[i].run(r, p, end, &op[i]); + } + + if (p == NULL) { + return NGX_ERROR; } if (log[l].syslog_peer) { @@ -757,7 +770,7 @@ ngx_http_log_flush_handler(ngx_event_t * static u_char * -ngx_http_log_copy_short(ngx_http_request_t *r, u_char *buf, +ngx_http_log_copy_short(ngx_http_request_t *r, u_char *buf, u_char *end, ngx_http_log_op_t *op) { size_t len; @@ -766,6 +779,10 @@ ngx_http_log_copy_short(ngx_http_request len = op->len; data = op->data; + if (ngx_http_log_check_length(r, buf, end, len) != NGX_OK) { + return NULL; + } + while (len--) { *buf++ = (u_char) (data & 0xff); data >>= 8; @@ -776,16 +793,25 @@ ngx_http_log_copy_short(ngx_http_request static u_char * -ngx_http_log_copy_long(ngx_http_request_t *r, u_char *buf, +ngx_http_log_copy_long(ngx_http_request_t *r, u_char *buf, u_char *end, ngx_http_log_op_t *op) { + if (ngx_http_log_check_length(r, buf, end, op->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, (u_char *) op->data, op->len); } static u_char * -ngx_http_log_pipe(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) +ngx_http_log_pipe(ngx_http_request_t *r, u_char *buf, u_char *end, + ngx_http_log_op_t *op) { + if (ngx_http_log_check_length(r, buf, end, 1) != NGX_OK) { + return NULL; + } + if (r->pipeline) { *buf = 'p'; } else { @@ -797,24 +823,43 @@ ngx_http_log_pipe(ngx_http_request_t *r, static u_char * -ngx_http_log_time(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) +ngx_http_log_time(ngx_http_request_t *r, u_char *buf, u_char *end, + ngx_http_log_op_t *op) { + if (ngx_http_log_check_length(r, buf, end, ngx_cached_http_log_time.len) + != NGX_OK) + { + return NULL; + } + return ngx_cpymem(buf, ngx_cached_http_log_time.data, ngx_cached_http_log_time.len); } static u_char * -ngx_http_log_iso8601(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) +ngx_http_log_iso8601(ngx_http_request_t *r, u_char *buf, u_char *end, + ngx_http_log_op_t *op) { + if (ngx_http_log_check_length(r, buf, end, ngx_cached_http_log_iso8601.len) + != NGX_OK) + { + return NULL; + } + return ngx_cpymem(buf, ngx_cached_http_log_iso8601.data, ngx_cached_http_log_iso8601.len); } static u_char * -ngx_http_log_msec(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) +ngx_http_log_msec(ngx_http_request_t *r, u_char *buf, u_char *end, + ngx_http_log_op_t *op) { ngx_time_t *tp; + if (ngx_http_log_check_length(r, buf, end, NGX_TIME_T_LEN + 4) != NGX_OK) { + return NULL; + } + tp = ngx_timeofday(); return ngx_sprintf(buf, "%T.%03M", tp->sec, tp->msec); @@ -822,11 +867,15 @@ ngx_http_log_msec(ngx_http_request_t *r, static u_char * -ngx_http_log_request_time(ngx_http_request_t *r, u_char *buf, +ngx_http_log_request_time(ngx_http_request_t *r, u_char *buf, u_char *end, ngx_http_log_op_t *op) { ngx_msec_int_t ms; + if (ngx_http_log_check_length(r, buf, end, NGX_TIME_T_LEN + 4) != NGX_OK) { + return NULL; + } + ms = (ngx_msec_int_t) (ngx_current_msec - r->start_time); ms = ngx_max(ms, 0); @@ -835,10 +884,15 @@ ngx_http_log_request_time(ngx_http_reque static u_char * -ngx_http_log_status(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) +ngx_http_log_status(ngx_http_request_t *r, u_char *buf, u_char *end, + ngx_http_log_op_t *op) { ngx_uint_t status; + if (ngx_http_log_check_length(r, buf, end, NGX_INT_T_LEN) != NGX_OK) { + return NULL; + } + if (r->err_status) { status = r->err_status; @@ -857,9 +911,13 @@ ngx_http_log_status(ngx_http_request_t * static u_char * -ngx_http_log_bytes_sent(ngx_http_request_t *r, u_char *buf, +ngx_http_log_bytes_sent(ngx_http_request_t *r, u_char *buf, u_char *end, ngx_http_log_op_t *op) { + if (ngx_http_log_check_length(r, buf, end, NGX_OFF_T_LEN) != NGX_OK) { + return NULL; + } + return ngx_sprintf(buf, "%O", r->connection->sent); } @@ -870,11 +928,15 @@ ngx_http_log_bytes_sent(ngx_http_request */ static u_char * -ngx_http_log_body_bytes_sent(ngx_http_request_t *r, u_char *buf, +ngx_http_log_body_bytes_sent(ngx_http_request_t *r, u_char *buf, u_char *end, ngx_http_log_op_t *op) { off_t length; + if (ngx_http_log_check_length(r, buf, end, NGX_OFF_T_LEN) != NGX_OK) { + return NULL; + } + length = r->connection->sent - r->header_size; if (length > 0) { @@ -888,9 +950,13 @@ ngx_http_log_body_bytes_sent(ngx_http_re static u_char * -ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, +ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, u_char *end, ngx_http_log_op_t *op) { + if (ngx_http_log_check_length(r, buf, end, NGX_OFF_T_LEN) != NGX_OK) { + return NULL; + } + return ngx_sprintf(buf, "%O", r->request_length); } @@ -951,21 +1017,39 @@ ngx_http_log_variable_getlen(ngx_http_re static u_char * -ngx_http_log_variable(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) +ngx_http_log_variable(ngx_http_request_t *r, u_char *buf, u_char *end, + ngx_http_log_op_t *op) { + uintptr_t len; ngx_http_variable_value_t *value; value = ngx_http_get_indexed_variable(r, op->data); if (value == NULL || value->not_found) { + if (ngx_http_log_check_length(r, buf, end, 1) != NGX_OK) { + return NULL; + } + *buf = '-'; return buf + 1; } if (value->escape == 0) { + if (ngx_http_log_check_length(r, buf, end, value->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, value->data, value->len); } else { + len = ngx_http_log_escape(NULL, value->data, value->len); + + if (ngx_http_log_check_length(r, buf, end, value->len + len * 3) + != NGX_OK) + { + return NULL; + } + return (u_char *) ngx_http_log_escape(buf, value->data, value->len); } } @@ -1052,9 +1136,10 @@ ngx_http_log_json_variable_getlen(ngx_ht static u_char * -ngx_http_log_json_variable(ngx_http_request_t *r, u_char *buf, +ngx_http_log_json_variable(ngx_http_request_t *r, u_char *buf, u_char *end, ngx_http_log_op_t *op) { + uintptr_t len; ngx_http_variable_value_t *value; value = ngx_http_get_indexed_variable(r, op->data); @@ -1064,9 +1149,21 @@ ngx_http_log_json_variable(ngx_http_requ } if (value->escape == 0) { + if (ngx_http_log_check_length(r, buf, end, value->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, value->data, value->len); } else { + len = ngx_escape_json(NULL, value->data, value->len); + + if (ngx_http_log_check_length(r, buf, end, value->len + len) + != NGX_OK) + { + return NULL; + } + return (u_char *) ngx_escape_json(buf, value->data, value->len); } } @@ -1091,7 +1188,7 @@ ngx_http_log_unescaped_variable_getlen(n static u_char * ngx_http_log_unescaped_variable(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op) + u_char *end, ngx_http_log_op_t *op) { ngx_http_variable_value_t *value; @@ -1101,10 +1198,28 @@ ngx_http_log_unescaped_variable(ngx_http return buf; } + if (ngx_http_log_check_length(r, buf, end, value->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, value->data, value->len); } +static ngx_int_t +ngx_http_log_check_length(ngx_http_request_t *r, u_char *buf, u_char *end, + size_t len) +{ + if (end - buf < (ssize_t) len) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "no buffer space in log script copy"); + return NGX_ERROR; + } + + return NGX_OK; +} + + static void * ngx_http_log_create_main_conf(ngx_conf_t *cf) { diff --git a/src/stream/ngx_stream_log_module.c b/src/stream/ngx_stream_log_module.c --- a/src/stream/ngx_stream_log_module.c +++ b/src/stream/ngx_stream_log_module.c @@ -17,7 +17,7 @@ typedef struct ngx_stream_log_op_s ngx_stream_log_op_t; typedef u_char *(*ngx_stream_log_op_run_pt) (ngx_stream_session_t *s, - u_char *buf, ngx_stream_log_op_t *op); + u_char *buf, u_char *end, ngx_stream_log_op_t *op); typedef size_t (*ngx_stream_log_op_getlen_pt) (ngx_stream_session_t *s, uintptr_t data); @@ -115,16 +115,19 @@ static ngx_int_t ngx_stream_log_variable static size_t ngx_stream_log_variable_getlen(ngx_stream_session_t *s, uintptr_t data); static u_char *ngx_stream_log_variable(ngx_stream_session_t *s, u_char *buf, - ngx_stream_log_op_t *op); + u_char *end, ngx_stream_log_op_t *op); static uintptr_t ngx_stream_log_escape(u_char *dst, u_char *src, size_t size); static size_t ngx_stream_log_json_variable_getlen(ngx_stream_session_t *s, uintptr_t data); static u_char *ngx_stream_log_json_variable(ngx_stream_session_t *s, - u_char *buf, ngx_stream_log_op_t *op); + u_char *buf, u_char *end, ngx_stream_log_op_t *op); static size_t ngx_stream_log_unescaped_variable_getlen(ngx_stream_session_t *s, uintptr_t data); static u_char *ngx_stream_log_unescaped_variable(ngx_stream_session_t *s, - u_char *buf, ngx_stream_log_op_t *op); + u_char *buf, u_char *end, ngx_stream_log_op_t *op); + +static ngx_int_t ngx_stream_log_check_length(ngx_stream_session_t *s, + u_char *buf, u_char *end, size_t len); static void *ngx_stream_log_create_main_conf(ngx_conf_t *cf); @@ -200,7 +203,7 @@ ngx_module_t ngx_stream_log_module = { static ngx_int_t ngx_stream_log_handler(ngx_stream_session_t *s) { - u_char *line, *p; + u_char *line, *p, *end; size_t len; ngx_str_t val; ngx_uint_t i, l; @@ -256,6 +259,8 @@ ngx_stream_log_handler(ngx_stream_sessio } } + len += NGX_LINEFEED_SIZE; + if (log[l].syslog_peer) { /* length of syslog's PRI and HEADER message parts */ @@ -266,8 +271,6 @@ ngx_stream_log_handler(ngx_stream_sessio goto alloc_line; } - len += NGX_LINEFEED_SIZE; - buffer = log[l].file ? log[l].file->data : NULL; if (buffer) { @@ -283,13 +286,18 @@ ngx_stream_log_handler(ngx_stream_sessio if (len <= (size_t) (buffer->last - buffer->pos)) { p = buffer->pos; + end = p + len - NGX_LINEFEED_SIZE; if (buffer->event && p == buffer->start) { ngx_add_timer(buffer->event, buffer->flush); } - for (i = 0; i < log[l].format->ops->nelts; i++) { - p = op[i].run(s, p, &op[i]); + for (i = 0; i < log[l].format->ops->nelts && p; i++) { + p = op[i].run(s, p, end, &op[i]); + } + + if (p == NULL) { + return NGX_ERROR; } ngx_linefeed(p); @@ -312,13 +320,18 @@ ngx_stream_log_handler(ngx_stream_sessio } p = line; + end = p + len - NGX_LINEFEED_SIZE; if (log[l].syslog_peer) { p = ngx_syslog_add_header(log[l].syslog_peer, line); } - for (i = 0; i < log[l].format->ops->nelts; i++) { - p = op[i].run(s, p, &op[i]); + for (i = 0; i < log[l].format->ops->nelts && p; i++) { + p = op[i].run(s, p, end, &op[i]); + } + + if (p == NULL) { + return NGX_ERROR; } if (log[l].syslog_peer) { @@ -650,7 +663,7 @@ ngx_stream_log_flush_handler(ngx_event_t static u_char * -ngx_stream_log_copy_short(ngx_stream_session_t *s, u_char *buf, +ngx_stream_log_copy_short(ngx_stream_session_t *s, u_char *buf, u_char *end, ngx_stream_log_op_t *op) { size_t len; @@ -659,6 +672,10 @@ ngx_stream_log_copy_short(ngx_stream_ses len = op->len; data = op->data; + if (ngx_stream_log_check_length(s, buf, end, len) != NGX_OK) { + return NULL; + } + while (len--) { *buf++ = (u_char) (data & 0xff); data >>= 8; @@ -669,9 +686,13 @@ ngx_stream_log_copy_short(ngx_stream_ses static u_char * -ngx_stream_log_copy_long(ngx_stream_session_t *s, u_char *buf, +ngx_stream_log_copy_long(ngx_stream_session_t *s, u_char *buf, u_char *end, ngx_stream_log_op_t *op) { + if (ngx_stream_log_check_length(s, buf, end, op->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, (u_char *) op->data, op->len); } @@ -732,9 +753,10 @@ ngx_stream_log_variable_getlen(ngx_strea static u_char * -ngx_stream_log_variable(ngx_stream_session_t *s, u_char *buf, +ngx_stream_log_variable(ngx_stream_session_t *s, u_char *buf, u_char *end, ngx_stream_log_op_t *op) { + uintptr_t len; ngx_stream_variable_value_t *value; value = ngx_stream_get_indexed_variable(s, op->data); @@ -745,9 +767,21 @@ ngx_stream_log_variable(ngx_stream_sessi } if (value->escape == 0) { + if (ngx_stream_log_check_length(s, buf, end, value->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, value->data, value->len); } else { + len = ngx_stream_log_escape(NULL, value->data, value->len); + + if (ngx_stream_log_check_length(s, buf, end, value->len + len * 3) + != NGX_OK) + { + return NULL; + } + return (u_char *) ngx_stream_log_escape(buf, value->data, value->len); } } @@ -834,9 +868,10 @@ ngx_stream_log_json_variable_getlen(ngx_ static u_char * -ngx_stream_log_json_variable(ngx_stream_session_t *s, u_char *buf, +ngx_stream_log_json_variable(ngx_stream_session_t *s, u_char *buf, u_char *end, ngx_stream_log_op_t *op) { + uintptr_t len; ngx_stream_variable_value_t *value; value = ngx_stream_get_indexed_variable(s, op->data); @@ -846,9 +881,21 @@ ngx_stream_log_json_variable(ngx_stream_ } if (value->escape == 0) { + if (ngx_stream_log_check_length(s, buf, end, value->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, value->data, value->len); } else { + len = ngx_escape_json(NULL, value->data, value->len); + + if (ngx_stream_log_check_length(s, buf, end, value->len + len) + != NGX_OK) + { + return NULL; + } + return (u_char *) ngx_escape_json(buf, value->data, value->len); } } @@ -874,7 +921,7 @@ ngx_stream_log_unescaped_variable_getlen static u_char * ngx_stream_log_unescaped_variable(ngx_stream_session_t *s, u_char *buf, - ngx_stream_log_op_t *op) + u_char *end, ngx_stream_log_op_t *op) { ngx_stream_variable_value_t *value; @@ -884,10 +931,28 @@ ngx_stream_log_unescaped_variable(ngx_st return buf; } + if (ngx_stream_log_check_length(s, buf, end, value->len) != NGX_OK) { + return NULL; + } + return ngx_cpymem(buf, value->data, value->len); } +static ngx_int_t +ngx_stream_log_check_length(ngx_stream_session_t *s, u_char *buf, u_char *end, + size_t len) +{ + if (end - buf < (ssize_t) len) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, 0, + "no buffer space in log script copy"); + return NGX_ERROR; + } + + return NGX_OK; +} + + static void * ngx_stream_log_create_main_conf(ngx_conf_t *cf) { From mdounin at mdounin.ru Sun Jun 14 21:00:31 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 15 Jun 2026 00:00:31 +0300 Subject: [PATCH] Tests: access_log variable evaluation tests In-Reply-To: References: Message-ID: <2df9be66ac8aeb3db99f.1781470831@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1781470636 -10800 # Sun Jun 14 23:57:16 2026 +0300 # Node ID 2df9be66ac8aeb3db99f9d8bf8fedf6209ed833f # Parent 41adf25aa8f593212bec1d4ed7cc1f3270016b1a Tests: access_log variable evaluation tests. The access log module uses its own script engine, which, however, suffers from similar issues when trying to use variables with side effects, or non-cacheable variables along with variables which use ngx_http_script_run() and therefore flush all other non-cacheable variables ($document_root, $realpath_root). diff --git a/access_log_script.t b/access_log_script.t new file mode 100644 --- /dev/null +++ b/access_log_script.t @@ -0,0 +1,96 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for access_log, script execution. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite map/)->plan(2) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + log_format map_capture "start $capture $map_capture end"; + log_format map_volatile + "start $map_volatile $document_root $realpath_root end"; + + map $uri $map_capture { + ~(?.*) $capture; + } + + map prefix:$capture $map_volatile { + volatile; + ~(?.*) $capture; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /map { + access_log map.log map_capture; + } + + location /map_volatile { + root html/$pid; + access_log map.log map_volatile; + } + } +} + +EOF + +$t->run(); + +############################################################################### + +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(); + +# map with side effects might result in incorrect buffer size +# and buffer overrun + +http_get('/map'); + +# using a non-cacheable variable might result in incorrect buffer +# size and buffer overrun + +http_get('/map_volatile'); + +$t->stop(); + +my $log = $t->read_file('map.log'); + +like($log, qr!start /map /map end!, 'log and map with side effects'); +like($log, qr!start prefix: .* end!, 'log and volatile map'); + +} + +############################################################################### diff --git a/stream_access_log_script.t b/stream_access_log_script.t new file mode 100644 --- /dev/null +++ b/stream_access_log_script.t @@ -0,0 +1,77 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Stream tests for access_log, script execution. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/stream stream_map stream_return http rewrite/)->plan(1) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + log_format map_capture "start $capture $map_capture end"; + + map $pid $map_capture { + ~(?.*) $capture; + } + + server { + listen 127.0.0.1:8080; + return ok; + + access_log map.log map_capture; + } +} + +EOF + +$t->run(); + +############################################################################### + +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(); + +# map with side effects might result in incorrect buffer size +# and buffer overrun + +http_get('/'); + +$t->stop(); + +my $log = $t->read_file('map.log'); + +like($log, qr!start /map /map end!, 'log and map with side effects'); + +} + +############################################################################### From mdounin at mdounin.ru Thu Jun 18 22:30:42 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 19 Jun 2026 01:30:42 +0300 Subject: [PATCH] Charset: fixed handling of invalid UTF-8 characters Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781821809 -10800 # Fri Jun 19 01:30:09 2026 +0300 # Node ID a779d9d41aaa5ff1091f052d59beb9fb368f55cd # Parent ec34a87ad20c74ac8ed95ddff647fb03aaf48beb Charset: fixed handling of invalid UTF-8 characters. Previously, if an invalid UTF-8 character was split between buffers, ngx_decode_utf8() might return an error and a position within the bytes saved from the first buffer, but the following code did not expect this and might end up reading a byte before the second buffer (CVE-2026-48142). The fix is to copy bytes from ctx->saved directly, and only adjust src for the bytes which are from the second buffer. See also: https://github.com/nginx/nginx/commit/319a0bff157b15d9061f4712b2edbe6fdd2dee66 diff --git a/src/http/modules/ngx_http_charset_filter_module.c b/src/http/modules/ngx_http_charset_filter_module.c --- a/src/http/modules/ngx_http_charset_filter_module.c +++ b/src/http/modules/ngx_http_charset_filter_module.c @@ -865,6 +865,10 @@ ngx_http_charset_recode_from_utf8(ngx_po ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pool->log, 0, "http charset invalid utf 1"); + while (saved < ctx->saved + ctx->saved_len) { + *dst++ = *saved++; + } + } else { dst = ngx_sprintf(dst, "&#%uD;", n); } From mdounin at mdounin.ru Thu Jun 18 22:32:20 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 19 Jun 2026 01:32:20 +0300 Subject: [PATCH] Tests: charset tests with invalid characters in multiple buffers In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781821839 -10800 # Fri Jun 19 01:30:39 2026 +0300 # Node ID c931cb42d5fe8d563a00dae54cda5c8cec29da92 # Parent 2df9be66ac8aeb3db99f9d8bf8fedf6209ed833f Tests: charset tests with invalid characters in multiple buffers. diff --git a/charset_perl.t b/charset_perl.t --- a/charset_perl.t +++ b/charset_perl.t @@ -21,7 +21,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http charset perl/)->plan(1) +my $t = Test::Nginx->new()->has(qw/http charset perl/)->plan(2) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -78,6 +78,38 @@ http { return OK; }'; } + + location /invalid { + perl 'sub { + my $r = shift; + $r->send_http_header("text/html"); + return OK if $r->header_only; + + # 2-byte invalid character + + $r->print("\xc2\x61"); + $r->print("\xc2"); + $r->print("\x61"); + + # 3-byte invalid character + + $r->print("\xe2\x61\x61"); + $r->print("\xe2"); + $r->print("\x61"); + $r->print("\x61"); + + # 4-byte invalid character + + $r->print("\xf0\x61\x61\x61"); + + $r->print("\xf0"); + $r->print("\x61"); + $r->print("\x61"); + $r->print("\x61"); + + return OK; + }'; + } } } @@ -98,4 +130,16 @@ like(http_get('/multi'), qr/^CCTT𐀀 } +TODO: { +local $TODO = 'not yet' + unless $t->has_version('1.31.3'); +todo_skip 'might coredump', 1 + unless $t->has_version('1.31.3') + or $ENV{TEST_NGINX_UNSAFE}; + +like(http_get('/invalid'), qr/^\Q???a?a?aa?aa\E$/m, + 'invalid in multiple buffers'); + +} + ############################################################################### From mdounin at mdounin.ru Thu Jun 18 22:37:28 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 19 Jun 2026 01:37:28 +0300 Subject: [PATCH 1 of 2] Perl: added handler type checks Message-ID: # HG changeset patch # User Maxim Dounin # Date 1781821958 -10800 # Fri Jun 19 01:32:38 2026 +0300 # Node ID ca69a1ef6bbbacebead36bcd48fb2b7428138ae2 # Parent a779d9d41aaa5ff1091f052d59beb9fb368f55cd Perl: added handler type checks. Previously, calling $r->sleep() and $r->has_request_body() with an invalid handler argument, such as a string, resulted in a segmentation fault. 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 @@ -395,6 +395,7 @@ has_request_body(r, next) dXSTARG; ngx_http_request_t *r; ngx_http_perl_ctx_t *ctx; + SV *next; ngx_int_t rc; ngx_http_perl_set_request(r, ctx); @@ -411,7 +412,13 @@ has_request_body(r, next) XSRETURN_UNDEF; } - ctx->next = SvRV(ST(1)); + next = ST(1); + + if (!SvROK(next) || SvTYPE(SvRV(next)) != SVt_PVCV) { + croak("has_request_body(): no handler provided"); + } + + ctx->next = SvRV(next); r->request_body_in_single_buf = 1; r->request_body_in_persistent_file = 1; @@ -1129,6 +1136,7 @@ sleep(r, sleep, next) ngx_http_request_t *r; ngx_http_perl_ctx_t *ctx; + SV *next; ngx_msec_t sleep; ngx_http_perl_set_request(r, ctx); @@ -1146,7 +1154,13 @@ sleep(r, sleep, next) ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "perl sleep: %M", sleep); - ctx->next = SvRV(ST(2)); + next = ST(2); + + if (!SvROK(next) || SvTYPE(SvRV(next)) != SVt_PVCV) { + croak("sleep(): no handler provided"); + } + + ctx->next = SvRV(next); r->connection->write->delayed = 1; ngx_add_timer(r->connection->write, sleep); From mdounin at mdounin.ru Thu Jun 18 22:37:29 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 19 Jun 2026 01:37:29 +0300 Subject: [PATCH 2 of 2] Perl: introduced reference counting for perl scalars In-Reply-To: References: Message-ID: <893e98b138a335d6a558.1781822249@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1781822183 -10800 # Fri Jun 19 01:36:23 2026 +0300 # Node ID 893e98b138a335d6a558128a856e2c90d953ca40 # Parent ca69a1ef6bbbacebead36bcd48fb2b7428138ae2 Perl: introduced reference counting for perl scalars. Perl scalars might have a limited lifetime, and using them without appropriate reference counting is incorrect. In particular, heap use-after-free was observed in the following configuration (note the "eval", which limits lifetime of the string being printed), which demonstrates that the previously used SvREADONLY() optimizations are incorrect: location / { perl 'sub { my $r = shift; $r->send_http_header; eval q!$r->print("it works")!; return OK; }'; } Similarly, errors were observed with handlers in $r->sleep() and $r->has_request_body() when a handler comes from an eval, such as in the following configuration: location / { perl 'sub { my $r = shift; $r->sleep(100, eval q!sub { my $r = shift; $r->send_http_header; $r->print("it works"); return OK; }!); return OK; }'; } Accordingly, the SvREADONLY() optimization was removed in ngx_http_perl_sv2str(), since it is expected to be used for small strings, and using proper reference counting likely will be more costly than just copying the string. In $r->print(), $r->sleep(), and $r->has_request_body() proper reference counting was implemented, with decrement operations being performed by pool cleanup handlers. As a positive side effect, $r->print() can now avoid copying any single scalar, not just read-only scalars. Reported by Evan Hellman, https://github.com/freenginx/nginx/issues/26 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 @@ -41,16 +41,6 @@ ngx_http_perl_sv2str(pTHX_ ngx_http_requ p = (u_char *) SvPV(sv, len); s->len = len; - - if (SvREADONLY(sv) && SvPOK(sv)) { - s->data = p; - - ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "perl sv2str: %08XD \"%V\"", sv->sv_flags, s); - - return NGX_OK; - } - s->data = ngx_pnalloc(r->pool, len); if (s->data == NULL) { return NGX_ERROR; @@ -66,6 +56,29 @@ ngx_http_perl_sv2str(pTHX_ ngx_http_requ static ngx_int_t +ngx_http_perl_refcount(pTHX_ ngx_http_request_t *r, SV *sv) +{ + ngx_pool_cleanup_t *cln; + ngx_http_perl_cleanup_t *clnp; + + cln = ngx_pool_cleanup_add(r->pool, sizeof(ngx_http_perl_cleanup_t)); + if (cln == NULL) { + return NGX_ERROR; + } + + cln->handler = ngx_http_perl_refcount_cleanup; + + clnp = cln->data; + clnp->request = r; + clnp->sv = sv; + + SvREFCNT_inc(sv); + + return NGX_OK; +} + + +static ngx_int_t ngx_http_perl_output(ngx_http_request_t *r, ngx_http_perl_ctx_t *ctx, ngx_buf_t *b) { @@ -420,6 +433,12 @@ has_request_body(r, next) ctx->next = SvRV(next); + if (ngx_http_perl_refcount(aTHX_ r, ctx->next) != NGX_OK) { + ctx->error = 1; + ctx->next = NULL; + croak("ngx_http_perl_refcount() failed"); + } + r->request_body_in_single_buf = 1; r->request_body_in_persistent_file = 1; r->request_body_in_clean_file = 1; @@ -670,7 +689,7 @@ print(r, ...) if (items == 2) { /* - * do zero copy for prolate single read-only SV: + * do zero copy for prolate single SV: * $r->print("some text\n"); */ @@ -680,7 +699,7 @@ print(r, ...) sv = SvRV(sv); } - if (SvREADONLY(sv) && SvPOK(sv)) { + if (SvPOK(sv)) { p = (u_char *) SvPV(sv, len); @@ -688,6 +707,11 @@ print(r, ...) XSRETURN_EMPTY; } + if (ngx_http_perl_refcount(aTHX_ r, sv) != NGX_OK) { + ctx->error = 1; + croak("ngx_http_perl_refcount() failed"); + } + b = ngx_calloc_buf(r->pool); if (b == NULL) { ctx->error = 1; @@ -701,7 +725,7 @@ print(r, ...) b->end = b->last; ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "$r->print: read-only SV: %z", len); + "$r->print: single SV: %z", len); goto out; } @@ -1162,6 +1186,12 @@ sleep(r, sleep, next) ctx->next = SvRV(next); + if (ngx_http_perl_refcount(aTHX_ r, ctx->next) != NGX_OK) { + ctx->error = 1; + ctx->next = NULL; + croak("ngx_http_perl_refcount() failed"); + } + r->connection->write->delayed = 1; ngx_add_timer(r->connection->write, sleep); 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 @@ -308,6 +308,29 @@ ngx_http_perl_sleep_handler(ngx_http_req } +void +ngx_http_perl_refcount_cleanup(void *data) +{ + ngx_http_perl_cleanup_t *clnp = data; + + ngx_http_request_t *r; + ngx_http_perl_main_conf_t *pmcf; + + r = clnp->request; + pmcf = ngx_http_get_module_main_conf(r, ngx_http_perl_module); + + { + + dTHXa(pmcf->perl); + PERL_SET_CONTEXT(pmcf->perl); + PERL_SET_INTERP(pmcf->perl); + + SvREFCNT_dec(clnp->sv); + + } +} + + static ngx_int_t ngx_http_perl_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) 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 @@ -50,6 +50,12 @@ typedef struct { } ngx_http_perl_var_t; +typedef struct { + ngx_http_request_t *request; + SV *sv; +} ngx_http_perl_cleanup_t; + + extern ngx_module_t ngx_http_perl_module; @@ -68,6 +74,7 @@ extern void boot_DynaLoader(pTHX_ CV* cv void ngx_http_perl_handle_request(ngx_http_request_t *r); void ngx_http_perl_sleep_handler(ngx_http_request_t *r); +void ngx_http_perl_refcount_cleanup(void *data); #endif /* _NGX_HTTP_PERL_MODULE_H_INCLUDED_ */ From mdounin at mdounin.ru Thu Jun 18 22:38:36 2026 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 19 Jun 2026 01:38:36 +0300 Subject: [PATCH] Tests: perl reference counting tests In-Reply-To: <893e98b138a335d6a558.1781822249@vm-bsd.mdounin.ru> References: <893e98b138a335d6a558.1781822249@vm-bsd.mdounin.ru> Message-ID: <4906a88658f86f4476b6.1781822316@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1781821998 -10800 # Fri Jun 19 01:33:18 2026 +0300 # Node ID 4906a88658f86f4476b6821e196a86c544ec61bf # Parent c931cb42d5fe8d563a00dae54cda5c8cec29da92 Tests: perl reference counting tests. diff --git a/perl_refcount.t b/perl_refcount.t new file mode 100644 --- /dev/null +++ b/perl_refcount.t @@ -0,0 +1,124 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for embedded perl module, various reference counting tests. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http perl/)->plan(4) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /sleep { + perl 'sub { + my $r = shift; + + $r->sleep(100, eval q!sub { + my $r = shift; + $r->send_http_header; + $r->print("it works"); + return OK; + }!); + + return OK; + }'; + } + + location /body { + perl 'sub { + my $r = shift; + + $r->has_request_body(eval q!sub { + my $r = shift; + $r->send_http_header; + $r->print("it works"); + return OK; + }!); + + return OK; + }'; + } + + location /print { + perl 'sub { + my $r = shift; + $r->send_http_header; + eval q!$r->print("it works")!; + return OK; + }'; + } + + location /redirect { + perl 'sub { + my $r = shift; + eval q!$r->internal_redirect("/print")!; + return OK; + }'; + } + } +} + +EOF + +$t->run(); + +############################################################################### + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.31.3'); + +# $r->sleep() handler from an eval() might be freed by perl, +# and needs to be properly refcounted till it's no longer needed + +like(http_get('/sleep'), qr/works/, 'perl sleep and eval'); + +# similarly, $r->has_request_body() handler from an eval() +# also needs to be properly refcounted + +like(http( + 'GET /body HTTP/1.0' . CRLF + . 'Host: localhost' . CRLF + . 'Content-Length: 10' . CRLF . CRLF, + sleep => 0.1, + body => '1234567890' +), qr/works/, 'perl body and eval'); + +# similarly, even read-only strings in an eval() might be freed, +# and need to be either properly refcounted or copied + +like(http_get('/print'), qr/works/, 'perl print in eval'); +like(http_get('/redirect'), qr/works/, 'perl redirect in eval'); + +} + +###############################################################################