security(sqlite): enable `secure_delete` pragma (#966)

bbrodriges and Nuno Cruces created

Co-authored-by: Nuno Cruces <ncruces@users.noreply.github.com>

Change summary

internal/app/app.go             | 15 ++++++++++---
internal/db/connect.go          | 36 ++++++++++++++++++----------------
internal/llm/agent/mcp-tools.go |  9 ++++++-
3 files changed, 37 insertions(+), 23 deletions(-)

Detailed changes

internal/app/app.go 🔗

@@ -50,7 +50,7 @@ type App struct {
 
 	// global context and cleanup functions
 	globalCtx    context.Context
-	cleanupFuncs []func()
+	cleanupFuncs []func() error
 }
 
 // New initializes a new applcation instance.
@@ -88,6 +88,9 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
 	// Initialize LSP clients in the background.
 	app.initLSPClients(ctx)
 
+	// cleanup database upon app shutdown
+	app.cleanupFuncs = append(app.cleanupFuncs, conn.Close)
+
 	// TODO: remove the concept of agent config, most likely.
 	if cfg.IsConfigured() {
 		if err := app.InitCoderAgent(); err != nil {
@@ -221,9 +224,10 @@ func (app *App) setupEvents() {
 	setupSubscriber(ctx, app.serviceEventsWG, "history", app.History.Subscribe, app.events)
 	setupSubscriber(ctx, app.serviceEventsWG, "mcp", agent.SubscribeMCPEvents, app.events)
 	setupSubscriber(ctx, app.serviceEventsWG, "lsp", SubscribeLSPEvents, app.events)
-	cleanupFunc := func() {
+	cleanupFunc := func() error {
 		cancel()
 		app.serviceEventsWG.Wait()
+		return nil
 	}
 	app.cleanupFuncs = append(app.cleanupFuncs, cleanupFunc)
 }
@@ -297,10 +301,11 @@ func (app *App) Subscribe(program *tea.Program) {
 
 	app.tuiWG.Add(1)
 	tuiCtx, tuiCancel := context.WithCancel(app.globalCtx)
-	app.cleanupFuncs = append(app.cleanupFuncs, func() {
+	app.cleanupFuncs = append(app.cleanupFuncs, func() error {
 		slog.Debug("Cancelling TUI message handler")
 		tuiCancel()
 		app.tuiWG.Wait()
+		return nil
 	})
 	defer app.tuiWG.Done()
 
@@ -350,7 +355,9 @@ func (app *App) Shutdown() {
 	// Call call cleanup functions.
 	for _, cleanup := range app.cleanupFuncs {
 		if cleanup != nil {
-			cleanup()
+			if err := cleanup(); err != nil {
+				slog.Error("Failed to cleanup app properly on shutdown", "error", err)
+			}
 		}
 	}
 }

internal/db/connect.go 🔗

@@ -7,7 +7,8 @@ import (
 	"log/slog"
 	"path/filepath"
 
-	_ "github.com/ncruces/go-sqlite3/driver"
+	"github.com/ncruces/go-sqlite3"
+	"github.com/ncruces/go-sqlite3/driver"
 	_ "github.com/ncruces/go-sqlite3/embed"
 
 	"github.com/pressly/goose/v3"
@@ -18,17 +19,6 @@ func Connect(ctx context.Context, dataDir string) (*sql.DB, error) {
 		return nil, fmt.Errorf("data.dir is not set")
 	}
 	dbPath := filepath.Join(dataDir, "crush.db")
-	// Open the SQLite database
-	db, err := sql.Open("sqlite3", dbPath)
-	if err != nil {
-		return nil, fmt.Errorf("failed to open database: %w", err)
-	}
-
-	// Verify connection
-	if err = db.PingContext(ctx); err != nil {
-		db.Close()
-		return nil, fmt.Errorf("failed to connect to database: %w", err)
-	}
 
 	// Set pragmas for better performance
 	pragmas := []string{
@@ -37,14 +27,25 @@ func Connect(ctx context.Context, dataDir string) (*sql.DB, error) {
 		"PRAGMA page_size = 4096;",
 		"PRAGMA cache_size = -8000;",
 		"PRAGMA synchronous = NORMAL;",
+		"PRAGMA secure_delete = ON;",
 	}
 
-	for _, pragma := range pragmas {
-		if _, err = db.ExecContext(ctx, pragma); err != nil {
-			slog.Error("Failed to set pragma", pragma, err)
-		} else {
-			slog.Debug("Set pragma", "pragma", pragma)
+	db, err := driver.Open(dbPath, func(c *sqlite3.Conn) error {
+		for _, pragma := range pragmas {
+			if err := c.Exec(pragma); err != nil {
+				return fmt.Errorf("failed to set pragma `%s`: %w", pragma, err)
+			}
 		}
+		return nil
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to open database: %w", err)
+	}
+
+	// Verify connection
+	if err = db.PingContext(ctx); err != nil {
+		db.Close()
+		return nil, fmt.Errorf("failed to connect to database: %w", err)
 	}
 
 	goose.SetBaseFS(FS)
@@ -58,5 +59,6 @@ func Connect(ctx context.Context, dataDir string) (*sql.DB, error) {
 		slog.Error("Failed to apply migrations", "error", err)
 		return nil, fmt.Errorf("failed to apply migrations: %w", err)
 	}
+
 	return db, nil
 }

internal/llm/agent/mcp-tools.go 🔗

@@ -4,6 +4,7 @@ import (
 	"cmp"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"log/slog"
 	"maps"
@@ -253,11 +254,15 @@ func updateMCPState(name string, state MCPState, err error, client *client.Clien
 }
 
 // CloseMCPClients closes all MCP clients. This should be called during application shutdown.
-func CloseMCPClients() {
+func CloseMCPClients() error {
+	var errs []error
 	for c := range mcpClients.Seq() {
-		_ = c.Close()
+		if err := c.Close(); err != nil {
+			errs = append(errs, err)
+		}
 	}
 	mcpBroker.Shutdown()
+	return errors.Join(errs...)
 }
 
 var mcpInitRequest = mcp.InitializeRequest{