Skip to content

Commit 6707eac

Browse files
scsiguymeta-codesync[bot]
authored andcommitted
Add is_trained check to IndexIVF search and range_search to prevent querying untrained indexes (#5114)
Summary: Pull Request resolved: #5114 Add FAISS_THROW_IF_NOT(is_trained) to IndexIVF::search(), IndexIVF::search_preassigned(), IndexIVF::range_search(), and IndexIVF::range_search_preassigned(), mirroring the existing check in IndexScalarQuantizer::search(). This prevents querying untrained IVF indexes deserialized from corrupt data where the ScalarQuantizer trained vector is empty. The existing deserialization validation in read_ScalarQuantizer correctly allows untrained indexes (is_trained=false with empty trained) to be deserialized, since these are legitimately produced by index_factory before training. However, IndexIVF search methods lacked the is_trained guard that IndexScalarQuantizer::search() has, allowing a deserialized untrained IndexIVFScalarQuantizer to be queried, which causes null-deref in QuantizerTemplate when it indexes into the empty trained vector. Reviewed By: mnorris11 Differential Revision: D101243973 fbshipit-source-id: eca68dc82e5cca37d4c461b735c5d59a66349248
1 parent 349df70 commit 6707eac

2 files changed

Lines changed: 107 additions & 0 deletions

File tree

faiss/IndexIVF.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ void IndexIVF::search(
312312
const SearchParameters* params_in) const {
313313
FAISS_THROW_IF_NOT(k > 0);
314314
FAISS_THROW_IF_NOT_MSG(quantizer, "IVF quantizer must not be null");
315+
FAISS_THROW_IF_NOT_MSG(is_trained, "IVF index is not trained");
315316
FAISS_THROW_IF_NOT_MSG(invlists, "IVF index has no inverted lists");
316317
const IVFSearchParameters* params = nullptr;
317318
if (params_in) {
@@ -409,6 +410,7 @@ void IndexIVF::search_preassigned(
409410
const IVFSearchParameters* params,
410411
IndexIVFStats* ivf_stats) const {
411412
FAISS_THROW_IF_NOT(k > 0);
413+
FAISS_THROW_IF_NOT_MSG(is_trained, "IVF index is not trained");
412414
FAISS_THROW_IF_NOT_MSG(invlists, "IVF index has no inverted lists");
413415

414416
idx_t cur_nprobe = params ? params->nprobe : this->nprobe;
@@ -738,6 +740,7 @@ void IndexIVF::range_search(
738740
RangeSearchResult* result,
739741
const SearchParameters* params_in) const {
740742
FAISS_THROW_IF_NOT_MSG(quantizer, "IVF quantizer must not be null");
743+
FAISS_THROW_IF_NOT_MSG(is_trained, "IVF index is not trained");
741744
const IVFSearchParameters* params = nullptr;
742745
const SearchParameters* quantizer_params = nullptr;
743746
if (params_in) {
@@ -782,6 +785,7 @@ void IndexIVF::range_search_preassigned(
782785
bool store_pairs,
783786
const IVFSearchParameters* params,
784787
IndexIVFStats* stats) const {
788+
FAISS_THROW_IF_NOT_MSG(is_trained, "IVF index is not trained");
785789
idx_t cur_nprobe = params ? params->nprobe : this->nprobe;
786790
cur_nprobe = std::min((idx_t)nlist, cur_nprobe);
787791
FAISS_THROW_IF_NOT(cur_nprobe > 0);

tests/test_read_index_deserialize.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,109 @@ TEST(ReadIndexDeserialize, IVFQuantizerUntrained) {
16801680
EXPECT_NO_THROW(read_index_up(&reader));
16811681
}
16821682

1683+
// -----------------------------------------------------------------------
1684+
// Test: IndexIVFScalarQuantizer with empty trained vector and
1685+
// is_trained=false deserializes successfully (legitimate untrained index),
1686+
// but searching it throws because IndexIVF::search checks is_trained.
1687+
// -----------------------------------------------------------------------
1688+
TEST(ReadIndexDeserialize, IVFScalarQuantizerUntrainedSearchRejected) {
1689+
std::vector<uint8_t> buf;
1690+
push_fourcc(buf, "IwSq");
1691+
// IVF header: index_header + nlist + nprobe + quantizer + direct_map
1692+
push_index_header(buf, /*d=*/4, /*ntotal=*/0, /*is_trained=*/false);
1693+
push_val<size_t>(buf, 1); // nlist
1694+
push_val<size_t>(buf, 1); // nprobe
1695+
push_minimal_flat(buf, /*d=*/4);
1696+
push_empty_direct_map(buf);
1697+
// ScalarQuantizer fields:
1698+
push_val<int>(buf, 0); // qtype = QT_8bit
1699+
push_val<int>(buf, 0); // rangestat
1700+
push_val<float>(buf, 0.0f); // rangestat_arg
1701+
push_val<size_t>(buf, 4); // d
1702+
push_val<size_t>(buf, 4); // code_size
1703+
push_vector<float>(buf, {}); // trained (empty — untrained)
1704+
// IwSq additional fields:
1705+
push_val<size_t>(buf, 4); // code_size
1706+
push_val<bool>(buf, false); // by_residual
1707+
push_null_invlists(buf);
1708+
1709+
// Deserialization should succeed — untrained indexes are legitimate
1710+
VectorIOReader reader;
1711+
reader.data = buf;
1712+
auto idx = read_index_up(&reader);
1713+
ASSERT_NE(idx, nullptr);
1714+
EXPECT_FALSE(idx->is_trained);
1715+
1716+
// search should throw — is_trained check in IndexIVF::search
1717+
std::vector<float> xq(4, 0.0f);
1718+
std::vector<float> distances(1);
1719+
std::vector<idx_t> labels(1);
1720+
EXPECT_THROW(
1721+
idx->search(1, xq.data(), 1, distances.data(), labels.data()),
1722+
FaissException);
1723+
1724+
// range_search should throw — is_trained check in IndexIVF::range_search
1725+
RangeSearchResult rsr(1);
1726+
EXPECT_THROW(idx->range_search(1, xq.data(), 1.0f, &rsr), FaissException);
1727+
1728+
// search_preassigned should throw directly
1729+
auto* ivf = dynamic_cast<IndexIVF*>(idx.get());
1730+
ASSERT_NE(ivf, nullptr);
1731+
idx_t key = 0;
1732+
float coarse_dis = 0.0f;
1733+
EXPECT_THROW(
1734+
ivf->search_preassigned(
1735+
1,
1736+
xq.data(),
1737+
1,
1738+
&key,
1739+
&coarse_dis,
1740+
distances.data(),
1741+
labels.data(),
1742+
false,
1743+
nullptr,
1744+
nullptr),
1745+
FaissException);
1746+
1747+
// range_search_preassigned should throw directly
1748+
RangeSearchResult rsr2(1);
1749+
EXPECT_THROW(
1750+
ivf->range_search_preassigned(
1751+
1,
1752+
xq.data(),
1753+
1.0f,
1754+
&key,
1755+
&coarse_dis,
1756+
&rsr2,
1757+
false,
1758+
nullptr,
1759+
nullptr),
1760+
FaissException);
1761+
}
1762+
1763+
// -----------------------------------------------------------------------
1764+
// Test: IndexIVFScalarQuantizer with is_trained=true but empty trained
1765+
// is rejected at deserialization time — corrupt data.
1766+
// -----------------------------------------------------------------------
1767+
TEST(ReadIndexDeserialize, IVFScalarQuantizerTrainedEmptyTrained) {
1768+
std::vector<uint8_t> buf;
1769+
push_fourcc(buf, "IwSq");
1770+
push_index_header(buf, /*d=*/4, /*ntotal=*/0, /*is_trained=*/true);
1771+
push_val<size_t>(buf, 1); // nlist
1772+
push_val<size_t>(buf, 1); // nprobe
1773+
push_minimal_flat(buf, /*d=*/4);
1774+
push_empty_direct_map(buf);
1775+
// ScalarQuantizer fields:
1776+
push_val<int>(buf, 0); // qtype = QT_8bit
1777+
push_val<int>(buf, 0); // rangestat
1778+
push_val<float>(buf, 0.0f); // rangestat_arg
1779+
push_val<size_t>(buf, 4); // d
1780+
push_val<size_t>(buf, 4); // code_size
1781+
push_vector<float>(buf, {}); // trained (empty — but is_trained=true!)
1782+
1783+
expect_read_throws_with(buf, "ScalarQuantizer trained size");
1784+
}
1785+
16831786
// -----------------------------------------------------------------------
16841787
// Test: initialize_IVFPQ_precomputed_table rejects a null quantizer.
16851788
// Protects against null-deref from corrupt serialized data where the

0 commit comments

Comments
 (0)