Skip to content

Commit bef51bf

Browse files
committed
Protect against integer overflow with sanisizer.
In particular, the calculation of search_k needs some care because it is computed from a floating-point multiplication. We don't throw an error if the product overflows but instead we just cap it at the int's max.
1 parent 8520d75 commit bef51bf

5 files changed

Lines changed: 81 additions & 17 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ if(KNNCOLLE_ANNOY_FETCH_EXTERN)
2323
add_subdirectory(extern)
2424
else()
2525
find_package(knncolle_knncolle 3.0.0 CONFIG REQUIRED)
26+
find_package(ltla_sanisizer 0.1.0 CONFIG REQUIRED)
2627
find_package(Annoy CONFIG REQUIRED)
2728
endif()
2829

29-
target_link_libraries(knncolle_annoy INTERFACE knncolle::knncolle Annoy::Annoy)
30+
target_link_libraries(knncolle_annoy INTERFACE knncolle::knncolle ltla::sanisizer Annoy::Annoy)
3031

3132
# Tests
3233
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)

cmake/Config.cmake.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
include(CMakeFindDependencyMacro)
44
find_dependency(knncolle_knncolle 3.0.0 CONFIG REQUIRED)
5+
find_dependency(ltla_sanisizer 0.1.0 CONFIG REQUIRED)
56
find_dependency(Annoy CONFIG REQUIRED)
67

78
include("${CMAKE_CURRENT_LIST_DIR}/knncolle_knncolle_annoyTargets.cmake")

extern/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ FetchContent_Declare(
66
GIT_TAG master # ^3.0.0
77
)
88

9+
FetchContent_Declare(
10+
sanisizer
11+
GIT_REPOSITORY https://github.com/LTLA/sanisizer
12+
GIT_TAG master # ^0.1.0
13+
)
14+
15+
916
FetchContent_Declare(
1017
Annoy
1118
GIT_REPOSITORY https://github.com/spotify/Annoy

include/knncolle_annoy/Annoy.hpp

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <cstddef>
88
#include <cstring>
99
#include <stdexcept>
10+
#include <limits>
1011

1112
#include "knncolle/knncolle.hpp"
1213
#include "annoy/annoylib.h"
@@ -68,18 +69,28 @@ class AnnoySearcher final : public knncolle::Searcher<Index_, Data_, Distance_>
6869
static constexpr bool same_internal_distance = std::is_same<Distance_, AnnoyData_>::value;
6970
typename std::conditional<!same_internal_distance, std::vector<AnnoyData_>, bool>::type my_distances;
7071

71-
int get_search_k(int k) const {
72+
typedef int SearchKType;
73+
74+
Index_ my_capped_k = 0;
75+
76+
SearchKType get_search_k(Index_ k) const {
7277
if (my_parent.my_search_mult < 1) {
7378
return -1; // instructs Annoy to use k * num_trees.
74-
} else {
79+
} else if (k <= my_capped_k) {
7580
return my_parent.my_search_mult * static_cast<double>(k) + 0.5; // rounded.
81+
} else {
82+
return std::numeric_limits<SearchKType>::max(); // cap to avoid overflow.
7683
}
7784
}
7885

7986
public:
8087
AnnoySearcher(const AnnoyPrebuilt<Index_, Data_, Distance_, AnnoyDistance_, AnnoyIndex_, AnnoyData_, AnnoyRng_, AnnoyThreadPolicy_>& parent) : my_parent(parent) {
8188
if constexpr(!same_internal_data) {
82-
my_buffer.resize(my_parent.my_dim);
89+
sanisizer::resize(my_buffer, my_parent.my_dim);
90+
}
91+
92+
if (my_parent.my_search_mult >= 1) {
93+
my_capped_k = static_cast<double>(std::numeric_limits<SearchKType>::max()) / my_parent.my_search_mult;
8394
}
8495
}
8596

@@ -139,18 +150,26 @@ class AnnoySearcher final : public knncolle::Searcher<Index_, Data_, Distance_>
139150

140151
public:
141152
void search(Index_ i, Index_ k, std::vector<Index_>* output_indices, std::vector<Distance_>* output_distances) {
142-
Index_ kp1 = k + 1; // +1, as it forgets to discard 'self'.
153+
// +1, as it forgets to discard 'self'. This should not overflow as k < num_obs.
154+
const auto kp1 = k + 1;
155+
143156
auto ptrs = obtain_pointers(output_indices, output_distances, kp1);
144157
auto icopy_ptr = ptrs.first;
145158
auto dcopy_ptr = ptrs.second;
146159

147-
my_parent.my_index.get_nns_by_item(i, kp1, get_search_k(kp1), icopy_ptr, dcopy_ptr);
160+
my_parent.my_index.get_nns_by_item(
161+
i,
162+
sanisizer::cast<std::size_t>(sanisizer::attest_gez(kp1)),
163+
get_search_k(kp1),
164+
icopy_ptr,
165+
dcopy_ptr
166+
);
148167

149168
std::size_t at;
150169
{
151170
const auto& cur_i = *icopy_ptr;
152171
at = cur_i.size();
153-
AnnoyIndex_ icopy = i;
172+
const AnnoyIndex_ icopy = i; // cast to AnnoyIndex_ to avoid signed/unsigned comparisons.
154173
for (std::size_t x = 0, end = cur_i.size(); x < end; ++x) {
155174
if (cur_i[x] == icopy) {
156175
at = x;
@@ -182,7 +201,13 @@ class AnnoySearcher final : public knncolle::Searcher<Index_, Data_, Distance_>
182201
auto icopy_ptr = ptrs.first;
183202
auto dcopy_ptr = ptrs.second;
184203

185-
my_parent.my_index.get_nns_by_vector(query, k, get_search_k(k), icopy_ptr, dcopy_ptr);
204+
my_parent.my_index.get_nns_by_vector(
205+
query,
206+
sanisizer::cast<std::size_t>(sanisizer::attest_gez(k)),
207+
get_search_k(k),
208+
icopy_ptr,
209+
dcopy_ptr
210+
);
186211

187212
if (output_indices) {
188213
if constexpr(!same_internal_index) {
@@ -229,14 +254,17 @@ class AnnoyPrebuilt final : public knncolle::Prebuilt<Index_, Data_, Distance_>
229254
my_search_mult(options.search_mult),
230255
my_index(my_dim)
231256
{
257+
// check that we can, in fact, represent the number of observations in the specified AnnoyIndex_ type.
258+
sanisizer::cast<AnnoyIndex_>(sanisizer::attest_gez(my_obs));
259+
232260
auto work = data.new_known_extractor();
233261
if constexpr(std::is_same<Data_, AnnoyData_>::value) {
234262
for (Index_ i = 0; i < my_obs; ++i) {
235263
auto ptr = work->next();
236264
my_index.add_item(i, ptr);
237265
}
238266
} else {
239-
std::vector<AnnoyData_> incoming(my_dim);
267+
auto incoming = sanisizer::create<std::vector<AnnoyData_> >(my_dim);
240268
for (Index_ i = 0; i < my_obs; ++i) {
241269
auto ptr = work->next();
242270
std::copy_n(ptr, my_dim, incoming.begin());
@@ -310,8 +338,11 @@ class AnnoyPrebuilt final : public knncolle::Prebuilt<Index_, Data_, Distance_>
310338

311339
// Not bothering to save anything else; the RNG and thread policy
312340
// should not be relevant once the index is built.
341+
342+
// For reasons unknown to us, AnnoyIndex::save() is not const, so we have to do it manually.
343+
// Hopefully this will be fixed in the future.
313344
const auto idxpath = prefix + "index";
314-
knncolle::quick_save(idxpath, reinterpret_cast<char*>(my_index.get_nodes()), static_cast<std::size_t>(my_index.get_s()) * my_index.get_n_nodes());
345+
knncolle::quick_save(idxpath, reinterpret_cast<char*>(my_index.get_nodes()), sanisizer::product<std::streamsize>(my_index.get_s(), my_index.get_n_nodes()));
315346
}
316347

317348
AnnoyPrebuilt(const std::string prefix, std::size_t ndim) : my_dim(ndim), my_index(ndim) {

tests/src/Annoy.cpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,40 @@ class AnnoyMiscTest : public TestCore, public ::testing::Test {
248248
TEST_F(AnnoyMiscTest, SearchMult) {
249249
knncolle_annoy::AnnoyBuilder<int, double, double, Annoy::Euclidean> ab;
250250
auto& an_opt = ab.get_options();
251-
an_opt.search_mult = 20;
252251

253252
knncolle::SimpleMatrix<int, double> mat(ndim, nobs, data.data());
254-
auto ptr = ab.build_known_shared(mat); // test coverage for the known override.
255-
auto sptr = ptr->initialize();
253+
auto default_ptr = ab.build_known_shared(mat); // test coverage for the known override.
254+
255+
// Manually setting the search multiplier and checking we get the same result.
256+
{
257+
an_opt.search_mult = an_opt.num_trees;
258+
auto manual_ptr = ab.build_known_shared(mat);
259+
auto dptr = default_ptr->initialize();
260+
auto mptr = manual_ptr->initialize();
261+
262+
std::vector<int> output_i, output_i2;
263+
std::vector<double> output_d, output_d2;
264+
for (int x = 0; x < nobs; ++x) {
265+
dptr->search(x, 5, &output_i, &output_d);
266+
mptr->search(x, 5, &output_i2, &output_d2);
267+
EXPECT_EQ(output_i, output_i2);
268+
EXPECT_EQ(output_d, output_d2);
269+
}
270+
}
256271

257-
std::vector<int> ires;
258-
std::vector<double> dres;
259-
sptr->search(0, 10, &ires, &dres);
260-
sanity_checks(ires, dres, 10, 0);
272+
// Using an obscenely large multiplier.
273+
{
274+
an_opt.search_mult = std::numeric_limits<double>::max();
275+
auto super_ptr = ab.build_known_shared(mat);
276+
auto sptr = super_ptr->initialize();
277+
278+
std::vector<int> output_i;
279+
std::vector<double> output_d;
280+
for (int x = 0; x < nobs; ++x) {
281+
sptr->search(x, 5, &output_i, &output_d);
282+
sanity_checks(output_i, output_d, 5, x);
283+
}
284+
}
261285
}
262286

263287
TEST(Annoy, Duplicates) {

0 commit comments

Comments
 (0)