Memory Leak in Axum with Cockroachdb
Memory Leak in Axum with Cockroachdb — how this specific combination creates or exposes the vulnerability
A memory leak in an Axum service that uses CockroachDB typically arises when application-level handling of database connections, rows, or prepared statements prevents timely release of resources. Unlike connection-pool exhaustion, which limits concurrent requests, a leak means each request or operation incrementally retains objects that should be garbage-collected, gradually increasing resident memory. In the Axum + CockroachDB context, common patterns that contribute include:
- Holding onto
Roworstreamobjects beyond the request scope, preventing the stream from being dropped and releasing network and deserialization buffers. - Not consuming or dropping a SQL stream fully (e.g., breaking a loop early or returning early from an async function while a stream is still alive).
- Retaining references to query results (e.g., collecting into a
Vec) when only a subset is needed, or caching large result sets without size bounds. - Prepared statement handles that are acquired per-request but not released, or misuse of
tokio_postgres-style patterns under a CockroachDB-compatible driver where async cleanup is deferred.
Because CockroachDB wire protocol and SQL semantics are strict, an unclosed stream can keep server-side cursors or result buffers alive, and Axum’s async runtime will hold Rust futures and their captured environments until dropped. If a handler does not .await streams to completion or moves them into long-lived tasks, memory grows with each request. This is observable as a steady increase in RSS over time, higher GC pressure (if using tracing alloc instrumentation), and eventual degradation or OOM kills under load.
Detection in a middleBrick scan appears under the Unsafe Consumption and Input Validation checks when payload sizes or connection churn correlate with increasing memory footprint across repeated scans; Data Exposure and Inventory Management checks may also surface unencrypted or poorly managed session state that prolongs object lifetimes. The 5–15 second scan window captures runtime behavior patterns that suggest retention issues, enabling teams to correlate findings with memory metrics.
Cockroachdb-Specific Remediation in Axum — concrete code fixes
Remediation focuses on strict resource discipline: fully consume or drop streams, bound collections, release prepared statements, and avoid moving database objects into long-lived contexts. Below are concrete, working Axum examples using a CockroachDB-compatible driver such as tokio-postgres with TLS or native TLS omitted for clarity.
1. Fully consume SQL streams and drop them promptly
Ensure every query stream is driven to completion within the handler scope. Do not store rows or rows futures beyond the request.
// axum_handler.rs
use axum::{routing::get, Router};
use tokio_postgres::{NoTls, Error};
async fn list_users_handler() -> Result {
let (client, connection) = tokio_postgres::connect("postgresql://user@localhost:26257/mydb", NoTls)
.await
.map_err(|e| e.to_string())?;
// Spawn connection handler to drive the connection future
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {e}");
}
});
// Stream rows and consume fully
let rows = client
.query("SELECT id, name FROM users WHERE active = $1", &[&true])
.await
.map_err(|e| e.to_string())?;
// Process and drop rows within scope
let count = rows.len();
// Explicitly drop if you break early; in this simple example rows is dropped at end of scope
Ok(format!("Active users: {count}"))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/users", get(list_users_handler));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
2. Use streaming with explicit drop or take to avoid unbounded retention
If you only need a subset, consume and break explicitly, ensuring the stream is dropped.
// limited_query.rs
use tokio_postgres::{NoTls, Error};
async fn fetch_limited() -> Result, Error> {
let (client, connection) = tokio_postgres::connect("postgresql://user@localhost:26257/mydb", NoTls).await?;
tokio::spawn(async move { connection.await.unwrap(); });
let mut results = Vec::new();
let mut stream = client.query_raw("SELECT name FROM logs ORDER BY ts DESC", &[]).await?;
// Take only the first 100 rows and then drop the stream
while let Some(row) = stream.next().await {
let row = row?;
let name: String = row.try_get(0)?;
results.push(name);
if results.len() >= 100 {
break;
}
}
// stream dropped here, releasing any held buffers
Ok(results)
}
3. Avoid collecting large result sets into unbounded structures
Prefer streaming processing or bounded pagination. If you must collect, enforce a strict limit and clear aggressively.
// paginated_handler.rs
async fn paginated_handler() -> Result {
let (client, connection) = tokio_postgres::connect("postgresql://user@localhost:26257/mydb", NoTls).await.map_err(|e| e.to_string())?;
tokio::spawn(async move { connection.await.unwrap(); });
let limit = 50;
let rows = client
.query(&format!("SELECT id, data FROM events LIMIT {limit}"), &[])
.await
.map_err(|e| e.to_string())?;
// Process and drop; do not cache rows globally
let summary: Vec<_> = rows.iter().map(|r| r.get::<_, i64>(0)).collect();
Ok(format!("Processed {}", summary.len()))
}
4. Reuse prepared statements carefully and release handles
Prepare once at startup if needed, but ensure that per-request usage does not accumulate unclosed cursors. For CockroachDB, prefer simple queries unless you have measurable benefit from prepared statements.
// prepared_once.rs
use tokio_postgres::{Client, NoTls};
async fn prepare_once(client: &Client) -> Result {
client.prepare("SELECT id, name FROM users WHERE id = $1").await.map_err(|e| e.to_string())
}
// In handler, pass the prepared statement and ensure no long-lived cursor state
async fn get_user_handler(client: &Client, stmt: &tokio_postgres::Statement) -> Result {
let row = client.query_one(stmt, &[&1i32]).await.map_err(|e| e.to_string())?;
let name: String = row.try_get(1)?;
Ok(name)
}
5. Monitor and correlate with middleBrick findings
Use middleBrick’s Unsafe Consumption and Data Exposure checks to validate that your endpoints do not retain or expose sensitive or large payloads. Combine scan results with runtime memory metrics to confirm that fixes reduce RSS growth and eliminate leak patterns flagged by the scanner.