Preserve Windows errors when following filesystem links

This commit is contained in:
Chris Tate
2026-10-04 09:14:08 -05:00
parent c4316d6693
commit 449f6bef8c
3 changed files with 27 additions and 5 deletions
+15 -4
View File
@@ -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;
+3 -1
View File
@@ -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 => {
+9
View File
@@ -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);