dagger/dagger · critical

failed to start session subprocess: %w

Error message

failed to start session subprocess: %w

What it means

startSessionSubprocess (cmd/init, running inside the engine init process) forks a detached session subprocess (Setsid) with a pipe for readiness signaling. This error wraps any failure from cmd.Start() — the OS refused to spawn the child process. The wrapped error carries the underlying syscall reason.

Source

Thrown at cmd/init/main.go:227

	r, w, err := os.Pipe()
	if err != nil {
		return err
	}

	// start the session subprocess
	cmd := exec.Command("/proc/self/exe")

	// forwarding our stdio ensures that a panic in the child process won't get hidden and any other logging works too
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	cmd.ExtraFiles = []*os.File{w}
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setsid: true,
	}
	err = cmd.Start()
	if err != nil {
		return fmt.Errorf("failed to start session subprocess: %w", err)
	}

	// wait for the session attachables to be ready (or the child to die)

	// need to close our dup of the write end of the pipe
	if err := w.Close(); err != nil {
		return fmt.Errorf("failed to close pipe: %w", err)
	}

	doneCh := make(chan struct{})
	go func() {
		defer close(doneCh)
		io.Copy(io.Discard, r)
	}()
	// something really really wrong would have to happen for this to block indefinitely, but be
	// cautious anyways w/ an overly generous timeout
	select {
	case <-doneCh:

View on GitHub (pinned to 82ba2681db)

Solutions

  1. Read the wrapped %w error to identify the syscall cause (ENOENT/EACCES/EAGAIN)
  2. Verify the session subprocess binary exists and is executable at the configured path
  3. Check container security profiles (seccomp, AppArmor) and ulimits permit spawning the process
  4. Free memory / raise process limits (nproc) if EAGAIN or OOM is reported
  5. Restart the engine/pod if a transient resource exhaustion caused the fork failure

Example fix

// before: image stripped of the session binary -> ENOENT
// after: ensure the binary ships and is executable
//   Dockerfile: COPY --from=build /out/dagger-session /usr/local/bin/dagger-session
//   RUN chmod +x /usr/local/bin/dagger-session
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: ensure the session binary is present and executable
if fi, err := os.Stat(sessionBinPath); err != nil || fi.IsDir() || fi.Mode()&0o111 == 0 {
    return fmt.Errorf("session binary %s missing or not executable", sessionBinPath)
}

Try / catch

if err := startSessionSubprocess(...); err != nil {
    var execErr *exec.Error
    if errors.As(err, &execErr) {
        log.Printf("session binary %q unusable: %v", execErr.Name, execErr.Err)
    } else if errors.Is(err, os.ErrPermission) {
        log.Printf("permission denied spawning session subprocess")
    }
    return err
}

Prevention

When it happens

Trigger: mainInit -> startSessionSubprocess calls cmd.Start() and the exec fails: binary not found (ENOENT), not executable (EACCES), fork/resource limits hit (EAGAIN), or out of memory.

Common situations: Session binary missing or corrupted in the container image; wrong executable path; seccomp/AppArmor or PID namespace restrictions blocking exec; hitting process limits (ulimit -u) under heavy load.

Related errors


AI-assisted analysis of dagger/dagger@82ba2681db (2026-09-05). Data as JSON: /api/errors/fff7d681c53a1c18. Report an issue: GitHub.