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.
This commit is contained in:
aleiby
2026-01-25 17:59:57 -08:00
committed by GitHub
parent 9e85b9f5d7
commit 028921b04a
2 changed files with 39 additions and 1 deletions

View File

@@ -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

View File

@@ -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")
}
}