diff options
author | Anton Korshunov <anton.korshunov@mongodb.com> | 2019-03-08 14:48:45 +0000 |
---|---|---|
committer | Anton Korshunov <anton.korshunov@mongodb.com> | 2019-03-13 06:50:28 +0000 |
commit | 330c59b671ba62ad72f0a80e0c32beca7c69be2b (patch) | |
tree | 27f8c57dcb2feec4844b6cf12a7ec7f252adeb55 | |
parent | b0a74faa300b4448b9dfb04d4a646f366d9f4572 (diff) | |
download | mongo-330c59b671ba62ad72f0a80e0c32beca7c69be2b.tar.gz |
SERVER-39788 Large values in $skip and $limit stages may cause an arithmetic overflow
-rw-r--r-- | jstests/aggregation/bugs/skip_limit_overflow.js | 123 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_limit.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_sample.cpp | 5 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_skip.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_sort.cpp | 37 | ||||
-rw-r--r-- | src/mongo/db/sorter/sorter.cpp | 24 |
6 files changed, 172 insertions, 21 deletions
diff --git a/jstests/aggregation/bugs/skip_limit_overflow.js b/jstests/aggregation/bugs/skip_limit_overflow.js new file mode 100644 index 00000000000..f0d7e0b27c7 --- /dev/null +++ b/jstests/aggregation/bugs/skip_limit_overflow.js @@ -0,0 +1,123 @@ +/** + * SERVER-39788 Test that the values in the $limit and $skip stages do not overflow when the + * pipeline is optimized and the $sort stage doesn't crash the server due to unreasonable memory + * requirements. + * + * This test makes assumptions about how the explain output will be formatted, so cannot be + * transformed to be put inside a $facet stage or in a sharded explain output. + * @tags: [do_not_wrap_aggregations_in_facets, assumes_unsharded_collection] + */ +(function() { + "use strict"; + + load("jstests/libs/analyze_plan.js"); // For 'aggPlanHasStages' and other explain helpers. + + const coll = db.server39788; + coll.drop(); + + function testPipeline(pipeline, expectedResult, optimizedAwayStages) { + const explainOutput = coll.explain().aggregate(pipeline); + + assert(explainOutput.hasOwnProperty("stages"), + "Expected pipeline " + tojsononeline(pipeline) + + " to use an aggregation framework in the explain output: " + + tojson(explainOutput)); + + if (optimizedAwayStages) { + optimizedAwayStages.forEach( + (stage) => + assert(!aggPlanHasStage(explainOutput, stage), + "Expected pipeline " + tojsononeline(pipeline) + " to *not* include a " + + stage + " stage in the explain output: " + tojson(explainOutput))); + } + + for (let path in expectedResult) { + const subPaths = path.split("."); + const stageName = subPaths[0]; + const stages = getAggPlanStages(explainOutput, stageName); + assert(stages !== null, + "Expected pipeline " + tojsononeline(pipeline) + " to include a " + stageName + + " stage in the explain output: " + tojson(explainOutput)); + assert(stages.length == expectedResult[path].length, + "Expected pipeline " + tojsononeline(pipeline) + " to include " + + expectedResult[path].length + stageName + " stages in the explain output: " + + tojson(explainOutput)); + assert.eq( + stages.reduce( + (res, stage) => { + res.push(subPaths.reduce((res, cur) => res[cur], stage)); + return res; + }, + []), + expectedResult[path], + "Stage: " + stageName + ", path: " + path + ", explain: " + tojson(explainOutput)); + } + + // Ensure the aggregate command doesn't fail. + assert.eq(coll.aggregate(pipeline).toArray(), []); + } + + // Case where overflow of limit + skip prevents limit stage from being absorbed. Values + // are specified as integrals > MAX_LONG. Note that we cannot specify this huge value as + // a NumberLong, as we get a number conversion error (even if it's passed as a string). + testPipeline([{$sort: {x: -1}}, {$skip: 18446744073709552000}, {$limit: 6}], + {"$limit": [NumberLong(6)], "$skip": [NumberLong("9223372036854775807")]}); + testPipeline([{$sort: {x: -1}}, {$skip: 6}, {$limit: 18446744073709552000}], + {"$limit": [NumberLong("9223372036854775807")], "$skip": [NumberLong(6)]}); + + // Case where overflow of limit + skip prevents limit stage from being absorbed. One of the + // values == MAX_LONG, another one is 1. + testPipeline([{$sort: {x: -1}}, {$skip: NumberLong("9223372036854775807")}, {$limit: 1}], + {"$limit": [NumberLong(1)], "$skip": [NumberLong("9223372036854775807")]}); + testPipeline([{$sort: {x: -1}}, {$skip: 1}, {$limit: NumberLong("9223372036854775807")}], + {"$limit": [NumberLong("9223372036854775807")], "$skip": [NumberLong(1)]}); + + // Case where limit + skip do not overflow. Limit == MAX_LONG and skip is 0. Should be able to + // absorb the limit and skip stages. + // Note that we cannot specify limit == 0, so we expect an error in this case. + testPipeline([{$sort: {x: -1}}, {$skip: 0}, {$limit: NumberLong("9223372036854775807")}], + {"$cursor.limit": [NumberLong("9223372036854775807")]}, + ["$skip", "$limit"]); + + // Case where limit + skip do not overflow. One value is MAX_LONG - 1 and another one is 1. + // Should be able to absorb the limit stage. + testPipeline([{$sort: {x: -1}}, {$skip: NumberLong("9223372036854775806")}, {$limit: 1}], + { + "$cursor.limit": [NumberLong("9223372036854775807")], + "$skip": [NumberLong("9223372036854775806")] + }, + ["$limit"]); + testPipeline([{$sort: {x: -1}}, {$skip: 1}, {$limit: NumberLong("9223372036854775806")}], + {"$cursor.limit": [NumberLong("9223372036854775807")], "$skip": [NumberLong(1)]}, + ["$limit"]); + + // Case where limit + skip do not overflow. Both values are < MAX_LONG. + testPipeline([{$sort: {x: -1}}, {$skip: 674761616283}, {$limit: 35361718}], + {"$cursor.limit": [NumberLong(674796978001)], "$skip": [NumberLong(674761616283)]}, + ["$limit"]); + testPipeline([{$sort: {x: -1}}, {$skip: 35361718}, {$limit: 674761616283}], + {"$cursor.limit": [NumberLong(674796978001)], "$skip": [NumberLong(35361718)]}, + ["$limit"]); + + // Case where where overflow of limit + skip + skip prevents limit stage from being absorbed. + // One skip == MAX_LONG - 1, another one is 1. Should merge two skip stages into one. + testPipeline( + [{$sort: {x: -1}}, {$skip: 1}, {$skip: NumberLong("9223372036854775806")}, {$limit: 1}], + {"$limit": [NumberLong(1)], "$skip": [NumberLong("9223372036854775807")]}); + + // Case where where overflow of limit + skip + skip prevents limit stage from being absorbed. + // One skip == MAX_LONG, another one is 1. Should not absorb or merge any stages. + testPipeline( + [{$sort: {x: -1}}, {$skip: 1}, {$skip: NumberLong("9223372036854775807")}, {$limit: 1}], + {"$limit": [NumberLong(1)], "$skip": [NumberLong(1), NumberLong("9223372036854775807")]}); + + // Case where sample size is > MAX_LONG. + testPipeline([{$sample: {size: 18446744073709552000}}], + {"$sample.size": [NumberLong("9223372036854775807")]}); + // Case where sample size is == MAX_LONG. + testPipeline([{$sample: {size: NumberLong("9223372036854775807")}}], + {"$sample.size": [NumberLong("9223372036854775807")]}); + // Case where sample size is == MAX_LONG - 1. + testPipeline([{$sample: {size: NumberLong("9223372036854775806")}}], + {"$sample.size": [NumberLong("9223372036854775806")]}); +})(); diff --git a/src/mongo/db/pipeline/document_source_limit.cpp b/src/mongo/db/pipeline/document_source_limit.cpp index abe70f0d3db..bcc10f4f892 100644 --- a/src/mongo/db/pipeline/document_source_limit.cpp +++ b/src/mongo/db/pipeline/document_source_limit.cpp @@ -99,7 +99,7 @@ intrusive_ptr<DocumentSource> DocumentSourceLimit::createFromBson( BSONElement elem, const intrusive_ptr<ExpressionContext>& pExpCtx) { uassert(15957, "the limit must be specified as a number", elem.isNumber()); - long long limit = elem.numberLong(); + long long limit = elem.safeNumberLong(); return DocumentSourceLimit::create(pExpCtx, limit); } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_sample.cpp b/src/mongo/db/pipeline/document_source_sample.cpp index 154fbe60174..b0f709366a1 100644 --- a/src/mongo/db/pipeline/document_source_sample.cpp +++ b/src/mongo/db/pipeline/document_source_sample.cpp @@ -102,8 +102,9 @@ intrusive_ptr<DocumentSource> DocumentSourceSample::createFromBson( if (fieldName == "size") { uassert(28746, "size argument to $sample must be a number", elem.isNumber()); - uassert(28747, "size argument to $sample must not be negative", elem.numberLong() >= 0); - sample->_size = elem.numberLong(); + auto size = elem.safeNumberLong(); + uassert(28747, "size argument to $sample must not be negative", size >= 0); + sample->_size = size; sizeSpecified = true; } else { uasserted(28748, str::stream() << "unrecognized option to $sample: " << fieldName); diff --git a/src/mongo/db/pipeline/document_source_skip.cpp b/src/mongo/db/pipeline/document_source_skip.cpp index e58259232c7..364f148162e 100644 --- a/src/mongo/db/pipeline/document_source_skip.cpp +++ b/src/mongo/db/pipeline/document_source_skip.cpp @@ -107,7 +107,7 @@ intrusive_ptr<DocumentSource> DocumentSourceSkip::createFromBson( uassert(15972, str::stream() << "Argument to $skip must be a number not a " << typeName(elem.type()), elem.isNumber()); - auto nToSkip = elem.numberLong(); + auto nToSkip = elem.safeNumberLong(); uassert(15956, "Argument to $skip cannot be negative", nToSkip >= 0); return DocumentSourceSkip::create(pExpCtx, nToSkip); diff --git a/src/mongo/db/pipeline/document_source_sort.cpp b/src/mongo/db/pipeline/document_source_sort.cpp index 0d5f0b331c8..9f9832fd961 100644 --- a/src/mongo/db/pipeline/document_source_sort.cpp +++ b/src/mongo/db/pipeline/document_source_sort.cpp @@ -40,6 +40,7 @@ #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/pipeline/value.h" #include "mongo/db/query/collation/collation_index_key.h" +#include "mongo/platform/overflow_arithmetic.h" #include "mongo/s/query/document_source_merge_cursors.h" namespace mongo { @@ -200,24 +201,34 @@ Pipeline::SourceContainer::iterator DocumentSourceSort::doOptimizeAt( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { invariant(*itr == this); - auto sortItr = std::next(itr); - long long skipSum = 0; - while (sortItr != container->end()) { - auto nextStage = (*sortItr).get(); - - if (auto nextSkip = dynamic_cast<DocumentSourceSkip*>(nextStage)) { - skipSum += nextSkip->getSkip(); - ++sortItr; - } else if (auto nextLimit = dynamic_cast<DocumentSourceLimit*>(nextStage)) { - nextLimit->setLimit(nextLimit->getLimit() + skipSum); + using SumType = long long; + // Just in case, since we're passing an address of a signed 64bit below. + static_assert(std::is_signed<SumType>::value && sizeof(SumType) == 8); + + auto stageItr = std::next(itr); + SumType skipSum = 0; + while (stageItr != container->end()) { + auto nextStage = (*stageItr).get(); + auto nextSkip = dynamic_cast<DocumentSourceSkip*>(nextStage); + auto nextLimit = dynamic_cast<DocumentSourceLimit*>(nextStage); + SumType safeSum = 0; + + // The skip and limit values can be very large, so we need to make sure the sum doesn't + // overflow before applying an optimiztion to pull the limit into the sort stage. + if (nextSkip && !mongoSignedAddOverflow64(skipSum, nextSkip->getSkip(), &safeSum)) { + skipSum = safeSum; + ++stageItr; + } else if (nextLimit && + !mongoSignedAddOverflow64(nextLimit->getLimit(), skipSum, &safeSum)) { + nextLimit->setLimit(safeSum); setLimitSrc(nextLimit); - container->erase(sortItr); - sortItr = std::next(itr); + container->erase(stageItr); + stageItr = std::next(itr); skipSum = 0; } else if (!nextStage->constraints().canSwapWithLimitAndSample) { return std::next(itr); } else { - ++sortItr; + ++stageItr; } } diff --git a/src/mongo/db/sorter/sorter.cpp b/src/mongo/db/sorter/sorter.cpp index a6e038191fe..0e3e1ad7f38 100644 --- a/src/mongo/db/sorter/sorter.cpp +++ b/src/mongo/db/sorter/sorter.cpp @@ -59,6 +59,7 @@ #include "mongo/db/storage/encryption_hooks.h" #include "mongo/db/storage/storage_options.h" #include "mongo/platform/atomic_word.h" +#include "mongo/platform/overflow_arithmetic.h" #include "mongo/s/is_mongos.h" #include "mongo/util/assert_util.h" #include "mongo/util/bufreader.h" @@ -655,10 +656,25 @@ public: _fileName = _opts.tempDir + "/" + nextFileName(); } - // Preallocate a fixed sized vector of the required size if we - // don't expect it to have a major impact on our memory budget. - // This is the common case with small limits. - if ((sizeof(Data) * opts.limit) < opts.maxMemoryUsageBytes / 10) { + // Preallocate a fixed sized vector of the required size if we don't expect it to have a + // major impact on our memory budget. This is the common case with small limits. If the + // limit is really large, we need to take care when doing the check below. Both 'opts.limit' + // and 'sizeof(Data)' are unsigned long's, it's always clearly defined behaviour to multiply + // these two numbers, but the result may be wrapped around (truncated) due to modular + // arithmetics. So, the result of the multiplication may be < maxMemoryUsageBytes / 10, but + // 'opts.limit' may be still large enough to trigger an std::length_error exception and + // abnormally terminate the program. So, we'll check here if the multiply operation was safe + // and whether the limit fits into the maximum allowed vector size. + using MultiplicationType = size_t; + // Just in case, since we're passing an address of an unsigned 64bit below. + static_assert(std::is_unsigned<MultiplicationType>::value && + sizeof(MultiplicationType) == 8); + + MultiplicationType requiredBytes; + auto isSafeMultiplication = mongoUnsignedMultiplyOverflow64( + sizeof(typename decltype(_data)::value_type), opts.limit, &requiredBytes); + if (isSafeMultiplication && requiredBytes < opts.maxMemoryUsageBytes / 10 && + opts.limit <= _data.max_size()) { _data.reserve(opts.limit); } } |