Repository navigation
Parser: limit body part nesting depth and bound boundary searches - #367
Merged
vincent-richard merged 1 commit intoOct 23, 2026
Conversation
A deeply nested multipart message could exhaust the stack, as each level of nesting recurses into body::parseImpl(), and the parsing time grew with the square of the nesting depth. Stack exhaustion: - Body parts are no longer split beyond a maximum nesting depth, which defaults to 100 and can be changed with parsingContext::setMaxNestingDepth(). A multipart part at this depth keeps its body contents as is, like a non-multipart part, so no data is lost. Parsing time: - component::parse() wrapped the input stream in a new parserInputStreamAdapter even if it already was one, so each level of nesting added indirections to all the stream accesses of the levels below it. - The boundary search did not stop at the end of the enclosing part, and was run once more after the closing delimiter: each level scanned the rest of the message, up to the end of the stream. - Each match rejected by findNextBoundaryPosition() (not at the beginning of a line, or prefix of another boundary) restarted a search which read 4 KiB ahead. The search is now for "\n--boundary", and parserInputStreamAdapter::findNext() starts with small reads, so that its cost depends on the distance to the match. With boundaries "a", "aa", "aaa", etc. nested 100 levels deep, parsing a 1 MiB message took 150 s. parserInputStreamAdapter::findNext() takes an optional end position. It no longer searches the bytes of its buffer twice, nor returns wrong positions with streams which return fewer bytes than requested. Behaviour change: the boundary of a multipart part is now only searched for within this part. If its closing delimiter is missing, its last part extends to the end of the enclosing part; before, it became epilog text if the boundary appeared again further in the message. Fixes #366. Reported by @DrNbnonono.
vincent-richard
deleted the
366-deeply-nested-multipart-quadratic-parse-cost-and-sigsegv-from-unguarded-recursion
branch
October 23, 2026 11:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A deeply nested multipart message could exhaust the stack, as each level of nesting recurses into body::parseImpl(), and the parsing time grew with the square of the nesting depth.
Stack exhaustion:
Parsing time:
component::parse() wrapped the input stream in a new parserInputStreamAdapter even if it already was one, so each level of nesting added indirections to all the stream accesses of the levels below it.
The boundary search did not stop at the end of the enclosing part, and was run once more after the closing delimiter: each level scanned the rest of the message, up to the end of the stream.
Each match rejected by findNextBoundaryPosition() (not at the beginning of a line, or prefix of another boundary) restarted a search which read 4 KiB ahead. The search is now for "\n--boundary", and parserInputStreamAdapter::findNext() starts with small reads, so that its cost depends on the distance to the match. With boundaries "a", "aa", "aaa", etc. nested 100 levels deep, parsing a 1 MiB message took 150 s.
parserInputStreamAdapter::findNext() takes an optional end position. It no longer searches the bytes of its buffer twice, nor returns wrong positions with streams which return fewer bytes than requested.
Behaviour change: the boundary of a multipart part is now only searched for within this part. If its closing delimiter is missing, its last part extends to the end of the enclosing part; before, it became epilog text if the boundary appeared again further in the message.
Fixes #366. Reported by @DrNbnonono.