[Bug libstdc++/91263] New: unordered_map and unordered_set operator== double key comparison causes exponential behavior

terrelln at fb dot com gcc-bugzilla@gcc.gnu.org
Thu Jul 25 20:07:00 GMT 2019


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


More information about the Gcc-bugs mailing list