bneradt commented on code in PR #13574:
URL: https://github.com/apache/trafficserver/pull/13574#discussion_r3825870825


##########
src/proxy/http/HttpSM.cc:
##########
@@ -2145,6 +2145,17 @@ HttpSM::state_read_server_response_header(int event, 
void *data)
     // If there is a post body in transit, give up on it
     if (tunnel.is_tunnel_alive()) {
       tunnel.abort_tunnel();
+      // abort_tunnel() does not clean up vc_table entries.  If a request
+      // transform is present, post_transform_info.entry still points at the
+      // TransformVConnection whose chain will be freed by the abort cascade.
+      // Clean it up now so cleanup_all() in kill_this() does not call
+      // do_io_close() on freed memory.

Review Comment:
   This comment doesn't match what the code below actually does, and I don't 
think the crash it describes can happen.
   
   `HttpVCTable::cleanup_entry()` only calls `do_io_close()` when `in_tunnel == 
false`:
   
   ```cpp
   void
   HttpVCTable::cleanup_entry(HttpVCTableEntry *e)
   {
     ink_assert(e->vc);
     if (e->in_tunnel == false) {
       ...
       e->vc->do_io_close();
       e->vc = nullptr;
     }
     remove_entry(e);
   }
   ```
   
   At this point `post_transform_info.entry->in_tunnel` is `true`. It is set in 
`do_setup_client_request_body_tunnel()` (`case HttpVC_t::TRANSFORM_VC`) and 
again in `setup_transform_to_server_transfer()`, and it is cleared only in 
`tunnel_handler_transform_write()` — which `abort_tunnel()` never invokes, 
since `abort_tunnel()` doesn't call any consumer/producer handlers. So 
`cleanup_all()` in `kill_this()` falls straight through to `remove_entry()`, 
which never dereferences `e->vc`. There is no `do_io_close()` on freed memory 
to prevent.
   
   The two cases:
   
   * `in_tunnel == true` — the real case, and this patch having to clear it is 
the proof: `cleanup_all()` never touched `e->vc`, so the described 
use-after-free cannot have occurred.
   * `in_tunnel == false` — hypothetically: the assignment is a no-op, and 
`cleanup_entry()` performs the very same `do_io_close()` on the very same 
pointer, just earlier in the call chain. Also not a fix.
   
   That said, I think you have found a real bug, just a different one. 
`abort_tunnel()` only issues `do_io_read/do_io_write(this, 0, nullptr)` on the 
producers/consumers and then `reset()`; it never closes the transform chain. 
`kill_tunnel()` afterwards finds an empty tunnel, and `transform_cleanup()` 
skips the chain because `post_transform_info.vc != nullptr`. So today 
`TransformVConnection::m_closed` is never set, 
`TransformTerminus::handle_event()` never reaches `delete m_tvc`, and the 
TransformVConnection plus the plugin's transform continuations **leak**. 
Forcing a close here does fix that.
   
   So this looks like a fix for a leak / missing teardown rather than for a 
use-after-free. Could you attach the actual stack trace or ASAN report? If 
there genuinely is a crash, I want to be sure this isn't masking it rather than 
fixing it. And either way the comment, the PR description, and the test 
docstring should be rewritten around the real mechanism.



##########
src/proxy/http/HttpSM.cc:
##########
@@ -2145,6 +2145,17 @@ HttpSM::state_read_server_response_header(int event, 
void *data)
     // If there is a post body in transit, give up on it
     if (tunnel.is_tunnel_alive()) {
       tunnel.abort_tunnel();
+      // abort_tunnel() does not clean up vc_table entries.  If a request
+      // transform is present, post_transform_info.entry still points at the
+      // TransformVConnection whose chain will be freed by the abort cascade.
+      // Clean it up now so cleanup_all() in kill_this() does not call
+      // do_io_close() on freed memory.
+      if (post_transform_info.entry != nullptr) {
+        post_transform_info.entry->in_tunnel = false;
+        vc_table.cleanup_entry(post_transform_info.entry);
+        post_transform_info.entry = nullptr;
+      }
+      ink_release_assert(post_transform_info.entry == nullptr);

Review Comment:
   This assert is tautological: the block immediately above sets 
`post_transform_info.entry = nullptr`, and if the `if` wasn't taken it was 
already nullptr. It can never fire. Looks like leftover repro scaffolding — 
please drop it.
   
   Relatedly, the PR description still says to "uncomment the assert after 
`abort_tunnel()`" to reproduce, which no longer matches the diff.



##########
src/proxy/http/HttpSM.cc:
##########
@@ -2145,6 +2145,17 @@ HttpSM::state_read_server_response_header(int event, 
void *data)
     // If there is a post body in transit, give up on it
     if (tunnel.is_tunnel_alive()) {
       tunnel.abort_tunnel();
+      // abort_tunnel() does not clean up vc_table entries.  If a request
+      // transform is present, post_transform_info.entry still points at the
+      // TransformVConnection whose chain will be freed by the abort cascade.
+      // Clean it up now so cleanup_all() in kill_this() does not call
+      // do_io_close() on freed memory.
+      if (post_transform_info.entry != nullptr) {
+        post_transform_info.entry->in_tunnel = false;

Review Comment:
   Overwriting `in_tunnel` to coerce a side effect out of `cleanup_entry()` is 
a blunt instrument, and it works against the rest of the file. Every other 
post-transform cleanup site deliberately preserves this flag:
   
   * `tunnel_handler_post_or_put()` asserts 
`post_transform_info.entry->in_tunnel == true` and then calls `cleanup_entry()` 
*specifically so that it will not close*, with a comment explaining why.
   * `state_common_wait_for_transform_read()` and `handle_server_setup_error()` 
likewise call `cleanup_entry()` without touching the flag.
   
   `in_tunnel` encodes who owns the VC — the tunnel or the SM. If the intent is 
"close the transform chain now", it reads much better to say that directly:
   
   ```cpp
   post_transform_info.vc->do_io_close();
   vc_table.cleanup_entry(post_transform_info.entry);
   post_transform_info.entry = nullptr;
   ```
   
   There's a robustness cost too. The `in_tunnel == true` check is currently 
what protects paths where the TVC has already been closed but the entry is 
still populated — `tunnel_handler_transform_write()`'s `VC_EVENT_ERROR` case 
does `c->vc->do_io_close(EHTTP_ERROR)` and leaves the entry alone. Clearing the 
flag unconditionally removes that protection and turns any such path into a 
genuine use-after-free.
   
   Last thought on placement: `abort_tunnel()` is the function that leaves the 
vc_table inconsistent, and `kill_tunnel()` has the same shape. Patching this 
one caller leaves the trap set for the next person. A small SM helper (or 
handling it on the abort path itself) would cover both.



##########
src/proxy/http/HttpSM.cc:
##########
@@ -2145,6 +2145,17 @@ HttpSM::state_read_server_response_header(int event, 
void *data)
     // If there is a post body in transit, give up on it
     if (tunnel.is_tunnel_alive()) {
       tunnel.abort_tunnel();
+      // abort_tunnel() does not clean up vc_table entries.  If a request
+      // transform is present, post_transform_info.entry still points at the
+      // TransformVConnection whose chain will be freed by the abort cascade.
+      // Clean it up now so cleanup_all() in kill_this() does not call
+      // do_io_close() on freed memory.
+      if (post_transform_info.entry != nullptr) {
+        post_transform_info.entry->in_tunnel = false;
+        vc_table.cleanup_entry(post_transform_info.entry);
+        post_transform_info.entry = nullptr;

Review Comment:
   Nulling `entry` while leaving `post_transform_info.vc` non-null breaks the 
pairing that two sites rely on — they check `.vc` and then dereference `.entry`:
   
   * `tunnel_handler_post_or_put()`: `if (post_transform_info.vc != nullptr) { 
ink_assert(post_transform_info.entry->in_tunnel == true); ...; 
vc_table.cleanup_entry(post_transform_info.entry); }`
   * `handle_server_setup_error()`: `if (post_transform_info.vc) { ...; 
vc_table.cleanup_entry(post_transform_info.entry); }`
   
   The `handle_server_setup_error()` one is safe here only incidentally: 
`abort_tunnel()` already called `reset()`, so 
`tunnel.get_consumer(post_transform_info.vc)` returns nullptr and the inner 
guard short-circuits. Worth confirming `tunnel_handler_post_or_put()` can't be 
reached after this abort — in a release build the `ink_assert`s compile out and 
`cleanup_entry(nullptr)` faults on `e->in_tunnel`.
   
   `state_common_wait_for_transform_read()` already produces this `entry == 
nullptr && vc != nullptr` state in the TRANSFORM_FAIL case, so it may well be 
fine. I'd just rather see it checked than assumed.



##########
tests/gold_tests/slow_post/partial_post_client.py:
##########
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+"""Send a partial POST to trigger the abort_tunnel code path.
+
+Sends POST headers claiming a large Content-Length but only sends a small
+chunk of body data. When a request transform plugin is active, this causes
+ATS to call abort_tunnel() while the transform entry is still in the vc_table.
+"""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import socket
+import sys
+
+
+def main() -> int:
+    """Run the client."""
+    host = sys.argv[1] if len(sys.argv) > 1 else '127.0.0.1'
+    port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080
+
+    request = (
+        f'POST / HTTP/1.1\r\n'
+        f'Host: quick.server.com\r\n'
+        f'Content-Type: application/octet-stream\r\n'
+        f'Content-Length: 100000\r\n'
+        f'\r\n').encode()

Review Comment:
   Minor: none of these `f` prefixes have placeholders, so plain string 
literals would do (yapf/flake8 will likely flag them).
   
   More generally, this overlaps `slow_post_client.py` in the same directory, 
which already does the "send a POST and don't finish it" dance. The difference 
that matters is `Content-Length` vs. `Transfer-Encoding: chunked`, so a 
`--content-length N --send-bytes M` mode there would avoid a third client 
script in this directory. Your call — the mechanism is different enough that a 
separate file is defensible.



##########
tests/tools/plugins/null_transform_request.cc:
##########
@@ -0,0 +1,162 @@
+/** @file
+
+  Null request transform plugin hooked at TS_HTTP_READ_REQUEST_HDR_HOOK.
+
+  Used by post_early_response_transform.test.py to reproduce a use-after-free
+  in HttpSM::state_read_server_response_header() when abort_tunnel() is called
+  while a request transform is active. The transform passes request body data
+  through unmodified.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <cstdio>
+#include <cinttypes>
+
+#include "ts/ts.h"
+
+#define PLUGIN_NAME "null_transform_request"
+
+typedef struct {
+  TSVIO            output_vio;
+  TSIOBuffer       output_buffer;
+  TSIOBufferReader output_reader;
+} TransformData;

Review Comment:
   `typedef struct { ... } TransformData;` is a C idiom; in C++ this is just 
`struct TransformData { ... };`.
   
   Also `<cstdio>` and `<cinttypes>` (lines 29-30) are unused now that the 
`Dbg()` calls are gone.



##########
tests/tools/plugins/null_transform_request.cc:
##########
@@ -0,0 +1,162 @@
+/** @file
+
+  Null request transform plugin hooked at TS_HTTP_READ_REQUEST_HDR_HOOK.
+
+  Used by post_early_response_transform.test.py to reproduce a use-after-free
+  in HttpSM::state_read_server_response_header() when abort_tunnel() is called
+  while a request transform is active. The transform passes request body data
+  through unmodified.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <cstdio>
+#include <cinttypes>
+
+#include "ts/ts.h"
+
+#define PLUGIN_NAME "null_transform_request"
+
+typedef struct {
+  TSVIO            output_vio;
+  TSIOBuffer       output_buffer;
+  TSIOBufferReader output_reader;
+} TransformData;
+
+static TransformData *
+transform_data_alloc()
+{
+  auto *data          = static_cast<TransformData 
*>(TSmalloc(sizeof(TransformData)));
+  data->output_vio    = nullptr;
+  data->output_buffer = nullptr;
+  data->output_reader = nullptr;
+  return data;
+}
+
+static void
+transform_data_destroy(TransformData *data)
+{
+  if (data) {
+    if (data->output_buffer) {
+      TSIOBufferDestroy(data->output_buffer);
+    }
+    TSfree(data);
+  }
+}
+
+static void
+handle_transform(TSCont contp)
+{
+  TSVConn        output_conn = TSTransformOutputVConnGet(contp);
+  TSVIO          input_vio   = TSVConnWriteVIOGet(contp);
+  TransformData *data        = static_cast<TransformData 
*>(TSContDataGet(contp));
+
+  if (!data) {
+    data                = transform_data_alloc();
+    data->output_buffer = TSIOBufferCreate();
+    data->output_reader = TSIOBufferReaderAlloc(data->output_buffer);
+    data->output_vio    = TSVConnWrite(output_conn, contp, 
data->output_reader, TSVIONBytesGet(input_vio));
+    TSContDataSet(contp, data);
+  }
+
+  if (!TSVIOBufferGet(input_vio)) {
+    TSVIONBytesSet(data->output_vio, TSVIONDoneGet(input_vio));
+    TSVIOReenable(data->output_vio);
+    return;
+  }
+
+  int64_t towrite = TSVIONTodoGet(input_vio);
+  if (towrite > 0) {
+    int64_t avail = TSIOBufferReaderAvail(TSVIOReaderGet(input_vio));
+    if (towrite > avail) {
+      towrite = avail;
+    }
+    if (towrite > 0) {
+      TSIOBufferCopy(TSVIOBufferGet(data->output_vio), 
TSVIOReaderGet(input_vio), towrite, 0);
+      TSIOBufferReaderConsume(TSVIOReaderGet(input_vio), towrite);
+      TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + towrite);
+    }
+  }
+
+  if (TSVIONTodoGet(input_vio) > 0) {
+    if (towrite > 0) {
+      TSVIOReenable(data->output_vio);
+      TSContCall(TSVIOContGet(input_vio), TS_EVENT_VCONN_WRITE_READY, 
input_vio);
+    }
+  } else {
+    TSVIONBytesSet(data->output_vio, TSVIONDoneGet(input_vio));
+    TSVIOReenable(data->output_vio);
+    TSContCall(TSVIOContGet(input_vio), TS_EVENT_VCONN_WRITE_COMPLETE, 
input_vio);
+  }
+}
+
+static int
+null_transform(TSCont contp, TSEvent event, void * /* edata ATS_UNUSED */)
+{
+  if (TSVConnClosedGet(contp)) {
+    transform_data_destroy(static_cast<TransformData *>(TSContDataGet(contp)));
+    TSContDestroy(contp);
+    return 0;
+  }
+
+  switch (event) {
+  case TS_EVENT_ERROR: {
+    TSVIO input_vio = TSVConnWriteVIOGet(contp);
+    TSContCall(TSVIOContGet(input_vio), TS_EVENT_ERROR, input_vio);
+    break;
+  }
+  case TS_EVENT_VCONN_WRITE_COMPLETE:
+    TSVConnShutdown(TSTransformOutputVConnGet(contp), 0, 1);
+    break;
+  default:
+    handle_transform(contp);
+    break;
+  }
+
+  return 0;
+}
+
+static int
+transform_plugin(TSCont /* contp ATS_UNUSED */, TSEvent event, void *edata)
+{
+  if (event == TS_EVENT_HTTP_READ_REQUEST_HDR) {

Review Comment:
   Following up on the de-duplication thread above: after the trimming, the 
only remaining difference from `tunnel_transform.cc` is this hook point (plus 
not adding the response transform). The ~70-line transform body is a verbatim 
copy.
   
   `PrepareTestPlugin` already forwards `plugin_args` into `plugin.config`, so 
`tunnel_transform` could take an argument selecting 
`TS_HTTP_READ_REQUEST_HDR_HOOK` vs. `TS_HTTP_TUNNEL_START_HOOK` (and whether to 
add the response transform), and this file could go away entirely. That's a 
better outcome than two copies of the same null transform to keep in sync.



##########
tests/gold_tests/slow_post/post_early_response_transform.test.py:
##########
@@ -0,0 +1,112 @@
+"""Verify ATS does not crash when a server replies before receiving the full 
POST body and a request transform plugin is active.
+
+When a POST request has a request transform and the origin responds before the
+full body is forwarded through the transform chain, abort_tunnel() is called.
+Without the fix, post_transform_info.entry is left stale in the vc_table,
+causing a use-after-free in cleanup_all().
+"""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import os
+from ports import get_port
+import sys
+
+Test.Summary = __doc__
+
+
+class PostEarlyResponseTransformTest:
+    """Verify abort_tunnel with a request transform does not crash ATS."""
+
+    _partial_post_client = 'partial_post_client.py'
+    _quick_server = 'quick_server.py'
+    _init_file = '__init__.py'
+    _http_utils = 'http_utils.py'

Review Comment:
   +1 to Copilot on this one: `_http_utils` is never used — line 94 rebuilds 
the path inline. Please drop the class attribute.



##########
tests/gold_tests/slow_post/post_early_response_transform.test.py:
##########
@@ -0,0 +1,112 @@
+"""Verify ATS does not crash when a server replies before receiving the full 
POST body and a request transform plugin is active.
+
+When a POST request has a request transform and the origin responds before the
+full body is forwarded through the transform chain, abort_tunnel() is called.
+Without the fix, post_transform_info.entry is left stale in the vc_table,
+causing a use-after-free in cleanup_all().

Review Comment:
   Two things.
   
   First, this docstring restates the "use-after-free in `cleanup_all()`" 
mechanism, which I don't think holds — see my comment on `HttpSM.cc`. Whatever 
the real root cause turns out to be, this needs to match it.
   
   Second, and more important: **does this test fail on master?** AuTest would 
catch a genuine traffic_server crash — `MakeATSProcess` sets `p.ReturnCode = 0` 
on the ATS process and adds an `ExcludesExpression("FATAL:")` tester on 
diags.log — so a real SIGSEGV/SIGABRT does fail the run. But if the pre-patch 
defect is a leak rather than a crash (which is what I believe it is), this test 
passes with and without the fix and isn't a regression test at all. Could you 
post the failing output from master?
   
   I also don't see any CI runs on this branch yet. Worth getting Jenkins green 
before this goes further.



##########
tests/gold_tests/slow_post/post_early_response_transform.test.py:
##########
@@ -0,0 +1,112 @@
+"""Verify ATS does not crash when a server replies before receiving the full 
POST body and a request transform plugin is active.
+
+When a POST request has a request transform and the origin responds before the
+full body is forwarded through the transform chain, abort_tunnel() is called.
+Without the fix, post_transform_info.entry is left stale in the vc_table,
+causing a use-after-free in cleanup_all().
+"""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import os
+from ports import get_port
+import sys
+
+Test.Summary = __doc__
+
+
+class PostEarlyResponseTransformTest:

Review Comment:
   This class re-implements `QuickServerTest` in `quick_server.test.py` almost 
line for line: same DNS server, same `quick_server.py` origin, same remap line, 
the same four `records.yaml` keys, the same `StartBefore` chain, the same 
`tr.Timeout = 10`. That file already parameterizes over three booleans.
   
   Adding a `use_request_transform` parameter there — installing the plugin and 
swapping in the partial-POST client — would fold this in without a second copy 
of the scaffolding.



##########
tests/gold_tests/slow_post/post_early_response_transform.test.py:
##########
@@ -0,0 +1,112 @@
+"""Verify ATS does not crash when a server replies before receiving the full 
POST body and a request transform plugin is active.
+
+When a POST request has a request transform and the origin responds before the
+full body is forwarded through the transform chain, abort_tunnel() is called.
+Without the fix, post_transform_info.entry is left stale in the vc_table,
+causing a use-after-free in cleanup_all().
+"""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import os
+from ports import get_port
+import sys
+
+Test.Summary = __doc__
+
+
+class PostEarlyResponseTransformTest:
+    """Verify abort_tunnel with a request transform does not crash ATS."""
+
+    _partial_post_client = 'partial_post_client.py'
+    _quick_server = 'quick_server.py'
+    _init_file = '__init__.py'
+    _http_utils = 'http_utils.py'
+
+    def __init__(self):
+        """Configure and run the test."""
+        tr = Test.AddTestRun('Partial POST with request transform and early 
server response')
+        self._configure_dns(tr)
+        self._configure_server(tr)
+        self._configure_traffic_server(tr)
+        self._configure_client(tr)
+
+    def _configure_dns(self, tr: 'TestRun') -> None:
+        """Configure the DNS process.
+
+        :param tr: The test run to associate the DNS process with.
+        """
+        self._dns = tr.MakeDNServer('dns', default='127.0.0.1')
+
+    def _configure_server(self, tr: 'TestRun') -> None:
+        """Configure the quick-responding origin server.
+
+        The server responds immediately after receiving the request headers,
+        before the full POST body arrives.
+
+        :param tr: The test run to associate the server process with.
+        """
+        server = tr.Processes.Process('server')
+        server_port = get_port(server, 'http_port')
+        server.Command = f'{sys.executable} {self._quick_server} 127.0.0.1 
{server_port}'
+        server.Ready = When.PortOpenv4(server_port)
+        self._server = server
+
+    def _configure_traffic_server(self, tr: 'TestRun') -> None:
+        """Configure ATS with the null_transform_request plugin.
+
+        :param tr: The test run to associate the ATS process with.
+        """
+        self._ts = tr.MakeATSProcess('ts')
+        self._ts.Disk.remap_config.AddLine(f'map / 
http://quick.server.com:{self._server.Variables.http_port}')
+        self._ts.Disk.records_config.update(
+            {
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'http',
+                'proxy.config.dns.nameservers': 
f'127.0.0.1:{self._dns.Variables.Port}',
+                'proxy.config.dns.resolv_conf': 'NULL',
+            })
+        Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 
'null_transform_request.so'), self._ts)
+
+    def _configure_client(self, tr: 'TestRun') -> None:
+        """Configure the partial POST client.
+
+        Sends a POST with a large Content-Length but only a small body,
+        triggering abort_tunnel when the origin responds early.
+
+        :param tr: The test run to associate the client process with.
+        """
+        tools_dir = self._ts.Variables.AtsTestToolsDir
+        http_utils = os.path.join(tools_dir, 'http_utils.py')
+        tr.Setup.CopyAs(self._init_file, Test.RunDirectory)
+        tr.Setup.CopyAs(http_utils, Test.RunDirectory)
+        tr.Setup.CopyAs(self._quick_server, Test.RunDirectory)
+        tr.Setup.CopyAs(self._partial_post_client, Test.RunDirectory)
+
+        p = tr.Processes.Default
+        p.Command = (f'{sys.executable} {self._partial_post_client} '
+                     f'127.0.0.1 {self._ts.Variables.port}')
+        p.ReturnCode = 0
+        p.Streams.All += Testers.ContainsExpression('Got response', 'Verify 
client received a response from ATS')

Review Comment:
   Both of these assertions are vacuous:
   
   * The client's `main()` unconditionally does `return 0` on every path, 
including the `socket.timeout` and `ConnectionError` handlers, so `p.ReturnCode 
= 0` cannot fail.
   * `ContainsExpression('Got response', ...)` matches `Got response: timeout 
(server may still be processing)` and `Got response: connection closed` just as 
happily as a real response.
   
   So the test currently rests entirely on the implicit "ATS didn't crash" 
check. Please assert on the actual status line the proxy is expected to return, 
and have the client exit non-zero when it doesn't get one — compare 
`quick_server.test.py`, which checks for `HTTP/1.1 200 OK` explicitly.



##########
tests/tools/plugins/null_transform_request.cc:
##########
@@ -0,0 +1,162 @@
+/** @file
+
+  Null request transform plugin hooked at TS_HTTP_READ_REQUEST_HDR_HOOK.
+
+  Used by post_early_response_transform.test.py to reproduce a use-after-free
+  in HttpSM::state_read_server_response_header() when abort_tunnel() is called
+  while a request transform is active. The transform passes request body data
+  through unmodified.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <cstdio>
+#include <cinttypes>
+
+#include "ts/ts.h"
+
+#define PLUGIN_NAME "null_transform_request"
+
+typedef struct {
+  TSVIO            output_vio;
+  TSIOBuffer       output_buffer;
+  TSIOBufferReader output_reader;
+} TransformData;
+
+static TransformData *
+transform_data_alloc()
+{
+  auto *data          = static_cast<TransformData 
*>(TSmalloc(sizeof(TransformData)));
+  data->output_vio    = nullptr;
+  data->output_buffer = nullptr;
+  data->output_reader = nullptr;
+  return data;
+}
+
+static void
+transform_data_destroy(TransformData *data)
+{
+  if (data) {
+    if (data->output_buffer) {
+      TSIOBufferDestroy(data->output_buffer);
+    }
+    TSfree(data);
+  }
+}
+
+static void
+handle_transform(TSCont contp)
+{
+  TSVConn        output_conn = TSTransformOutputVConnGet(contp);
+  TSVIO          input_vio   = TSVConnWriteVIOGet(contp);
+  TransformData *data        = static_cast<TransformData 
*>(TSContDataGet(contp));
+
+  if (!data) {
+    data                = transform_data_alloc();
+    data->output_buffer = TSIOBufferCreate();
+    data->output_reader = TSIOBufferReaderAlloc(data->output_buffer);
+    data->output_vio    = TSVConnWrite(output_conn, contp, 
data->output_reader, TSVIONBytesGet(input_vio));
+    TSContDataSet(contp, data);
+  }
+
+  if (!TSVIOBufferGet(input_vio)) {
+    TSVIONBytesSet(data->output_vio, TSVIONDoneGet(input_vio));
+    TSVIOReenable(data->output_vio);
+    return;
+  }
+
+  int64_t towrite = TSVIONTodoGet(input_vio);
+  if (towrite > 0) {
+    int64_t avail = TSIOBufferReaderAvail(TSVIOReaderGet(input_vio));
+    if (towrite > avail) {
+      towrite = avail;
+    }
+    if (towrite > 0) {
+      TSIOBufferCopy(TSVIOBufferGet(data->output_vio), 
TSVIOReaderGet(input_vio), towrite, 0);
+      TSIOBufferReaderConsume(TSVIOReaderGet(input_vio), towrite);
+      TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + towrite);
+    }
+  }
+
+  if (TSVIONTodoGet(input_vio) > 0) {
+    if (towrite > 0) {
+      TSVIOReenable(data->output_vio);
+      TSContCall(TSVIOContGet(input_vio), TS_EVENT_VCONN_WRITE_READY, 
input_vio);
+    }
+  } else {
+    TSVIONBytesSet(data->output_vio, TSVIONDoneGet(input_vio));
+    TSVIOReenable(data->output_vio);
+    TSContCall(TSVIOContGet(input_vio), TS_EVENT_VCONN_WRITE_COMPLETE, 
input_vio);
+  }
+}
+
+static int
+null_transform(TSCont contp, TSEvent event, void * /* edata ATS_UNUSED */)
+{
+  if (TSVConnClosedGet(contp)) {

Review Comment:
   This bot comment is incorrect — please don'''t act on it. 
`TSTransformCreate()` returns a `TSVConn` that *is* the continuation, so 
passing it to `TSVConnClosedGet()` is the standard idiom for every transform 
plugin in the tree: see `tests/tools/plugins/tunnel_transform.cc` and 
`example/plugins/c-api/null_transform/null_transform.cc`, which both do exactly 
this. The code here is correct as written.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to