Summary
The ingest pipeline has no per-request point count cap, unbounded in-memory caches, and a busy-spin backpressure mechanism that can saturate CPU under WAL stalls. An attacker with valid credentials can exhaust server memory or CPU.
Findings covered: #9 (capacity amplification), #10 (no point cap + unbounded caches), #11 (busy-spin backpressure)
#9 — Line-protocol capacity amplification
parse_line_body_to_points in application/line_protocol.rs:24:
let cap = body.iter().filter(|&&b| b == b'\n').count().max(1);
Capacity is based on newline count, but blank lines yield zero points. A body of \n characters pre-allocates a large Vec<Point> while producing zero valid points. Severity is Low (bounded by the 25 MiB body limit, and it's a pre-allocation hint only).
#10 — No per-request point cap + unbounded caches
No admission limit: The only HTTP-layer gate is the 25 MiB DefaultBodyLimit. A single write request can contain millions of points with no server-side throttling.
Unbounded caches in native adapter (adapters/chdb/native_adapter.rs:175-189, 386-399):
schemas: HashMap<TableKey, TableSchema> — grows without bound
series_schemas: HashMap<TableKey, TableSchema> — grows without bound
known_series: HashMap<TableKey, HashSet<u64>> — grows without bound
These maps have no eviction policy, no LRU, no size cap. High-cardinality workloads or adversarial writes inflate them indefinitely.
Cardinality limits (max_tag_values_per_measurement, max_measurements_per_database) bound distinct tags/measurements but not total rows per request.
#11 — Enqueue busy-spin backpressure
BatchingWal::enqueue in adapters/wal/batching_wal.rs:153-164:
loop {
match self.sender.try_send(req) {
Ok(()) => break,
Err(TrySendError::Full(pending)) => {
tokio::task::yield_now().await; // spin, don't park
req = pending;
}
Err(TrySendError::Disconnected(_)) => { ... }
}
}
When the channel is full (default depth: 64), callers busy-spin with yield_now() — no backoff, no sleep, no timeout. Under sustained WAL backpressure (slow disk, writer thread stall), every concurrent writer consumes CPU competing for tokio executor slots. This amplifies any WAL stall across all tokio workers.
Recommended Fix
- Per-request point cap: Reject writes exceeding a configurable max points per request at the HTTP handler level.
- Bounded caches: Replace
HashMap with lru::LruCache for schemas, series_schemas, and known_series with configurable max sizes.
- Backpressure: Replace
try_send + yield_now with tokio::sync::mpsc::Sender::send().await (async-aware, parks when full), or add tokio::time::sleep backoff + timeout returning 503.
Confidence
High. All three sub-findings verified against the source.
Summary
The ingest pipeline has no per-request point count cap, unbounded in-memory caches, and a busy-spin backpressure mechanism that can saturate CPU under WAL stalls. An attacker with valid credentials can exhaust server memory or CPU.
Findings covered: #9 (capacity amplification), #10 (no point cap + unbounded caches), #11 (busy-spin backpressure)
#9 — Line-protocol capacity amplification
parse_line_body_to_pointsinapplication/line_protocol.rs:24:Capacity is based on newline count, but blank lines yield zero points. A body of
\ncharacters pre-allocates a largeVec<Point>while producing zero valid points. Severity is Low (bounded by the 25 MiB body limit, and it's a pre-allocation hint only).#10 — No per-request point cap + unbounded caches
No admission limit: The only HTTP-layer gate is the 25 MiB
DefaultBodyLimit. A single write request can contain millions of points with no server-side throttling.Unbounded caches in native adapter (
adapters/chdb/native_adapter.rs:175-189, 386-399):schemas: HashMap<TableKey, TableSchema>— grows without boundseries_schemas: HashMap<TableKey, TableSchema>— grows without boundknown_series: HashMap<TableKey, HashSet<u64>>— grows without boundThese maps have no eviction policy, no LRU, no size cap. High-cardinality workloads or adversarial writes inflate them indefinitely.
Cardinality limits (
max_tag_values_per_measurement,max_measurements_per_database) bound distinct tags/measurements but not total rows per request.#11 — Enqueue busy-spin backpressure
BatchingWal::enqueueinadapters/wal/batching_wal.rs:153-164:When the channel is full (default depth: 64), callers busy-spin with
yield_now()— no backoff, no sleep, no timeout. Under sustained WAL backpressure (slow disk, writer thread stall), every concurrent writer consumes CPU competing for tokio executor slots. This amplifies any WAL stall across all tokio workers.Recommended Fix
HashMapwithlru::LruCacheforschemas,series_schemas, andknown_serieswith configurable max sizes.try_send+yield_nowwithtokio::sync::mpsc::Sender::send().await(async-aware, parks when full), or addtokio::time::sleepbackoff + timeout returning 503.Confidence
High. All three sub-findings verified against the source.