win,fs: handle AppExecLink reparse points in fstat

Currently, libuv throws EACCES when trying to fstat AppExecLink
reparse points, because Win32's CreateFileW can't resolve them on
its own.

Use `fs__readlink_handle` as a fallback for reparse points
that CreateFileW can't handle. Also change it to accept
a preallocated buffer.
This commit is contained in:
11soda11 2025-11-17 23:44:19 +01:00 committed by 11soda11
parent 3e9ae1bca0
commit 5fbb760427
3 changed files with 176 additions and 35 deletions

View File

@ -182,14 +182,27 @@ void uv__fs_init(void) {
uv__fd_hash_init();
}
/* Puts the utf16 link target path in the buffer pointed to by *w_target_ptr if
* w_target_ptr != NULL. If *w_target_ptr is null, a new buffer is allocated.
* If *w_target_ptr is nonnull, *w_target_size_ptr MUST contain the size of the
* buffer (in WCHARs), including space for NUL. If that is too small,
* `UV_ENOBUFS` is returned, and `*w_target_size_ptr` is set to the necessary
* size.
*
* On success, `*w_target_size_ptr` represents the string length(in WCHARs) of
* the target path, excluding NUL.
*
* The wtf8 length of target_path is placed in w_target_wtf8_len_ptr if it's
* nonnull. */
static int fs__readlink_handle(HANDLE handle,
char** target_ptr,
size_t* target_len_ptr) {
WCHAR** w_target_ptr,
size_t* w_target_size_ptr,
size_t* w_target_wtf8_len_ptr) {
char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
REPARSE_DATA_BUFFER* reparse_data = (REPARSE_DATA_BUFFER*) buffer;
WCHAR* w_target;
DWORD w_target_len;
DWORD w_target_size;
DWORD bytes;
size_t i;
size_t len;
@ -315,8 +328,132 @@ static int fs__readlink_handle(HANDLE handle,
return -1;
}
assert(target_ptr == NULL || *target_ptr == NULL);
return uv_utf16_to_wtf8(w_target, w_target_len, target_ptr, target_len_ptr);
if (w_target_wtf8_len_ptr != NULL)
*w_target_wtf8_len_ptr = uv_utf16_length_as_wtf8(w_target, w_target_len);
w_target_size = w_target_len + 1;
if (w_target_ptr != NULL) {
if (*w_target_ptr != NULL) { /* preallocated buffer */
assert(w_target_size_ptr);
/* return if buffer is too small */
if (*w_target_size_ptr < w_target_size) {
*w_target_size_ptr = w_target_size;
return UV_ENOBUFS;
}
} else { /* buffer needs to be allocated by us */
*w_target_ptr = uv__malloc(w_target_size * sizeof(WCHAR));
if (*w_target_ptr == NULL) {
if (w_target_size_ptr != NULL)
*w_target_size_ptr = w_target_size;
SetLastError(ERROR_OUTOFMEMORY);
return -1;
}
}
memcpy(*w_target_ptr, w_target, w_target_len * sizeof(WCHAR));
(*w_target_ptr)[w_target_len] = '\0';
}
if (w_target_size_ptr != NULL)
*w_target_size_ptr = w_target_len;
return 0;
}
/* CreateFile wrapper. This uses our own readlink logic if windows can't do it. */
static HANDLE fs__create_file(WCHAR* path, DWORD desired_access, int do_lstat) {
HANDLE handle;
size_t target_path_size;
WCHAR target_path_scratch[512];
WCHAR* target_path = target_path_scratch;
size_t target_buf_size = ARRAY_SIZE(target_path_scratch);
int err;
if (do_lstat) {
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);
}
for (uint32_t i = 0; i < 32; i++) {
/* try to get a handle normally */
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)
goto clean_and_ret_handle;
/* try again, but tell windows not to resolve reparse points */
handle = 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);
/* return if there is another issue */
if (handle == INVALID_HANDLE_VALUE)
goto clean_and_ret_handle;
target_path_size = target_buf_size;
/* try to use our own reparse point handling */
err = fs__readlink_handle(handle, &target_path, &target_path_size, NULL);
if (err < 0) {
if (err == UV_ENOBUFS) { /* buffer was too small */
if (target_path != target_path_scratch)
uv__free(target_path);
/* make readlink allocate the buffer itself */
target_path = NULL;
target_path_size = 0;
err = fs__readlink_handle(handle, &target_path, &target_path_size, NULL);
target_buf_size = target_path_size + 1;
if (err < 0) {
CloseHandle(handle);
handle = INVALID_HANDLE_VALUE;
goto ret_handle;
}
} else {
CloseHandle(handle);
handle = INVALID_HANDLE_VALUE;
goto clean_and_ret_handle;
}
}
CloseHandle(handle);
path = target_path;
}
handle = INVALID_HANDLE_VALUE;
SetLastError(ERROR_CANT_RESOLVE_FILENAME);
clean_and_ret_handle:
if (target_path != target_path_scratch)
uv__free(target_path);
ret_handle:
return handle;
}
@ -1152,7 +1289,7 @@ static void fs__unlink_rmdir(uv_fs_t* req, BOOL isrmdir) {
/* Read the reparse point and check if it is a valid symlink. If not, don't
* unlink. */
if (fs__readlink_handle(handle, NULL, NULL) < 0) {
if (fs__readlink_handle(handle, NULL, NULL, NULL) < 0) {
error = GetLastError();
if (error == ERROR_SYMLINK_NOT_SUPPORTED)
error = ERROR_ACCESS_DENIED;
@ -1853,7 +1990,7 @@ static int fs__stat_handle(HANDLE handle, uv_stat_t* statbuf, int do_lstat) {
* to be treated as a regular file. The higher level lstat function will
* detect this failure and retry without do_lstat if appropriate.
*/
if (fs__readlink_handle(handle, NULL, &target_length) != 0) {
if (fs__readlink_handle(handle, NULL, NULL, &target_length) != 0) {
return -1;
}
stat_info.EndOfFile.QuadPart = target_length;
@ -2059,13 +2196,7 @@ static DWORD fs__stat_directory(WCHAR* path,
}
/* Get directory handle */
handle = CreateFileW(path_dirpath,
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL);
handle = fs__create_file(path_dirpath, FILE_LIST_DIRECTORY, 0);
if (handle == INVALID_HANDLE_VALUE) {
ret_error = GetLastError();
@ -2186,17 +2317,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();
@ -2959,6 +3080,10 @@ static void fs__symlink(uv_fs_t* req) {
static void fs__readlink(uv_fs_t* req) {
HANDLE handle;
WCHAR initial_path_buf[512];
WCHAR* target_path = (WCHAR*)initial_path_buf;
size_t target_path_size = 512;
int err;
handle = CreateFileW(req->file.pathw,
0,
@ -2974,15 +3099,30 @@ static void fs__readlink(uv_fs_t* req) {
}
assert(req->ptr == NULL);
if (fs__readlink_handle(handle, (char**) &req->ptr, NULL) != 0) {
DWORD error = GetLastError();
SET_REQ_WIN32_ERROR(req, error);
if (error == ERROR_NOT_A_REPARSE_POINT)
req->result = UV_EINVAL;
CloseHandle(handle);
return;
err = fs__readlink_handle(handle, &target_path, &target_path_size, NULL);
if (err != 0) {
if (err == UV_ENOBUFS) {
target_path = NULL;
target_path_size = 0;
err = fs__readlink_handle(handle, &target_path, &target_path_size, NULL);
}
if (err != 0) {
DWORD error = GetLastError();
SET_REQ_WIN32_ERROR(req, error);
if (error == ERROR_NOT_A_REPARSE_POINT)
req->result = UV_EINVAL;
CloseHandle(handle);
return;
}
}
uv_utf16_to_wtf8(target_path, target_path_size, (char**) &req->ptr, NULL);
if (target_path != initial_path_buf)
uv__free(target_path);
req->flags |= UV_FS_FREE_PTR;
SET_REQ_RESULT(req, 0);

View File

@ -2693,7 +2693,7 @@ TEST_FS_IMPL(fs_non_symlink_reparse_point) {
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];
@ -2739,6 +2739,7 @@ TEST_FS_IMPL(fs_lstat_windows_store_apps) {
continue;
}
ASSERT_OK(uv_fs_lstat(loop, &stat_req, file_path, NULL));
ASSERT_OK(uv_fs_stat(loop, &stat_req, file_path, NULL));
}
MAKE_VALGRIND_HAPPY(loop);
return 0;

View File

@ -385,7 +385,7 @@ TEST_FS_DECLARE (fs_symlink_dir)
#ifdef _WIN32
TEST_FS_DECLARE (fs_symlink_junction)
TEST_FS_DECLARE (fs_non_symlink_reparse_point)
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)
@ -1113,7 +1113,7 @@ TASK_LIST_START
#ifdef _WIN32
TEST_FS_ENTRY (fs_symlink_junction)
TEST_FS_ENTRY (fs_non_symlink_reparse_point)
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)