Skip to content

Commit a7755e5

Browse files
steveyeggeclaude
andcommitted
fix: prevent bd init from creating DB on another project's Dolt server (GH#2336)
During bd init, port resolution via DefaultConfig falls through to config.yaml / global config when no port file exists yet for the new project. This can resolve to another project's server, causing the new database to be created on the wrong server (cross-project data leakage). Fix: bd init now uses only project-local port sources (env var, port file) instead of DefaultConfig. For fresh inits with no port file, port 0 forces auto-start to allocate an ephemeral port for THIS project. Shared server mode is handled explicitly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c9998fc commit a7755e5

File tree

3 files changed

+65
-5
lines changed

3 files changed

+65
-5
lines changed

cmd/bd/init.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"strconv"
1011
"strings"
1112
"time"
1213

@@ -404,15 +405,29 @@ environment variable.`,
404405
// Build config. Beads always uses dolt sql-server.
405406
// AutoStart is always enabled during init — we need a server to initialize the database.
406407
//
407-
// Use doltserver.DefaultConfig to resolve the port via the standard chain
408-
// (env var → port file → config.yaml). Port 0 means auto-start will
409-
// allocate an ephemeral port (GH#2098, GH#2372).
410-
doltDefaults := doltserver.DefaultConfig(beadsDir)
408+
// Port resolution for init: use ONLY project-local sources (env var, port file)
409+
// to prevent cross-project data leakage (GH#2336). DefaultConfig falls through
410+
// to config.yaml / global config, which may resolve to another project's server
411+
// because metadata.json doesn't exist yet during init. For fresh inits, port 0
412+
// forces auto-start to allocate an ephemeral port for THIS project.
413+
initPort := 0
414+
if p := os.Getenv("BEADS_DOLT_SERVER_PORT"); p != "" {
415+
if port, err := strconv.Atoi(p); err == nil && port > 0 {
416+
initPort = port
417+
}
418+
}
419+
if initPort == 0 {
420+
initPort = doltserver.ReadPortFile(beadsDir)
421+
}
422+
// Shared server mode intentionally uses a common port for all projects.
423+
if initPort == 0 && doltserver.IsSharedServerMode() {
424+
initPort = doltserver.DefaultSharedServerPort
425+
}
411426
doltCfg := &dolt.Config{
412427
Path: storagePath,
413428
BeadsDir: beadsDir,
414429
Database: dbName,
415-
ServerPort: doltDefaults.Port,
430+
ServerPort: initPort,
416431
CreateIfMissing: true, // bd init is the only path that should create databases
417432
AutoStart: os.Getenv("BEADS_DOLT_AUTO_START") != "0",
418433
}

internal/doltserver/doltserver.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ func EnsurePortFile(beadsDir string, port int) error {
288288
return writePortFile(beadsDir, port)
289289
}
290290

291+
// ReadPortFile returns the port from the project's dolt-server.port file,
292+
// or 0 if the file doesn't exist or is invalid. Exported for use by bd init
293+
// to detect whether this project has its own running server (GH#2336).
294+
func ReadPortFile(beadsDir string) int {
295+
return readPortFile(beadsDir)
296+
}
297+
291298
// DefaultConfig returns config with sensible defaults.
292299
// Priority: env var > port file > config.yaml / global config > metadata.json.
293300
// Returns port 0 when no source provides a port, meaning Start() should

internal/doltserver/doltserver_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,44 @@ func TestDefaultConfigPortFileTakesPrecedence(t *testing.T) {
603603
}
604604
}
605605

606+
func TestReadPortFile_Empty(t *testing.T) {
607+
// ReadPortFile on a directory with no port file should return 0.
608+
dir := t.TempDir()
609+
if p := ReadPortFile(dir); p != 0 {
610+
t.Errorf("expected 0 for missing port file, got %d", p)
611+
}
612+
}
613+
614+
func TestReadPortFile_Valid(t *testing.T) {
615+
dir := t.TempDir()
616+
if err := writePortFile(dir, 12345); err != nil {
617+
t.Fatal(err)
618+
}
619+
if p := ReadPortFile(dir); p != 12345 {
620+
t.Errorf("expected 12345, got %d", p)
621+
}
622+
}
623+
624+
// TestReadPortFile_IgnoresConfigYaml verifies that ReadPortFile only reads
625+
// the port file, NOT config.yaml. This is the crux of the GH#2336 fix:
626+
// bd init uses ReadPortFile instead of DefaultConfig to avoid inheriting
627+
// another project's port from config.yaml or global config.
628+
func TestReadPortFile_IgnoresConfigYaml(t *testing.T) {
629+
dir := t.TempDir()
630+
631+
// Write a config.yaml with a dolt port (simulating another project's config)
632+
configPath := filepath.Join(dir, "config.yaml")
633+
if err := os.WriteFile(configPath, []byte("dolt:\n port: 9999\n"), 0600); err != nil {
634+
t.Fatal(err)
635+
}
636+
637+
// ReadPortFile must return 0 — it should ONLY read the port file,
638+
// not config.yaml. This prevents cross-project leakage during init.
639+
if p := ReadPortFile(dir); p != 0 {
640+
t.Errorf("ReadPortFile should ignore config.yaml, got port %d", p)
641+
}
642+
}
643+
606644
// --- Pre-v56 dolt database detection tests (GH#2137) ---
607645

608646
func TestIsPreV56DoltDir_NoMarker(t *testing.T) {

0 commit comments

Comments
 (0)