From 449f6bef8cfd3bb2c1fc4caa5d334ea95893d253 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 4 Oct 2026 09:14:08 -0500 Subject: [PATCH] Preserve Windows errors when following filesystem links --- packages/runtime/src/scr_lib.c | 19 +++++++++++++++---- tests/corpus/node24-fs-links-callbacks.cjs | 4 +++- tests/corpus/node24-fs-links.ts | 9 +++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index b355a4c31..0377e5a4c 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -4243,8 +4243,21 @@ static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op, HANDLE h = CreateFileW(wide, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, flags, NULL); + if (h == INVALID_HANDLE_VALUE) { + DWORD error = GetLastError(); + DWORD attributes = GetFileAttributesW(wide); + free(wide); + /* CRT stat can report the link entry when its target cannot be opened, + * hiding dangling links and Windows file/directory type mismatches. + * libuv preserves the failed follow operation for reparse points. */ + if (!no_follow && attributes != INVALID_FILE_ATTRIBUTES && + (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) { + scr_fs_throw(scr_fs_win_errno(error), op, path); + return NULL; + } + return scr_stats_crt_fallback(path, op); + } free(wide); - if (h == INVALID_HANDLE_VALUE) return scr_stats_crt_fallback(path, op); DWORD file_type = GetFileType(h); if (file_type != FILE_TYPE_DISK) { @@ -4423,9 +4436,7 @@ void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind) { free(text); } scr_str_release(absolute); - /* Node's binding namespaces the link path before passing it to libuv. - * In particular, Windows can reject a file link to a directory differently - * when the destination uses the extended-length path syntax. */ + /* Node's binding namespaces the link path before passing it to libuv. */ ScrStr *link_path = scr_path_win32_to_namespaced_path(destination); WCHAR *from = scr_fs_win_wide(stored), *to = scr_fs_win_wide(link_path); DWORD error = !from || !to ? GetLastError() : 0; diff --git a/tests/corpus/node24-fs-links-callbacks.cjs b/tests/corpus/node24-fs-links-callbacks.cjs index 5002c7611..ed834af33 100644 --- a/tests/corpus/node24-fs-links-callbacks.cjs +++ b/tests/corpus/node24-fs-links-callbacks.cjs @@ -38,7 +38,9 @@ async function main() { // Buffer paths bypass that probe and use the file-link fallback. try { await call(fs.symlink, "dir", Buffer.from(join(root, "inferred"))); - console.log("inferred", fs.statSync(join(root, "inferred")).isDirectory()); + console.log("inferred link", fs.lstatSync(join(root, "inferred")).isSymbolicLink()); + try { console.log("inferred stat", fs.statSync(join(root, "inferred")).isDirectory()); } + catch (e) { console.log("inferred stat", e.name, e.code, e.message.includes("stat")); } } catch (e) { if (process.platform === "win32" && e.code === "EPERM") console.log("symlink privilege boundary"); else throw e; } let synchronous = true; await new Promise(resolve => { diff --git a/tests/corpus/node24-fs-links.ts b/tests/corpus/node24-fs-links.ts index 18d1ec2ef..043eb309e 100644 --- a/tests/corpus/node24-fs-links.ts +++ b/tests/corpus/node24-fs-links.ts @@ -32,8 +32,17 @@ try { console.log("absolute", fs.readlinkSync(join(root, "absolute")) === file); fs.symlinkSync("missing-target", join(root, "dangling"), null); console.log("dangling", fs.readlinkSync(join(root, "dangling")), fs.lstatSync(join(root, "dangling")).isSymbolicLink()); + try { fs.statSync(join(root, "dangling")); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("dangling stat", err.name, err.code); } fs.symlinkSync("nested", join(root, "directory"), "dir"); console.log("directory", fs.statSync(join(root, "directory")).isDirectory()); + fs.symlinkSync("nested", join(root, "file-directory"), "file"); + console.log("file-directory link", fs.lstatSync(join(root, "file-directory")).isSymbolicLink(), fs.readlinkSync(join(root, "file-directory")) === "nested"); + try { console.log("file-directory stat", fs.statSync(join(root, "file-directory")).isDirectory()); } + catch (e) { const err = e as NodeJS.ErrnoException; console.log("file-directory stat", err.name, err.code); } + fs.symlinkSync("target.txt", join(root, "directory-file"), "dir"); + console.log("directory-file link", fs.lstatSync(join(root, "directory-file")).isSymbolicLink()); + try { console.log("directory-file stat", fs.statSync(join(root, "directory-file")).isFile()); } + catch (e) { const err = e as NodeJS.ErrnoException; console.log("directory-file stat", err.name, err.code); } if (process.platform !== "win32") { fs.symlinkSync("x".repeat(600), join(root, "long"), "file"); console.log("long", fs.readlinkSync(join(root, "long")).length);