From 3278a8b9cc7c8b0b4e965821fabba43947ea4e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= <83272398+PoyrazK@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:45:43 +0300 Subject: [PATCH 1/2] fix(index): add MAX_SEARCH_SIZE and MAX_SEARCH_OFFSET bounds Clamp size and from in search() to prevent OOM from unbounded result allocation. Add validation in validate_search_request() to fail fast on requests that exceed limits. MAX_SEARCH_SIZE = 10,000 MAX_SEARCH_OFFSET = 1,000,000 Fixes: #83 --- rust/crates/cloudsearch-index/src/lib.rs | 18 +++++- .../cloudsearch-index/tests/coverage.rs | 58 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/rust/crates/cloudsearch-index/src/lib.rs b/rust/crates/cloudsearch-index/src/lib.rs index aaef2b9..82d7bfe 100644 --- a/rust/crates/cloudsearch-index/src/lib.rs +++ b/rust/crates/cloudsearch-index/src/lib.rs @@ -33,6 +33,8 @@ use tokio::{ const MAX_FIELDS_PER_INDEX: usize = 1000; const MERGE_TRIGGER_DOCUMENT_COUNT: usize = 8; +const MAX_SEARCH_SIZE: usize = 10_000; +const MAX_SEARCH_OFFSET: usize = 1_000_000; #[derive(Debug, Clone, PartialEq, Eq)] pub struct MergePlan { @@ -825,8 +827,8 @@ impl IndexHandle { }); } - let from = request.from.unwrap_or(0); - let size = request.size.unwrap_or(total); + let from = request.from.unwrap_or(0).min(MAX_SEARCH_OFFSET); + let size = request.size.unwrap_or(total).min(MAX_SEARCH_SIZE); let hits = scored .into_iter() @@ -915,6 +917,18 @@ impl IndexHandle { ))); } + if let Some(size) = request.size && size > MAX_SEARCH_SIZE { + return Err(CloudSearchError::InvalidSearchRequest(format!( + "size ({size}) exceeds maximum allowed value ({MAX_SEARCH_SIZE})" + ))); + } + + if let Some(from) = request.from && from > MAX_SEARCH_OFFSET { + return Err(CloudSearchError::InvalidSearchRequest(format!( + "from ({from}) exceeds maximum allowed value ({MAX_SEARCH_OFFSET})" + ))); + } + if let Some(aggs) = &request.aggs { for (name, agg) in aggs { match agg { diff --git a/rust/crates/cloudsearch-index/tests/coverage.rs b/rust/crates/cloudsearch-index/tests/coverage.rs index 151facf..e67aa12 100644 --- a/rust/crates/cloudsearch-index/tests/coverage.rs +++ b/rust/crates/cloudsearch-index/tests/coverage.rs @@ -133,3 +133,61 @@ async fn validate_search_request_rejects_nested_bool_with_object_field() { "nested bool with object sort field should be rejected" ); } + +#[tokio::test] +async fn validate_search_request_rejects_size_exceeding_max() { + let temp_dir = TempDir::new().expect("temp dir"); + let catalog = Arc::new(IndexCatalog::new(temp_dir.path())); + catalog.initialize().await.expect("init catalog"); + let _metadata = catalog + .create_index( + "test", + CreateIndexRequest { + settings: IndexSettings::default(), + ..Default::default() + }, + ) + .await + .expect("create index"); + let handle = catalog.open_index("test").await.expect("open index"); + + let request = SearchRequest { + size: Some(100_000), + ..Default::default() + }; + + let result = handle.validate_search_request(&request); + assert!( + result.is_err(), + "size exceeding MAX_SEARCH_SIZE should be rejected" + ); +} + +#[tokio::test] +async fn validate_search_request_rejects_from_exceeding_max() { + let temp_dir = TempDir::new().expect("temp dir"); + let catalog = Arc::new(IndexCatalog::new(temp_dir.path())); + catalog.initialize().await.expect("init catalog"); + let _metadata = catalog + .create_index( + "test", + CreateIndexRequest { + settings: IndexSettings::default(), + ..Default::default() + }, + ) + .await + .expect("create index"); + let handle = catalog.open_index("test").await.expect("open index"); + + let request = SearchRequest { + from: Some(2_000_000), + ..Default::default() + }; + + let result = handle.validate_search_request(&request); + assert!( + result.is_err(), + "from exceeding MAX_SEARCH_OFFSET should be rejected" + ); +} From ea9a3857cebfb124e2a9052d041b7ccd043637a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= <83272398+PoyrazK@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:19:22 +0300 Subject: [PATCH 2/2] style: apply rustfmt to search size bounds validation --- rust/crates/cloudsearch-index/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rust/crates/cloudsearch-index/src/lib.rs b/rust/crates/cloudsearch-index/src/lib.rs index 82d7bfe..ba28f7f 100644 --- a/rust/crates/cloudsearch-index/src/lib.rs +++ b/rust/crates/cloudsearch-index/src/lib.rs @@ -917,13 +917,17 @@ impl IndexHandle { ))); } - if let Some(size) = request.size && size > MAX_SEARCH_SIZE { + if let Some(size) = request.size + && size > MAX_SEARCH_SIZE + { return Err(CloudSearchError::InvalidSearchRequest(format!( "size ({size}) exceeds maximum allowed value ({MAX_SEARCH_SIZE})" ))); } - if let Some(from) = request.from && from > MAX_SEARCH_OFFSET { + if let Some(from) = request.from + && from > MAX_SEARCH_OFFSET + { return Err(CloudSearchError::InvalidSearchRequest(format!( "from ({from}) exceeds maximum allowed value ({MAX_SEARCH_OFFSET})" )));