Skip to content

Improve handling of problematic chunked-encoding trailers - #2476

Open
rousskov wants to merge 11 commits into
squid-cache:masterfrom
measurement-factory:SQUID-1175-chunked-error-signalling
Open

Improve handling of problematic chunked-encoding trailers#2476
rousskov wants to merge 11 commits into
squid-cache:masterfrom
measurement-factory:SQUID-1175-chunked-error-signalling

Conversation

@rousskov

Copy link
Copy Markdown
Contributor

Squid misinterpreted certain chunked-encoded HTTP requests, leading to
framing errors. Chunked HTTP responses were similarly affected, but only
where read_ahead_gap was configured to exceed 64 KB. ICAP response
parsing 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 ChunkedCodingParser class used C++ exceptions to
report parsing errors. Its HTTP and ICAP users relied on that mechanism.
2015 commit 350ec67 shoveled that class into Http1::Parser hierarchy
but did not update its parsing method to match Http1::Parser API that
uses a combination of false parse() and false needsMoreData()
(instead of C++ exceptions) to report parsing errors. The follow-up
commit be29ee3 then upgraded ChunkedCodingParser trailer parsing code
to call Http1::Parser::grabMimeBlock() that used Http1::Parser error
reporting API, breaking all ChunkedCodingParser users (that continued
to rely exclusively on C++ exceptions for parsing error detection).

This change wraps that grabMimeBlock() call to provide exception-based
error 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_detail to
BAD_CHUNKED_REQUEST_BODY or BAD_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 traffic
handling 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 made
misleading 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::Parser parent class and its TeChunkedParser child.
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).

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 rousskov left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 original Http1::Parser API that uses a combination of false parse() and false needsMoreData() to detect parsing errors.
  • parseOrThrowXXX() callers rely on the original ChunkedCodingParser API 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.

@rousskov rousskov added M-cleared-for-merge https://github.com/measurement-factory/anubis#pull-request-labels S-could-use-an-approval An approval may speed this PR merger (but is not required) labels Aug 18, 2026
Comment thread src/http.cc
// fall through to handle this premature EOF as an error
} else {
debugs(33, 5, "Incomplete response, waiting for end of response headers");
Assure(!parsedOk);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why throw instead of waiting for more data that might make parsedOk true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why throw instead of waiting for more data that might make parsedOk true?

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;
    }
}

Comment on lines -78 to +85
return !needsMoreData() && !needsMoreSpace();
return !needsMoreData();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@rousskov rousskov added S-waiting-for-reviewer ready for review: Set this when requesting a (re)review using GitHub PR Reviewers box and removed M-cleared-for-merge https://github.com/measurement-factory/anubis#pull-request-labels S-could-use-an-approval An approval may speed this PR merger (but is not required) labels Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-for-reviewer ready for review: Set this when requesting a (re)review using GitHub PR Reviewers box

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants