Reject . and .. as vector names (#11290)

Since #11275 segments refuse to load a vector named `..`, but the API
still accepted it. `PUT /collections/{c}/vectors/%2E%2E` stored `..` in
the collection config while every shard declined the operation. After
that, snapshots and optimizations of the collection failed, and on
restart the node panicked loading the shard.

Reject both dot segments in `validate_vector_name`, so the API and the
segment check agree.

Claude-Session: https://claude.ai/code/session_012FyoTtcVeNR7DjV6nuWF3f

Co-authored-by: generall <andrey@qdrant.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f402454e2c)
This commit is contained in:
Andrey Vasnetsov authored and generall committed 2026-10-21 18:58:26 +02:00
1 parent 1d4bcbb88a
commit 523f5ab86c
2 files changed
+87 -3

No files matched your search

+53 -3
View File
@@ -160,8 +160,12 @@ pub fn validate_collection_name(value: &str) -> Result<(), ValidationError> {
///
/// Vector names become directory components on disk (see
/// `segment_constructor::get_vector_storage_path`), so they are subject to the same
/// rules as collection names: at most 200 bytes, and free of the
/// filesystem-unsafe characters listed in [`INVALID_NAME_CHARS`].
/// rules as collection names: at most 200 bytes, free of the
/// filesystem-unsafe characters listed in [`INVALID_NAME_CHARS`], and not a dot
/// segment (`.`, `..`). Segments refuse to load a vector named `..` (see
/// `segment_constructor::check_vector_name_path`), so the API must not create one.
///
/// Unlike a collection name, a vector name may be empty: that is the unnamed vector.
pub fn validate_vector_name(value: &str) -> Result<(), ValidationError> {
const MAX_LEN: usize = 200;
@@ -174,7 +178,11 @@ pub fn validate_vector_name(value: &str) -> Result<(), ValidationError> {
return Err(err);
}
check_invalid_name_chars(value, "vector name")
check_invalid_name_chars(value, "vector name")?;
if value.is_empty() {
return Ok(());
}
check_plain_dir_name(value, "vector name")
}
/// Validate the collection name contains no illegal characters, legacy edition
@@ -525,6 +533,48 @@ mod tests {
}
}
/// The API must reject every vector name that segment loading rejects
/// (`segment_constructor::check_vector_name_path`). Otherwise the name is stored in the
/// collection config while the segments refuse it, and the shard fails to load.
#[test]
fn test_validate_vector_name() {
let too_long = "v".repeat(201);
for name in [
"..",
".",
"x/../../escaped",
"../escaped",
"a/b",
"/abs",
"a\\b",
"a\0b",
too_long.as_str(),
] {
assert!(
validate_vector_name(name).is_err(),
"vector name {name:?} must be rejected",
);
}
// The empty name is the unnamed vector. Names merely containing dots are plain
// directory names.
let longest = "v".repeat(200);
for name in [
"",
"image",
"v1.2",
".hidden",
"..dots",
"...",
longest.as_str(),
] {
assert!(
validate_vector_name(name).is_ok(),
"vector name {name:?} must stay valid",
);
}
}
#[test]
fn test_validate_geo_polygon() {
let bad_polygon: Vec<(f64, f64)> = vec![];
+34
View File
@@ -1,6 +1,9 @@
import http.client
import json
import pytest
import time
import requests
from urllib.parse import urlparse
from .helpers.helpers import qdrant_host_headers, request_with_validation
from .helpers.settings import QDRANT_HOST
@@ -136,6 +139,37 @@ def test_create_vector_rejects_oversize(collection_name):
assert "size" in response.text
@pytest.mark.parametrize("encoded_name", ["%2E", "%2E%2E"])
def test_create_vector_rejects_dot_segment_name(collection_name, encoded_name):
"""`.` and `..` must be rejected at the API boundary.
Segments refuse to load a vector named `..`. Accepting it stored the name in the collection
config while every shard declined the operation, and the node then failed to load the shard
on restart.
"""
# `requests` unescapes `%2E` and drops dot segments, so send the path as is.
parsed = urlparse(QDRANT_HOST)
conn = http.client.HTTPConnection(parsed.hostname, parsed.port)
conn.request(
"PUT",
f"/collections/{collection_name}/vectors/{encoded_name}?wait=true",
body=json.dumps({"dense": {"size": VECTOR_SIZE1, "distance": "Cosine"}}),
headers={**qdrant_host_headers(), "Content-Type": "application/json"},
)
response = conn.getresponse()
text = response.read().decode()
assert response.status == 422, text
response = request_with_validation(
api='/collections/{collection_name}',
method="GET",
path_params={'collection_name': collection_name},
)
assert response.ok
vectors = response.json()["result"]["config"]["params"]["vectors"]
assert "." not in vectors and ".." not in vectors, vectors
def test_delete_recreate_indexed_vector_scroll():
"""
Delete and recreate a named vector on indexed segments (HNSW built),