prefetch: don't drop replacements for non-participating optional capture groups - #13352
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes how the prefetch plugin expands $N references in --fetch-path-pattern replacements so that defined-but-non-participating optional capture groups expand to an empty string (PCRE2 semantics) instead of invalidating the entire replacement. It also tightens config-load validation for replacement references and several prefetch options, and adds AuTest coverage for the reported regressions.
Changes:
- Validate
$Nreplacement references at config-load time against the regex’s defined capture-group count, and expand non-participating optional groups to""at match time. - Treat invalid prefetch configuration (bad pattern / bad numeric options / invalid overflow policy) as a remap-load failure rather than silently disabling prefetch.
- Add gold tests covering optional-group non-participation, empty-collapsing replacements, and over-limit capture-group patterns.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.test.py | New autest verifying optional trailing group $3 does not cause per-request replacement rejection. |
| tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.gold | Expected request log lines for the optional-group test. |
| tests/gold_tests/pluginTest/prefetch/prefetch_empty_replacement.test.py | New autest verifying empty-expanded paths are logged and skipped (no self-prefetch). |
| tests/gold_tests/pluginTest/prefetch/prefetch_bad_pattern_refused.test.py | New autest verifying over-limit capture-group patterns are refused at config load. |
| plugins/prefetch/plugin.cc | Skip scheduling when a replacement collapses to an empty expanded path. |
| plugins/prefetch/pattern.h | Remove now-dead helper declarations. |
| plugins/prefetch/pattern.cc | Adjust replacement expansion semantics; add config-load validation for $N references. |
| plugins/prefetch/configs.h | Change setFetchOverflow to return bool for validation. |
| plugins/prefetch/configs.cc | Enforce stricter option validation and fail remap instance creation on invalid config. |
| if (nullptr == optarg || '\0' == *optarg) { | ||
| return false; | ||
| } | ||
| for (const char *p = optarg; '\0' != *p; ++p) { |
There was a problem hiding this comment.
nitpicky but does this need any range-checking?
There was a problem hiding this comment.
Good catch.
Switched both --fetch-count and --fetch-max to std::from_chars() parsing directly into the destination unsigned, which reports out-of-range and trailing junk in the same call. Added prefetch_bad_count_refused to cover it; it fails against the previous code.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
plugins/prefetch/configs.cc:187
- The parser rejects inputs like
+10and any leading whitespace (which some users may still think of as 'non-negative integers'), but the error message suggests those should be accepted. Consider tightening the wording to match the actual accepted format (e.g., 'expected an unsigned decimal integer without sign characters') or explicitly documenting/allowing a leading '+'.
if (unsigned count = 0; parseUnsignedInt(optarg, count)) {
setFetchCount(count);
} else {
PrefetchError("invalid --fetch-count '%s': expected a non-negative integer", optarg ? optarg : "");
status = false;
}
tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.test.py:2
- The file starts with two empty triple-quoted string literals; these create an empty module docstring/no-op string expression and add noise. Remove them, or replace with a single meaningful module docstring (you already have
Test.Summary).
'''
'''
| for (int i = 0; i < _tokenCount; i++) { | ||
| int replIndex = _tokens[i]; | ||
| std::string_view dst = matches[replIndex]; | ||
| int replIndex = _tokens[i]; |
There was a problem hiding this comment.
Fixed. The view is now built from "" so data() is never null, and the append uses the std::string_view overload rather than the pointer/length pair, so neither the "%.*s" nor the append passes a null pointer.
| * absent) would otherwise be scheduled with a zero-length path, which BgFetch skips -- | ||
| * leaving the original request path in place and prefetching the pristine URL itself. | ||
| * Stop rather than issue that self-prefetch. */ | ||
| PrefetchError("prefetch pattern produced an empty path; check the fetch-path-pattern replacement"); |
There was a problem hiding this comment.
If this is per transaction, will this flood our logs with this error?
There was a problem hiding this comment.
Good catch, and worse than per transaction: whether the replacement collapses depends on the request, so for a misconfigured pattern it recurs indefinitely rather than being a one-time complaint at load.
PrefetchConfig::shouldReportEmptyPath() now reports it at error level once per remap instance and drops to the debug tag afterwards. A config reload builds a new instance, so a genuine misconfiguration stays visible after each reload.
|
since validation behavior changes, does this need a doc update? |
Pattern::replace() rejected any replacement reference $N whose index was >= the value returned by the match. But that value is one past the highest capture group that *participated* in the match, not the number of groups the pattern defines. A trailing optional group such as "(\?.*)?" that does not participate -- e.g. a request with no query string -- lowers that count and makes a valid $N look out of range, so every such request logged "invalid reference in replacement string: $N" and silently dropped the prefetch. Validate $N references once at config-load time against the pattern's actual capture-group count (Regex::get_capture_count()), and at match time substitute an empty string for a group that did not participate instead of failing the whole replacement -- the documented PCRE2 semantics for an unmatched group. Also treat an unusable fetch-path-pattern, and invalid --fetch-count / --fetch-max / --fetch-overflow values, as configuration errors so the remap rule is refused at load rather than silently running with prefetch disabled; skip an empty expanded path instead of self-prefetching the original request path; and remove the now-dead process()/capture() helpers. Adds autests for the non-participating-group fix, the rejected over-limit pattern, and the empty-replacement skip.
The digits-only check on --fetch-count and --fetch-max accepted any string of decimal digits. The value then went through strtoul() into a size_t and was assigned to an unsigned, so a value above UINT_MAX was silently truncated: --fetch-count=5000000000 became 705032704. Parse with std::from_chars() into the destination type instead. It reports an out-of-range value and a trailing non-digit, so both are configuration errors now, consistent with the other option values.
A group that did not participate yielded a default-constructed string_view, whose data() is null. Passing that to "%.*s" and to append(ptr, 0) is undefined even at zero length, so build the view from "" and append the view itself. Report an expanded path that collapsed to empty once per remap instance. The condition depends on the request, so at error level it repeated for every matching transaction. A config reload builds a new instance and reports again. Document that an invalid --fetch-count, --fetch-max, --fetch-overflow or --fetch-path-pattern makes the remap rule fail to load rather than loading with prefetch silently disabled.
2bdf0d5 to
3f15e3c
Compare
Yes, added. There is now a paragraph after the parameter reference in Rebased onto master while here, which picked up #71d40c137c ( |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
doc/admin-guide/plugins/prefetch.en.rst:300
- The new configuration-validation note says
--fetch-overflowmust be32or64, but the plugin documentation earlier (and the implementation) also supportsbignum. The plugin-parameter list here also omits--fetch-overflowentirely, even though it’s referenced immediately below. Please align this section with the supported values and list the option among the parameters.
An invalid parameter value is a configuration error. ``--fetch-count`` and ``--fetch-max`` must be
decimal numbers that fit in an unsigned integer, ``--fetch-overflow`` must be ``32`` or ``64``, and
``--fetch-path-pattern`` must compile and may only reference capture groups that the pattern
defines. A remap rule with an invalid value fails to load, instead of loading with prefetch
silently disabled. ``traffic_ctl config reload`` rejects such a configuration and keeps the running
|
The core diagnosis is right and I verified the pieces it rests on. The hardening is solid too. One thing I think is still open, and it is an ordering hazard with #13441. The null-pointer guard covers only one of the two ways std::string_view dst = (replIndex < matchCount) ? matches[replIndex] : std::string_view{""};The Today that yields a garbage pointer with zero length. Once #13441 lands, PrefetchDebug("replacing '$%d' with '%.*s'", replIndex, static_cast<int>(dst.length()), dst.data());
result.append(dst);which is the exact It is reachable with an ordinary pattern, not just a contrived one: any optional group that precedes a participating group, e.g. Normalizing regardless of which branch produced the view closes it and makes the guard say what the comment already claims: std::string_view dst = (replIndex < matchCount) ? matches[replIndex] : std::string_view{};
if (nullptr == dst.data()) {
dst = std::string_view{""};
}That also means this PR stops depending on #13441's choice of return value either way, which seems worth having given both are in flight — right now the correctness of this line is coupled to a decision made in another PR. Two smaller notes:
Nothing else from me — with the null normalization I would be happy with this. |
|
Holding this until #13517 merges. As it stands here, |
…ure groups (#13352) Pattern::replace() rejected any $N whose index was >= the match's return value. That value is one past the highest capture group that *participated*, not the number of groups the pattern defines, so a trailing optional group such as "(\?.*)?" that did not participate made a valid $N look out of range. Every such request logged "invalid reference in replacement string" and silently dropped the prefetch. Validate $N once at config-load time against the pattern's actual capture-group count, and substitute an empty string at match time for a group that did not participate, per PCRE2 semantics. Also treat an unusable --fetch-path-pattern and invalid --fetch-count / --fetch-max / --fetch-overflow values as configuration errors, so the remap rule is refused at load rather than running with prefetch silently disabled. --fetch-count and --fetch-max now parse with std::from_chars(), which rejects a value above UINT_MAX instead of truncating it. Skip an empty expanded path rather than self- prefetching the original, and report that once per remap instance since the condition is request-dependent. Removes the now-dead Pattern::process() and Pattern::capture(). (cherry picked from commit adbd85d)
|
Cherry-picked to the 10.2.x branch as 2fe4afb for the 10.2.0 release. |
Three late bug fixes on 10.2.x. All are fixes with no new configuration, metrics or API surface, so only the changelog and the commit/PR counts change.
* Add 10.2.0 changelog and release notes Generate CHANGELOG-10.2.0 from the 10.2.0 milestone and document the release in whats-new and upgrading. The connect retry change (#13102) is called out as a necessary incompatible change, since the retry limits were not previously applied according to origin state. * Address review: fix PR count and token_key markup The PR count was 655 before five stale milestone entries were dropped; the changelog has 650. Use :ts:cv: for proxy.config.quic.server.token_key.filename, which is documented on 10.2.x even though it is absent from master, where it was first checked. * Add late 10.2.x additions to changelog and release notes Picks up #13328 (shared-memory cache directory for fast restart) and #13418 (traffic_ctl cache clear). The shm directory gets its own section since it is a new opt-in feature with four new records and a traffic_ctl subcommand. * Add July 2026 security fixes to changelog and release notes The Release 2 security bundle (#13452) landed directly on 10.2.x without public PRs, so those commits never appear in a milestone. Source them from the commit range with the changelog tool's git-range mode and append them as bare subjects, matching how CHANGELOG-10.1.4 lists them. Link the advisory from whats-new for the CVE mapping. * Add #13352, #13517 and #13523 to the changelog Three late bug fixes on 10.2.x. All are fixes with no new configuration, metrics or API surface, so only the changelog and the commit/PR counts change.
Problem
Pattern::replace()in the prefetch plugin rejects any replacement reference$Nwhose index is>=the value returned by the match. But that value is one past the highest capture group that participated in the match, not the number of groups the pattern defines. A trailing optional group such as(\?.*)?that does not participate — e.g. a request with no query string — lowers that count and makes a perfectly valid$Nlook out of range.Concretely, with a remap like:
every matching request without a query string logs:
and the prefetch is silently dropped (the client is still served).
Fix
replace(): at match time, substitute an empty string for a group that did not participate instead of failing the whole replacement — the documented PCRE2 semantics for an unmatched group.compile(): validate$Nreferences once, at config-load time, against the pattern's actual capture-group count (Regex::get_capture_count()) — the correct place to catch a genuinely out-of-range reference such as$5against a 3-group pattern, and a pattern defining more groups than can be captured ($0..$9).Additional hardening
--fetch-path-pattern, and invalid--fetch-count/--fetch-max/--fetch-overflowvalues, as configuration errors: the remap instance fails to load instead of silently running with prefetch disabled. On a running server,traffic_ctl config reloadsafely rejects the bad config and keeps the current one.BgFetchturns into a self-prefetch of the original request path).Pattern::process()/Pattern::capture()helpers (no callers).Tests
Adds three autests under
tests/gold_tests/pluginTest/prefetch/:prefetch_optional_group— the non-participating optional-group case (fails on the pre-fix code).prefetch_bad_pattern_refused— an over-limit (10-group) pattern makes ATS refuse to load the remap.prefetch_empty_replacement— an empty-collapsing replacement is logged and skipped, not self-prefetched.Built locally (macOS, PCRE2); all prefetch autests pass. Draft pending CI.