Skip to content

Commit 64d10d8

Browse files
Fix: Bound file-controlled sizes in view() (#776)
index_gt::view walked the memory-mapped levels[] array (sized from the file's header.size) before the only file-size check, and index_dense::view formed a matrix_rows*matrix_cols span into the mapping with no bound plus an unsigned-underflow header guard. A crafted .usearch index over-read the mapping. Bound the levels region before the offset-precompute loop, bound the vectors span with a checked multiply, and make the header guard underflow-safe. Co-authored-by: Nathan K <professor-moody@users.noreply.github.com> Co-authored-by: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>
1 parent 810c172 commit 64d10d8

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

cpp/test.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,67 @@ void test_load_after_metric_make() {
14281428
std::remove(path);
14291429
}
14301430

1431+
/**
1432+
* @brief Feeds `view` a truncated index file.
1433+
*
1434+
* The matrix dimensions and the node count come from the file itself, so a short file
1435+
* describes a matrix and a level array that reach past the mapping. Both must be rejected
1436+
* rather than walked, and a caller-supplied offset past the end must fail the same way.
1437+
*/
1438+
void test_view_of_truncated_file() {
1439+
std::printf("Testing view of a truncated index file\n");
1440+
1441+
using index_t = index_dense_gt<std::int64_t, std::uint32_t>;
1442+
std::size_t const dimensions = 64;
1443+
std::size_t const collection = 2048;
1444+
char const* path = "tmp_truncated.usearch";
1445+
1446+
metric_punned_t metric(dimensions, metric_kind_t::l2sq_k, scalar_kind<f32_t>());
1447+
index_t::state_result_t built = index_t::make(metric);
1448+
expect(built);
1449+
expect(built.index.try_reserve(collection));
1450+
std::vector<float> vector(dimensions);
1451+
for (std::size_t i = 0; i != collection; ++i) {
1452+
for (std::size_t d = 0; d != dimensions; ++d)
1453+
vector[d] = static_cast<float>((i * 31 + d) % 997) / 997.f;
1454+
expect(built.index.add(static_cast<std::int64_t>(i), vector.data()));
1455+
}
1456+
expect(built.index.save(path));
1457+
1458+
std::vector<char> prefix;
1459+
{
1460+
std::FILE* file = std::fopen(path, "rb");
1461+
expect(file != nullptr);
1462+
std::fseek(file, 0, SEEK_END);
1463+
long const bytes = std::ftell(file);
1464+
std::fseek(file, 0, SEEK_SET);
1465+
prefix.resize(static_cast<std::size_t>(bytes) / 8);
1466+
expect(std::fread(prefix.data(), 1, prefix.size(), file) == prefix.size());
1467+
std::fclose(file);
1468+
}
1469+
{
1470+
std::FILE* file = std::fopen(path, "wb");
1471+
expect(file != nullptr);
1472+
std::fwrite(prefix.data(), 1, prefix.size(), file);
1473+
std::fclose(file);
1474+
}
1475+
1476+
index_t::state_result_t viewed = index_t::make(metric);
1477+
expect(viewed);
1478+
serialization_result_t truncated = viewed.index.view(path);
1479+
expect(!truncated);
1480+
truncated.error.release();
1481+
1482+
// A caller-supplied offset past the end must fail the same way.
1483+
index_t::state_result_t offset_viewed = index_t::make(metric);
1484+
expect(offset_viewed);
1485+
serialization_result_t beyond = offset_viewed.index.view(memory_mapped_file_t(path), prefix.size() * 4);
1486+
expect(!beyond);
1487+
beyond.error.release();
1488+
1489+
std::remove(path);
1490+
}
1491+
14311492
int main(int, char**) {
14321493
install_crash_handlers();
14331494

@@ -1524,5 +1585,6 @@ int main(int, char**) {
15241585
test_filtered_search();
15251586
test_isolate();
15261587
test_load_after_metric_make();
1588+
test_view_of_truncated_file();
15271589
return 0;
15281590
}

include/usearch/index.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3970,6 +3970,10 @@ class index_gt {
39703970
: checked_size_overflow();
39713971
if (!first_offset)
39723972
return result.failed("Index is too large");
3973+
if (file.size() < first_offset.value) {
3974+
reset();
3975+
return result.failed("File is corrupted and can't fit the node levels");
3976+
}
39733977
offsets[0u] = first_offset.value;
39743978
for (std::size_t i = 1; i < header_size.value; ++i) {
39753979
checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));

include/usearch/index_dense.hpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,6 +1358,11 @@ class index_dense_gt {
13581358
if (!result)
13591359
return result;
13601360

1361+
// `offset` is caller-supplied, so every `file.size() - offset` below would
1362+
// otherwise underflow into a huge span instead of failing.
1363+
if (offset > file.size())
1364+
return result.failed("File is corrupted and lacks matrix dimensions");
1365+
13611366
// Infer the new index size
13621367
std::uint64_t matrix_rows = 0;
13631368
std::uint64_t matrix_cols = 0;
@@ -1383,7 +1388,12 @@ class index_dense_gt {
13831388
matrix_cols = dimensions[1];
13841389
offset += sizeof(dimensions);
13851390
}
1386-
vectors_buffer = {file.data() + offset, static_cast<std::size_t>(matrix_rows * matrix_cols)};
1391+
// bound the vectors matrix span against the file (overflow-safe; offset <= file.size() holds here)
1392+
checked_size_result_t vectors_bytes =
1393+
checked_mul(static_cast<std::size_t>(matrix_rows), static_cast<std::size_t>(matrix_cols));
1394+
if (!vectors_bytes || file.size() - offset < vectors_bytes.value)
1395+
return result.failed("File is corrupted: vectors matrix exceeds file size");
1396+
vectors_buffer = {file.data() + offset, vectors_bytes.value};
13871397
offset += vectors_buffer.size();
13881398
}
13891399

0 commit comments

Comments
 (0)