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 14bcf29953 header_rewrite: reject bad run-plugin at config load 
(#13493)
14bcf29953 is described below

commit 14bcf29953a07797e4661eac0a812bfa907b5b7d
Author: Brian Neradt <[email protected]>
AuthorDate: Wed Aug 5 16:59:51 2026 -0500

    header_rewrite: reject bad run-plugin at config load (#13493)
    
    A run-plugin whose target plugin failed to load left a null instance
    that tripped a release assert and aborted the server on the first
    request. Propagate the load failure as an exception so the rule is
    rejected at config load time (a reload simply keeps the running
    config), and guard exec() so a stray bad rule can never abort the
    process.
    
    Co-authored-by: Leif Hedstrom <[email protected]>
---
 plugins/header_rewrite/header_rewrite.cc           |  31 ++--
 plugins/header_rewrite/operators.cc                |  36 +++--
 plugins/header_rewrite/ruleset.cc                  |  25 ++--
 plugins/header_rewrite/ruleset.h                   |   2 +-
 .../header_rewrite_bad_run_plugin.test.py          | 156 +++++++++++++++++++++
 5 files changed, 201 insertions(+), 49 deletions(-)

diff --git a/plugins/header_rewrite/header_rewrite.cc 
b/plugins/header_rewrite/header_rewrite.cc
index b8c4f3c6db..80f720a935 100644
--- a/plugins/header_rewrite/header_rewrite.cc
+++ b/plugins/header_rewrite/header_rewrite.cc
@@ -202,12 +202,12 @@ validate_rule_completion(RuleSet *rule, const std::string 
&fname, int lineno)
 bool
 RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook, 
char *from_url, char *to_url)
 {
-  std::unique_ptr<RuleSet>     rule(nullptr);
-  std::string                  filename;
-  int                          lineno = 0;
-  ConditionGroup              *group  = nullptr;
-  std::stack<ConditionGroup *> group_stack;
-  std::stack<OperatorIf *>     if_stack;
+  std::unique_ptr<RuleSet>                rule(nullptr);
+  std::string                             filename;
+  int                                     lineno = 0;
+  ConditionGroup                         *group  = nullptr;
+  std::stack<ConditionGroup *>            group_stack;
+  std::stack<std::unique_ptr<OperatorIf>> if_stack;
 
   constexpr int MAX_IF_NESTING_DEPTH = 10;
 
@@ -366,10 +366,8 @@ RulesConfig::parse_config(const std::string &fname, 
TSHttpHookID default_hook, c
             throw std::runtime_error("maximum if nesting depth exceeded");
           }
 
-          auto *op_if = new OperatorIf();
-
-          if_stack.push(op_if);
-          group = op_if->get_group(); // Set group to the new OperatorIf's 
group
+          if_stack.push(std::make_unique<OperatorIf>());
+          group = if_stack.top()->get_group(); // Set group to the new 
OperatorIf's group
           Dbg(dbg_ctl, "Started nested OperatorIf, depth: %zu", 
if_stack.size());
 
         } else if (p.is_endif()) {
@@ -377,21 +375,20 @@ RulesConfig::parse_config(const std::string &fname, 
TSHttpHookID default_hook, c
             throw std::runtime_error("endif without matching if");
           }
 
-          OperatorIf *op_if = if_stack.top();
+          auto op_if = std::move(if_stack.top());
 
           if_stack.pop();
           if (!if_stack.empty()) {
             auto *parent_sec = if_stack.top()->cur_section();
 
             if (parent_sec->ops.oper) {
-              parent_sec->ops.oper->append(op_if);
+              parent_sec->ops.oper->append(op_if.release());
             } else {
-              parent_sec->ops.oper.reset(op_if);
+              parent_sec->ops.oper = std::move(op_if);
             }
             group = if_stack.top()->get_group();
           } else {
-            if (!rule->add_operator(op_if)) {
-              delete op_if;
+            if (!rule->add_operator(std::move(op_if))) {
               throw std::runtime_error("Failed to add nested OperatorIf to 
RuleSet");
             }
             group = rule->get_group();
@@ -434,10 +431,6 @@ RulesConfig::parse_config(const std::string &fname, 
TSHttpHookID default_hook, c
   // Check for unmatched if statements
   if (!if_stack.empty()) {
     TSError("[%s] %zu unmatched 'if' statement(s) without 'endif' in file: 
%s", PLUGIN_NAME, if_stack.size(), fname.c_str());
-    while (!if_stack.empty()) {
-      delete if_stack.top();
-      if_stack.pop();
-    }
     return false;
   }
 
diff --git a/plugins/header_rewrite/operators.cc 
b/plugins/header_rewrite/operators.cc
index 8013990285..5d7bbab386 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -23,6 +23,7 @@
 #include <cstring>
 #include <algorithm>
 #include <iomanip>
+#include <stdexcept>
 
 #include "records/RecCore.h"
 #include "ts/ts.h"
@@ -1272,8 +1273,7 @@ OperatorRunPlugin::initialize(Parser &p)
   auto plugin_args = p.get_value();
 
   if (plugin_name.empty()) {
-    TSError("[%s] missing plugin name", PLUGIN_NAME);
-    return;
+    throw std::runtime_error("run-plugin missing plugin name");
   }
 
   std::vector<std::string> tokens;
@@ -1284,15 +1284,10 @@ OperatorRunPlugin::initialize(Parser &p)
     tokens.push_back(token);
   }
 
-  // Create argc and argv
-  int    argc = tokens.size() + 2;
-  char **argv = new char *[argc];
-
-  argv[0] = p.from_url();
-  argv[1] = p.to_url();
+  std::vector<char *> argv{p.from_url(), p.to_url()};
 
-  for (size_t i = 0; i < tokens.size(); ++i) {
-    argv[i + 2] = const_cast<char *>(tokens[i].c_str());
+  for (auto const &argument : tokens) {
+    argv.push_back(const_cast<char *>(argument.c_str()));
   }
 
   std::string error;
@@ -1304,14 +1299,12 @@ OperatorRunPlugin::initialize(Parser &p)
     elevate_access = 
RecGetRecordInt("proxy.config.plugin.load_elevated").value_or(0);
     ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0);
 
-    _plugin = plugin_factory.getRemapPlugin(swoc::file::path(plugin_name), 
argc, const_cast<char **>(argv), error,
+    _plugin = plugin_factory.getRemapPlugin(swoc::file::path(plugin_name), 
static_cast<int>(argv.size()), argv.data(), error,
                                             isPluginDynamicReloadEnabled());
   } // done elevating access
 
-  delete[] argv;
-
   if (!_plugin) {
-    TSError("[%s] Unable to load plugin '%s': %s", PLUGIN_NAME, 
plugin_name.c_str(), error.c_str());
+    throw std::runtime_error("run-plugin unable to load plugin '" + 
std::string{plugin_name} + "': " + error);
   }
 }
 
@@ -1326,7 +1319,11 @@ OperatorRunPlugin::initialize_hooks()
 bool
 OperatorRunPlugin::exec(const Resources &res) const
 {
-  TSReleaseAssert(_plugin != nullptr);
+  // Rejected at config load (see initialize); guard anyway so a stray bad 
rule can't abort the server.
+  if (!_plugin) {
+    Dbg(pi_dbg_ctl, "OperatorRunPlugin::exec skipped, plugin was not loaded");
+    return true;
+  }
 
   if (res._rri && res.state.txnp) {
     _plugin->doRemap(res.state.txnp, res._rri);
@@ -1654,7 +1651,7 @@ OperatorIf::new_section(Parser::CondClause clause)
 bool
 OperatorIf::add_operator(Parser &p, const char *filename, int lineno)
 {
-  Operator *op = operator_factory(p.get_op());
+  std::unique_ptr<Operator> op{operator_factory(p.get_op())};
 
   if (!op) {
     TSError("[%s] Unknown operator: %s, file: %s, line: %d", PLUGIN_NAME, 
p.get_op().c_str(), filename, lineno);
@@ -1667,7 +1664,6 @@ OperatorIf::add_operator(Parser &p, const char *filename, 
int lineno)
   try {
     op->initialize(p);
   } catch (std::exception const &ex) {
-    delete op;
     TSError("[%s] Failed to initialize operator: %s, file: %s, line: %d, 
error: %s", PLUGIN_NAME, p.get_op().c_str(), filename,
             lineno, ex.what());
     return false;
@@ -1675,10 +1671,10 @@ OperatorIf::add_operator(Parser &p, const char 
*filename, int lineno)
 
   // Add to current section
   if (_cur_section->ops.oper) {
-    _cur_section->ops.oper->append(op);
+    _cur_section->ops.oper->append(op.release());
   } else {
-    _cur_section->ops.oper.reset(op);
-    _cur_section->ops.oper_mods = op->get_oper_modifiers();
+    _cur_section->ops.oper      = std::move(op);
+    _cur_section->ops.oper_mods = _cur_section->ops.oper->get_oper_modifiers();
   }
 
   return true;
diff --git a/plugins/header_rewrite/ruleset.cc 
b/plugins/header_rewrite/ruleset.cc
index acbeafc72f..eae2ff0af1 100644
--- a/plugins/header_rewrite/ruleset.cc
+++ b/plugins/header_rewrite/ruleset.cc
@@ -19,6 +19,7 @@
 // ruleset.cc: implementation of the ruleset class
 //
 //
+#include <exception>
 #include <string>
 
 #include "ruleset.h"
@@ -89,14 +90,20 @@ RuleSet::make_condition(Parser &p, const char *filename, 
int lineno)
 bool
 RuleSet::add_operator(Parser &p, const char *filename, int lineno)
 {
-  Operator *op = operator_factory(p.get_op());
+  std::unique_ptr<Operator> op{operator_factory(p.get_op())};
 
-  if (nullptr != op) {
+  if (op) {
     Dbg(pi_dbg_ctl, "    Adding operator: %s(%s)=\"%s\"", p.get_op().c_str(), 
p.get_arg().c_str(), p.get_value().c_str());
     op->set_config_location(filename, lineno);
-    op->initialize(p);
+
+    try {
+      op->initialize(p);
+    } catch (std::exception const &ex) {
+      TSError("[%s] in %s:%d: failed to initialize operator %s: %s", 
PLUGIN_NAME, filename, lineno, p.get_op().c_str(), ex.what());
+      return false;
+    }
+
     if (!op->is_hook_valid(_hook)) {
-      delete op;
       Dbg(pi_dbg_ctl, "in %s:%d: can't use this operator in hook=%s:  %s(%s)", 
filename, lineno, TSHttpHookNameLookup(_hook),
           p.get_op().c_str(), p.get_arg().c_str());
       TSError("[%s] in %s:%d: can't use this operator in hook=%s:  %s(%s)", 
PLUGIN_NAME, filename, lineno,
@@ -107,9 +114,9 @@ RuleSet::add_operator(Parser &p, const char *filename, int 
lineno)
     auto *cur_sec = _op_if.cur_section();
 
     if (!cur_sec->ops.oper) {
-      cur_sec->ops.oper.reset(op);
+      cur_sec->ops.oper = std::move(op);
     } else {
-      cur_sec->ops.oper->append(op);
+      cur_sec->ops.oper->append(op.release());
     }
 
     cur_sec->ops.oper_mods = static_cast<OperModifiers>(cur_sec->ops.oper_mods 
| cur_sec->ops.oper->get_oper_modifiers());
@@ -136,14 +143,14 @@ RuleSet::get_all_resource_ids() const
 }
 
 bool
-RuleSet::add_operator(Operator *op)
+RuleSet::add_operator(std::unique_ptr<Operator> op)
 {
   auto *cur_sec = _op_if.cur_section();
 
   if (!cur_sec->ops.oper) {
-    cur_sec->ops.oper.reset(op);
+    cur_sec->ops.oper = std::move(op);
   } else {
-    cur_sec->ops.oper->append(op);
+    cur_sec->ops.oper->append(op.release());
   }
 
   // Update some ruleset state based on this new operator
diff --git a/plugins/header_rewrite/ruleset.h b/plugins/header_rewrite/ruleset.h
index 78680bc9d7..0ad40f7183 100644
--- a/plugins/header_rewrite/ruleset.h
+++ b/plugins/header_rewrite/ruleset.h
@@ -49,7 +49,7 @@ public:
   Condition  *make_condition(Parser &p, const char *filename, int lineno);
   ResourceIDs get_all_resource_ids() const;
   bool        add_operator(Parser &p, const char *filename, int lineno);
-  bool        add_operator(Operator *op);
+  bool        add_operator(std::unique_ptr<Operator> op);
 
   ConditionGroup *
   get_group()
diff --git 
a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py
 
b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py
new file mode 100644
index 0000000000..c5371f5c9e
--- /dev/null
+++ 
b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py
@@ -0,0 +1,156 @@
+#  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.
+'''
+Verify header_rewrite rejects a run-plugin operator whose target plugin fails 
to
+load. The failure must be caught at config load time, not aborted at request 
time.
+'''
+
+Test.Summary = '''
+header_rewrite must reject a run-plugin whose target plugin fails to load, at
+config load time, rather than aborting the server on the first request.
+'''
+
+# Reproduce the reported crash: run-plugin against a plugin whose 
instance-init fails
+# (conf_remap + a missing file) hands header_rewrite a null instance, which 
old code aborted on.
+Test.SkipUnless(
+    Condition.PluginExists('header_rewrite.so'),
+    Condition.PluginExists('conf_remap.so'),
+)
+
+
+class TestBadRunPlugin:
+    '''Verify failed run-plugin initialization is rejected safely.'''
+
+    ERROR_MARKER: str = 'run-plugin unable to load'
+    BAD_RULE_LINES: list[str] = [
+        'cond %{REMAP_PSEUDO_HOOK}',
+        '  run-plugin conf_remap.so no_such_conf_remap_file.yaml',
+    ]
+    NESTED_BAD_RULE_LINES: list[str] = [
+        'cond %{REMAP_PSEUDO_HOOK}',
+        '  if',
+        '    cond %{TRUE}',
+        '      run-plugin conf_remap.so no_such_conf_remap_file.yaml',
+        '  endif',
+    ]
+
+    def __init__(self) -> None:
+        '''Configure startup and reload rejection scenarios.'''
+        self._configure_startup_rejection()
+        self._server = self._configure_origin_server()
+        self._ts = self._configure_traffic_server()
+        self._configure_baseline_request()
+        self._configure_bad_remap_install()
+        self._configure_failed_reload()
+        self._configure_post_reload_request()
+        self._ts.Disk.diags_log.Content = Testers.IncludesExpression(
+            self.ERROR_MARKER, 'the rejected reload should log the run-plugin 
failure')
+
+    def _configure_startup_rejection(self) -> None:
+        '''Verify a bad top-level run-plugin fails startup cleanly.'''
+        ts = Test.MakeATSProcess("ts-startup", disable_log_checks=True)
+        ts.Disk.records_config.update({
+            'proxy.config.diags.debug.enabled': 1,
+            'proxy.config.diags.debug.tags': 'header_rewrite',
+        })
+        
ts.Disk.MakeConfigFile('bad_run_plugin.conf').AddLines(self.BAD_RULE_LINES)
+        ts.Disk.remap_config.AddLine(
+            'map http://startup.example.com/ http://127.0.0.1/ '
+            '@plugin=header_rewrite.so @pparam=bad_run_plugin.conf')
+
+        # Invalid remap.config triggers a controlled exit rather than SIGABRT.
+        ts.ReturnCode = 33
+        ts.Ready = 0
+        ts.Disk.diags_log.Content = Testers.IncludesExpression(
+            self.ERROR_MARKER, 'header_rewrite must report the failed 
run-plugin load')
+        ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+            'Traffic Server is fully initialized', 'ATS must not initialize 
with a bad run-plugin config')
+
+        tr = Test.AddTestRun("Bad run-plugin config fails startup instead of 
crashing")
+        tr.Processes.Default.Command = 'echo verifying startup rejection'
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.StartBefore(ts)
+
+    def _configure_origin_server(self) -> 'Process':
+        '''Configure the origin used to verify reload behavior.'''
+        server = Test.MakeOriginServer("server")
+        request_header = {
+            "headers": "GET / HTTP/1.1\r\nHost: reload.example.com\r\n\r\n",
+            "timestamp": "1469733493.993",
+            "body": ""
+        }
+        response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: 
close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+        server.addResponse("sessionfile.log", request_header, response_header)
+        return server
+
+    def _configure_traffic_server(self) -> 'Process':
+        '''Configure ATS with a valid initial remap table.'''
+        ts = Test.MakeATSProcess("ts-reload", disable_log_checks=True)
+        ts.Disk.records_config.update({
+            'proxy.config.diags.debug.enabled': 1,
+            'proxy.config.diags.debug.tags': 'header_rewrite',
+        })
+        
ts.Disk.MakeConfigFile('nested_bad_run_plugin.conf').AddLines(self.NESTED_BAD_RULE_LINES)
+        ts.Disk.remap_config.AddLine(f'map http://reload.example.com 
http://127.0.0.1:{self._server.Variables.Port}')
+        return ts
+
+    def _configure_curl_run(self, name: str, expectation: str) -> 'TestRun':
+        '''Configure a request that verifies ATS still serves traffic.'''
+        tr = Test.AddTestRun(name)
+        tr.MakeCurlCommand(
+            f'--proxy 127.0.0.1:{self._ts.Variables.port} 
"http://reload.example.com"; '
+            '-H "Proxy-Connection: keep-alive" --verbose',
+            ts=self._ts)
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stderr = Testers.IncludesExpression('200 
OK', expectation)
+        tr.StillRunningAfter = self._ts
+        tr.StillRunningAfter = self._server
+        return tr
+
+    def _configure_baseline_request(self) -> None:
+        '''Verify the valid initial configuration serves requests.'''
+        tr = self._configure_curl_run("Baseline request is served before 
reload", 'baseline request should be served')
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts)
+
+    def _configure_bad_remap_install(self) -> None:
+        '''Replace remap.config with one containing a bad nested run-plugin.'''
+        tr = Test.AddTestRun("Install a remap.config with a bad run-plugin")
+        remap_path = self._ts.Disk.remap_config.AbsPath
+        tr.Disk.File(remap_path, id="remap_bad", typename="ats:config")
+        tr.Disk.remap_bad.AddLine(
+            f'map http://reload.example.com 
http://127.0.0.1:{self._server.Variables.Port} '
+            '@plugin=header_rewrite.so @pparam=nested_bad_run_plugin.conf')
+        tr.Processes.Default.Command = 'echo installed bad remap.config'
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Env = self._ts.Env
+        tr.StillRunningAfter = self._ts
+        tr.StillRunningAfter = self._server
+
+    def _configure_failed_reload(self) -> None:
+        '''Verify the bad remap table is rejected without stopping ATS.'''
+        tr = Test.AddConfigReload(
+            self._ts, expect="fail", delay_start=2, description="Reload with 
bad run-plugin must be rejected, not fatal")
+        tr.StillRunningAfter = self._ts
+        tr.StillRunningAfter = self._server
+
+    def _configure_post_reload_request(self) -> None:
+        '''Verify the rejected reload leaves the old configuration active.'''
+        self._configure_curl_run(
+            "Server still serves the old config after the rejected reload", 
'old config should still serve after a rejected reload')
+
+
+TestBadRunPlugin()

Reply via email to