Skip to content

Bug: Integer underflow in Index::size() during concurrent add/remove.Β #697

@knowack1

Description

@knowack1

Describe the bug

When performing concurrent add and remove operations, the Index::size() method can return an extremely large value (e.g., 18446744073709551614).
This occurs because the underlying C++ implementation calculates size as: return typed_->size() - free_keys_.size();.
Under high concurrency, it is possible for the free_keys_ size to be larger than the typed_ size, causing a std::size_t underflow.

Debugging reveals the following state during the crash:

  • typed_->size(): 88
  • free_keys_.size(): 91
  • Result: 88 - 91 = -3 -> 18446744073709551613 (approx).

Steps to reproduce

This issue was reproduced using the Rust bindings with the tokio multi-threaded runtime. Note that even when using index.read() (assuming internal thread-safety of usearch), the inconsistency between the two internal counters is exposed.

#[tokio::test(flavor = "multi_thread")]
async fn test_concurrent_usearch_add_remove_size() {
    let options = IndexOptions {
        dimensions: 3,
        ..Default::default()
    };

    let index = Arc::new(RwLock::new(
        usearch::Index::new(&options).expect("failed to create index"),
    ));

    const VECTORS: usize = 50;
    let threads = tokio::runtime::Handle::current().metrics().num_workers();

    index.write().unwrap().reserve(VECTORS * 10).expect("failed to reserve");

    let mut handles = vec![];

    for _ in 0..threads {
        let idx = Arc::clone(&index);
        // Task 1: Concurrent Adds
        handles.push(tokio::spawn(async move {
            for i in 0..VECTORS {
                let _ = idx.read().unwrap().add(i as u64, &[i as f32; 3]);
            }
        }));

        // Task 2: Concurrent Removes
        let idx = Arc::clone(&index);
        handles.push(tokio::spawn(async move {
            for i in 0..VECTORS {
                let _ = idx.read().unwrap().remove(i as u64);
            }
        }));

        // Task 3: Monitor Size
        let idx = Arc::clone(&index);
        handles.push(tokio::spawn(async move {
            for _ in 0..VECTORS * 10 {
                let size = idx.read().unwrap().size();
                assert!(size <= VECTORS * threads, "Underflow detected! Size: {}", size);
                tokio::task::yield_now().await;
            }
        }));
    }

    for handle in handles {
        handle.await.expect("Task failed");
    }
}

Expected behavior

Index::size() to always return valid number of vectors inside the index.

USearch version

2.23.0

Operating System

Fedora release 42 (Adams)

Hardware architecture

x86

Which interface are you using?

Other bindings

Contact Details

No response

Are you open to being tagged as a contributor?

  • I am open to being mentioned in the project .git history as a contributor

Is there an existing issue for this?

  • I have searched the existing issues

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv3Breaking changes planned for v3

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions