From 8524ecbf312791990d9f10c5b17b2c4e0e5ce800 Mon Sep 17 00:00:00 2001 From: macskas Date: Wed, 23 Sep 2026 08:38:20 +0200 Subject: [PATCH 1/3] ngx_cache_purge: fix segfault on PURGE in a location without proxy_cache The inline form (proxy_cache_purge PURGE from ...) is accepted in a location that has no proxy_cache of its own, either written there directly or inherited from the server level. In such a location both u->conf->cache_zone and u->conf->cache_value are NULL at request time, and ngx_http_cache_purge_cache_get() passed the NULL complex value to ngx_http_complex_value(), which dereferences it: nginx[1297]: segfault at 18 ip ... error 4 in nginx (ngx_http_complex_value+0x1b: mov 0x18(%rsi),%rdi -> val->lengths) Return 404 and log an error instead. All four callers (fastcgi, proxy, scgi, uwsgi) pass the return code straight through as the response. --- ngx_cache_purge_module.c | 6 ++++++ t/basic.t | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index 5a31506..2d4a338 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -2887,6 +2887,12 @@ ngx_http_cache_purge_cache_get(ngx_http_request_t *r, ngx_http_upstream_t *u, return NGX_OK; } + if (u->conf->cache_value == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_cache_purge: no cache configured for this location"); + return NGX_HTTP_NOT_FOUND; + } + if (ngx_http_complex_value(r, u->conf->cache_value, &val) != NGX_OK) { return NGX_ERROR; } diff --git a/t/basic.t b/t/basic.t index d60b9aa..a729ba1 100644 --- a/t/basic.t +++ b/t/basic.t @@ -499,3 +499,42 @@ X-Trigger-If: 1 ["X-Cache-Status: MISS", "X-Cache-Status: HIT"] --- no_error_log [error] + +=== TEST 21: PURGE in a location without proxy_cache returns 404 (was segfault) +# The inline form is accepted in a location that has no proxy_cache, so +# upstream.cache_zone and upstream.cache_value are both NULL at request +# time. Evaluating the NULL complex value crashed the worker. +--- http_config eval: $::HttpConfig +--- config + location /nocache { + proxy_pass http://backend/origin; + proxy_cache_purge PURGE from 127.0.0.1; + } + location /origin { + return 200 "ok"; + } +--- request +PURGE /nocache/t21 +--- error_code: 404 +--- error_log +no cache configured for this location +--- no_error_log +[alert] + +=== TEST 22: PURGE inherited from server level into a location without proxy_cache +--- http_config eval: $::HttpConfig +--- config + proxy_cache_purge PURGE from 127.0.0.1; + location /nocache { + proxy_pass http://backend/origin; + } + location /origin { + return 200 "ok"; + } +--- request +PURGE /nocache/t22 +--- error_code: 404 +--- error_log +no cache configured for this location +--- no_error_log +[alert] From ad74d43a2c2bdb35ed7ddfcb73280c1999a3d960 Mon Sep 17 00:00:00 2001 From: macskas Date: Wed, 23 Sep 2026 08:38:20 +0200 Subject: [PATCH 2/3] ngx_cache_purge: inherit separate-location purge config into "if" blocks An "if" block inside a location creates an anonymous child location. When the 3-argument form (proxy_cache_purge zone key) is used in the parent, the child inherits proxy.enable == 0 but not the zone and key, which live in proxy_separate_zone / proxy_separate_value / proxy_separate_key. r->content_handler is still the purge handler of the parent, so a request that takes the if-branch runs the handler with the child's config, fails the separate-syntax check and falls through to the inline code path, where no cache is configured. A common configuration triggers this for every request with a query string: location ~ "/purge(?/.*)$" { set $params ""; if ($args != "") { set $params ?$args; } proxy_cache_purge zone GET$path$params; } Without the previous commit this was a segfault; with it, a 404. Copy the three fields from the parent in merge_loc_conf when the child does not configure proxy_cache_purge itself. --- ngx_cache_purge_module.c | 9 +++++++++ t/basic.t | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index 2d4a338..9283d98 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -3420,6 +3420,15 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) # endif # if (NGX_HTTP_PROXY) + if (conf->proxy.enable == NGX_CONF_UNSET + && conf->proxy_separate_zone == NULL + && conf->proxy_separate_value == NULL) + { + conf->proxy_separate_zone = prev->proxy_separate_zone; + conf->proxy_separate_value = prev->proxy_separate_value; + conf->proxy_separate_key = prev->proxy_separate_key; + } + ngx_http_cache_purge_merge_conf(&conf->proxy, &prev->proxy); if (conf->proxy.enable) { diff --git a/t/basic.t b/t/basic.t index a729ba1..aa04122 100644 --- a/t/basic.t +++ b/t/basic.t @@ -538,3 +538,39 @@ PURGE /nocache/t22 no cache configured for this location --- no_error_log [alert] + +=== TEST 23: separate-location purge whose key is built inside an if block +# The if block creates an anonymous child location. The zone and key of +# the 3-arg form must be inherited by it, otherwise a request that takes +# the if-branch falls through to the inline code path with no cache +# configured (segfault before the NULL check, 404 after it). +--- http_config eval: $::HttpConfig +--- config + location /cache { + proxy_pass http://backend/origin; + proxy_cache cache_zone; + proxy_cache_key "$uri$is_args$args"; + proxy_cache_valid 200 1m; + } + location ~ ^/purge(?/.*)$ { + set $purge_args ""; + if ($args != "") { + set $purge_args "?$args"; + } + proxy_cache_purge cache_zone "$purge_uri$purge_args"; + } + location /origin { + return 200 "if-separate"; + } +--- request eval +[ + "GET /cache/if23", + "GET /cache/if23?cno=1", + "GET /purge/cache/if23", + "GET /purge/cache/if23?cno=1", + "GET /purge/cache/if23?cno=1" +] +--- error_code eval +[200, 200, 200, 200, 412] +--- no_error_log +[alert] From 939c39254e09fa71bf69b123e045dab1f611aee6 Mon Sep 17 00:00:00 2001 From: macskas Date: Wed, 23 Sep 2026 08:38:41 +0200 Subject: [PATCH 3/3] ngx_cache_purge: fix 404 on non-PURGE requests when the directive is inherited Commit 11b1c94 takes original_handler from the parent whenever the purge directive was not set in the location itself, to handle the anonymous child location that nginx creates for an "if" block. The same condition is true for every named location that inherits the directive from the server level or from an enclosing location, for example: server { proxy_cache_purge PURGE from 127.0.0.1; location / { proxy_pass http://backend; proxy_cache zone; } } There prev->original_handler is the server-level value, NULL, so every non-PURGE request to "location /" returned 404 instead of being proxied. v3.0.2 is not affected. Take the parent's handler only for anonymous (if / limit_except) locations, identified by clcf->noname. --- ngx_cache_purge_module.c | 19 +++++++++++++------ t/basic.t | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index 9283d98..cb9fa29 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -3354,14 +3354,21 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) * * was_set_* captures whether each protocol's purge directive was * explicitly present in THIS location block BEFORE merging from the - * parent. This is the only reliable way to distinguish two cases: + * parent. Together with clcf->noname it distinguishes three cases: * * Case A -- explicit (enable == 1 before merge): * proxy_cache_purge is in this location. clcf->handler is the * real upstream handler (e.g. ngx_http_proxy_handler set by * proxy_pass). Save it as original_handler and install ours. * - * Case B -- inherited (enable == NGX_CONF_UNSET before merge): + * Case B -- inherited into a named location (enable == + * NGX_CONF_UNSET, clcf->noname == 0): + * proxy_cache_purge is set at the server level or in an enclosing + * location. clcf->handler is this location's own handler, as in + * case A. + * + * Case C -- inherited into an anonymous location (enable == + * NGX_CONF_UNSET, clcf->noname == 1): * This is an anonymous if-child location synthesised by nginx when * it encounters an "if" block. The if-block has no handler * directive, so clcf->handler is NULL. Saving NULL as @@ -3411,7 +3418,7 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) if (conf->fastcgi.enable) { conf->conf = &conf->fastcgi; conf->handler = ngx_http_fastcgi_cache_purge_handler; - conf->original_handler = was_set_fastcgi + conf->original_handler = (was_set_fastcgi || !clcf->noname) ? clcf->handler : prev->original_handler; clcf->handler = ngx_http_cache_purge_access_handler; @@ -3434,7 +3441,7 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) if (conf->proxy.enable) { conf->conf = &conf->proxy; conf->handler = ngx_http_proxy_cache_purge_handler; - conf->original_handler = was_set_proxy + conf->original_handler = (was_set_proxy || !clcf->noname) ? clcf->handler : prev->original_handler; clcf->handler = ngx_http_cache_purge_access_handler; @@ -3448,7 +3455,7 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) if (conf->scgi.enable) { conf->conf = &conf->scgi; conf->handler = ngx_http_scgi_cache_purge_handler; - conf->original_handler = was_set_scgi + conf->original_handler = (was_set_scgi || !clcf->noname) ? clcf->handler : prev->original_handler; clcf->handler = ngx_http_cache_purge_access_handler; @@ -3462,7 +3469,7 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) if (conf->uwsgi.enable) { conf->conf = &conf->uwsgi; conf->handler = ngx_http_uwsgi_cache_purge_handler; - conf->original_handler = was_set_uwsgi + conf->original_handler = (was_set_uwsgi || !clcf->noname) ? clcf->handler : prev->original_handler; clcf->handler = ngx_http_cache_purge_access_handler; diff --git a/t/basic.t b/t/basic.t index aa04122..edeab68 100644 --- a/t/basic.t +++ b/t/basic.t @@ -574,3 +574,28 @@ no cache configured for this location [200, 200, 200, 200, 412] --- no_error_log [alert] + +=== TEST 24: non-PURGE request in a location that inherits the purge directive from the server level +# original_handler must come from the location's own clcf->handler +# (ngx_http_proxy_handler here). Only an anonymous "if" / "limit_except" +# child location has to take it from its parent. +--- http_config eval: $::HttpConfig +--- config + proxy_cache_purge PURGE from 127.0.0.1; + location /cache { + proxy_pass http://backend/origin; + proxy_cache cache_zone; + proxy_cache_key "$uri"; + proxy_cache_valid 200 1m; + } + location /origin { + return 200 "inherited"; + } +--- request eval +["GET /cache/t24", "PURGE /cache/t24", "PURGE /cache/t24"] +--- error_code eval +[200, 200, 412] +--- response_body eval +["inherited", qr/purged/i, qr/412/] +--- no_error_log +[alert]