-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug 5266: Do not parse the same TLS record fragments twice #240
base: master
Are you sure you want to change the base?
Changes from 7 commits
350f3a6
75f04a1
6ef59b6
f3650cf
095601c
328b704
6932606
2180c95
32c24e5
c5f70c1
9a96bc7
eba1b8e
5d4b9b5
e304647
3a88001
71802a6
dbf2481
6b12750
47eb627
4b525e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -274,24 +274,30 @@ Security::HandshakeParser::parseModernRecord() | |||||
Must(record.fragment.length() || record.type == ContentType::ctApplicationData); | ||||||
|
||||||
if (currentContentType != record.type) { | ||||||
tkMessages.expectMore(false); | ||||||
parseMessages(); | ||||||
Must(tkMessages.atEnd()); // no currentContentType leftovers | ||||||
fragments = record.fragment; | ||||||
currentContentType = record.type; | ||||||
} else { | ||||||
fragments.append(record.fragment); | ||||||
} | ||||||
|
||||||
if (tkRecords.atEnd() && !done) | ||||||
rousskov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
parseMessages(); | ||||||
const auto haveUnparsedRecordBytes = !tkRecords.atEnd(); | ||||||
const auto expectMoreRecordLayerBytes = tkRecords.expectMore(); | ||||||
// TODO: consider adding BinaryTokenizer::exhausted() instead | ||||||
const auto expectMoreMessageLayerBytes = haveUnparsedRecordBytes || expectMoreRecordLayerBytes; | ||||||
|
||||||
tkMessages.expectMore(expectMoreMessageLayerBytes); | ||||||
tkMessages.append(record.fragment); | ||||||
|
||||||
parseMessages(); | ||||||
} | ||||||
|
||||||
/// parses one or more "higher-level protocol" frames of currentContentType | ||||||
void | ||||||
Security::HandshakeParser::parseMessages() | ||||||
{ | ||||||
tkMessages.reset(fragments, false); | ||||||
for (; !tkMessages.atEnd(); tkMessages.commit()) { | ||||||
rousskov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
tkMessages.rollback(); | ||||||
|
||||||
while (!tkMessages.atEnd() && !done) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably need to ensure somehow that tkMessages.commit() was called after each iteration. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Option 1: "Ensure that tkMessages.commit() was called" by parseMessageFoo()I doubt that checking whether tkMessages.commit() was called is the best way forward because even if we, say, count the number of commits per iteration, and assert that there was at least one, we still would not protect ourselves from buggy code that commits the current message and then extracts/saves something from the next message (without committing). Option 2: Move parsing to parseMessage()The underlying/core problem here is that, with the current design, the loop code does not and, IMHO, should not know what happens inside each parseFoo() function it calls. There is an implicit assumption that each of those functions commits after parsing and before accumulation, but we cannot really "assert" that. To make this code safer, we could remove tkMessages manipulation from individual parseFooMessage() functions: while (...) {
switch (currentContentType) {
case FooMessage:
interpretFooMessage(parseMessage<FooMesasge>());
continue;
case BarMessage:
interpretBarMessage(parseMessage<BarMessage>());
continue;
}
skipMessage(...); where parseMessage() is a templated method that parses a single message, commits tkMessages state, and returns the parsed message object. This would require creation of a ChangeCipherSpecMessage class to parse ctChangeCipherSpec messages. However, I am not sure the above restructuring is the best way forward. It feels like it would make the code slightly more complex while only offering a small safety improvement -- an interpretFooMessage() function can still violate our expectations, of course; it would be just easier to spot those violations. This option also raises performance questions related to copying of parsed messages, although most of those copies may be eluded by a C++17 compiler (I have not checked). Option 3: Move tkMessages.commit() calls into the loopWe know that every parseFooMessage() function must parse a single message1 (or throw). If it parses less or more than that, it is buggy. If it accumulates something before finishing parsing, it is buggy. This loop cannot do anything to detect those bugs. However, this loop can assume correct parseFooMessage() implementation and commit() after calling parseFooMessage() or skipMessage()! If parseFooMessage() is buggy, committing at the loop level will not make things worse AFAICT or, at the very least, those bugs will not be the loop's fault. When a successful parsing attempt is expected/required to commit, then performing that commit at the loop level is better because it reduces the probability that a parseFooMessage() function forgets to commit (in some cases, especially when parseFooMessage() delegates parsing to another method or a chain of methods). Committing at the loop level clarifies the code structure/intent, especially when loop-level commit() calls are paired with loop-level rollback() calls. If you agree that Option 3 is the best way forward, please implement Option 3 by moving commit() calls from parseFooMessage() functions into the loop. Please use the Option 4: Individual message parsers should commit()This option does not really "ensure that tkMessages.commit() was called after each iteration". It is discussed here because it contradicts Option 3. This option is already implemented in this PR because I suggested to do so. My suggestion was based on the following logic:
I think I was wrong because I missed the fact that relevant InsufficientInput exceptions cannot happen here. There are two kinds of exceptions we need to consider in this context:
Footnotes
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, I think it is the best among other options that you outlined. Done (32c24e5). |
||||||
switch (currentContentType) { | ||||||
case ContentType::ctChangeCipherSpec: | ||||||
parseChangeCipherCpecMessage(); | ||||||
|
@@ -335,6 +341,7 @@ Security::HandshakeParser::parseAlertMessage() | |||||
{ | ||||||
Must(currentContentType == ContentType::ctAlert); | ||||||
const Alert alert(tkMessages); | ||||||
tkMessages.commit(); | ||||||
debugs(83, (alert.fatal() ? 2:3), | ||||||
"level " << static_cast<int>(alert.level) << | ||||||
" description " << static_cast<int>(alert.description)); | ||||||
|
@@ -349,6 +356,7 @@ Security::HandshakeParser::parseHandshakeMessage() | |||||
Must(currentContentType == ContentType::ctHandshake); | ||||||
|
||||||
const Handshake message(tkMessages); | ||||||
tkMessages.commit(); | ||||||
|
||||||
switch (message.msg_type) { | ||||||
case HandshakeType::hskClientHello: | ||||||
|
@@ -631,10 +639,11 @@ Security::HandshakeParser::parseSupportedVersionsExtension(const SBuf &extension | |||||
void | ||||||
Security::HandshakeParser::skipMessage(const char *description) | ||||||
{ | ||||||
// tkMessages/fragments can only contain messages of the same ContentType. | ||||||
// tkMessages can only contain messages of the same ContentType. | ||||||
// To skip a message, we can and should skip everything we have [left]. If | ||||||
// we have partial messages, debugging will mislead about their boundaries. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that "debugging will mislead" comment is stale because all skipMessage() callers add
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
tkMessages.skip(tkMessages.leftovers().length(), description); | ||||||
tkMessages.commit(); | ||||||
} | ||||||
|
||||||
bool | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.