From 028921b04a279604d5a9c82dc69ca8cad1df017e Mon Sep 17 00:00:00 2001 From: aleiby Date: Sun, 25 Jan 2026 17:59:57 -0800 Subject: [PATCH] fix(daemon): prevent stack overflow on empty database path (#1288) (#1313) When metadata.json has an empty database field and no beads.db file exists, filepath.Dir("") returns "." which causes lock file operations to use the current directory instead of the beads directory. This could lead to stack overflow or unexpected behavior. Add early validation in tryAutoStartDaemon to check if dbPath is empty and return false gracefully, consistent with other early-return conditions. --- cmd/bd/daemon_autostart.go | 8 +++++++ cmd/bd/daemon_autostart_unit_test.go | 32 +++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/cmd/bd/daemon_autostart.go b/cmd/bd/daemon_autostart.go index 23b5074d..7df83b48 100644 --- a/cmd/bd/daemon_autostart.go +++ b/cmd/bd/daemon_autostart.go @@ -256,6 +256,14 @@ func tryAutoStartDaemon(socketPath string) bool { return false } + // Empty dbPath causes filepath.Dir("") to return "." which breaks lock + // file operations. This can happen when metadata.json has an empty database + // field and no beads.db file exists. Skip daemon start gracefully. + if dbPath == "" { + debugLog("skipping auto-start: no database path configured") + return false + } + if !canRetryDaemonStart() { debugLog("skipping auto-start due to recent failures") return false diff --git a/cmd/bd/daemon_autostart_unit_test.go b/cmd/bd/daemon_autostart_unit_test.go index 62a332c6..05d2a2a4 100644 --- a/cmd/bd/daemon_autostart_unit_test.go +++ b/cmd/bd/daemon_autostart_unit_test.go @@ -245,14 +245,24 @@ func TestDaemonAutostart_HandleExistingSocket_StaleCleansUp(t *testing.T) { func TestDaemonAutostart_TryAutoStartDaemon_EarlyExits(t *testing.T) { oldFailures := daemonStartFailures oldLast := lastDaemonStartAttempt + oldDbPath := dbPath defer func() { daemonStartFailures = oldFailures lastDaemonStartAttempt = oldLast + dbPath = oldDbPath }() + // Set up a valid dbPath to pass the empty dbPath check (GH#1288) + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0o750); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + dbPath = filepath.Join(beadsDir, "test.db") + daemonStartFailures = 1 lastDaemonStartAttempt = time.Now() - if tryAutoStartDaemon(filepath.Join(t.TempDir(), "bd.sock")) { + if tryAutoStartDaemon(filepath.Join(tmpDir, "bd.sock")) { t.Fatalf("expected tryAutoStartDaemon to skip due to backoff") } @@ -260,6 +270,8 @@ func TestDaemonAutostart_TryAutoStartDaemon_EarlyExits(t *testing.T) { lastDaemonStartAttempt = time.Time{} socketPath, cleanup := startTestRPCServer(t) defer cleanup() + // Update dbPath to match the test server's directory + dbPath = filepath.Join(filepath.Dir(socketPath), "test.db") if !tryAutoStartDaemon(socketPath) { t.Fatalf("expected tryAutoStartDaemon true when daemon already healthy") } @@ -598,3 +610,21 @@ func TestIsWispOperation(t *testing.T) { }) } } + +// TestTryAutoStartDaemon_EmptyDbPath verifies that tryAutoStartDaemon returns +// false early when dbPath is empty, preventing stack overflow from +// filepath.Dir("") returning "." (GH#1288) +func TestTryAutoStartDaemon_EmptyDbPath(t *testing.T) { + // Save and restore global state + oldDbPath := dbPath + defer func() { dbPath = oldDbPath }() + + // Set dbPath to empty string (simulates corrupted metadata.json) + dbPath = "" + + // tryAutoStartDaemon should return false without crashing + result := tryAutoStartDaemon("/tmp/test.sock") + if result { + t.Errorf("tryAutoStartDaemon() = true, want false when dbPath is empty") + } +}