Skip to content

Fix memory leak when SecGeoLookupDb loads a database more than once - #3637

Open
tomsommer wants to merge 1 commit into
owasp-modsecurity:v3/masterfrom
tomsommer:fix/geolookup-maxmind-reopen-leak
Open

tomsommer wants to merge 1 commit into
owasp-modsecurity:v3/masterfrom
tomsommer:fix/geolookup-maxmind-reopen-leak

Conversation

@tomsommer

@tomsommer tomsommer commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

what

  • GeoLookup::setDataBase() calls the existing cleanUp() before opening the new database, so a previously loaded database is released instead of overwritten.
  • Adds test/test-cases/regression/config-secgeolookupdb.json: SecGeoLookupDb listed twice with the same file, plus a @geoLookup rule that must still resolve GEO:COUNTRY_NAME.

why

  • GeoLookup is a process-wide singleton. Under WITH_MAXMIND, setDataBase() called MMDB_open() into the mmdb member unconditionally, so the previous handle (heap metadata plus the mmap of the whole database file) was never closed. cleanUp() was only reached from the singleton destructor and, for GeoIP builds only, from ~ModSecurity.
  • Every parse of SecGeoLookupDb therefore leaked one database mapping: every nginx -s reload, every nginx -t, and every server or location whose rules include the directive within one configuration. For GeoLite2-City that is roughly 70 MB of address space per parse plus one more mapping toward vm.max_map_count, accumulating in the master and inherited by workers.
  • Behaviour changes, both intentional: a SecGeoLookupDb that fails after a good one now leaves the singleton in NOT_LOADED (lookups log "Database is not open" instead of "search tree is corrupt" over a clobbered struct); in GeoIP-only builds a later directive now replaces the database instead of being silently ignored.
  • Deliberately not changed: ~ModSecurity still calls cleanUp() only under WITH_GEOIP. Adding it for MaxMind would close the database the new nginx cycle is already using, since the old cycle's instance is destroyed after the new configuration is parsed. That ordering hazard already exists for GeoIP builds and is out of scope here.

Evidence, unfixed tree, valgrind --leak-check=full ./.libs/regression_tests test-cases/regression/config-secgeolookupdb.json (MaxMind-only build):

==2== 142 (16 direct, 126 indirect) bytes in 1 blocks are definitely lost in loss record 8 of 8
==2==    by ...: MMDB_open (libmaxminddb)
==2==    by ...: modsecurity::Utils::GeoLookup::setDataBase(...) (geo_lookup.cc:70)
==2==    by ...: yy::seclang_parser::parse() (seclang-parser.yy:1608)
==2== LEAK SUMMARY: definitely lost: 82 bytes in 4 blocks, indirectly lost: 132 bytes in 8 blocks

The mmap of the database file itself is not tracked by leak-check, so the real loss is larger than the bytes shown. With the fix: All heap blocks were freed -- no leaks are possible; variable-GEO.json still passes.

references

Summary by CodeRabbit

  • Bug Fixes

    • Fixed repeated geolocation database configuration so previously loaded database resources are released before a new database is opened.
    • Prevented resource leaks when configuration is reloaded or the geolocation database directive is applied multiple times.
  • Tests

    • Added regression coverage confirming repeated geolocation database configuration continues to return the expected country information.

GeoLookup is a process wide singleton and GeoLookup::setDataBase() used to
call MMDB_open() unconditionally, overwriting the MMDB_s member without
closing the database that was open before. Every additional evaluation of
SecGeoLookupDb therefore leaked the MMDB_s heap metadata plus the mmap()ed
copy of the whole database file.

This is not limited to a configuration that repeats the directive: under
nginx the master process re-parses all rules on every reload and on every
configuration test, and each server/location context that includes the
directive parses it again, so the leak accumulates for the lifetime of the
master process and is inherited by every worker.

cleanUp() already knows how to release either back end and resets m_version,
so calling it at the top of setDataBase() is enough. The GeoIP back end was
not leaking (GeoIP_open is guarded by m_version == NOT_LOADED), but it kept
the first database forever and ignored any later SecGeoLookupDb; it is now
replaced like the MaxMind one.

Behaviour change on the error path: when a SecGeoLookupDb fails to open after
a good database was already loaded, the previous database is now released and
m_version is left as NOT_LOADED, so a later @geoLookup logs "Database is not
open. Use: SecGeoLookupDb directive." Before this change m_version stayed as
VERSION_MAXMIND while MMDB_open() had already clobbered the MMDB_s holding
the good database, so @geoLookup set no GEO variable and reported the
confusing "MaxMind: Got an error from libmaxminddb: The MaxMind DB file's
search tree is corrupt" instead. In both cases the configuration carrying the
failing directive is rejected with a parser error.

Out of scope, and deliberately not changed: ~ModSecurity only calls
GeoLookup::cleanUp() under WITH_GEOIP. Calling it for MaxMind as well would
be wrong under nginx, where the old cycle's ModSecurity instance is destroyed
*after* the new cycle has already parsed its rules -- it would close the
database the new configuration is about to use. The same hazard already
exists for the GEOIP build.
@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: 7e2a29ba-d10b-4cae-8610-e5e6a45b4a51

📥 Commits

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

📒 Files selected for processing (3)
  • src/utils/geo_lookup.cc
  • test/test-cases/regression/config-secgeolookupdb.json
  • test/test-suite.in

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


📝 Walkthrough

Walkthrough

GeoLookup::setDataBase now cleans up an existing database before loading another one. A regression test configures the same database twice and verifies the resulting country lookup.

Changes

Geo lookup database reload handling

Layer / File(s) Summary
Reload cleanup and regression coverage
src/utils/geo_lookup.cc, test/test-cases/regression/config-secgeolookupdb.json, test/test-suite.in
setDataBase calls cleanUp() before opening a database. The regression test configures SecGeoLookupDb twice, checks the Brazil country result, and is added to the test suite.

Priority: ⬇️ Low

Estimated code review effort: 1 (Trivial) | ~5 minutes

Change: Bug fix

🚥 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. (2 skipped: 2 … 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 change: fixing a memory leak caused by loading a database multiple times through SecGeoLookupDb.
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. (2 skipped: 2 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