This is an automated email from the ASF dual-hosted git repository.
bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 3b04db3474 Fix redirected cache write without write VC (#13309)
3b04db3474 is described below
commit 3b04db3474c674aa286c9b3406e00a649ea50783
Author: Brian Neradt <[email protected]>
AuthorDate: Wed Jun 24 12:28:49 2026 -0500
Fix redirected cache write without write VC (#13309)
Redirected transactions can retain CACHE_WL_SUCCESS after the concrete
cache write VC has been cleared. When a later response reaches cache
write setup, we crash dereferencing the missing write VC.
This only reuses a redirected cache write when the prepared write VC is
still available. Otherwise, this resets the write state so ATS prepares
a fresh cache write. The added test simply adds some coverage around the
scenario but is not a true regression test. That is, the test still
passes without the src/ code change.
This addresses a crash in the cache write setup path:
```
#0 HttpSM::setup_cache_write_transfer(...)
#1 HttpSM::perform_cache_write_action()
#2 HttpSM::handle_api_return()
#3 HttpSM::state_api_callout()
#4 HttpSM::state_api_callback()
#5 TSHttpTxnReenable()
#6 EscalateResponse()
```
---
src/proxy/http/HttpSM.cc | 2 ++
src/proxy/http/HttpTransact.cc | 11 +++--------
src/proxy/http/HttpTunnel.cc | 2 ++
tests/gold_tests/pluginTest/escalate/escalate.test.py | 10 +++++++---
4 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index e12b2c4f6d..4a3cb451f9 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -3932,6 +3932,8 @@ HttpSM::tunnel_handler_cache_write(int event,
HttpTunnelConsumer *c)
server_response_body_bytes = c->bytes_written;
}
+ // The cache write VC is closed now, so any prepared write lock is gone.
+ t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::INIT;
Metrics::Gauge::decrement(http_rsb.current_cache_connections);
return 0;
}
diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc
index df56761a64..a8746b2e21 100644
--- a/src/proxy/http/HttpTransact.cc
+++ b/src/proxy/http/HttpTransact.cc
@@ -3565,14 +3565,9 @@
HttpTransact::set_cache_prepare_write_action_for_new_request(State *s)
// This method must be called no more than one time per request. It should
// not be called for non-cacheable requests.
if (s->cache_info.write_lock_state == CacheWriteLock_t::SUCCESS) {
- // If and only if this is a redirected request, we may have already
- // prepared a cache write (during the handling of the previous request
- // which got the 3xx response) and can safely re-use it. Otherwise, we
- // risk storing the response under the wrong cache key. This is a release
- // assert because the correct behavior would be to prepare a new write,
- // but we can't do that because we failed to release the lock. To recover
- // we would have to tell the state machine to abort its write, and we
- // don't have a state for that.
+ // If and only if this is a redirected request, we may have already
prepared
+ // a cache write during the handling of the previous request and can re-use
+ // it. Otherwise, we risk storing the response under the wrong cache key.
ink_release_assert(s->redirect_info.redirect_in_process);
s->cache_info.action = CacheAction_t::WRITE;
} else if (s->cache_info.write_lock_state == CacheWriteLock_t::READ_RETRY &&
diff --git a/src/proxy/http/HttpTunnel.cc b/src/proxy/http/HttpTunnel.cc
index 3695edf87a..4588dae087 100644
--- a/src/proxy/http/HttpTunnel.cc
+++ b/src/proxy/http/HttpTunnel.cc
@@ -1755,6 +1755,8 @@ HttpTunnel::chain_abort_cache_write(HttpTunnelProducer *p)
c->write_vio = nullptr;
c->vc->do_io_close(EHTTP_ERROR);
c->alive = false;
+ // The cache write VC is closed now, so any prepared write lock is
gone.
+ sm->t_state.cache_info.write_lock_state =
HttpTransact::CacheWriteLock_t::INIT;
Metrics::Gauge::decrement(http_rsb.current_cache_connections);
} else if (c->self_producer) {
chain_abort_cache_write(c->self_producer);
diff --git a/tests/gold_tests/pluginTest/escalate/escalate.test.py
b/tests/gold_tests/pluginTest/escalate/escalate.test.py
index ea9f2953b6..46756e5a64 100644
--- a/tests/gold_tests/pluginTest/escalate/escalate.test.py
+++ b/tests/gold_tests/pluginTest/escalate/escalate.test.py
@@ -38,11 +38,14 @@ class EscalateTest:
_server_failover_file: str = 'escalate_failover_server_default.replay.yaml'
_process_counter: int = 0
- def __init__(self, disable_redirect_header: bool = False) -> None:
+ def __init__(self, disable_redirect_header: bool = False, enable_cache:
bool = False) -> None:
'''Configure the test run.
:param disable_redirect_header: Whether to use --no-redirect-header.
+ :param enable_cache: Whether to exercise the cached redirect path.
'''
- tr = Test.AddTestRun(f'Test escalate plugin.
disable_redirect_header={disable_redirect_header}')
+ tr = Test.AddTestRun(
+ f'Test escalate plugin.
disable_redirect_header={disable_redirect_header}, enable_cache={enable_cache}')
+ self._enable_cache = enable_cache
self._setup_dns(tr)
self._setup_servers(tr, disable_redirect_header)
self._setup_ts(tr, disable_redirect_header)
@@ -118,7 +121,7 @@ class EscalateTest:
:param disable_redirect_header: Whether ATS should be configured with
--no-redirect-header.
'''
process_name = f"ts_{EscalateTest._process_counter}"
- self._ts = tr.MakeATSProcess(process_name, enable_cache=False)
+ self._ts = tr.MakeATSProcess(process_name,
enable_cache=self._enable_cache)
# Select a port that is guaranteed to not be used at the moment.
dead_port = get_port(self._ts, "dead_port")
self._ts.Disk.records_config.update(
@@ -287,4 +290,5 @@ class EscalateNonGetMethodsTest:
EscalateTest(disable_redirect_header=False)
EscalateTest(disable_redirect_header=True)
+EscalateTest(disable_redirect_header=False, enable_cache=True)
EscalateNonGetMethodsTest()