header_rewrite: reject bad run-plugin at config load - #13425
Conversation
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.
There was a problem hiding this comment.
Pull request overview
This PR hardens the header_rewrite plugin’s run-plugin operator so that failures to load the target remap plugin are rejected during configuration load/reload rather than causing a TSReleaseAssert abort on first request. It also adds an AuTest gold test to prevent regressions and validate both cold-start and reload behavior.
Changes:
- Convert
OperatorRunPlugin::initialize()load failures into exceptions, so badrun-pluginrules are rejected during config parsing. - Catch operator initialization exceptions in
RuleSet::add_operator()and fail the ruleset cleanly with an error log. - Add a gold test covering both startup rejection and reload rejection while keeping the running config active.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py | Adds coverage ensuring a bad run-plugin is rejected at config load/reload and never crashes request processing. |
| plugins/header_rewrite/ruleset.cc | Catches Operator::initialize() exceptions so operator init failures reject the config cleanly. |
| plugins/header_rewrite/operators.cc | Makes run-plugin initialization throw on load failures and guards exec() against a null plugin instance. |
|
[approve ci] |
| if (plugin_name.empty()) { | ||
| TSError("[%s] missing plugin name", PLUGIN_NAME); | ||
| return; | ||
| throw std::runtime_error("run-plugin missing plugin name"); |
There was a problem hiding this comment.
Did Claude do this? We typically don't throw exceptions.
bneradt
left a comment
There was a problem hiding this comment.
I found two exception-safety/resource-lifetime issues that should be addressed:
Make the temporary argv exception-safe
OperatorRunPlugin::initialize() allocates argv with new[], calls getRemapPlugin(), and only then reaches delete[]. The factory and its filesystem/allocation helpers can throw standard exceptions; those are now caught by RuleSet::add_operator() or OperatorIf::add_operator(), but unwinding skips the manual delete. A rejected reload can therefore leak this allocation. Please use RAII here, such as std::vector<char *> or std::unique_ptr<char *[]>.
trafficserver/plugins/header_rewrite/operators.cc
Lines 1297 to 1324 in 079936f
Clean up active nested-if frames on failure
When a bad run-plugin appears inside an open if, OperatorIf::add_operator() catches the initialization exception and returns false. RulesConfig::parse_config() then takes its runtime-error return path, but if_stack still contains raw OperatorIf * values created for active frames. Those frames have not yet been transferred into the RuleSet, so returning without draining the stack leaks them and any successfully initialized operators or plugin instances they already own. Repeated rejected reloads can accumulate these leaks. Please make the stack RAII-owned or clean it on all error exits; adding a nested run-plugin case to this test would cover the second initialization path.
trafficserver/plugins/header_rewrite/header_rewrite.cc
Lines 369 to 417 in 079936f
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.