Conversation
Add std::move where a local's last use was a copy into a container or a by-value parameter, and bind a few config-parse locals by const reference where the expression already returns a reference into longer-lived storage. Each site was checked individually: the moved-from objects are never read again, and the const-reference binds point into yaml-cpp node storage owned by the document's shared memory holder rather than into the temporary Node handle, so they cannot dangle. Findings in test helpers and a few sites where the copy was either required or not actually a copy were left alone.
GCC's -Wdangling-reference cannot prove that the reference returned by YAML::Node::Scalar() does not point into the temporary Node returned by operator[], and the tree builds with -Werror. The copies these avoided are small and the analysis findings behind them are not worth a suppression, so keep the by-value locals.
There was a problem hiding this comment.
Pull request overview
This PR targets static-analysis findings about unnecessary std::string copies by moving from locals at their final use in several ATS plugins.
Changes:
- Move
std::stringvalues into_mods,_op, and_arginheader_rewritepreprocessing when the source tokens are no longer needed. - Move captured/replaced strings into the output vector in the
cachekeypattern processor. - Move
prefix/tagstrings intoRateLimiter::initializeMetrics()to avoid by-value copies.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| plugins/header_rewrite/parser.cc | Uses std::move when storing token-derived strings into parser members/containers. |
| plugins/experimental/rate_limit/limiter.h | Moves local prefix/tag into initializeMetrics() call to avoid copies. |
| plugins/cachekey/pattern.cc | Moves locally-built capture/replacement strings into the result vector. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
cmcfarlen
left a comment
There was a problem hiding this comment.
Checked each of the moves for reads-after-move. The mechanical work is right, including the two that look wrong at a glance:
parser.cc—_mods.push_back(std::move(t))sits insidewhile (getline(iss, t, ',')), which is the shape that usually is a bug. It is safe here becausestd::getlineis specified to callstr.erase()before appending and a moved-fromstd::stringis a valid object, so the next iteration re-initializes it. Thestd::findduplicate check runs before the move.parser.cc—_op = std::move(tokens[0])followed by_arg = std::move(tokens[1])is safe because moving elements does not changesize(), and the subsequent loop starts atbegin() + 2.tokensis a by-value parameter with a comment saying the caller passes a copy deliberately, so nothing escapes to the caller.pattern.cc—element,dst, and the localcapturesvector are all at last use, andCacheKeyDebugreadsdstbefore the move.
Two things I would like addressed before this lands.
pattern.cc:156 — the move escalates an existing out-of-bounds read into an out-of-bounds write
Regex::exec returns pcre2_match's rc verbatim, and rc == 0 means the ovector was too small (src/tsutil/Regex.cc:506-515). It fixes up matches._size from pcre2_get_ovector_count, but the return value stays 0. Pattern::capture uses a default-constructed RegexMatches, which is DEFAULT_MATCHES = 10 slots (include/tsutil/Regex.h:82), so any cachekey pattern with more than about nine capture groups reaches that path.
Pattern::capture then loops on the returned matchCount rather than matches.size(), so it pushes nothing and returns true with an empty vector. Pattern::process sees size() != 1 and runs:
StringVector::iterator it = captures.begin() + 1; // UB: begin() + 1 on an empty vector
for (; it != captures.end(); it++) {
result.push_back(std::move(*it)); // was push_back(*it)
}Before this patch that is a garbage read past the end. With std::move it becomes a write: the out-of-bounds std::string is mutated to empty and whatever pointer was read there is freed. Same underlying bug, materially worse failure mode, reachable from a user's cachekey configuration.
The underlying fix is for capture() to loop on matches.size() instead of the return value, which also stops silently dropping captures on a successful match, and for process() to guard the empty case rather than trusting begin() + 1. If you would rather keep this PR purely mechanical, reverting just this one line to push_back(*it) and filing the capture() bug separately is fine too — I mainly do not want the move to ship on top of the existing UB.
limiter.h:228 — now conflicts with master
This hunk had the arguments swapped relative to initializeMetrics(uint type, std::string tag, std::string prefix). #13601 landed in the meantime (merged as c9e9bfe) and fixed exactly that line, so master now reads:
initializeMetrics(RATE_LIMITER_TYPE_SNI, tag, prefix);A rebase is needed, and the moves should come back as std::move(tag), std::move(prefix) — both parameters are by-value std::string, so they are real wins, just on the corrected argument order.
Smaller items
parser.cc:196— aftertokens.pop_back()consumes a flags-only line (a line consisting solely of[L], say),tokensis empty and the next statement istokens[0].substr(0, 2). Pre-existing, and adjacent to a hunk here, so anif (tokens.empty())guard after the pop is cheap if you want it; otherwise a follow-up is fine.- The description is stale. It says "15 changes across 5 files" and describes the
autotoauto const &conversions inNextHopSelectionStrategy.ccandNextHopConsistentHash.cc, but commit 1f50277 reverted all five of those, so the diff is three files. More confusingly, the rationale still argues at length that the const-reference binds do not dangle becauseNode::Scalar()returns a reference intodetail::nodestorage, while that commit's own message says GCC's-Wdangling-referencecould not prove it and the tree builds with-Werror. The commit message is the better analysis; please update the description so the record does not contradict itself.
Everything else in the diff is good, and CI is green across all fifteen checks. The pattern.cc line is the only real blocker.
…y-copies # Conflicts: # plugins/experimental/rate_limit/limiter.h
Pattern::process walks captures.begin() + 1 without first checking that the vector is non-empty. Pattern::capture can return true with an empty vector: Regex::exec returns pcre2_match's rc verbatim, and rc == 0 means the ovector was too small, which a default-constructed RegexMatches (ten slots) hits for a pattern with more than about nine capture groups. capture() loops on that return value rather than on matches.size(), so it pushes nothing. The out-of-bounds access is pre-existing, but a copy only reads past the end while a move also writes there, emptying an out-of-bounds string and freeing whatever pointer it read. Keep the copy on this line and leave the underlying capture() bug to its own change.
|
Thanks, this was a careful read. I walked each link of the
|
There was a problem hiding this comment.
🔵 Needs a closer look
The PR description claims additional changes (and changed files) that are not present in the current diff/branch contents, so the description or the missing commits/files need to be reconciled before approval.
Review details
Suppressed comments (1)
plugins/header_rewrite/parser.cc:245
- PR description mentions additional copy-elimination changes in src/proxy/http/remap/NextHopSelectionStrategy.cc and NextHopConsistentHash.cc (and "15 changes across 5 files"), but those changes do not appear in the current diff/branch contents; please either update the PR description to match what’s actually included or add the missing commits/files so reviewers can validate them.
_op = std::move(tokens[0]);
if (tokens.size() > 1) {
_arg = std::move(tokens[1]);
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
Static analysis flagged a set of unnecessary
std::stringcopies. This fixes the ones that are real and leaves the ones that are not.What changed
std::moveon a local's last use inplugins/header_rewrite/parser.cc,plugins/cachekey/pattern.cc, andplugins/experimental/rate_limit/limiter.h, where a localstd::string(or an element of a local vector that is about to be destroyed) was copied into a container or into a by-value parameter.8 changes across 3 files.
Why these and not the others
Every site was checked individually rather than applied in bulk, and most of the reported findings were left alone.
The moved-from objects are never read again. In
parser.ccthetokensvector is a by-value parameter (the caller deliberately passes a copy), so moving out of its elements cannot be observed by the caller. Inlimiter.htheDbg()call readsprefixandtagbefore they are moved intoinitializeMetrics(), whosetagandprefixparameters are both by-valuestd::string.One line in
pattern.ccwas deliberately left as a copy.Pattern::process()runscaptures.begin() + 1on a vector thatPattern::capture()can return empty, so the loop body reads past the end. A copy only reads out of bounds there; a move would also write, so this stays a copy until the underlying bug is fixed. That is filed as #13638.Findings in test helpers were skipped: making a test copy one fewer string is not worth the churn. Findings where the "copy" is a
std::string_view, or where the expression returns by value so a const-reference bind would remove no copy at all, were skipped as false positives.The
autotoauto const &conversions were droppedAn earlier revision of this branch also converted five
autobinds toauto const &insrc/proxy/http/remap/NextHopSelectionStrategy.ccandNextHopConsistentHash.cc, whereYAML::Node::Scalar()returnsconst std::string &. Those five were reverted in 1f50277 and are not in this diff.GCC's
-Wdangling-referencecould not prove the reference outlives the temporaryNodehandle thatoperator[]returns, and the tree builds with-Werror. The lifetime argument for keeping them was thatScalar()returns a reference intodetail::nodestorage owned by the document's shared memory holder rather than into the handle, and that theMap ¶meter keeps that holder alive. That reasoning may well be right, but it is exactly the kind of claim a warning exists to make someone re-check, and it is not worth suppressing a diagnostic to save a handful of string copies on a config-parsing path. They can be revisited on their own.Big parameter passed by valuewas not addressedA larger group of those findings on this same code was deliberately left alone. They are almost entirely
ConfigContextandYAML::Node, which are reference-counted handles that a size-based heuristic flags bysizeof.ConfigContextdocuments at its declaration that copies are intentional and that move is suppressed sostd::movesilently copies, whichexecute_reload()depends on.const YAML::Node &would also changeoperator[]semantics, since the const overload does not create missing keys. Converting them would risk a silent config-parsing change for no measurable gain on a path that runs a handful of times per reload.Testing
Builds clean with experimental plugins enabled. All 166 unit tests pass.