win,fs: treat appexeclinks as regular files

Currently, fs__readlink_handle resolves AppExecLink Reparse points.
This however, is unsupported and userspace programs are supposed to
treat AppExecLinks as regular files, which is why this same kind of
handling logic was removed from powershell and rejected
by the dotnet runtime team.

Remove this logic and add a CreateFileW wrapper (used by fs_stat) which
opens unsupported reparse points (e.g. AppExecLinks) like regular files.
This commit is contained in:
11soda11 2026-01-01 20:14:51 +01:00
parent ec0ab5d77d
commit 8481eb2772
4 changed files with 42 additions and 56 deletions

View File

@ -191,8 +191,6 @@ static int fs__readlink_handle(HANDLE handle,
WCHAR* w_target;
DWORD w_target_len;
DWORD bytes;
size_t i;
size_t len;
if (!DeviceIoControl(handle,
FSCTL_GET_REPARSE_POINT,
@ -303,38 +301,6 @@ static int fs__readlink_handle(HANDLE handle,
w_target += 4;
w_target_len -= 4;
} else if (reparse_data->ReparseTag == IO_REPARSE_TAG_APPEXECLINK) {
/* String #3 in the list has the target filename. */
if (reparse_data->AppExecLinkReparseBuffer.StringCount < 3) {
SetLastError(ERROR_SYMLINK_NOT_SUPPORTED);
return -1;
}
w_target = reparse_data->AppExecLinkReparseBuffer.StringList;
/* The StringList buffer contains a list of strings separated by "\0", */
/* with "\0\0" terminating the list. Move to the 3rd string in the list: */
for (i = 0; i < 2; ++i) {
len = wcslen(w_target);
if (len == 0) {
SetLastError(ERROR_SYMLINK_NOT_SUPPORTED);
return -1;
}
w_target += len + 1;
}
w_target_len = wcslen(w_target);
if (w_target_len == 0) {
SetLastError(ERROR_SYMLINK_NOT_SUPPORTED);
return -1;
}
/* Make sure it is an absolute path. */
if (!(w_target_len >= 3 &&
((w_target[0] >= L'a' && w_target[0] <= L'z') ||
(w_target[0] >= L'A' && w_target[0] <= L'Z')) &&
w_target[1] == L':' &&
w_target[2] == L'\\')) {
SetLastError(ERROR_SYMLINK_NOT_SUPPORTED);
return -1;
}
} else {
/* Reparse tag does not indicate a symlink. */
SetLastError(ERROR_SYMLINK_NOT_SUPPORTED);
@ -345,6 +311,33 @@ static int fs__readlink_handle(HANDLE handle,
return uv_utf16_to_wtf8(w_target, w_target_len, target_ptr, target_len_ptr);
}
/* CreateFileW wrapper. Treats reparse points which windows can't resolve as regular files */
static HANDLE fs__create_file(WCHAR* path, DWORD desired_access, int do_lstat) {
if (!do_lstat) {
HANDLE handle;
handle = CreateFileW(path,
desired_access,
FILE_SHARE_READ | FILE_SHARE_WRITE |
FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL);
if (handle != INVALID_HANDLE_VALUE
|| GetLastError() != ERROR_CANT_ACCESS_FILE)
return handle;
}
return CreateFileW(path,
desired_access,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
NULL);
}
static int fs__capture_path(uv_fs_t* req,
const char* path,
@ -2212,17 +2205,7 @@ static DWORD fs__stat_impl_from_path(WCHAR* path,
}
/* If the new API does not exist, use the old API. */
flags = FILE_FLAG_BACKUP_SEMANTICS;
if (do_lstat)
flags |= FILE_FLAG_OPEN_REPARSE_POINT;
handle = CreateFileW(path,
FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
flags,
NULL);
handle = fs__create_file(path, FILE_READ_ATTRIBUTES, do_lstat);
if (handle == INVALID_HANDLE_VALUE) {
ret = GetLastError();

View File

@ -4177,10 +4177,6 @@ typedef struct _REPARSE_DATA_BUFFER {
struct {
UCHAR DataBuffer[1];
} GenericReparseBuffer;
struct {
ULONG StringCount;
WCHAR StringList[1];
} AppExecLinkReparseBuffer;
};
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
@ -4589,9 +4585,6 @@ typedef struct _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION {
#ifndef IO_REPARSE_TAG_LX_SYMLINK
# define IO_REPARSE_TAG_LX_SYMLINK (0xA000001DL)
#endif
#ifndef IO_REPARSE_TAG_APPEXECLINK
# define IO_REPARSE_TAG_APPEXECLINK (0x8000001BL)
#endif
typedef VOID (NTAPI *PIO_APC_ROUTINE)
(PVOID ApcContext,

View File

@ -2790,7 +2790,7 @@ TEST_FS_IMPL(fs_readlink_lx_symlink) {
return 0;
}
TEST_FS_IMPL(fs_lstat_windows_store_apps) {
TEST_FS_IMPL(fs_stat_windows_store_apps) {
uv_loop_t* loop;
char localappdata[MAX_PATH];
char windowsapps_path[MAX_PATH];
@ -2798,6 +2798,7 @@ TEST_FS_IMPL(fs_lstat_windows_store_apps) {
size_t len;
int r;
uv_fs_t req;
uv_fs_t lstat_req;
uv_fs_t stat_req;
uv_dirent_t dirent;
@ -2835,7 +2836,16 @@ TEST_FS_IMPL(fs_lstat_windows_store_apps) {
dirent.name) < 0) {
continue;
}
ASSERT_OK(uv_fs_lstat(loop, &stat_req, file_path, NULL));
ASSERT_OK(uv_fs_lstat(loop, &lstat_req, file_path, NULL));
ASSERT_OK(uv_fs_stat(loop, &stat_req, file_path, NULL));
/* Appexeclinks should be treated as regular files,
* so stat should return the same info as lstat
* see https://github.com/libuv/libuv/pull/4936#issuecomment-3703811492
* */
ASSERT_OK(strcmp(stat_req.path, lstat_req.path));
ASSERT_OK(memcmp(&stat_req.statbuf, &lstat_req.statbuf,
sizeof(stat_req.statbuf)));
}
MAKE_VALGRIND_HAPPY(loop);
return 0;

View File

@ -389,7 +389,7 @@ TEST_FS_DECLARE (fs_symlink_dir)
TEST_FS_DECLARE (fs_symlink_junction)
TEST_FS_DECLARE (fs_non_symlink_reparse_point)
TEST_FS_DECLARE (fs_readlink_lx_symlink)
TEST_FS_DECLARE (fs_lstat_windows_store_apps)
TEST_FS_DECLARE (fs_stat_windows_store_apps)
TEST_FS_DECLARE (fs_open_flags)
#endif
#if defined(_WIN32) && !defined(USING_UV_SHARED)
@ -1122,7 +1122,7 @@ TASK_LIST_START
TEST_FS_ENTRY (fs_symlink_junction)
TEST_FS_ENTRY (fs_non_symlink_reparse_point)
TEST_FS_ENTRY (fs_readlink_lx_symlink)
TEST_FS_ENTRY (fs_lstat_windows_store_apps)
TEST_FS_ENTRY (fs_stat_windows_store_apps)
TEST_FS_ENTRY (fs_open_flags)
#endif
#if defined(_WIN32) && !defined(USING_UV_SHARED)