Double Free in Chi with Cockroachdb
Double Free in Chi with Cockroachdb — how this specific combination creates or exposes the vulnerability
A double-free vulnerability occurs when a program deallocates a memory region twice, corrupting heap metadata. In the context of a Chi-based Go application interfacing with CockroachDB, the risk arises at the boundary between Go’s runtime and how rows are materialized from CockroachDB. While Go itself manages memory automatically, unsafe patterns—such as manually managed C memory via cgo, misuse of sync.Pool with pointers that back database rows, or driver-internal buffering—can reintroduce classic C/C++-style memory safety issues when the driver or surrounding C dependencies are involved.
When using CockroachDB’s Go driver (cockroachdb/cockroach-go/v2), a double-free exposure can occur if application code or a dependency incorrectly interacts with result sets after rows have been closed or released. For example, holding references to row values beyond the lifetime of a rows.Scan call, or passing pointers into C-based libraries that free and re-free the same block, can lead to heap corruption. This is especially relevant when using CGO to interface with C clients or drivers that manage their own memory. If Chi endpoints trigger queries that pass pointers into such C-based layers and the application does not ensure single ownership and proper lifetime control, the same memory region can be freed twice: once by the Go runtime deferring cleanup and once inside the C layer.
Another scenario involves misuse of context cancellation while streaming rows from CockroachDB. If a client cancels a long-running query and the surrounding Chi handler does not properly synchronize access to row buffers, the underlying driver may attempt to free a row slice that has already been released when the context’s Done channel triggered an early exit. Because Chi is a lightweight router without built-in database session management, developers must explicitly ensure that rows are closed once and that no background goroutine retains a pointer to a row buffer that has been deallocated. Without these guards, the combined stack—Chi routing, CockroachDB driver internals, and optional CGO usage—creates conditions where a double-free can corrupt heap structures, potentially leading to arbitrary code execution or denial of service.
An illustrative risk pattern involves passing sql.RawBytes or []byte pointers obtained from rows into C functions via cgo. If the C code calls free on that pointer and the Go side also relies on the garbage collector to eventually release the underlying slice, a double-free can occur when both paths attempt to release the same memory. This is not a flaw in CockroachDB per se, but a consequence of how pointers flow between Chi handlers, the database driver, and optional native code. Proper mitigation requires strict ownership semantics and avoiding manual memory management where Go interfaces with C.
Cockroachdb-Specific Remediation in Chi — concrete code fixes
Remediation focuses on ensuring each row buffer is freed exactly once, synchronizing access across goroutines, and avoiding pointer sharing with C code. Below are concrete patterns and code examples tailored to Chi and CockroachDB.
1. Ensure rows are closed exactly once and immediately after use
Always defer rows.Close() right after checking for errors, and avoid retaining references to row data after the function returns.
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func getUserHandler(pool *pgxpool.Pool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := chi.URLParam(r, "id")
row := pool.QueryRow(ctx, "SELECT username, email FROM users WHERE id = $1", id)
// Ensure rows are closed even with early returns; here we use a single row, so we close after scanning.
// For multiple rows, use rows.Close() in a defer inside the loop.
var username, email string
if err := row.Scan(&username, &email); err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write([]byte(username + " " + email))
}
}
2. Avoid passing row pointers to CGO or C-managed memory
Do not pass slices derived from rows directly to C functions without copying or ensuring the pointer is not freed twice. Use C.CBytes to copy data and take ownership in C, or avoid CGO entirely.
/*
#include <stdlib.h>
void process_data(char* data, int len) {
// do something with data
free(data); // C side takes ownership
}
*/
import "C"
import "unsafe"
func handleWithCGO(pool *pgxpool.Pool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var data []byte
pool.QueryRow(ctx, "SELECT payload FROM blobs WHERE id = $1", 1).Scan(&data)
// Copy to C-managed memory to avoid double-free
cdata := C.CBytes(data)
defer C.free(unsafe.Pointer(cdata))
C.process_data(cdata, C.int(len(data)))
w.WriteHeader(http.StatusOK)
}
}
3. Use context cancellation safely with streaming rows
When iterating over rows, ensure that cancellation does not leave rows open or cause double-free by closing rows in a defer and avoiding use after context cancellation.
func streamUsers(pool *pgxpool.Pool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
rows, err := pool.Query(ctx, "SELECT id, name FROM users")
if err != nil {
http.Error(w, "query failed", http.StatusInternalServerError)
return
}
// Ensure rows are closed exactly once
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
// handle error, but do not double-close rows; rows.Close() will handle cleanup
http.Error(w, "scan error", http.StatusInternalServerError)
return
}
w.Write([]byte(name + "\n"))
}
if err := rows.Err(); err != nil {
// handle error appropriately
}
}
}
4. Prefer high-level ORM/DB libraries that manage lifetimes
Using pgxpool with sqlx or standard database/sql reduces the surface area for manual memory handling. Ensure you configure max connections and timeouts appropriately to avoid resource leaks that can exacerbate memory issues.
5. Validate and sanitize inputs to prevent unsafe consumption paths
Chi middleware can validate request parameters before they reach handlers that query CockroachDB, reducing the chance of malformed queries that may stress the driver’s internal buffers.
import (
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Get("/users/{id}", validateID, getUserHandler(pool))
}
func validateID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// basic validation to avoid malformed queries
if id == "" {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
next.ServeHTTP(w, r)
})
}