Conversation
dd3ee23 to
0442cf1
Compare
0442cf1 to
656735c
Compare
52512ce to
25227f3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
tools/benchmark/benchmark_HdrParse.cc:797
- The --profile unknown-target error message omits the supported "wks-lower"/"wkslower" target even though parse_target() accepts it, which can mislead users.
Target t = parse_target(profile_target);
if (t == Target::Unknown) {
std::fprintf(stderr, "unknown target '%s' (want: request|response|mime|url|wks)\n", profile_target.c_str());
return 2;
src/proxy/hdrs/HdrToken.cc:606
- hdrtoken_tokenize_prehashed()’s comment says it does an “exact ASCII-case-insensitive byte compare”, but the implementation currently only checks (hash, length) and returns the bucket token without validating the bytes. Either adjust the comment or add the byte-compare; adding the compare also prevents accidental or crafted hash/length collisions from being misclassified as a WKS token.
// WKS lookup for a name whose FNV-1a hash the caller has already computed
// (e.g. fused into the field-name scan). Does the slot/length narrowing plus
// the exact ASCII-case-insensitive byte compare, but no hashing and no
// interned-pointer test, so it is only valid for a non-interned `string`.
int
hdrtoken_tokenize_prehashed(const char *string, int string_len, uint32_t hash, const char **wks_string_out)
{
uint32_t slot = hash_to_slot(hash);
HdrTokenHashBucket *bucket = &(hdrtoken_hash_table[slot]);
if ((bucket->wks != nullptr) && (bucket->hash == hash) && (hdrtoken_wks_to_length(bucket->wks) == string_len)) {
int wks_idx = hdrtoken_wks_to_index(bucket->wks);
if (wks_string_out) {
*wks_string_out = bucket->wks;
}
return wks_idx;
}
src/proxy/hdrs/URL.cc:1210
- url_is_mostly_compliant() no longer emits the debug message that previously identified the first offending byte (whitespace/non-printable). That makes troubleshooting strict_uri_parsing=2 failures harder. You can keep the vectorized scan and only do a second scalar scan when an invalid byte was detected, to log the first bad value.
// Mode 2 accepts exactly the printable, non-space ASCII range 0x21..0x7E --
// equivalent to the previous isspace()/isprint() pair, but locale-independent
// and call-free. This runs on every request target under the default
// strict_uri_parsing=2. OR-reducing an out-of-range flag over the whole target
// (no early exit, no data-dependent branch) lets the compiler auto-vectorize
// the scan to the build's SIMD; ATS builds -O3, where clang and GCC both do.
unsigned char bad = 0;
for (const char *i = start; i < end; ++i) {
unsigned char const c = static_cast<unsigned char>(*i);
bad |= static_cast<unsigned char>((c < 0x21) | (c > 0x7E));
}
return bad == 0;
25227f3 to
c0d1671
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
c0d1671 to
238fb0c
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
bneradt
left a comment
There was a problem hiding this comment.
Reviewed the parser changes, regression tests, and benchmark harness. One benchmark coverage issue is noted inline. This was a source review; I did not run a local build. The current PR checks are green.
There was a problem hiding this comment.
Superseded, see the full review and verdict in the next review. Left in place because GitHub does not allow a submitted review to be deleted.
There was a problem hiding this comment.
❌ Request changes
I reviewed this against base 9c69fe1bcc in a scratch worktree and ran a differential harness over the rewritten predicates on macOS/clang and Linux/GCC 16. The performance case holds up and most of the equivalence claims are measured rather than argued. Nothing I found is a live bug. What I am asking for is small, specific, and concentrated in the comments, which is deliberate: this is header parsing on every request, and three comments state the wrong reason for why something is safe. Two of them make a load-bearing check look redundant.
Verified: the fused name scan is identical
hdrtoken_field_name_scan agrees with the code it replaces over 594,256 modelled cases of the name-handling slice of mime_parser_parse (outcome x wks_idx x field_name x field_value, including the BWS-trim branch), with 0 disagreements, plus 281,112 direct hash and validity parity checks, 0 mismatches.
Corpus: all 135 well-known strings in four case forms, every prefix and extension of every well-known name, exhaustive byte injection at every position of seven base names, and all 65,792 one- and two-byte lines over the full 256-byte alphabet. Two platforms, three locales, both remove_ws_from_field_name settings. The FNV-1a fold is the same seed, same prime, same order, no length mixing.
The colon-split rewrite is exactly equivalent too: split_prefix_at(':') returns an empty view both when there is no colon and when the colon sits at index 0, which is precisely the new colon_idx <= 0 test. I checked that against the real TextView.cc rather than a model of it.
Verified: the URL predicate is identical, and it removes undefined behavior
url_is_mostly_compliant agrees with the old isspace/isprint pair in the C locale on both platforms, over all 256 bytes and all 65,536 two-byte strings.
It diverges only on glibc under ISO-8859-1/15 locales, where the old code accepted bytes 0xA0 through 0xFE. There is no setlocale call anywhere in the tree, so a real traffic_server never leaves the C locale and that divergence is unreachable.
Worth stating in the commit message, because it cuts in your favor: char is signed on both platforms, so the old code handed isprint a negative argument for every byte at or above 0x80, which is undefined behavior. This removes that. It is a fix, not only a speedup.
Still blocking: the validate_hdr_request_target comment states a false equivalence
The comment says get_scheme() is empty iff no well-known scheme index is set and the inline scheme length is zero. That is not what get_scheme() does. It goes through make_part_view, which returns {} whenever ptr == nullptr, regardless of the length. So get_scheme() is also empty for {nullptr, 5}, where m_len_scheme == 0 is false.
I enumerated the full URLImpl field state space: 864 states, 54 disagreements with the old code, every one requiring m_ptr_* == nullptr while m_len_* != 0, in both directions. A null host with nonzero length flips DONE to ERROR. A null scheme with nonzero length flips ERROR to DONE, which turns a rejection into an acceptance.
Restricted to states honoring ptr == nullptr implies len == 0: 378 states, 0 disagreements. That invariant does hold today. Every write to these fields goes through mime_str_u16_set, url_clear, or the guarded UrlPrintHack site in HTTP.cc. So this is not a live bug and I could not construct a request that reaches it.
I am still asking for it. validate_hdr_request_target is exported in HTTP.h, so the next caller gets no warning, and URLImpl::recompute_wks_idx three functions away in the same file guards on m_ptr_scheme != nullptr rather than on the length. The codebase already treats the pointer as the authoritative signal in exactly this situation. Test the pointer alongside the length, or state the invariant the comment is leaning on. Neither costs speed.
Still blocking: the check_for_dups skip is correct, but not for the reason given
The comment says a clear presence bit is "exactly the first negative test mime_hdr_field_find performs for an interned name." At this call site the name is not interned: field->m_ptr_name points into the network read buffer, so hdrtoken_is_wks(field_name.data()) is false and mime_hdr_field_find takes the linear walk, which never consults presence bits. The path the comment cites is not the path this code takes.
The optimization is still sound, for a different reason. Presence bits here are exact, not conservative: mime_hdr_field_detach re-sets the bit for next_dup when a duplicate survives and only unsets it when the last one goes, and MIME_HDR_SANITY_CHECK asserts masksum == mh->m_presence_bits. Bit clear therefore means no live field with that name, so the linear walk would have returned null too.
That exactness is the load-bearing invariant and it is currently unstated. Anyone who later relaxes presence bits to conservative, which is an obvious optimization (skip the unset on detach, accept false positives), breaks this fast path silently, and the comment points them away from the hazard rather than at it.
Still blocking: the tail-append comment describes the wrong function
mime_field_create hands out m_field_slots[m_freetop] and then increments, so
fieldsits at m_freetop - 1 and its predecessor at m_freetop - 2.
The call above it is mime_field_create_for_name, not mime_field_create. It delegates to mime_field_create only when the tail block has room. When the tail block is full it walks mh->m_free_slot and can return a reused, previously detached slot without touching m_freetop at all. The stated arithmetic is a property of one branch of the function actually called, and the comment omits the other.
What excludes that case is the explicit pointer comparison below, &tail_fblock->m_field_slots[tail_fblock->m_freetop - 1] == field. As written, the comment makes that check read as a restatement of arithmetic the reader has just been told is obviously true, which invites a maintainer to delete it as belt and suspenders. It is load-bearing. Please say so.
Still blocking: the default URI-parsing mode lost its diagnostics
Both Dbg lines naming the offending byte are gone from url_is_mostly_compliant. That is the function strict_uri_parsing = 2 runs, and 2 is the default. url_is_strictly_compliant, which almost nobody runs, still logs.
So an operator debugging "why is ATS rejecting this client's request" now gets the byte in the mode nobody uses and silence in the mode everybody uses. This is recoverable for free: the reject path is cold, so a reporting loop behind if (bad != 0) costs nothing on the accept path and keeps the branch-free scan intact.
Still blocking: the MIME fast path needs an ASAN run
All 14 CI checks pass, including all four AuTest shards, and I am not disputing any of that. My point is narrower, and it is about my own coverage rather than a claim that CI is wrong.
The fast_tail_append splice is the one part of this PR I could not verify in isolation, and nothing in the measurements above speaks to it. It needs a live HdrHeap, MIMEFieldBlockImpl, m_presence_bits, recompute_cooked_stuff and the slot accelerators, so a standalone harness cannot reach it. It is also the riskiest change here: it reaches into field-chain invariants and reasons explicitly about a freed HdrHeap coming back from the allocator at the same address in the HTTP/2 trailer path. That hazard is what ASAN is good at and what a passing functional suite is not.
Please run the MIME unit tests and the autests under ASAN before this merges.
Non-blocking: the new URL test is a tautology
url_mostly_compliant_reference in test_URL.cc is (c - 0x21u) > 0x5Du, which is the new semantics restated. It compares new against new and cannot catch a behavior change in either direction. I confirmed it agrees with the implementation over all 65,792 one- and two-byte inputs, which is what it is built to do.
The comparison that would catch a regression is old against new, which is the one I ran by hand above. Consider keeping a copy of the old isspace/isprint pair in the test as the reference, so the next person to touch this gets that check for free.
Non-blocking: the parity test exercises hash parity in 9 of its 15 cases
hdrtoken_tokenize_prehashed(...) == hdrtoken_tokenize(...) is only meaningful when the name resolves to a real index. For X-Custom-Header, sec-ch-ua, sec-fetch-mode, priority, X-My-Header and a, neither side is in the token table, so both return -1 whatever hash you feed in, and a wrong hash would pass. The nine live cases cover six distinct well-known names.
Driving that one assertion over the whole _hdrtoken_strs table would be a few lines and would turn a spot check into a proof. The code is right, as the harness above shows; the test does not yet demonstrate it.
Non-blocking: the fast tail append is the only attach path without the sanity check
mime_hdr_field_attach opens and closes with MIME_HDR_SANITY_CHECK. The fast path bypasses attach entirely and so never runs it. That is the one place a broken dup chain would now go undetected in a debug build, and it is the code that most needs the check.
Non-blocking: two smaller items and two wording nits
hdrtoken_tokenize_prehashed is a new extern that skips the hdrtoken_is_wks() interned-pointer check. Safe at its one call site, and the comment says why, but it is a sharp edge for any future caller passing an interned string. An ink_assert(!hdrtoken_is_wks(name)) would make it self-enforcing.
parsed.size() > INT_MAX returning ParseResult::ERROR is new behavior rather than a refactor. The guard is right and practically unreachable, but it belongs in the commit message.
In the URL.cc comment, "ATS builds -O3" is build-type dependent: -O3 for RelWithDebInfo and Release, not for Debug or ASAN builds, which run this scalar. "Equivalent to the previous isspace()/isprint() pair" is true as evaluated in the C locale, which is worth stating because it is what makes the substitution safe.
Agreed: what is good here
MimeParserTailAppendEquivalence is a real differential test. It builds the same field sequence through the parser and through explicit create-plus-attach, then compares dup chain shape, dup-head flag, well-known index, field count and presence bits across five scenarios. That is the right way to test an "equivalent to the slow path" claim, and it is the kind of test usually missing from a change like this.
MimeParserReuseAcrossHeaders targets the specific hazard its comment names rather than a generic happy path.
The oversized name/value rejection in mime_field_name_value_set checks before either branch mutates the field, so a rejected set is all-or-nothing rather than half-stored. Right shape, and the comment explains it accurately.
Happy to re-review quickly once the comments and the diagnostics are sorted.
Benchmark: base vs head, measuredSeparate from my review, here are A/B numbers for this PR. Posting them because the change is 70 days old and a measured result is more useful than another opinion. How the arms were builtThe benchmark source only exists on this branch, so comparing head's benchmark against anything else risks measuring the harness rather than the parser. Both arms therefore carry byte-identical benchmark source and differ only in
Build and invocation, so this is repeatable: GCC 16.2.1, Results, ns/op, median of 7 interleaved runs
Per-run figures for the cases that carry the result, so you can judge the spread yourself:
The three cases the PR does not touch move 1 to 2%, which is inside noise. That is the control you want to see. One measured regressionThe standalone WKS tokenize entry point is consistently slower. Since 1% is close to noise, those two cases were re-run alone, 10 interleaved rounds each, 300 samples, on a different core:
So: a reproducible ~1.1% slowdown on standalone It is dwarfed by the wins and I am not asking for anything about it. It is worth knowing it exists. Unit tests, Release build, head arm
What these numbers do not support
|
Header parsing needs repeatable measurements before its hot paths can be optimized. Cover zero-copy parsing under the default strict URI mode, copied inputs, modern browser headers, duplicate-heavy responses, and canonical and lowercase WKS names in one harness. Include a profiling loop and file-loaded corpora for investigating representative workloads.
Default URI validation pays for libc character classification on every byte. Use a branchless ASCII range reduction to enable vectorization and make acceptance locale-independent; rejection no longer logs the offending byte. Exhaustive differential tests cover every byte value across vector boundaries and scalar tails.
Request-target validation calls three URL getters and follows branches that reduce to a direct test of the stored host and scheme fields. Express that condition directly while preserving the existing treatment of origin, asterisk, absolute, and authority forms.
Field names were walked separately to find the colon, hash the name, and validate its characters. Fuse those passes and reuse the WKS lookup through a prehashed entry point, preserving whitespace normalization and the current table representation. Parity tests check the delimiter position, character validation, and token lookup.
A clear presence bit already proves that a well-known field has no duplicate in the header. Use that result when attaching parsed fields to avoid redundant lookup work. Well-known names without a presence mask and non-well-known names retain the normal duplicate search.
Consecutive fields such as Set-Cookie repeatedly search an existing duplicate chain even though their predecessor is already its tail. Derive that predecessor from the current header block and append directly, avoiding parser-held pointers that could outlive the header. Tests compare duplicate chains with normal attachment and cover parser reuse after a header is destroyed.
Prevent zero iteration counts from producing meaningless profile results. Reject malformed, missing, negative, and overflowing counts before entering the profiling loop.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
7e9255f to
265a139
Compare
|
Tail-append comment — Rewritten in 0fe3be7; it now says the pointer check is required because URL diagnostics — I agree that a rejection-only scan would preserve the vectorizable loop. I'd still prefer to leave these diagnostics removed: identifying the offending byte doesn't provide enough troubleshooting value to justify the additional scan and logging logic in this frequently executed validator. ASAN — Non-blocking:
|
bryancall
left a comment
There was a problem hiding this comment.
✅ Approve
Thank you for the careful reply. You fixed everything I asked for except one item you declined for reasons I accept, and you were right on two points where I was wrong. Details below, including those corrections, because the record should show them.
I reviewed the delta since my last review, 7 files and +161/-38, and a reviewer built the head and ran test_proxy_hdrs: 458,218 assertions in 62 test cases, all passing. Each test change was also checked by breaking the code it covers and confirming the test fails.
Verified: validate_hdr_request_target
host_present and scheme_absent now test the pointer along with the length, which matches get_host() and get_scheme() through make_part_view() exactly. The function no longer depends on the invariant that a null pointer always comes with a zero length. The comment is now accurate.
Agreed: the check_for_dups comment was right, and my objection was wrong
mime_hdr_field_attach() searches with field->name_get(), and name_get() returns hdrtoken_index_to_wks(m_wks_idx) whenever m_wks_idx >= 0. So mime_hdr_field_find() does take the well-known branch, and its first test is the same presence bit. I read the m_ptr_name field and missed the substitution in name_get(). My argument was also wrong in a second way: skipping on a clear bit only needs presence bits to have no false negatives, so even conservative bits would have been safe. The reworded comment is accurate.
Verified: the tail-append comment
It now names mime_field_create_for_name() and explains the slot reuse, which matches the free-list path. One small correction of my own: I called the pointer comparison load-bearing. Today the allocator only reuses a free slot that sits after the name's last duplicate, so the check guards against a future allocator change rather than a case that can occur now. It is still worth keeping; "guards against" would be slightly more precise than "required", but that is a nit.
Agreed: the ASAN run
test_proxy_hdrs and the trailer and chunked autests you listed are the right coverage for the fast path, in particular h2_origin_trailers_h1 for the reused-heap case.
Agreed: the sanity check is compiled out
MIME_HDR_SANITY_CHECK expands to (void) unless ENABLE_MIME_SANITY_CHECK is defined, and nothing in the tree defines it. My note that the fast path skipped a check that would catch a broken chain in a debug build was wrong.
Non-blocking: the URL diagnostics
I accept your decision to leave them out. Those were Dbg lines behind a debug tag, not production logging, and my earlier description of what an operator loses overstated it.
If you ever want them back, they can cost nothing on either path. DbgCtl::on() exists, so a report loop behind if (bad != 0 && dbg_ctl_http.on()) runs only when a request is rejected and the tag is enabled, and the scan loop stays branch-free.
Related, and worth one line: removing the whole comment block also removed the only explanation of why the loop has no early exit. The obvious way to restore the diagnostics, a Dbg and return false inside the loop, would quietly stop it vectorizing, and nothing now warns against that.
Non-blocking: the token-table sweep compares the tokenizer with itself
The sweep is a real improvement and catches things the fifteen hand-picked names cannot: a hash that goes wrong only past 17 bytes, and a case-folding error on a letter none of those names contain.
It has one blind spot. hdrtoken_tokenize() now ends with return hdrtoken_tokenize_prehashed(...), so both sides of CHECK(hdrtoken_tokenize_prehashed(...) == hdrtoken_tokenize(...)) go through the same function and fail together. For example, a change that made the prehashed lookup return -1 for all-uppercase well-known names of eight or more characters passes the sweep and the whole suite. TRANSFER-ENCODING is one of those names, and it is one the parser's framing depends on.
Comparing against the known index closes it:
CHECK(hdrtoken_tokenize(name.data(), name_len) == idx);It passes at head and fails on that change.
Non-blocking: cooked header state on the fast path is untested
Deleting the recompute_cooked_stuff(field) call on the tail-append path leaves the whole suite passing, because MimeParserTailAppendEquivalence never uses a cooked header. With the call removed, Cache-Control: no-cache followed by Cache-Control: max-age=5 loses the max-age. A Cache-Control scenario in the equivalence test, comparing get_cooked_cc_mask() and get_cooked_cc_max_age(), would pin it.
Non-blocking: three benchmark tool items
These are in the standalone tool, not the server, and the numbers I posted came from the built-in cases, which check their results.
- In profile mode an unrecognized argument is dropped, so
--profile url --iters=1000runs the default 5,000,000 iterations and exits 0. Rejecting leftover arguments in profile mode would catch that. drive_mimepassesremove_ws_from_field_name = falsewhile the response parser passes true, so a corpus response withServer : xmakes the newmime: corpusbenchmark time a parse that stopped early. Nothing in that path checks the parse result.- The file header says each target gets a corpus benchmark; the
wkstarget does not.
Non-blocking: test_mime.cc needs <cctype>
Copilot is right about this one. The case-parity test calls toupper and tolower (test_mime.cc:297, :300), but the file includes only <cstdio>, <climits>, <cstdint> and <cstring> from the C library, so it compiles only because some other header pulls <cctype> in. Add #include <cctype> next to the others.
Agreed: the rest
The URL test now uses the old isspace and isprint pair as its reference, passed an unsigned char, with the C locale reasoning written down. The ink_assert(!hdrtoken_is_wks(string)) makes the prehashed entry point enforce its own precondition. Leaving the INT_MAX guard out of the commit message is fine.
Catch shared token lookup failures and stale cooked Cache-Control state in the parser tests, with an explicit cctype dependency. Keep benchmark results meaningful by checking MIME parses, matching whitespace policy, and rejecting ignored profile arguments.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 3
Open (4)
hdrtoken_tokenize_prehashedis now part of the public header API but willink_assertif called… · New The validation step castsucback tochar, which reintroduces signedness-dependent behavior for… · Newstd::filesystem::directory_iteratorincrement (++it) andis_regular_file()can throw on… · New These calls use unqualifiedisspace/isprintafter including<cctype>. For… · New
Resolved since last review (1)
| hdrtoken_tokenize_prehashed(const char *string, int string_len, uint32_t hash, const char **wks_string_out) | ||
| { | ||
| ink_assert(string != nullptr); | ||
|
|
||
| if (hdrtoken_is_wks(string)) { | ||
| int const wks_idx = hdrtoken_wks_to_index(string); | ||
|
|
||
| if (wks_string_out) { | ||
| *wks_string_out = string; | ||
| } | ||
| return wks_idx; | ||
| } | ||
|
|
||
| uint32_t const hash = hdrtoken_hash(std::string_view{string, static_cast<size_t>(string_len)}); | ||
| ink_assert(!hdrtoken_is_wks(string)); |
| break; | ||
| } | ||
| hval = (hval ^ hdrtoken_ascii_toupper(uc)) * 0x01000193u; | ||
| all_valid &= (ParseRules::is_http_field_name(static_cast<char>(uc)) != 0); |
| for (auto it = std::filesystem::directory_iterator(d, ec); !ec && it != std::filesystem::directory_iterator(); ++it) { | ||
| if (it->is_regular_file()) { | ||
| load_file(it->path(), corp.cases); | ||
| } |
| { | ||
| for (const char *p = start; p < end; ++p) { | ||
| unsigned char const c = static_cast<unsigned char>(*p); | ||
| if (isspace(c) || !isprint(c)) { |



Reduce repeated work in HTTP header parsing.
Includes a header parsing benchmark harness and regression tests for validation, field-name scanning, duplicate attachment, and parser reuse.