Improve handling of problematic chunked-encoding trailers - #2476
Improve handling of problematic chunked-encoding trailers#2476rousskov wants to merge 11 commits into
Conversation
This change preserves existing functionality but highlights all parse() callers and disambiguates "throwing" from "non-throwing" variants.
... to improve the chances of the fixed bug remaining fixed and assist with the future refactoring to implement a better/comprehensive fix.
Since 2015 commit 350ec67, chunked parser's needsMoreData() and needsMoreSpace() methods have lost their independence: In modern code, false needsMoreData() implies false needsMoreSpace(). That implication means we do not need to check both methods to distinguish a parse() error (i.e. false needsMoreData()) from "just need another parse() call" outcome (i.e. true needsMoreData()). This change adjusts the corresponding checks to reflect always-true and always-false conditions. The goal here is not optimization -- some of those checks strongly yet incorrectly implied that TeChunkedParser::parse() outcome cannot be fully determined by needsMoreData() alone despite parent class (i.e. Parser) API claims. It turns out that those claims can be interpreted as correct in relevant contexts, and, hence, this branch does not have to mark more [API] bugs.
This improvement affects all chunking problems, not just bad responses
with problematic trailers. Prior to this change, they were all logged as
successful `TCP_MISS` transactions. Now:
%err_code=ERR_INVALID_RESP
%err_detail=BAD_CHUNKED_RESPONSE_BODY+WITH_CLIENT
%Ss=TCP_MISS_ABORTED
%>Hs=200 %<Hs=200
`WITH_CLIENT` tag is unfortunate, but fixing that old/known problem is
outside this branch scope.
N.B. Triggering trailer errors using large trailers requires custom
`read_ahead_gap` because the default 16KB gap does not allow buffering
of large-enough trailers. The default gap still triggers chunked
parsing-related errors because Squid knows that it needs more bytes to
parse the trailer, but it cannot accumulate more bytes due to the
`read_ahead_gap` limit:
http.cc(2675) abortAll: aborting transaction for more response bytes
required, but the read buffer is full and cannot be drained;...
TODO: The above abortAll() outcome is not marked in access.log at all!
That temporary highlighting was done in branch commit f1a3910. This change also removes a disabled bug-reproducing hack in TeChunkedParser::parseTrailerSection() and polishes that method declaration placement.
Asserting `needsMoreData()` would mislead some readers because we may not need more current chunk data bytes or even more encoding data bytes in general -- tok.remaining() may have the entire chunked-encoded message body already! Returning `true` happens to "works" because of the caller's loop structure but doing so also misleads because we have not successfully parsed the current chunk body. In fact, we may have parsed nothing new!
rousskov
left a comment
There was a problem hiding this comment.
To see all contexts where overloaded Http::One::Parser::parse() is called, look at branch commit f1a3910 diff. That temporary code is not visible in this PR diff, but it clearly shows two kinds of Http::One::Parser::parse() callers:
parse2()callers rely on the originalHttp1::ParserAPI that uses a combination of falseparse()and falseneedsMoreData()to detect parsing errors.parseOrThrowXXX()callers rely on the originalChunkedCodingParserAPI that uses C++ exceptions to detect parsing errors.
This PR fixes Http::One::TeChunkedParser::parse() (i.e. parseOrThrowXXX() cases) to throw those exceptions, as its callers expect.
TeChunkedParser still does not faithfully implement its parent class API, but fixing that design problem would require a lot more changes, and I bet that some of those changes would be controversial. Let's fix the "immediate" parsing bug first.
| // fall through to handle this premature EOF as an error | ||
| } else { | ||
| debugs(33, 5, "Incomplete response, waiting for end of response headers"); | ||
| Assure(!parsedOk); |
There was a problem hiding this comment.
Why throw instead of waiting for more data that might make parsedOk true?
There was a problem hiding this comment.
Why throw instead of waiting for more data that might make
parsedOktrue?
This code throws when parsedOk is already true, not when it is still false and "more data that might make parsedOk true". In other words, this code is "waiting for more data that might make parsedOk true".
This code throws when parsedOk is true here because this code snippet relies on 'true needsMoreData() implies "Incomplete response"' implication. One could argue that this code should have tested parsedOk first, but those arguments are outside this PR scope. This PR does not change this code logic or structure. We just add an assertion in hope to catch future bugs similar to the one fixed in this PR.
As this review question illustrates, this logic is tricky and dangerous, but this PR does not change any of that.
Finally, please note that there is an equivalent assertion a few lines above already. We are just improving assertion coverage here. Here is the abbidged version of this code showing both old/official and new/PR assertions:
if (hp->needsMoreData()) {
if (eof) {
assert(!parsedOk); // old
// fall through to handle this premature EOF as an error
} else {
Assure(!parsedOk); // new (previously missing)
return;
}
}If you insist, I can remove the old assertion and move the new one above the eof check:
if (hp->needsMoreData()) {
Assure(!parsedOk); // covers both cases in this position
if (!eof) {
; // fall through to handle this premature EOF as an error
} else {
return;
}
}
| return !needsMoreData() && !needsMoreSpace(); | ||
| return !needsMoreData(); |
There was a problem hiding this comment.
Why this conditional change?
The API can now produce the "successful parse COMPLETED" signal (aka true) if the entire buffer fills up before the parse completes.
There was a problem hiding this comment.
Why this conditional change?
While the spelling of this condition has changed, the condition remains the same (i.e. it produces the same true or false result in any valid parser state) because !needsMoreData() implies !needsMoreSpace(). In other words, when needsMoreData() is false, needsMoreSpace() is guaranteed to be false:
bool needsMoreData() { return parsingStage_ != Http1::HTTP_PARSE_DONE; }
bool needsMoreSpace() { return parsingStage_ == Http1::HTTP_PARSE_CHUNK && ...; }As mentioned in this PR description, 2015 commit 350ec67 made this code misleading: Prior to those changes, the two conditions -- "need additional bytes" and "need larger space" -- resided in an independent parser and were independent; ChunkedCodingParser::parse() returned true when both of them were false (i.e. when the parser did not need anything and, hence, was "done"). After those changes, now-inherited TeChunkedParser::needsMoreData() meaning has effectively changed from "need additional bytes" to "still parsing" or "want more parse() calls", which covers both "need additional bytes" and "need larger space" conditions. Current Http::One::TeChunkedParser() API/hierarchy is bad, but addressing those problems is outside this bug-fixing PR scope because it requires many more code changes, some of which are likely to be controversial.
The API can now produce the "successful parse COMPLETED" signal (aka true) if the entire buffer fills up before the parse completes.
The above assertion is false: "If the entire buffer fills up", then needsMoreData() still returns true, and, hence, this parse() method returns false rather than "true". Note how TeChunkedParser::parseChunkBody() preserves parsingStage_ when potentialSpaceSize is zero (i.e. when there are chunk bytes to parse but there is no space).
Squid misinterpreted certain chunked-encoded HTTP requests, leading to
framing errors. Chunked HTTP responses were similarly affected, but only
where
read_ahead_gapwas configured to exceed 64 KB. ICAP responseparsing code had a similar flaw but appears to be protected by a
hard-coded 64 KB TCP receive buffer size limit. ICAP trailers (see 2016
commit 69c698a) were not affected.
Background: The
ChunkedCodingParserclass used C++ exceptions toreport parsing errors. Its HTTP and ICAP users relied on that mechanism.
2015 commit 350ec67 shoveled that class into
Http1::Parserhierarchybut did not update its parsing method to match
Http1::ParserAPI thatuses a combination of false
parse()and falseneedsMoreData()(instead of C++ exceptions) to report parsing errors. The follow-up
commit be29ee3 then upgraded
ChunkedCodingParsertrailer parsing codeto call
Http1::Parser::grabMimeBlock()that usedHttp1::Parsererrorreporting API, breaking all
ChunkedCodingParserusers (that continuedto rely exclusively on C++ exceptions for parsing error detection).
This change wraps that
grabMimeBlock()call to provide exception-basederror reporting that all chunked transfer coding users still expect.
Those users now detect and handle trailer parsing errors. Also, Squid
now tags affected HTTP messages by setting
%err_detailtoBAD_CHUNKED_REQUEST_BODYorBAD_CHUNKED_RESPONSE_BODY.The same 2015 work also altered
ChunkedCodingParser::needsMoreData()meaning without updating its callers. This change updates the callers to
assert the expected
needsMoreData()state after a non-throwing (i.e.no error)
parse()call. These new assertions do not change traffichandling in known cases, but they may help prevent (or improve handling
of) yet-unknown parsing bugs similar to the one fixed here.
Also dropped two
needsMoreSpace()calls that those 2015 changes mademisleading and polished
Http::One::TeChunkedParser::parseChunkBody()to return false when the method cannot finish parsing the chunk body.
All other similar helper parsing methods in that class already do that.
TODO: This change preserves the clash of error-reporting mechanisms in
Http::One::Parserparent class and itsTeChunkedParserchild.Properly addressing that conflict is not necessary for fixing in-scope
parsing problems, and it requires making controversial design decisions
(and a lot more code changes).