Skip to content

Fix: Bound entry slot and max level from the file header - #824

Merged
ashvardanian merged 1 commit into
unum-cloud:main-devfrom
peterkc:fix/header-entry-slot-bounds
Oct 24, 2026
Merged

ashvardanian merged 1 commit into
unum-cloud:main-devfrom
peterkc:fix/header-entry-slot-bounds

Conversation

@peterkc

@peterkc peterkc commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Summary

index_gt loads entry_slot and max_level from the serialized header without checking them, in both the stream load() and view(). A corrupted or crafted .usearch file of the correct size opens without an error, and the first search reads out of bounds.

For a non-empty file, both paths now return an error:

  • entry_slot >= size: "File is corrupted: entry slot exceeds node count". Search starts at nodes_[entry_slot], past the node array.
  • max_level above the entry node's level: "File is corrupted: max level exceeds entry node level". search_for_one_ reads neighbors_non_base_(entry, level) for each level from max_level down to 1, past the entry node.
  • view() calls reset() before it fails, as Bound file-controlled sizes in view(): levels region and dense vectors span (OOB read on crafted index) #776 does. The empty-index early return is unchanged, because an empty save writes max_level = -1.
Diagrams
Before: header fields are trusted
  header.entry_slot  --> entry_slot_ = 100000   (size = 64)
  first search       --> nodes_[entry_slot_]    <-- past the node array
  header.max_level   --> neighbors_non_base_(entry, max_level..1)
                                                 <-- past the entry node

After: load() and view() check both fields before use
  entry_slot >= size                 --> "entry slot exceeds node count"
  max_level  >  levels[entry_slot]   --> "max level exceeds entry node level"

This follows #776, which bounded the node count and the dense matrix span in view(). As in that PR, the same checks appear in both paths.

Impact

Loading or viewing a corrupted or crafted index file crashes on the first search (SIGSEGV) or reads past the node array or the entry node without a crash. This applies to every binding that calls load() or view(). CWE-125. We found it while testing corrupted index files through the Go binding.

Validation

  • New test_corrupted_graph_header in cpp/test.cpp. It saves a 256-vector index, sets entry_slot = size, then max_level + 1, and expects load() and view() to fail cleanly for each.
  • Before (v2.26.5): test_cpp aborts because load() accepts the file. A bad entry_slot crashes both paths (SIGSEGV; ASan: heap-buffer-overflow). A bad max_level crashes view() and makes load() read out of bounds without a crash.
  • After: every probe returns an error, and the original Go sample is rejected.
  • test_cpp passes in the CMake Debug build with ASan and UBSan, with no sanitizer reports.
  • Not run: the Release build, the C tests and the language bindings.
  • Not covered: neighbor slot IDs inside node records. view() never reads node bytes, so checking them would add a full pass over the file. A load()-only check could follow.

@peterkc
peterkc marked this pull request as ready for review October 23, 2026 15:41
`load` and `view` took the graph header's `entry_slot` and `max_level`
without checks. A correctly sized file with a corrupted header was
accepted, and the first search read past the stored nodes and crashed.

Both paths now reject a non-empty file when `entry_slot` is not below
the node count, or when `max_level` exceeds the entry node's level.
`view` resets before failing, as for the other size checks. The test of
truncated files now also corrupts each field and expects `load` and
`view` to fail cleanly.
@ashvardanian
ashvardanian force-pushed the fix/header-entry-slot-bounds branch from 0995ef6 to cd4ef3e Compare October 24, 2026 21:21
@ashvardanian
ashvardanian merged commit 9c25176 into unum-cloud:main-dev Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants