gastownhall/beads · error

start proxy child: %w

Error message

start proxy child: %w

What it means

This error is wrapped in forkExecChild (called from spawnAndHandoff) when exec.Command.Start() fails to launch the detached dbproxy child process. cmd.Start() performs fork/exec and also fails if the binary is missing, not executable, or the process cannot be created; the log file and spawn marker are cleaned up before returning.

Source

Thrown at internal/storage/dbproxy/proxy/endpoint.go:529

	released = true
	lock.Unlock()
	beforeProxyChildStart()

	// GH#4634: same hazard as the direct sql-server spawn, one hop further
	// out. The proxy child is detached and long-lived, and it starts the
	// sql-server itself, so a caller's non-CLOEXEC descriptor would otherwise
	// be inherited twice over and pinned for the proxy's whole lifetime.
	if leaked := fdhygiene.MarkInheritedCloexec(); len(leaked) > 0 {
		// debug.Logf, not log.Printf: this fires in normal operation whenever
		// the caller's environment leaves any fd open, and the parent's stderr
		// may be parsed script output.
		debug.Logf("dbproxy: marked %d inherited fd(s) close-on-exec before starting proxy child: %v", len(leaked), leaked)
	}

	if err := cmd.Start(); err != nil {
		_ = logFile.Close()
		_ = clearOwnSpawnMarker(rootDir, marker)
		return nil, fmt.Errorf("start proxy child: %w", err)
	}

	done := make(chan error, 1)
	go func() {
		waitErr := cmd.Wait()
		_ = logFile.Close()
		done <- waitErr
		close(done)
	}()

	// Theoretical race: the birth token is captured after Start, so the child
	// could exit and its PID be recycled before Capture runs, making the
	// handle describe the unrelated replacement. The window is a few
	// milliseconds against OS PID-reuse latency, and the OS has no primitive
	// to atomically capture identity at spawn, so this is accepted and
	// documented rather than defended.
	var handle *procid.Handle
	childBirth, captureErr := procid.Capture(cmd.Process.Pid)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the running binary still exists and is executable (ls -l /proc/self/exe; re-run bd after reinstalling)
  2. Check process limits: ulimit -u, cgroup pids.max; raise or free processes
  3. Check disk space and filesystem mount flags (noexec/read-only) for the executable and log file directory
  4. Retry the command; if persistent, inspect the underlying error (os.StartProcess / fork errno) printed with %w

Example fix

// before
if err := cmd.Start(); err != nil {
    return nil, fmt.Errorf("start proxy child: %w", err)
}
// after
if err := cmd.Start(); err != nil {
    var le *os.LinkError
    if errors.As(err, &le) {
        return nil, fmt.Errorf("start proxy child (binary %q, errno %v): %w", le.Name, le.Err, err)
    }
    return nil, fmt.Errorf("start proxy child: %w", err)
}
Defensive patterns

Strategy: retry

Validate before calling

self, _ := os.Executable()
if fi, err := os.Stat(self); err != nil || fi.Mode()&0o111 == 0 {
    return fmt.Errorf("cannot spawn proxy: executable %q missing or not executable", self)
}

Try / catch

spawned, err := spawnAndHandoff(root)
if err != nil && strings.HasPrefix(err.Error(), "start proxy child:") {
    var le *os.LinkError
    if errors.As(err, &le) { /* inspect le.Err: EACCES/EAGAIN/ENOENT */ }
    // wait briefly and retry once
}

Prevention

When it happens

Trigger: spawnAndHandoff attempts to start the proxy child after releasing proxy.lock; cmd.Start() fails because the current executable (self) cannot be fork/exec'd or system process limits are exhausted.

Common situations: The bd binary was deleted or replaced (upgraded in place) between detection and spawn; EAGAIN from hitting RLIMIT_NPROC / cgroup process limits; tmpdir or exec resources unavailable; read-only or noexec filesystem preventing exec; permission denied on the binary.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/ba940f8af1ab57c2. Report an issue: GitHub.