{"record":{"id":"ce78f0463c7b5896","repo":"microsoft/typescript-go","slug":"error-reading-from-fanotify-w","errorCode":null,"errorMessage":"Error reading from fanotify: %w","messagePattern":"Error reading from fanotify: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/fswatch/fanotify_linux.go","lineNumber":390,"sourceCode":"func maybeWrapUnsupportedFilesystem(err error) error {\n\tif errors.Is(err, unix.EOPNOTSUPP) || errors.Is(err, unix.ENOTSUP) || errors.Is(err, unix.ENODEV) {\n\t\treturn fmt.Errorf(\"%w: %w\", err, ErrFilesystemUnsupported)\n\t}\n\treturn err\n}\n\n// handleEvents reads and dispatches fanotify events from the fd.\nfunc (b *fanotifyBackend) handleEvents() error {\n\tbuf := b.readBuf\n\twatchersTouched := b.watchersTouched\n\n\tfor {\n\t\tn, err := unix.Read(b.fanotifyFD, buf)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"Error reading from fanotify: %w\", err)\n\t\t}\n\t\tif n == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tmetaSize := int(unsafe.Sizeof(unix.FanotifyEventMetadata{}))\n\t\tdata := buf[:n]\n\t\tfor len(data) >= metaSize {\n\t\t\tmeta := (*unix.FanotifyEventMetadata)(unsafe.Pointer(&data[0]))\n\t\t\tif meta.Vers != unix.FANOTIFY_METADATA_VERSION {\n\t\t\t\treturn fmt.Errorf(\"unsupported fanotify metadata version: %d\", meta.Vers)\n\t\t\t}\n\t\t\teventLen := int(meta.Event_len)\n\t\t\tif eventLen < int(meta.Metadata_len) || eventLen > len(data) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// FID mode: fd should be FAN_NOFD, but close if somehow set.","sourceCodeStart":372,"sourceCodeEnd":408,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/fswatch/fanotify_linux.go#L372-L408","documentation":"The fanotify event-dispatch loop read(2) from the fanotify fd failed with an errno other than EAGAIN/EWOULDBLOCK (both mean 'queue drained, stop'). Because the fd was opened O_NONBLOCK, a genuine read error means the fd is bad or arguments invalid — the backend terminates and the error propagates.","triggerScenarios":"EBADF from the fanotify fd being closed by concurrent teardown (double Close, racing goroutines); EINVAL from a kernel/driver mismatch on buffer or flags; EIO from a failing filesystem underneath the watch.","commonSituations":"Multiple goroutines closing the same watcher; watchers stored in maps cleared during shutdown while events still flow; hardware/filesystem errors on the watched volume.","solutions":["Ensure exactly one owner closes each watcher; never share Close across goroutines","Treat the watcher as dead on this error: drop it and create a fresh subscription if watching must continue","If EIO recurs, check dmesg and the health of the underlying disk/mount"],"exampleFix":"// before\ngo func() { w.Close() }()\ngo func() { w.Close() }() // second close races event loop read -> EBADF\n\n// after: single-owner shutdown via sync.Once\nvar once sync.Once\nshutdown = func() { once.Do(w.Close) }","handlingStrategy":"retry","validationCode":null,"typeGuard":"func isFanotifyReadFailure(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"Error reading from fanotify\")\n}","tryCatchPattern":"err := watchUntilStopped(ctx, dir, cb)\nif isFanotifyReadFailure(err) {\n    // Backend fd went bad; rebuild the watcher from scratch (fresh fds).\n    err = watchUntilStopped(ctx, dir, cb)\n}\nreturn err","preventionTips":["Single-owner Close per watcher; use sync.Once if multiple call sites may shut down","Never call unix.Close on fds you obtained from the watcher internals","Recurring EIO reads point to a failing disk: surface dmesg/filesystem health to the user"],"tags":["linux","fanotify","file-descriptors","race-condition","fswatch"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}