mirror of
https://github.com/qdrant/qdrant.git
synced 2026-10-28 08:35:54 +08:00
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:
1 parent
1d4bcbb88a
commit
523f5ab86c
2 files changed
+87
-3
No files matched your search
@@ -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![];
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user