slackhq/nebula · error

failed to create shutdown pipe: %w

Error message

failed to create shutdown pipe: %w

What it means

newTun creates a nonblocking CLOEXEC pipe used as a shutdown signal: Close() writes to it so goroutines blocked in Poll wake up. If unix.Pipe2 with O_CLOEXEC|O_NONBLOCK fails, the tun fd is closed and this wrapped error is returned. Without the pipe, graceful shutdown of the device reader would hang, so construction is aborted.

Source

Thrown at overlay/tun_freebsd.go:310

	}
	if errors.Is(err, fs.ErrNotExist) || deviceName == "" {
		// If the device doesn't already exist, request a new one and rename it
		fd, err = unix.Open("/dev/tun", os.O_RDWR, 0)
	}
	if err != nil {
		return nil, err
	}

	if err = unix.SetNonblock(fd, true); err != nil {
		_ = unix.Close(fd)
		return nil, fmt.Errorf("failed to set tun device as nonblocking: %w", err)
	}

	// Shutdown pipe lets Close wake any reader/writer blocked in Poll.
	var pipeFds [2]int
	if err = unix.Pipe2(pipeFds[:], unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil {
		_ = unix.Close(fd)
		return nil, fmt.Errorf("failed to create shutdown pipe: %w", err)
	}
	shutdownR, shutdownW := pipeFds[0], pipeFds[1]

	closeOnErr := true
	defer func() {
		if closeOnErr {
			_ = unix.Close(fd)
			_ = unix.Close(shutdownR)
			_ = unix.Close(shutdownW)
		}
	}()

	// Read the name of the interface
	var name [16]byte
	arg := fiodgnameArg{length: 16, buf: unsafe.Pointer(&name)}
	ctrlErr := ioctl(uintptr(fd), FIODGNAME, uintptr(unsafe.Pointer(&arg)))

	if ctrlErr == nil {

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Free file descriptors / fix the fd leak; check with lsof or procstat how many fds the process holds
  2. Raise the soft RLIMIT_NOFILE (ulimit -n) for the daemon
  3. Check the wrapped errno to distinguish EMFILE (per-process) from ENFILE (system-wide)
  4. Retry device creation after shedding load
Defensive patterns

Strategy: retry

Validate before calling

var rl syscall.Rlimit
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rl)
if int(rl.Cur)-countOpenFds() < 8 {
    return errors.New("too few free file descriptors to create tun + shutdown pipe")
}

Try / catch

tunDev, err := newTun(cfg, log, prefixes, false)
if err != nil && strings.Contains(err.Error(), "failed to create shutdown pipe") {
    log.Error("pipe creation failed (likely fd exhaustion)", "err", err)
    // alert/rotate, then retry after fds are freed
}

Prevention

When it happens

Trigger: unix.Pipe2(pipeFds[:], unix.O_CLOEXEC|unix.O_NONBLOCK) fails during newTun — practically always due to hitting the process or system file descriptor limit (EMFILE/ENFILE), or an old kernel lacking pipe2.

Common situations: Long-running daemons leaking fds until pipe() returns EMFILE; heavily loaded hosts at the kernel file-max; running on stripped-down FreeBSD environments where pipe2 support or fd limits are constrained.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/bb3a34b2e6b44593. Report an issue: GitHub.