Give up on qsort for sk_FOO_sort
OpenSSL's API constraints are such that sk_FOO_sort must take a
comparison function of type
int (*cmp)(const FOO *const *a, const FOO *const *b)
However qsort expects a comparison function of type
int (*cmp)(const void *a, const void *b)
In C, it is UB to cast a function pointer to a different type and call
it, even if the underlying calling conventions are the same. Moreover,
as qsort doesn't take a context parameter on its comparisons, we cannot
do the usual convention with closures in C.
qsort_r and qsort_s would avoid this, but they are unusable. Too many
libcs don't have them, and those that do define them inconsistently. See
https://stackoverflow.com/a/39561369
It seems this UB has finally hit a sanitizer in fxbug.dev/128274.
Irritating as it is to not even have a working sort function, I think
the easiest option is to just give up on qsort. As we did with bsearch
in https://boringssl-review.googlesource.com/c/boringssl/+/35304, just
implement an in-place heap sort ourselves.
Bug: fxbug.dev/128274
Change-Id: I9de6b4018bf635da0d0c5a680bd7811d297b0bb3
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/60507
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Bob Beck <bbe@google.com>
diff --git a/crypto/stack/stack.c b/crypto/stack/stack.c
index a11533e..6b4f8bc 100644
--- a/crypto/stack/stack.c
+++ b/crypto/stack/stack.c
@@ -393,20 +393,52 @@
return NULL;
}
-#if defined(_MSC_VER)
-struct sort_compare_ctx {
- OPENSSL_sk_call_cmp_func call_cmp_func;
- OPENSSL_sk_cmp_func cmp_func;
-};
-
-static int sort_compare(void *ctx_v, const void *a, const void *b) {
- struct sort_compare_ctx *ctx = ctx_v;
- // |a| and |b| point to |void*| pointers which contain the actual values.
- const void *const *a_ptr = a;
- const void *const *b_ptr = b;
- return ctx->call_cmp_func(ctx->cmp_func, *a_ptr, *b_ptr);
+static size_t parent_idx(size_t idx) {
+ assert(idx > 0);
+ return (idx - 1) / 2;
}
-#endif
+
+static size_t left_idx(size_t idx) {
+ // The largest possible index is |PTRDIFF_MAX|, not |SIZE_MAX|. If
+ // |ptrdiff_t|, a signed type, is the same size as |size_t|, this cannot
+ // overflow.
+ assert(idx <= PTRDIFF_MAX);
+ static_assert(PTRDIFF_MAX <= (SIZE_MAX - 1) / 2, "2 * idx + 1 may oveflow");
+ return 2 * idx + 1;
+}
+
+// down_heap fixes the subtree rooted at |i|. |i|'s children must each satisfy
+// the heap property. Only the first |num| elements of |sk| are considered.
+static void down_heap(OPENSSL_STACK *sk, OPENSSL_sk_call_cmp_func call_cmp_func,
+ size_t i, size_t num) {
+ assert(i < num && num <= sk->num);
+ for (;;) {
+ size_t left = left_idx(i);
+ if (left >= num) {
+ break; // No left child.
+ }
+
+ // Swap |i| with the largest of its children.
+ size_t next = i;
+ if (call_cmp_func(sk->comp, sk->data[next], sk->data[left]) < 0) {
+ next = left;
+ }
+ size_t right = left + 1; // Cannot overflow because |left < num|.
+ if (right < num &&
+ call_cmp_func(sk->comp, sk->data[next], sk->data[right]) < 0) {
+ next = right;
+ }
+
+ if (i == next) {
+ break; // |i| is already larger than its children.
+ }
+
+ void *tmp = sk->data[i];
+ sk->data[i] = sk->data[next];
+ sk->data[next] = tmp;
+ i = next;
+ }
+}
void OPENSSL_sk_sort(OPENSSL_STACK *sk,
OPENSSL_sk_call_cmp_func call_cmp_func) {
@@ -415,25 +447,25 @@
}
if (sk->num >= 2) {
-#if defined(_MSC_VER)
- // MSVC's |qsort_s| is different from the C11 one.
- // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/qsort-s?view=msvc-170
- struct sort_compare_ctx ctx = {call_cmp_func, sk->comp};
- qsort_s(sk->data, sk->num, sizeof(void *), sort_compare, &ctx);
-#else
- // sk->comp is a function that takes pointers to pointers to elements, but
- // qsort take a comparison function that just takes pointers to elements.
- // However, since we're passing an array of pointers to qsort, we can just
- // cast the comparison function and everything works.
- //
- // TODO(davidben): This is undefined behavior, but the call is in libc so,
- // e.g., CFI does not notice. |qsort| is missing a void* parameter in its
- // callback, while no one defines |qsort_r| or |qsort_s| consistently. See
+ // |qsort| lacks a context parameter in the comparison function for us to
+ // pass in |call_cmp_func| and |sk->comp|. While we could cast |sk->comp| to
+ // the expected type, it is undefined behavior in C can trip sanitizers.
+ // |qsort_r| and |qsort_s| avoid this, but using them is impractical. See
// https://stackoverflow.com/a/39561369
- int (*comp_func)(const void *, const void *) =
- (int (*)(const void *, const void *))(sk->comp);
- qsort(sk->data, sk->num, sizeof(void *), comp_func);
-#endif
+ //
+ // Use our own heap sort instead. This is not performance-sensitive, so we
+ // optimize for simplicity and size. First, build a max-heap in place.
+ for (size_t i = parent_idx(sk->num - 1); i < sk->num; i--) {
+ down_heap(sk, call_cmp_func, i, sk->num);
+ }
+
+ // Iteratively remove the maximum element to populate the result in reverse.
+ for (size_t i = sk->num - 1; i > 0; i--) {
+ void *tmp = sk->data[0];
+ sk->data[0] = sk->data[i];
+ sk->data[i] = tmp;
+ down_heap(sk, call_cmp_func, 0, i);
+ }
}
sk->sorted = 1;
}
diff --git a/crypto/stack/stack_test.cc b/crypto/stack/stack_test.cc
index 7900fc9..228182b 100644
--- a/crypto/stack/stack_test.cc
+++ b/crypto/stack/stack_test.cc
@@ -24,6 +24,7 @@
#include <gtest/gtest.h>
#include <openssl/mem.h>
+#include <openssl/rand.h>
// Define a custom stack type for testing.
@@ -481,3 +482,38 @@
sk_TEST_INT_set_cmp_func(sk.get(), nullptr);
EXPECT_FALSE(sk_TEST_INT_is_sorted(sk.get()));
}
+
+TEST(StackTest, Sort) {
+ constexpr size_t kMaxLength = 100;
+ constexpr int kIterations = 500;
+ for (size_t len = 0; len < kMaxLength; len++) {
+ SCOPED_TRACE(len);
+ for (int iter = 0; iter < kIterations; iter++) {
+ // Make a random input list.
+ std::vector<int> vec(len);
+ RAND_bytes(reinterpret_cast<uint8_t *>(vec.data()),
+ sizeof(int) * vec.size());
+ SCOPED_TRACE(testing::PrintToString(vec));
+
+ // Convert it to a |STACK_OF(TEST_INT)|.
+ bssl::UniquePtr<STACK_OF(TEST_INT)> sk(sk_TEST_INT_new(compare));
+ ASSERT_TRUE(sk);
+ for (int v : vec) {
+ auto value = TEST_INT_new(v);
+ ASSERT_TRUE(value);
+ ASSERT_TRUE(bssl::PushToStack(sk.get(), std::move(value)));
+ }
+
+ // Sort it with our sort implementation.
+ sk_TEST_INT_sort(sk.get());
+ std::vector<int> result;
+ for (const TEST_INT *v : sk.get()) {
+ result.push_back(*v);
+ }
+
+ // The result must match the STL's version.
+ std::sort(vec.begin(), vec.end());
+ EXPECT_EQ(vec, result);
+ }
+ }
+}