diff options
author | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2018-04-10 16:40:41 -0400 |
---|---|---|
committer | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2018-04-26 19:10:55 -0400 |
commit | 7cd83e5486cfb3383c2e4e5671ae9602be297547 (patch) | |
tree | 50241dbde52f940fb033211371d06501da8a44e4 | |
parent | c799c6c67537e0d6f65003c141b444897ad9e408 (diff) | |
download | mongo-7cd83e5486cfb3383c2e4e5671ae9602be297547.tar.gz |
SERVER-34399: $changeStream with invalid resume token crashes the server
-rw-r--r-- | src/mongo/db/pipeline/resume_token.cpp | 8 | ||||
-rw-r--r-- | src/mongo/db/pipeline/resume_token_test.cpp | 55 | ||||
-rw-r--r-- | src/mongo/db/storage/key_string.cpp | 92 | ||||
-rw-r--r-- | src/mongo/db/storage/key_string.h | 13 | ||||
-rw-r--r-- | src/mongo/db/storage/key_string_test.cpp | 72 |
5 files changed, 194 insertions, 46 deletions
diff --git a/src/mongo/db/pipeline/resume_token.cpp b/src/mongo/db/pipeline/resume_token.cpp index e602379dd0c..ded14a8be02 100644 --- a/src/mongo/db/pipeline/resume_token.cpp +++ b/src/mongo/db/pipeline/resume_token.cpp @@ -169,10 +169,10 @@ ResumeTokenData ResumeToken::getData() const { // We validate the type at parse time. MONGO_UNREACHABLE; } - auto internalBson = KeyString::toBson(static_cast<const char*>(keyStringBinData.data), - keyStringBinData.length, - Ordering::make(BSONObj()), - typeBits); + auto internalBson = KeyString::toBsonSafe(static_cast<const char*>(keyStringBinData.data), + keyStringBinData.length, + Ordering::make(BSONObj()), + typeBits); BSONObjIterator i(internalBson); ResumeTokenData result; diff --git a/src/mongo/db/pipeline/resume_token_test.cpp b/src/mongo/db/pipeline/resume_token_test.cpp index 6f7eaf4bbb4..84273b7a05e 100644 --- a/src/mongo/db/pipeline/resume_token_test.cpp +++ b/src/mongo/db/pipeline/resume_token_test.cpp @@ -26,14 +26,18 @@ * it in the license file. */ +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery + #include "mongo/db/pipeline/resume_token.h" #include <algorithm> #include <boost/optional/optional_io.hpp> +#include <random> #include "mongo/db/pipeline/document.h" #include "mongo/db/pipeline/document_source_change_stream.h" #include "mongo/unittest/unittest.h" +#include "mongo/util/log.h" namespace mongo { @@ -162,8 +166,7 @@ TEST(ResumeToken, TestMissingTypebitsOptimization) { ASSERT_EQ(BSONType::NumberInt, rtNoTypeBitsData.documentKey["_id"].getType()); } - -TEST(ResumeToken, CorruptTokens) { +TEST(ResumeToken, FailsToParseForInvalidTokenFormats) { // Missing document. ASSERT_THROWS(ResumeToken::parse(Document()), AssertionException); // Missing data field. @@ -195,34 +198,56 @@ TEST(ResumeToken, CorruptTokens) { ResumeToken::parse(Document{{"_data"_sd, goodData}, {"_typeBits", BSONBinData(goodData.data, 0, newUUID)}}), AssertionException); +} +TEST(ResumeToken, FailsToDecodeInvalidKeyStringBinData) { + Timestamp ts(1010, 4); + ResumeTokenData tokenData; + tokenData.clusterTime = ts; + auto goodTokenDocBinData = ResumeToken(tokenData).toDocument(Format::kBinData); + auto goodData = goodTokenDocBinData["_data"].getBinData(); const unsigned char zeroes[] = {0, 0, 0, 0, 0}; const unsigned char nonsense[] = {165, 85, 77, 86, 255}; + // Data of correct type, but empty. This won't fail until we try to decode the data. auto emptyToken = ResumeToken::parse(Document{{"_data"_sd, BSONBinData(zeroes, 0, BinDataGeneral)}}); - ASSERT_THROWS(emptyToken.getData(), AssertionException); - emptyToken = ResumeToken::parse(Document{{"_data"_sd, "string"_sd}}); - ASSERT_THROWS(emptyToken.getData(), AssertionException); + ASSERT_THROWS_CODE(emptyToken.getData(), AssertionException, 40649); + + auto incorrectType = ResumeToken::parse(Document{{"_data"_sd, "string"_sd}}); + ASSERT_THROWS_CODE(incorrectType.getData(), AssertionException, ErrorCodes::FailedToParse); // Data of correct type with a bunch of zeros. - auto zeroesToken = - ResumeToken::parse(Document{{"_data"_sd, BSONBinData(zeroes, 5, BinDataGeneral)}}); - ASSERT_THROWS(emptyToken.getData(), AssertionException); + auto zeroesToken = ResumeToken::parse( + Document{{"_data"_sd, BSONBinData(zeroes, sizeof(zeroes), BinDataGeneral)}}); + ASSERT_THROWS_CODE(zeroesToken.getData(), AssertionException, 50811); zeroesToken = ResumeToken::parse(Document{{"_data"_sd, "00000"_sd}}); - ASSERT_THROWS(emptyToken.getData(), AssertionException); + ASSERT_THROWS_CODE(zeroesToken.getData(), AssertionException, ErrorCodes::FailedToParse); // Data of correct type with a bunch of nonsense. - auto nonsenseToken = - ResumeToken::parse(Document{{"_data"_sd, BSONBinData(nonsense, 5, BinDataGeneral)}}); - ASSERT_THROWS(emptyToken.getData(), AssertionException); + auto nonsenseToken = ResumeToken::parse( + Document{{"_data"_sd, BSONBinData(nonsense, sizeof(nonsense), BinDataGeneral)}}); + ASSERT_THROWS_CODE(nonsenseToken.getData(), AssertionException, 50811); nonsenseToken = ResumeToken::parse(Document{{"_data"_sd, "nonsense"_sd}}); - ASSERT_THROWS(emptyToken.getData(), AssertionException); + ASSERT_THROWS_CODE(nonsenseToken.getData(), AssertionException, ErrorCodes::FailedToParse); // Valid data, bad typeBits; note that an all-zeros typebits is valid so it is not tested here. auto badTypeBitsToken = ResumeToken::parse( - Document{{"_data"_sd, goodData}, {"_typeBits", BSONBinData(nonsense, 5, BinDataGeneral)}}); - ASSERT_THROWS(badTypeBitsToken.getData(), AssertionException); + Document{{"_data"_sd, goodData}, + {"_typeBits", BSONBinData(nonsense, sizeof(nonsense), BinDataGeneral)}}); + ASSERT_THROWS_CODE(badTypeBitsToken.getData(), AssertionException, ErrorCodes::Overflow); + + const unsigned char invalidString[] = { + 60, // CType::kStringLike + 55, // Non-null terminated + }; + auto invalidStringToken = ResumeToken::parse( + Document{{"_data"_sd, BSONBinData(invalidString, sizeof(invalidString), BinDataGeneral)}}); + ASSERT_THROWS_WITH_CHECK( + invalidStringToken.getData(), AssertionException, [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), 50816); + ASSERT_STRING_CONTAINS(exception.reason(), "Failed to find null terminator in string"); + }); } TEST(ResumeToken, WrongVersionToken) { diff --git a/src/mongo/db/storage/key_string.cpp b/src/mongo/db/storage/key_string.cpp index 0685576dabc..2e62f8489a5 100644 --- a/src/mongo/db/storage/key_string.cpp +++ b/src/mongo/db/storage/key_string.cpp @@ -250,7 +250,7 @@ T readType(BufReader* reader, bool inverted) { StringData readCString(BufReader* reader) { const char* start = static_cast<const char*>(reader->pos()); const char* end = static_cast<const char*>(memchr(start, 0x0, reader->remaining())); - invariant(end); + uassert(50816, "Failed to find null terminator in string.", end); size_t actualBytes = end - start; reader->skip(1 + actualBytes); return StringData(start, actualBytes); @@ -282,7 +282,7 @@ StringData readCStringWithNuls(BufReader* reader, std::string* scratch) { string readInvertedCString(BufReader* reader) { const char* start = static_cast<const char*>(reader->pos()); const char* end = static_cast<const char*>(memchr(start, 0xFF, reader->remaining())); - invariant(end); + uassert(50817, "Failed to find '0xFF' in inverted string.", end); size_t actualBytes = end - start; string s(start, actualBytes); for (size_t i = 0; i < s.size(); i++) { @@ -307,7 +307,7 @@ string readInvertedCStringWithNuls(BufReader* reader) { const char* start = static_cast<const char*>(reader->pos()); const char* end = static_cast<const char*>(memchr(start, 0xFF, reader->remaining())); - invariant(end); + uassert(50820, "Failed to find '0xFF' in inverted string.", end); size_t actualBytes = end - start; out.append(start, actualBytes); @@ -1122,7 +1122,9 @@ Decimal128 readDecimalContinuation(BufReader* reader, bool inverted, Decimal128 uint64_t continuation = endian::bigToNative(readType<uint64_t>(reader, inverted)); num = num.normalize(); num = num.add(Decimal128(num.isNegative(), num.getBiasedExponent(), 0, continuation), &flags); - invariant(!(Decimal128::hasFlag(flags, Decimal128::kInexact))); + uassert(50815, + "Unexpected inexact flag set after Decimal addition.", + !(Decimal128::hasFlag(flags, Decimal128::kInexact))); return num; } @@ -1182,7 +1184,9 @@ void toBsonValue(uint8_t ctype, if (originalType == TypeBits::kString) { *stream << readInvertedCStringWithNuls(reader); } else { - dassert(originalType == TypeBits::kSymbol); + uassert(50827, + "Expected original type to be Symbol.", + originalType == TypeBits::kSymbol); *stream << BSONSymbol(readInvertedCStringWithNuls(reader)); } @@ -1191,7 +1195,9 @@ void toBsonValue(uint8_t ctype, if (originalType == TypeBits::kString) { *stream << readCStringWithNuls(reader, &scratch); } else { - dassert(originalType == TypeBits::kSymbol); + uassert(50828, + "Expected original type to be Symbol.", + originalType == TypeBits::kSymbol); *stream << BSONSymbol(readCStringWithNuls(reader, &scratch)); } } @@ -1302,7 +1308,9 @@ void toBsonValue(uint8_t ctype, if (type == TypeBits::kDouble) { *stream << std::numeric_limits<double>::quiet_NaN(); } else { - invariant(type == TypeBits::kDecimal && version == KeyString::Version::V1); + uassert(50819, + "Invalid type bits for numeric NaN", + type == TypeBits::kDecimal && version == KeyString::Version::V1); *stream << Decimal128::kPositiveNaN; } break; @@ -1361,7 +1369,9 @@ void toBsonValue(uint8_t ctype, bin = isNegative ? -std::numeric_limits<double>::infinity() : std::numeric_limits<double>::infinity(); } else { // Huge decimal number, directly output - invariant(originalType == TypeBits::kDecimal); + uassert(50818, + "Invalid type bits for decimal number.", + originalType == TypeBits::kDecimal); uint64_t highbits = encoded & ~(1ULL << 63); uint64_t lowbits = endian::bigToNative(readType<uint64_t>(reader, inverted)); Decimal128 dec(Decimal128::Value{lowbits, highbits}); @@ -1378,10 +1388,14 @@ void toBsonValue(uint8_t ctype, *stream << bin; } else if (originalType == TypeBits::kLong) { // This can only happen for a single number. - invariant(bin == static_cast<double>(std::numeric_limits<long long>::min())); + uassert(50821, + "Unexpected value for large number.", + bin == static_cast<double>(std::numeric_limits<long long>::min())); *stream << std::numeric_limits<long long>::min(); } else { - invariant(originalType == TypeBits::kDecimal && version != KeyString::Version::V0); + uassert(50826, + "Unexpected type of large number.", + originalType == TypeBits::kDecimal && version != KeyString::Version::V0); const auto roundAwayFromZero = isNegative ? Decimal128::kRoundTowardNegative : Decimal128::kRoundTowardPositive; Decimal128 dec(bin, Decimal128::kRoundTo34Digits, roundAwayFromZero); @@ -1406,7 +1420,9 @@ void toBsonValue(uint8_t ctype, if (version == KeyString::Version::V0) { // for these, the raw double was stored intact, including sign bit. - invariant(originalType == TypeBits::kDouble); + uassert(50812, + "Invalid type bits for small number.", + originalType == TypeBits::kDouble); double d; memcpy(&d, &encoded, sizeof(d)); *stream << d; @@ -1438,12 +1454,14 @@ void toBsonValue(uint8_t ctype, double scaledBin; memcpy(&scaledBin, &encoded, sizeof(scaledBin)); if (originalType == TypeBits::kDouble) { - invariant(!hasDecimalContinuation); + uassert(50822, "Unexpected decimal continuation.", !hasDecimalContinuation); double bin = scaledBin * kTinyDoubleExponentDownshiftFactor; *stream << (isNegative ? -bin : bin); break; } - invariant(originalType == TypeBits::kDecimal && hasDecimalContinuation); + uassert(50823, + "Expected decimal continuation.", + originalType == TypeBits::kDecimal && hasDecimalContinuation); // If the actual double would be subnormal, scale in decimal domain. Decimal128 dec; @@ -1474,13 +1492,17 @@ void toBsonValue(uint8_t ctype, double bin; memcpy(&bin, &encoded, sizeof(bin)); if (originalType == TypeBits::kDouble) { - invariant(dcm == KeyString::kDCMEqualToDouble); + uassert(50824, + "Decimal contuation mismatch.", + dcm == KeyString::kDCMEqualToDouble); *stream << (isNegative ? -bin : bin); break; } // Deal with decimal cases - invariant(originalType == TypeBits::kDecimal); + uassert(50825, + "Unexpected type for small number, expected decimal.", + originalType == TypeBits::kDecimal); Decimal128 dec; switch (dcm) { case KeyString::kDCMEqualToDoubleRoundedUpTo15Digits: @@ -1562,14 +1584,16 @@ void toBsonValue(uint8_t ctype, *stream << adjustDecimalExponent(typeBits, Decimal128(integerPart)); break; default: - MONGO_UNREACHABLE; + uasserted(50831, "Unexpected type for positive int."); } break; } // KeyString V0: anything fractional is a double if (version == KeyString::Version::V0) { - invariant(originalType == TypeBits::kDouble); + uassert(50832, + "Expected type double for fractional part.", + originalType == TypeBits::kDouble); const uint64_t exponent = (64 - countLeadingZeros64(integerPart)) - 1; const size_t fractionalBits = (52 - exponent); const size_t fractionalBytes = (fractionalBits + 7) / 8; @@ -1614,7 +1638,7 @@ void toBsonValue(uint8_t ctype, : KeyString::kDCMHasContinuationLargerThanDoubleRoundedUpTo15Digits; // Deal with decimal cases - invariant(originalType == TypeBits::kDecimal); + uassert(50810, "Expected type Decimal.", originalType == TypeBits::kDecimal); Decimal128 dec; switch (dcm) { case KeyString::kDCMEqualToDoubleRoundedUpTo15Digits: @@ -1637,7 +1661,7 @@ void toBsonValue(uint8_t ctype, break; } default: - MONGO_UNREACHABLE; + uasserted(50811, str::stream() << "Unknown type: " << ctype); } } @@ -1936,7 +1960,7 @@ Decimal128 adjustDecimalExponent(TypeBits::Reader* typeBits, Decimal128 num) { // possible, we must be able to scale up. Scaling always must be exact and not change the value. const uint32_t kMaxExpAdjust = 33; const uint32_t kMaxExpIncrementForZeroHighCoefficient = 19; - dassert(!num.isZero()); + uassert(50829, "Expected non-zero number for decimal.", !num.isZero()); const uint32_t origExp = num.getBiasedExponent(); const uint8_t storedBits = typeBits->readDecimalExponent(); @@ -1958,16 +1982,22 @@ Decimal128 adjustDecimalExponent(TypeBits::Reader* typeBits, Decimal128 num) { // Increase exponent and decrease (right shift) coefficient. uint32_t flags = Decimal128::SignalingFlag::kNoFlag; auto quantized = num.quantize(Decimal128(0, highExp, 0, 1), &flags); - invariant(flags == Decimal128::SignalingFlag::kNoFlag); // must be exact + uassert(50813, + "Unexpected signaling flag for Decimal quantization.", + flags == Decimal128::SignalingFlag::kNoFlag); // must be exact num = quantized; } else { // Decrease exponent and increase (left shift) coefficient. uint32_t lowExp = highExp - (1U << KeyString::TypeBits::kStoredDecimalExponentBits); - invariant(lowExp >= origExp - kMaxExpAdjust); + uassert(50814, + "Unexpected exponent values after adjusting Decimal.", + lowExp >= origExp - kMaxExpAdjust); num = num.add(Decimal128(0, lowExp, 0, 0)); } - dassert((num.getBiasedExponent() & KeyString::TypeBits::kStoredDecimalExponentMask) == - (highExp & KeyString::TypeBits::kStoredDecimalExponentMask)); + uassert(50830, + "Unexpected biased exponent in decimal.", + (num.getBiasedExponent() & KeyString::TypeBits::kStoredDecimalExponentMask) == + (highExp & KeyString::TypeBits::kStoredDecimalExponentMask)); return num; } @@ -1996,7 +2026,10 @@ size_t KeyString::getKeySize(const char* buffer, return (len - (remainingBytes + 1)); } -BSONObj KeyString::toBson(const char* buffer, size_t len, Ordering ord, const TypeBits& typeBits) { +BSONObj KeyString::toBsonSafe(const char* buffer, + size_t len, + Ordering ord, + const TypeBits& typeBits) { BSONObjBuilder builder; BufReader reader(buffer, len); TypeBits::Reader typeBitsReader(typeBits); @@ -2018,6 +2051,13 @@ BSONObj KeyString::toBson(const char* buffer, size_t len, Ordering ord, const Ty return builder.obj(); } +BSONObj KeyString::toBson(const char* buffer, + size_t len, + Ordering ord, + const TypeBits& typeBits) noexcept { + return toBsonSafe(buffer, len, ord, typeBits); +} + BSONObj KeyString::toBson(StringData data, Ordering ord, const TypeBits& typeBits) { return toBson(data.rawData(), data.size(), ord, typeBits); } @@ -2180,7 +2220,7 @@ uint8_t KeyString::TypeBits::Reader::readBit() { const uint8_t offsetInByte = _curBit % 8; _curBit++; - dassert(byte <= _typeBits.getSizeByte()); + uassert(50615, "Invalid size byte.", byte <= _typeBits.getSizeByte()); return (_typeBits._buf[byte] & (1 << offsetInByte)) ? 1 : 0; } diff --git a/src/mongo/db/storage/key_string.h b/src/mongo/db/storage/key_string.h index e9ed78a9cf6..a144a5096aa 100644 --- a/src/mongo/db/storage/key_string.h +++ b/src/mongo/db/storage/key_string.h @@ -305,7 +305,18 @@ public: Ordering ord, const TypeBits& typeBits); static BSONObj toBson(StringData data, Ordering ord, const TypeBits& types); - static BSONObj toBson(const char* buffer, size_t len, Ordering ord, const TypeBits& types); + /** + * Decodes the given KeyString buffer into it's BSONObj representation. This is marked as + * noexcept since the assumption is that 'buffer' is a valid KeyString buffer and this method + * is not expected to throw. + * + * If the buffer provided may not be valid, use the 'safe' version instead. + */ + static BSONObj toBson(const char* buffer, + size_t len, + Ordering ord, + const TypeBits& types) noexcept; + static BSONObj toBsonSafe(const char* buffer, size_t len, Ordering ord, const TypeBits& types); /** * Decodes a RecordId from the end of a buffer. diff --git a/src/mongo/db/storage/key_string_test.cpp b/src/mongo/db/storage/key_string_test.cpp index 8efd5ff7197..aa0974bd486 100644 --- a/src/mongo/db/storage/key_string_test.cpp +++ b/src/mongo/db/storage/key_string_test.cpp @@ -50,6 +50,7 @@ #include "mongo/stdx/functional.h" #include "mongo/stdx/future.h" #include "mongo/stdx/memory.h" +#include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" #include "mongo/util/hex.h" #include "mongo/util/log.h" @@ -1178,6 +1179,68 @@ TEST_F(KeyStringTest, KeyWithTooManyTypeBitsCausesUassert) { ASSERT_THROWS_CODE(key.resetToKey(obj, ONE_ASCENDING), DBException, ErrorCodes::KeyTooLong); } +TEST_F(KeyStringTest, ToBsonSafeShouldNotTerminate) { + KeyString::TypeBits typeBits(KeyString::Version::V1); + + const char invalidString[] = { + 60, // CType::kStringLike + 55, // Non-null terminated + }; + ASSERT_THROWS_CODE( + KeyString::toBsonSafe(invalidString, sizeof(invalidString), ALL_ASCENDING, typeBits), + AssertionException, + 50816); + + const char invalidNumber[] = { + 43, // CType::kNumericPositive1ByteInt + 1, // Encoded integer part, least significant bit indicates there's a fractional part. + 0, // Since the integer part is 1 byte, the next 7 bytes are expected to be the fractional + // part and are needed to prevent the BufReader from overflowing. + 0, + 0, + 0, + 0, + 0, + 0, + }; + ASSERT_THROWS_CODE( + KeyString::toBsonSafe(invalidNumber, sizeof(invalidNumber), ALL_ASCENDING, typeBits), + AssertionException, + 50810); +} + +TEST_F(KeyStringTest, RandomizedInputsForToBsonSafe) { + std::mt19937 gen(newSeed()); + std::uniform_int_distribution<> randomByte(std::numeric_limits<unsigned char>::min(), + std::numeric_limits<unsigned char>::max()); + + const auto interestingElements = getInterestingElements(KeyString::Version::V1); + for (auto elem : interestingElements) { + const KeyString ks(KeyString::Version::V1, elem, ALL_ASCENDING); + + char* ksBuffer = (char*)ks.getBuffer(); + + // Select a random byte to change, except for the first byte as it will likely become an + // invalid CType and not test anything interesting. + ksBuffer[randomByte(gen) % ks.getSize() + 1] = randomByte(gen); + + try { + KeyString::toBsonSafe(ksBuffer, ks.getSize(), ALL_ASCENDING, ks.getTypeBits()); + } catch (const AssertionException&) { + // The expectation is that the randomized buffer is likely an invalid KeyString, + // however attempting to decode it should fail gracefully. + } + + // Retest with descending. + try { + KeyString::toBsonSafe(ksBuffer, ks.getSize(), ONE_DESCENDING, ks.getTypeBits()); + } catch (const AssertionException&) { + // The expectation is that the randomized buffer is likely an invalid KeyString, + // however attempting to decode it should fail gracefully. + } + } +} + namespace { const uint64_t kMinPerfMicros = 20 * 1000; const uint64_t kMinPerfSamples = 50 * 1000; @@ -1330,3 +1393,12 @@ TEST_F(KeyStringTest, DecimalFromUniformDoublePerf) { } perfTest(version, numbers); } + +DEATH_TEST(KeyStringTest, ToBsonPromotesAssertionsToTerminate, "terminate() called") { + const char invalidString[] = { + 60, // CType::kStringLike + 55, // Non-null terminated + }; + KeyString::TypeBits typeBits(KeyString::Version::V1); + KeyString::toBson(invalidString, sizeof(invalidString), ALL_ASCENDING, typeBits); +} |