{"record":{"id":"ec7f79a544de3ea1","repo":"slackhq/nebula","slug":"error-closing-tun-file-w-ec7f79","errorCode":null,"errorMessage":"error closing tun file: %w","messagePattern":"error closing tun file: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"overlay/tun_openbsd.go","lineNumber":116,"sourceCode":"\terr = t.reload(c, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.RegisterReloadCallback(func(c *config.C) {\n\t\terr := t.reload(c, false)\n\t\tif err != nil {\n\t\t\tutil.LogWithContextIfNeeded(\"failed to reload tun device\", err, t.l)\n\t\t}\n\t})\n\n\treturn t, nil\n}\n\nfunc (t *tun) Close() error {\n\tif t.f != nil {\n\t\tif err := t.f.Close(); err != nil {\n\t\t\treturn fmt.Errorf(\"error closing tun file: %w\", err)\n\t\t}\n\n\t\t// t.f.Close should have handled it for us but let's be extra sure\n\t\t_ = unix.Close(t.fd)\n\t}\n\treturn nil\n}\n\n// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the\n// calls go through libc's pinned trampoline. OpenBSD's pinsyscall protection rejects a raw\n// syscall.Syscall(SYS_WRITEV/SYS_READV, ...) because it doesn't originate from a libc-pinned\n// address, so we can't use the syscall.Syscall pattern that freebsd / netbsd use. We pull the\n// low-level stubs instead of calling unix.Writev/unix.Readv because those take [][]byte and rebuild\n// the []Iovec every call, which heap-allocates the header; linkname'ing the stubs lets us hand them\n// our own stack-allocated iovecs. See golang/go#78049.\n\n//go:linkname tunWritev golang.org/x/sys/unix.writev\n//go:noescape","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/overlay/tun_openbsd.go#L98-L134","documentation":"Close() closes the os.File wrapping the tun file descriptor and reports any error returned by f.Close(), wrapped with this message. A failing close can indicate the fd was already closed or an underlying I/O problem flushing state. The code still force-closes the raw fd as a safety net.","triggerScenarios":"Calling tun.Close() (directly or during shutdown) when the underlying file/fd is already invalid or was closed elsewhere, or the kernel reports an error on close.","commonSituations":"Double-close during shutdown ordering bugs; another goroutine closing the tun file; fd corruption after a signal-triggered teardown race.","solutions":["Ensure Close() is called only once per tun instance (use sync.Once)","Check for other code paths closing the same fd/file","Inspect the wrapped error for the underlying cause (EBADF usually means double close)","Restart the process if the tun device is left in a bad state"],"exampleFix":"// before\nt.Close()\nt.Close() // second call errors\n// after\nvar closeOnce sync.Once\ncloseOnce.Do(func() { _ = t.Close() })","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := t.Close(); err != nil {\n    var errno syscall.Errno\n    if errors.As(err, &errno) && errno == syscall.EBADF {\n        // already closed; safe to ignore\n    } else {\n        return fmt.Errorf(\"tun close: %w\", err)\n    }\n}","preventionTips":["Close each tun exactly once (sync.Once)","Never close the tun fd from other goroutines","Log and continue on EBADF during shutdown"],"tags":["openbsd","tun","close","file-descriptor"],"backgroundTag":"file-close-error","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}