#5270·libuv

win: uv_fs_event_start() fails with EBUSY on files held without FILE_SHARE_READ (regression in 1.52.0)

Author: MaxenceMouchardCreated Sep 11, 2026Updated Sep 11, 2026

Version / platform

libuv 1.52.0 and 1.52.1 (reproduced through Node.js, which vendors it), Windows 11 26200 x64. Last good: 1.51.0.

What happens

Since 1.52.0, uv_fs_event_start() on a file path fails with UV_EBUSY when another process holds that file open without granting FILE_SHARE_READ. On 1.51.0 the same call succeeded.

libuv via Node result
1.51.0 22.23.2 / 24.0.2 / 24.10.0 / 24.15.0 ok
1.52.1 24.16.0 / 24.17.0 / 24.18.0 / 24.21.0 EBUSY

This is a common transient state on Windows (a compiler emitting an assembly, MSBuild copying output, an antivirus scanning a fresh file). Watchers that install a per-file watch over a directory tree — chokidar does, so every Vite / webpack-dev-server / nodemon user on Windows — now get a hard error where they previously got a working watch.

Root cause

#4948 ("win: fix race in uv_fs_event_start()", 1.52.0) replaced the GetFileAttributesW() probe, which never opened the path, with:

c
file_handle = CreateFileW(pathw,
                          FILE_LIST_DIRECTORY,   /* == FILE_READ_DATA (0x1) on a file */
                          FILE_SHARE_READ|FILE_SHARE_DELETE|FILE_SHARE_WRITE,
                          NULL, OPEN_EXISTING,
                          FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED, NULL);
if (file_handle == INVALID_HANDLE_VALUE) {
  last_error = GetLastError();                   /* ERROR_SHARING_VIOLATION -> UV_EBUSY */
  goto error;
}
GetFileInformationByHandle(file_handle, &info);
is_path_dir = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
if (!is_path_dir) {
  CloseHandle(file_handle);                      /* handle discarded right away */
  file_handle = CreateFileW(dir, ...);           /* parent directory is what gets watched */
}

For a file path the handle is used only for GetFileInformationByHandle and closed immediately — read-data access on the file is never actually needed. But FILE_LIST_DIRECTORY is 0x1, i.e. FILE_READ_DATA on a file, which triggers the Windows sharing check.

Measured with CreateFileW directly, same flags and share mask as above, against a file another process holds as FileShare.None:

access = 0 (query only)               -> OK
access = FILE_READ_ATTRIBUTES (0x80)  -> OK
access = FILE_LIST_DIRECTORY (0x1)    -> FAIL, GetLastError() = 32 ERROR_SHARING_VIOLATION

The parent directory opens fine in every case, so the existing fallback path would have worked.

Reproduction

Terminal A (PowerShell), hold a file open with no sharing:

powershell
$f = [System.IO.File]::Open('C:/temp/locked.bin','Create','ReadWrite','None')
Start-Sleep -Seconds 30
$f.Close()

Terminal B, Node 24.16.0+ (libuv 1.52.x):

javascript
const fs = require('node:fs');
try { fs.watch('C:/temp/locked.bin', () => {}); console.log('watch ok'); }
catch (e) { console.log('threw', e.code); }   // -> threw EBUSY

Node 24.15.0 or older (libuv 1.51.0) prints watch ok.

Suggested fix

Either use 0 or FILE_READ_ATTRIBUTES as the desired access for the metadata probe — both succeed regardless of the holder's sharing mode and are enough for GetFileInformationByHandle, so the race fix of #4948 is preserved — or fall back to opening the parent directory on ERROR_SHARING_VIOLATION.

Happy to send a PR if you agree on the direction.