-
Notifications
You must be signed in to change notification settings - Fork 11.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[libc++] Add input validation for set_intersection() in debug mode. #101508
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -20,18 +20,18 @@ | |||||
|
||||||
_LIBCPP_BEGIN_NAMESPACE_STD | ||||||
|
||||||
template <class _Compare, class _ForwardIterator> | ||||||
template <class _Compare, class _ForwardIterator, class _Sent> | ||||||
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _ForwardIterator | ||||||
__is_sorted_until(_ForwardIterator __first, _ForwardIterator __last, _Compare __comp) { | ||||||
__is_sorted_until(_ForwardIterator __first, _Sent __last, _Compare&& __comp) { | ||||||
if (__first != __last) { | ||||||
_ForwardIterator __i = __first; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This makes the code quite a bit clearer. |
||||||
while (++__i != __last) { | ||||||
if (__comp(*__i, *__first)) | ||||||
return __i; | ||||||
__first = __i; | ||||||
while (++__first != __last) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: what is the reason to swap |
||||||
if (__comp(*__first, *__i)) | ||||||
return __first; | ||||||
__i = __first; | ||||||
} | ||||||
} | ||||||
return __last; | ||||||
return __first; | ||||||
} | ||||||
|
||||||
template <class _ForwardIterator, class _Compare> | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,12 +11,15 @@ | |
|
||
#include <__algorithm/comp.h> | ||
#include <__algorithm/comp_ref_type.h> | ||
#include <__algorithm/is_sorted_until.h> | ||
#include <__algorithm/iterator_operations.h> | ||
#include <__algorithm/lower_bound.h> | ||
#include <__assert> | ||
#include <__config> | ||
#include <__functional/identity.h> | ||
#include <__iterator/iterator_traits.h> | ||
#include <__iterator/next.h> | ||
#include <__type_traits/is_constant_evaluated.h> | ||
#include <__type_traits/is_same.h> | ||
#include <__utility/exchange.h> | ||
#include <__utility/move.h> | ||
|
@@ -95,6 +98,14 @@ __set_intersection( | |
_Compare&& __comp, | ||
std::forward_iterator_tag, | ||
std::forward_iterator_tag) { | ||
#if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG | ||
if (!__libcpp_is_constant_evaluated()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this required? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because Edit: Sorry, now that I said it I'm not sure =/. Maybe it wasn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still don't understand. Can you try removing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 -- also curious to see the error. |
||
_LIBCPP_ASSERT_INTERNAL( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should likely be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should have added a comment: I didn't do that because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I think this should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
std::__is_sorted_until(__first1, __last1, __comp) == __last1, "set_intersection: input range 1 must be sorted"); | ||
_LIBCPP_ASSERT_INTERNAL( | ||
std::__is_sorted_until(__first2, __last2, __comp) == __last2, "set_intersection: input range 2 must be sorted"); | ||
} | ||
#endif | ||
_LIBCPP_CONSTEXPR std::__identity __proj; | ||
bool __prev_may_be_equal = false; | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -43,33 +43,31 @@ | |||||
|
||||||
#include "test_iterators.h" | ||||||
|
||||||
namespace { | ||||||
|
||||||
// __debug_less will perform an additional comparison in an assertion | ||||||
static constexpr unsigned std_less_comparison_count_multiplier() noexcept { | ||||||
#if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG | ||||||
return 2; | ||||||
// Debug mode provides no complexity guarantees, testing them would be a waste of effort. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree here. While we do not conform to the standards requirements, we do try to avoid increasing the complexity. A constant factor is usually OK, and it would be good to set the current increase here to avoid increasing it accidentally. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with @philnik777. @var-const should chime in, but basically while we are OK with larger performance penalties in the debug mode, we don't "not care" about changing the complexity. @var-const It turns out this is probably something we should have documented in the Hardening mode documentation, and we should go ahead and document it now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with what you're saying, I unfortunately have a ton of first-hand experience with not being able to use the best tool for the job because it's too slow, but this isn't as simple as it may seem. I'll argue the case for what I did a little bit out of pride, but mostly because I believe I made this choice for valid reasons which I probably should have shared in the commit description or a code comment. You'll see we had previously a function called I agree that performance on debug mode is not something we want to ignore, but I don't have a good solution for any of it, and, since I don't have a good solution for automating validation, I would personally prefer relying on the safety net of code reviews to catch this sort of stuff. Validating the number of operations is really strict, it's a tricky thing to mix with debug instrumentation. Having said all that, I'm happy to change this proposal if you disagree. We could have a single constant multiplier to be used for all operations, or one constant for comparisons+projections and another one for iterator operations. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks a lot for writing this out! I think we've been somewhat consciously postponing making the decision on what kind of performance guarantees the debug mode should provide (if any) -- there was no pressing need to make a commitment there, and I wanted to gain some usage experience to inform this decision. I would say that from what I've seen so far, I'm leaning towards making the debug mode guarantee big-O complexity (the Standard sometimes mandates the exact number of operations which would be too strict, IMO). I'm very concerned that unless we keep even the debug mode relatively lightweight and performant, it will end up a mode that no one enables and thus is effectively useless; I'm especially concerned about a potential "death from a thousand cuts" scenario where many checks, each one not that heavyweight on its own, add up to something that is unacceptably slow for too many users. IIUC, the new checks don't really check the complexity here as well (it makes the average case worse, but worst case is the same). We do need to test for the exact complexity in regular modes (since it's mandated by the Standard), but I can relate to your perspective that these tests aren't really well-suited for the debug mode. Not checking for complexity in the debug mode makes sense to me; we should change the comment, however, like Louis suggested. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In light of the discussion above (thanks for the detailed explanation BTW), I would change to this:
Suggested change
That way we're not making a statement about whether the complexity is supposed to be the same or not. I'm basically sweeping this whole thing under the rug. |
||||||
#ifdef _LIBCPP_HARDENING_MODE_DEBUG | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We still need to check for |
||||||
# define ASSERT_COMPLEXITY(expression) (void)(expression) | ||||||
#else | ||||||
return 1; | ||||||
# define ASSERT_COMPLEXITY(expression) assert(expression) | ||||||
#endif | ||||||
} | ||||||
|
||||||
namespace { | ||||||
|
||||||
struct [[nodiscard]] OperationCounts { | ||||||
std::size_t comparisons{}; | ||||||
struct PerInput { | ||||||
std::size_t proj{}; | ||||||
IteratorOpCounts iterops; | ||||||
|
||||||
[[nodiscard]] constexpr bool isNotBetterThan(const PerInput& other) { | ||||||
[[nodiscard]] constexpr bool isNotBetterThan(const PerInput& other) const noexcept { | ||||||
return proj >= other.proj && iterops.increments + iterops.decrements + iterops.zero_moves >= | ||||||
other.iterops.increments + other.iterops.decrements + other.iterops.zero_moves; | ||||||
} | ||||||
}; | ||||||
std::array<PerInput, 2> in; | ||||||
|
||||||
[[nodiscard]] constexpr bool isNotBetterThan(const OperationCounts& expect) { | ||||||
return std_less_comparison_count_multiplier() * comparisons >= expect.comparisons && | ||||||
in[0].isNotBetterThan(expect.in[0]) && in[1].isNotBetterThan(expect.in[1]); | ||||||
[[nodiscard]] constexpr bool isNotBetterThan(const OperationCounts& expect) const noexcept { | ||||||
return comparisons >= expect.comparisons && in[0].isNotBetterThan(expect.in[0]) && | ||||||
in[1].isNotBetterThan(expect.in[1]); | ||||||
} | ||||||
}; | ||||||
|
||||||
|
@@ -80,16 +78,17 @@ struct counted_set_intersection_result { | |||||
|
||||||
constexpr counted_set_intersection_result() = default; | ||||||
|
||||||
constexpr explicit counted_set_intersection_result(std::array<int, ResultSize>&& contents) : result{contents} {} | ||||||
constexpr explicit counted_set_intersection_result(std::array<int, ResultSize>&& contents) noexcept | ||||||
: result{contents} {} | ||||||
|
||||||
constexpr void assertNotBetterThan(const counted_set_intersection_result& other) { | ||||||
constexpr void assertNotBetterThan(const counted_set_intersection_result& other) const noexcept { | ||||||
assert(result == other.result); | ||||||
assert(opcounts.isNotBetterThan(other.opcounts)); | ||||||
ASSERT_COMPLEXITY(opcounts.isNotBetterThan(other.opcounts)); | ||||||
} | ||||||
}; | ||||||
|
||||||
template <std::size_t ResultSize> | ||||||
counted_set_intersection_result(std::array<int, ResultSize>) -> counted_set_intersection_result<ResultSize>; | ||||||
counted_set_intersection_result(std::array<int, ResultSize>) noexcept -> counted_set_intersection_result<ResultSize>; | ||||||
|
||||||
template <template <class...> class InIterType1, | ||||||
template <class...> | ||||||
|
@@ -306,7 +305,7 @@ constexpr bool testComplexityBasic() { | |||||
std::array<int, 5> r2{2, 4, 6, 8, 10}; | ||||||
std::array<int, 0> expected{}; | ||||||
|
||||||
const std::size_t maxOperation = std_less_comparison_count_multiplier() * (2 * (r1.size() + r2.size()) - 1); | ||||||
const std::size_t maxOperation = 2 * (r1.size() + r2.size()) - 1; | ||||||
|
||||||
// std::set_intersection | ||||||
{ | ||||||
|
@@ -321,7 +320,7 @@ constexpr bool testComplexityBasic() { | |||||
std::set_intersection(r1.begin(), r1.end(), r2.begin(), r2.end(), out.data(), comp); | ||||||
|
||||||
assert(std::ranges::equal(out, expected)); | ||||||
assert(numberOfComp <= maxOperation); | ||||||
ASSERT_COMPLEXITY(numberOfComp <= maxOperation); | ||||||
} | ||||||
|
||||||
// ranges::set_intersection iterator overload | ||||||
|
@@ -349,9 +348,9 @@ constexpr bool testComplexityBasic() { | |||||
std::ranges::set_intersection(r1.begin(), r1.end(), r2.begin(), r2.end(), out.data(), comp, proj1, proj2); | ||||||
|
||||||
assert(std::ranges::equal(out, expected)); | ||||||
assert(numberOfComp <= maxOperation); | ||||||
assert(numberOfProj1 <= maxOperation); | ||||||
assert(numberOfProj2 <= maxOperation); | ||||||
ASSERT_COMPLEXITY(numberOfComp <= maxOperation); | ||||||
ASSERT_COMPLEXITY(numberOfProj1 <= maxOperation); | ||||||
ASSERT_COMPLEXITY(numberOfProj2 <= maxOperation); | ||||||
} | ||||||
|
||||||
// ranges::set_intersection range overload | ||||||
|
@@ -379,9 +378,9 @@ constexpr bool testComplexityBasic() { | |||||
std::ranges::set_intersection(r1, r2, out.data(), comp, proj1, proj2); | ||||||
|
||||||
assert(std::ranges::equal(out, expected)); | ||||||
assert(numberOfComp < maxOperation); | ||||||
assert(numberOfProj1 < maxOperation); | ||||||
assert(numberOfProj2 < maxOperation); | ||||||
ASSERT_COMPLEXITY(numberOfComp < maxOperation); | ||||||
ASSERT_COMPLEXITY(numberOfProj1 < maxOperation); | ||||||
ASSERT_COMPLEXITY(numberOfProj2 < maxOperation); | ||||||
} | ||||||
return true; | ||||||
} | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a release note in 20.rst?