Summary
The proposal aims to fix strict weak ordering problems in comparison-based algorithms to prevent security issues and uncover bugs in the user code. The proposal includes adding debug checks for strict weak ordering within _LIBCPP_ENABLE_DEBUG_MODE
.
Background
Not so long ago we proposed and changed std::sort algorithm. However, it was rolled back in 16.0.1 because of failures within the broken comparators. That was even true for the previous implementation, however, the new one exposed the problems more often.
At Google we constantly find failures and out of access memory bugs. Every single one of them results in a security issue. Say, if we sort 5 elements with the broken comparator, it might work fine but if the attacker manages to get more than 30 elements, it can results in a out of bound access with the current implementation.
We want to add debug checks for the strict weak ordering. It imposes several issues about the complexity of those as the default definition of this takes triples, and O(N^3) algorithms even in debug checks is not something libc++ user should experience.
We already added some debug capabilities as range randomization which served us well to find brittle golden tests and, more importantly, bugs. libc++ debug mode has irreflexivity checks and randomization can help finding some problems. We still can do better.
Proposal
Within the _LIBCPP_ENABLE_DEBUG_MODE
, we are going to introduce iterator out of bounds checks where appropriate. One of such changes is considered in an ongoing review.
Within the _LIBCPP_ENABLE_DEBUG_MODE
, we are going to introduce the strict weak ordering checks for the following set of algorithms:
Most important ones that can result in out of bounds accesses:
- std::sort, std::stable_sort, std::nth_element, std::partial_sort
- std::make_heap, std::sort_heap
Less important ones (they can only return an incorrect result for now):
- std::{min,max,minmax}_element
- std::{lower,upper}_bound
- std::lexicographical_compare
Algorithms for checking strict weak ordering have obvious O(N^3) complexity for forward iterators. O(N^3) is going to hurt performance. For such iterators we are going to check only min(20, size) random or first elements and exclude primitive types with the default std::less<> comparator.
We know better O(N^2) algorithms for checking strict weak ordering. It allows us to check even more elements for functions which require random access iterators (std::sort, std::nth_element, std::stable_sort, std::partial_sort, std::{make,sort}_heap). Say, we will check min(100, size) elements per call. On the downside, such checks do not show a triple which violates strict weak order but only tell if the bug exists. This algorithm works in the following way: sorts the range somehow with the algorithm that does not crash, then if std::is_sorted is false, there is a bug in the comparator. Otherwise, we go through the ranges of equal elements and check they are equal among each other and are less than all that come after. The proof can be read in a repo.
At Google we found that checks for 100 elements rather than for 20 elements uncover 20% more bugs. However, checks for 500 elements uncovered only 1 more bug. We want to claim that most sorts are quite small and 95%+ of them are within the range of a hundred. We don’t want to hurt performance a lot and that’s why the final proposal looks like this:
- For random access iterators we are going to use an O(N^2) algorithm for min(100, last - first) elements. This includes std::make_heap, std::sort_heap, std::sort, std::stable_sort, std::partial_sort, std::nth_element.
- For forward iterators we are going to use an O(N^3) algorithm for min(20, last - first) elements. This includes all other algorithms.
Alternatives considered
We can compromise in some cases.
One option will be to drop the O(N^2) algorithm because it is some work to prove that it does check the strict weak ordering correctly, however, as said before, 20 elements uncover fewer bugs.
Another option is to drop O(N^3) algorithms for std::{min,max}_element algorithms as they cannot result in an out of bound access for most incorrect comparators.
Last option is not to follow the approach at all. Given the number of questions asked on stackoverflow and bugs it uncovers, we strongly believe this is the right call to make. It has the potential to save many hours and sleepless nights. There were cases where at Google we could not understand why the failure was happening for weeks and the bug existed for years.
Looking forward to your feedback.
-Daniel @danlark1