Implement sk_find manually.

glibc inlines bsearch, so CFI does observe the function pointer mishap.
Binary search is easy enough, aside from thinking through the edge case
at the end, so just implement it by hand. As a bonus, it actually gives
O(lg N) behavior.

sk_*_find needs to return the *first* match, while bsearch does not
promise a particular one. sk_find thus performs a fixup step to find the
first one, but this is linear in the number of matching elements.
Instead, the binary search should take this into account.

This still leaves qsort, but it's not inlined, so hopefully we can leave
it alone.

Bug: chromium:941463
Change-Id: I5c94d6b15423beea3bdb389639466f8b3ff0dc5d
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/35304
Reviewed-by: Adam Langley <agl@google.com>
This commit is contained in:
David Benjamin 2019-03-13 13:44:31 -05:00 committed by Adam Langley
parent 35941f2923
commit aadcce380f

View File

@ -56,6 +56,7 @@
#include <openssl/stack.h>
#include <assert.h>
#include <string.h>
#include <openssl/mem.h>
@ -272,36 +273,39 @@ int sk_find(const _STACK *sk, size_t *out_index, const void *p,
return 0;
}
// sk->comp is a function that takes pointers to pointers to elements, but
// qsort and bsearch take a comparison function that just takes pointers to
// elements. However, since we're passing an array of pointers to
// qsort/bsearch, we can just cast the comparison function and everything
// works.
// The stack is sorted, so binary search to find the element.
//
// TODO(davidben): This is undefined behavior, but the call is in libc so,
// e.g., CFI does not notice. Unfortunately, |bsearch| is missing a void*
// parameter in its callback and |bsearch_s| is a mess of incompatibility.
const void *const *r = bsearch(&p, sk->data, sk->num, sizeof(void *),
(int (*)(const void *, const void *))sk->comp);
if (r == NULL) {
return 0;
}
size_t idx = ((void **)r) - sk->data;
// This function always returns the first result. Note this logic is, in the
// worst case, O(N) rather than O(log(N)). If this ever becomes a problem,
// restore https://boringssl-review.googlesource.com/c/boringssl/+/32115/
// which integrates the preference into the binary search.
while (idx > 0) {
const void *elem = sk->data[idx - 1];
if (call_cmp_func(sk->comp, &p, &elem) != 0) {
break;
// |lo| and |hi| maintain a half-open interval of where the answer may be. All
// indices such that |lo <= idx < hi| are candidates.
size_t lo = 0, hi = sk->num;
while (lo < hi) {
// Bias |mid| towards |lo|. See the |r == 0| case below.
size_t mid = lo + (hi - lo - 1) / 2;
assert(lo <= mid && mid < hi);
const void *elem = sk->data[mid];
int r = call_cmp_func(sk->comp, &p, &elem);
if (r > 0) {
lo = mid + 1; // |mid| is too low.
} else if (r < 0) {
hi = mid; // |mid| is too high.
} else {
// |mid| matches. However, this function returns the earliest match, so we
// can only return if the range has size one.
if (hi - lo == 1) {
if (out_index != NULL) {
*out_index = mid;
}
return 1;
}
// The sample is biased towards |lo|. |mid| can only be |hi - 1| if
// |hi - lo| was one, so this makes forward progress.
assert(mid + 1 < hi);
hi = mid + 1;
}
idx--;
}
if (out_index) {
*out_index = idx;
}
return 1;
assert(lo == hi);
return 0; // Not found.
}
void *sk_shift(_STACK *sk) {
@ -362,7 +366,10 @@ void sk_sort(_STACK *sk) {
return;
}
// See the comment in sk_find about this cast.
// 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. Unfortunately, |qsort| is missing a void*