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
- Free file descriptors / fix the fd leak; check with lsof or procstat how many fds the process holds
- Raise the soft RLIMIT_NOFILE (ulimit -n) for the daemon
- Check the wrapped errno to distinguish EMFILE (per-process) from ENFILE (system-wide)
- 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
- Fix fd leaks; audit with lsof/procstat on long-running processes
- Raise ulimit -n for the daemon's service unit
- Distinguish EMFILE (process) vs ENFILE (system) from the wrapped errno
- Keep shutdown-pipe creation close to device open to limit fd pressure window
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
- newTunFromFd not supported in FreeBSD
- failed to set tun device as nonblocking: %w
- unable to determine IP version from packet
- failed to set tun address %s: %s
- unknown address type %v
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/bb3a34b2e6b44593.
Report an issue: GitHub.