fix(api): reject empty multi-vector in flattened vectors_count path (#9308)

`validate_multi_vector_len(N, &[])` with N > 0 previously returned Ok:
it passes the `vectors_count != 0` check, an empty `flatten_dense_vector`
clears the size check, and `0.is_multiple_of(N)` is true, so it falls into
the Ok branch. The unvalidated value then reaches
`convert_to_plain_multi_vector`, where `dim = data.len() / vectors_count = 0`,
the divisibility check `dim * vectors_count != data.len()` (0 == 0) passes,
and `data.into_iter().chunks(0)` panics (itertools asserts the chunk size is
non-zero). This is reachable from a single malformed gRPC Upsert via the
deprecated `vectors_count` field.

Add an `is_empty()` guard to `validate_multi_vector_len`, mirroring the
sibling `validate_multi_vector_by_length`, so empty flattened data is
rejected with a clear validation error before any conversion. Also add a
defensive `vectors_count == 0 || data.is_empty()` guard at the top of
`convert_to_plain_multi_vector`, which aligns it with the already-guarded
`MultiDenseVectorInternal::try_from_flatten` and closes the same panic on the
internal node-to-node `sync` path (where validation is log-only and
`SyncPoints.points` is not validated).

This completes the empty-vector hardening started in #9070, which covered the
REST side only and did not touch the gRPC flattened `vectors_count` form.

Includes a regression test covering both the rejected (empty) and accepted
(consistent multivector) cases.
This commit is contained in:
Marcelo Machuca
2026-06-04 04:28:18 -04:00
committed by GitHub
parent 3ce151632a
commit 4d00a5e628
2 changed files with 25 additions and 0 deletions

View File

@@ -15,6 +15,12 @@ fn convert_to_plain_multi_vector(
data: Vec<f32>,
vectors_count: usize,
) -> Result<MultiDenseVector, OperationError> {
if vectors_count == 0 || data.is_empty() {
return Err(OperationError::validation_error(format!(
"Empty multi-vector data with vectors count: {vectors_count}"
)));
}
let dim = data.len() / vectors_count;
if dim * vectors_count != data.len() {
return Err(OperationError::validation_error(format!(

View File

@@ -302,6 +302,14 @@ pub fn validate_multi_vector_len(
return Err(errors);
}
if flatten_dense_vector.is_empty() {
let mut errors = ValidationErrors::default();
let mut err = ValidationError::new("empty_multi_vector");
err.add_param(Cow::from("message"), &"multi vector must not be empty");
errors.add("data", err);
return Err(errors);
}
let dense_vector_len = flatten_dense_vector.len();
if dense_vector_len >= MAX_MULTIVECTOR_FLATTENED_LEN {
let mut errors = ValidationErrors::default();
@@ -328,6 +336,17 @@ pub fn validate_multi_vector_len(
mod tests {
use super::*;
#[test]
fn test_validate_multi_vector_len_rejects_empty_data() {
// Regression: empty flattened data with a positive vectors_count must be
// rejected. Previously this returned Ok (0.is_multiple_of(N) == true), and
// the value then reached convert_to_plain_multi_vector, which builds
// chunks(dim) with dim == 0 and panics on the gRPC upsert path.
assert!(validate_multi_vector_len(2, &[]).is_err());
// A non-empty, consistent multivector still validates.
assert!(validate_multi_vector_len(2, &[1.0, 2.0, 3.0, 4.0]).is_ok());
}
#[test]
fn test_validate_range_generic() {
assert!(validate_range_generic(u64::MIN, None, None).is_ok());