https://gcc.gnu.org/bugzilla/show_bug.cgi?id=91263
Bug ID: 91263 Summary: unordered_map and unordered_set operator== double key comparison causes exponential behavior Product: gcc Version: 10.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: libstdc++ Assignee: unassigned at gcc dot gnu.org Reporter: terrelln at fb dot com Target Milestone: --- When unordered_map or unordered_set has a nested unordered_{map,set} as a key, operator== becomes exponential in the depth of the nesting. This is because unordered_{map,set}::operator== does two key comparisons, one in find() using key_eq() and one when comparing the key-value pairs using the key’s operator==. These two comparisons can theoretically be different but are almost always the same in practice. unordered_{map,set} should only do one key comparison in operator== to avoid this. It is also generally more efficient even in the non-nesting cases. Nathan Bronson helped create a minimal repro below that demonstrates this exponential behavior. #include <unordered_set> #include <cassert> #include <memory> struct Nested; struct NestedHash { std::size_t operator()(Nested const& n) const; }; struct Nested { std::unique_ptr<std::unordered_set<Nested, NestedHash>> set; explicit Nested(int depth) : set(std::make_unique<std::unordered_set<Nested, NestedHash>>()) { if (depth > 0) { set->emplace(depth - 1); } } }; std::size_t NestedHash::operator()(Nested const& n) const { std::size_t rv = 1; for (auto& k : *n.set) { rv += operator()(k); } return rv; } bool operator==(Nested const& lhs, Nested const& rhs) { return *lhs.set == *rhs.set; } bool operator!=(Nested const& lhs, Nested const& rhs) { return !(lhs == rhs); } void testNestedSetEquality() { auto n1 = Nested(100); auto n2 = Nested(100); auto n3 = Nested(99); assert(n1 == n1); assert(n1 == n2); assert(n1 != n3); } This is a contrived example, but this shows up in practice in folly::dynamic::operator== [1] as a worst case exponential algorithm. No one will create objects like this intentionally, but they can be produced with folly::parseJson() [2] when non-string keys are allowed. If an attacker controlled the input to parseJson() they could easily DDOS the machine with a payload like "{{{{{0:0}:0}:0}:0}:0}" but nested 100 layers deep instead of 5. unordered_map and unordered_set should only do one key comparison per item in operator==. This is possible [3] because it is undefined behavior to call the containers operator== if the key’s operator== isn’t a refinement of key_eq(). Fixing this bug in unordered_map and unordered_set avoids the exponential behavior with nested keys, and it is generally more efficient even in the non-nesting cases. An implementation of set and map equality with only one key comparison is included below, thanks to Nathan Bronson. template <typename S> bool eqSet(S const& lhs, S const& rhs) { if (lhs.size() != rhs.size()) { return false; } for (auto& v : lhs) { auto slot = rhs.bucket(v); auto b = rhs.begin(slot); auto e = rhs.end(slot); while (b != e && !(*b == v)) { ++b; } if (b == e) { return false; } } return true; } template <typename M> bool eqMap(M const& lhs, M const& rhs) { if (lhs.size() != rhs.size()) { return false; } for (auto& v : lhs) { auto slot = rhs.bucket(v.first); auto b = rhs.begin(slot); auto e = rhs.end(slot); while (b != e && !(b->first == v.first)) { ++b; } if (b == e || !(b->second == v.second)) { return false; } } return true; } [1] https://github.com/facebook/folly/blob/master/folly/dynamic.cpp#L113 [2] https://github.com/facebook/folly/blob/master/folly/json.h#L194 [3] https://github.com/facebook/folly/commit/11855c21b21fb46fe1004de8c0412dd94eb5c45f