When querying an Annoy index using the Python API, if a negative integer (e.g. n = -1) is passed as the n argument to get_nns_by_item or get_nns_by_vector, the function returns completely incorrect results or empty lists [] rather than throwing a Python exception.
Reproduction
from annoy import AnnoyIndex
t = AnnoyIndex(10, 'angular')
for i in range(10):
v = [1 if j == i else 0 for j in range(10)]
t.add_item(i, v)
t.build(10)
# Expected: [0], but gets completely unrelated item or empty list
print(t.get_nns_by_item(0, -1))
print(t.get_nns_by_item(0, -2))
Root Cause
In annoymodule.cc, n is parsed as a signed int32_t:
int32_t item, n, search_k=-1, include_distances=0;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|ii", (char**)kwlist, &item, &n, &search_k, &include_distances))
However, in annoylib.h, the corresponding argument is an unsigned size_t n:
void get_nns_by_item(S item, size_t n, int search_k, vector<S>* result, vector<T>* distances) const
When n = -1 is passed, it is sign-extended to SIZE_MAX (e.g., 18446744073709551615).
This leads to cascading issues in _get_all_nns:
search_k Overflow: search_k = n * _roots.size(); overflows. Because search_k is int, it silently wraps around to a negative number.
- Unbounded Search Condition:
while (nns.size() < (size_t)search_k && !q.empty()) converts the negative search_k back to SIZE_MAX, causing the search to attempt exploring the entire tree unbounded.
- Garbage Results:
size_t p = n < m ? n : m; evaluates to m (because SIZE_MAX < m is false), causing all explored items to be returned. Due to unbounded exploration boundaries, this often yields completely random incorrect items or [].
Suggested Fix
Add a parameter constraint check in annoymodule.cc to ensure n >= 0 before making calls to the C++ core:
if (n < 0) {
PyErr_SetString(PyExc_ValueError, "n must be a non-negative integer");
return NULL;
}
When querying an Annoy index using the Python API, if a negative integer (e.g.
n = -1) is passed as thenargument toget_nns_by_itemorget_nns_by_vector, the function returns completely incorrect results or empty lists[]rather than throwing a Python exception.Reproduction
Root Cause
In
annoymodule.cc,nis parsed as a signedint32_t:However, in
annoylib.h, the corresponding argument is an unsignedsize_t n:When
n = -1is passed, it is sign-extended toSIZE_MAX(e.g.,18446744073709551615).This leads to cascading issues in
_get_all_nns:search_kOverflow:search_k = n * _roots.size();overflows. Becausesearch_kisint, it silently wraps around to a negative number.while (nns.size() < (size_t)search_k && !q.empty())converts the negativesearch_kback toSIZE_MAX, causing the search to attempt exploring the entire tree unbounded.size_t p = n < m ? n : m;evaluates tom(becauseSIZE_MAX < mis false), causing all explored items to be returned. Due to unbounded exploration boundaries, this often yields completely random incorrect items or[].Suggested Fix
Add a parameter constraint check in
annoymodule.ccto ensuren >= 0before making calls to the C++ core: