A URL whose query embeds a percent-escaped URL is unwrapped
recursively (PR #6066): rspamd_url_find_in_query re-enters
rspamd_multipattern_lookup on the URL trie while the enclosing scan
is still on the stack. Each scan borrows one of MAX_SCRATCH hyperscan
scratch contexts; once the recursion nests deeper than the pool, the
slot loop leaves scr == NULL and g_assert(scr != NULL) aborts the
worker. A crafted message with a few levels of nested query URLs thus
crashes a normal worker (DoS).
The peak number of simultaneously-held scratch contexts on the
deepest chain is RSPAMD_URL_QUERY_MAX_NESTING + 2: one for the
enclosing text/subject scan and one for the per-URL TLD lookup that
rspamd_url_parse runs on each freshly extracted leaf. The old pool of
4 with a nesting cap of 5 needed 7 -> assertion.
- Introduce RSPAMD_MULTIPATTERN_MAX_REENTRANCY (10) and size the
scratch stack from it; a scratch context is ~2.5-4 KiB, so the
deeper stack costs only tens of KiB per multipattern.
- Tie RSPAMD_URL_QUERY_MAX_NESTING to that budget (minus the two
implicit levels) so normal nesting stays on the fast path.
- Make scratch exhaustion non-fatal: allocate a one-off scratch for
the scan instead of aborting the worker on attacker input.
- Guard the unsigned-int scratch bitmask with a static assert.
Add functional regression test 170_url_query_nesting.