Skip to content

Fix rules-loading memory leaks on SecDefaultAction error paths and repeated msg/severity/logdata - #3638

Open
tomsommer wants to merge 2 commits into
owasp-modsecurity:v3/masterfrom
tomsommer:fix/parser-config-time-leaks
Open

tomsommer wants to merge 2 commits into
owasp-modsecurity:v3/masterfrom
tomsommer:fix/parser-config-time-leaks

Conversation

@tomsommer

@tomsommer tomsommer commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

what

Two commits, one fix each:

  • SecDefaultAction: the actions are no longer released into a raw std::vector<Action *>*; they stay in unique_ptrs (the $2 vector and a std::vector<std::unique_ptr<Action>> for the accepted ones) and are moved into driver.m_defaultActions at the end. The regenerated seclang-parser.cc is included (Bison 3.8.2, same as the checked-in file; regenerating the unmodified grammar reproduces it byte for byte, so the diff is only the rule body plus #line renumbering).
  • RuleWithActions constructor: delete the previous m_severity / m_logData / m_msg before overwriting it when a rule repeats severity:, logdata: or msg: (last one still wins).
  • Tests: a "no disruptive action" case in config-secdefaultaction.json with the expected parser error, and a "msg informed twice, last one wins" case in action-msg.json.

why

  • The four YYERROR exits of the SecDefaultAction rule (t:none, unsuitable action, no disruptive action, duplicate phase) jumped out before delete actions, leaking the vector and every released action. Only invalid configurations hit this, but under nginx a repeatedly failing nginx -s reload or nginx -t accumulates it in the master. With unique_ptrs, both the goto out of the block and Bison's pop of $2 during error recovery destroy everything.
  • A rule with two msg: (or severity: / logdata:) actions leaked one Action per parse; the members are null-initialised in the constructor and deleted in the destructor, so deleting before overwrite is safe.

Evidence, unfixed tree:

# config-secdefaultaction.json
==2== 2,088 (96 direct, 1,992 indirect) bytes in 4 blocks are definitely lost
==2==    at operator new(unsigned long)
==2==    by yy::seclang_parser::parse() (seclang-parser.yy:1214)
# action-msg.json
==2== 247 (72 direct, 175 indirect) bytes in 1 blocks are definitely lost
==2==    by yy::seclang_parser::parse() (seclang-parser.yy:2834)   <- the first msg:

With the fixes both files report All heap blocks were freed -- no leaks are possible. make check on this branch: TOTAL 5045, PASS 5029, SKIP 16, FAIL 0.

references

Summary by CodeRabbit

  • Bug Fixes
    • Fixed handling of multiple msg actions so the final message is consistently retained and reported.
    • Improved cleanup of repeated action values, preventing stale messages, severity settings, or log data from affecting processing.
    • Confirmed that SecDefaultAction configurations without a disruptive action continue to be rejected with a clear validation error.

Tom Sommer added 2 commits September 19, 2026 16:27
The SecDefaultAction rule released the parsed actions out of the bison
semantic value into a raw std::vector<actions::Action *> allocated with new,
and only deleted that vector on the last line of the action. All four
validation failures in between (transformation none, action not suitable for
SecDefaultAction, missing disruptive action, phase already configured) leave
the block through YYERROR, so the vector and every Action it had taken
ownership of were leaked.

A configuration error is not a one-off: nginx re-parses the whole rule set in
the master process on every reload and on every configuration test, so a
single bad SecDefaultAction leaks on each of them.

Instead of releasing the actions, iterate over the semantic value by
reference and move the suitable ones into a local
std::vector<std::unique_ptr<actions::Action>>, which is then moved into
driver.m_defaultActions. The phase action no longer needs an explicit delete,
it is simply left behind and destroyed with the semantic value. YYERROR is a
goto, so the destructors of the block scope locals still run, and bison
reclaims the rule's right hand side with yypop_() in yyerrorlab, which
destroys the variant holding the vector of unique_ptr. Nothing is leaked on
either path.

The regenerated seclang-parser.cc is included; it was produced with the same
bison 3.8.2 that generated the checked-in file, so the diff is limited to the
rule body, the yyrline_ table and the #line directives.

A regression test covering a SecDefaultAction without a disruptive action was
added to test/test-cases/regression/config-secdefaultaction.json.
RuleWithActions keeps a single pointer for each of the msg, logdata and
severity actions and deletes them in its destructor, but the constructor
assigned over whatever was already stored there. A rule that names one of
those actions more than once, for example

    SecRule ARGS "@contains x" "id:1,phase:2,pass,msg:'a',msg:'b'"

therefore leaked one Action (and the RunTimeString it owns) per rule, per
parse. Rule sets are re-parsed on every nginx reload and on every nginx
configuration test, so the loss accumulates in the master process.

Delete the previous action before storing the new one. The last occurrence
still wins, which is the behaviour that was already in place.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: c793740a-5389-48f8-9b44-1a43266ac91b

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea9fef and c9c963e.

📒 Files selected for processing (5)
  • src/parser/seclang-parser.cc
  • src/parser/seclang-parser.yy
  • src/rule_with_actions.cc
  • test/test-cases/regression/action-msg.json
  • test/test-cases/regression/config-secdefaultaction.json

Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The parser now uses std::unique_ptr for SecDefaultAction actions. Runtime action replacement deletes prior Severity, LogData, and Msg values. Regression tests cover missing disruptive actions and repeated msg actions.

Changes

Action memory management

Layer / File(s) Summary
SecDefaultAction ownership and validation
src/parser/seclang-parser.yy, test/test-cases/regression/config-secdefaultaction.json
SecDefaultAction moves actions through std::unique_ptr containers. Existing validation remains unchanged. A regression case checks the missing disruptive-action error.
Runtime action replacement
src/rule_with_actions.cc, test/test-cases/regression/action-msg.json
Runtime-only handling deletes previous Severity, LogData, and Msg values before replacement. A regression case verifies that the last msg value is logged.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (3 skipped: 3 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: fixing rule-loading memory leaks for SecDefaultAction error paths and repeated msg, severity, and logdata actions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant