[PATCH v5 0/3] MR9732: ntdll: Support FSCTL_DUPLICATE_EXTENTS_TO_FILE
Implement FSCTL_DUPLICATE_EXTENTS_TO_FILE handling in ntdll on Linux by wiring the IOCTL to FICLONERANGE/FICLONE where available and returning STATUS_INVALID_DEVICE_REQUEST otherwise. Adds the necessary constants/types when missing. -- v5: ntdll: Reuse fstatfs result in volume attribute query. https://gitlab.winehq.org/wine/wine/-/merge_requests/9732
From: strudelll <strudelll@etersoft.ru> --- dlls/ntdll/unix/file.c | 59 ++++++++++++++++++++++++++++++++++++++++++ include/winioctl.h | 10 +++++++ 2 files changed, 69 insertions(+) diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c index fdc0c32fc4f..b6641f31eb9 100644 --- a/dlls/ntdll/unix/file.c +++ b/dlls/ntdll/unix/file.c @@ -52,6 +52,9 @@ #include <sys/socket.h> #include <sys/time.h> #include <sys/ioctl.h> +#ifdef __linux__ +# include <linux/fs.h> +#endif #ifdef HAVE_SYS_ATTR_H #include <sys/attr.h> #endif @@ -6643,6 +6646,57 @@ static void ignore_server_ioctl_struct_holes( ULONG code, const void *in_buffer, #endif } +static NTSTATUS duplicate_extents_to_file( HANDLE dst_handle, const void *in_buf, ULONG in_len ) +{ + const DUPLICATE_EXTENTS_DATA *data = in_buf; + NTSTATUS status; + int dst_fd = -1, src_fd = -1; + int needs_close_dst = 0, needs_close_src = 0; + + if (!data || in_len < sizeof(*data)) return STATUS_BUFFER_TOO_SMALL; + + status = server_get_unix_fd( dst_handle, FILE_WRITE_DATA, &dst_fd, &needs_close_dst, NULL, NULL ); + if (status) return status; + status = server_get_unix_fd( data->FileHandle, FILE_READ_DATA, &src_fd, &needs_close_src, NULL, NULL ); + if (status) goto done; + +#ifdef FICLONERANGE + struct file_clone_range r = + { + .src_fd = src_fd, + .src_offset = data->SourceFileOffset.QuadPart, + .src_length = data->ByteCount.QuadPart, + .dest_offset = data->TargetFileOffset.QuadPart, + }; + + if (!ioctl( dst_fd, FICLONERANGE, &r )) { status = STATUS_SUCCESS; goto done; } + if (!(errno == EOPNOTSUPP || errno == ENOTTY || errno == EXDEV || errno == EINVAL)) + { + status = errno_to_status( errno ); + goto done; + } +#endif + +#ifdef FICLONE + if (!data->SourceFileOffset.QuadPart && !data->TargetFileOffset.QuadPart) + { + if (!ioctl( dst_fd, FICLONE, src_fd )) { status = STATUS_SUCCESS; goto done; } + if (!(errno == EOPNOTSUPP || errno == ENOTTY || errno == EXDEV)) + { + status = errno_to_status( errno ); + goto done; + } + } +#endif + + status = STATUS_INVALID_DEVICE_REQUEST; + +done: + if (needs_close_src && src_fd != -1) close( src_fd ); + if (needs_close_dst && dst_fd != -1) close( dst_fd ); + return status; +} + /****************************************************************************** * NtFsControlFile (NTDLL.@) @@ -6733,6 +6787,11 @@ NTSTATUS WINAPI NtFsControlFile( HANDLE handle, HANDLE event, PIO_APC_ROUTINE ap break; } + case FSCTL_DUPLICATE_EXTENTS_TO_FILE: + status = duplicate_extents_to_file( handle, in_buffer, in_size ); + break; + + case FSCTL_SET_SPARSE: TRACE("FSCTL_SET_SPARSE: Ignoring request\n"); status = STATUS_SUCCESS; diff --git a/include/winioctl.h b/include/winioctl.h index 08fb307a321..3bb26f4dc42 100644 --- a/include/winioctl.h +++ b/include/winioctl.h @@ -315,6 +315,16 @@ #define FSCTL_DELETE_CORRUPTED_REFS_CONTAINER CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 253, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_SCRUB_UNDISCOVERABLE_ID CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 254, METHOD_BUFFERED, FILE_ANY_ACCESS) +#ifndef _DUPLICATE_EXTENTS_DATA_DEFINED +typedef struct _DUPLICATE_EXTENTS_DATA { + HANDLE FileHandle; + LARGE_INTEGER SourceFileOffset; + LARGE_INTEGER TargetFileOffset; + LARGE_INTEGER ByteCount; +} DUPLICATE_EXTENTS_DATA, *PDUPLICATE_EXTENTS_DATA; +#define _DUPLICATE_EXTENTS_DATA_DEFINED +#endif + #define FSCTL_PIPE_ASSIGN_EVENT CTL_CODE(FILE_DEVICE_NAMED_PIPE, 0, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_DISCONNECT CTL_CODE(FILE_DEVICE_NAMED_PIPE, 1, METHOD_BUFFERED, FILE_ANY_ACCESS) #define FSCTL_PIPE_LISTEN CTL_CODE(FILE_DEVICE_NAMED_PIPE, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9732
From: strudelll <strudelll@etersoft.ru> Detect reflink-capable filesystems (btrfs or successful FICLONERANGE) and set FILE_SUPPORTS_BLOCK_REFCOUNTING in FileFsAttributeInformation. Add a conformance test that uses FSCTL_DUPLICATE_EXTENTS_TO_FILE and expects the flag when reflink works, skipping otherwise. --- dlls/kernel32/tests/volume.c | 86 ++++++++++++++++++++++++++++++++++++ dlls/ntdll/unix/file.c | 34 +++++++++++++- 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/dlls/kernel32/tests/volume.c b/dlls/kernel32/tests/volume.c index 975aa7bb69d..5573d5e1cb0 100644 --- a/dlls/kernel32/tests/volume.c +++ b/dlls/kernel32/tests/volume.c @@ -2014,6 +2014,91 @@ static void test_GetVolumeInformationByHandle(void) CloseHandle( file ); } +static void test_block_refcounting_flag(void) +{ + WCHAR tmp_path[MAX_PATH], src_path[MAX_PATH], dst_path[MAX_PATH]; + HANDLE src = INVALID_HANDLE_VALUE, dst = INVALID_HANDLE_VALUE; + DWORD sectors_per_cluster = 0, bytes_per_sector = 0, cluster_size, flags; + DWORD written, bytes_returned; + DUPLICATE_EXTENTS_DATA dup = {0}; + char *buffer = NULL; + BOOL ret; + + if (!GetTempPathW( ARRAY_SIZE(tmp_path), tmp_path ) || !tmp_path[0]) + { + skip("GetTempPathW failed, skipping block refcounting test.\n"); + return; + } + + if (!GetTempFileNameW( tmp_path, L"cln", 0, src_path )) + { + skip("GetTempFileNameW failed, skipping block refcounting test.\n"); + return; + } + + if (!GetTempFileNameW( tmp_path, L"cln", 0, dst_path )) + { + DeleteFileW( src_path ); + skip("GetTempFileNameW failed, skipping block refcounting test.\n"); + return; + } + + src = CreateFileW( src_path, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL ); + ok(src != INVALID_HANDLE_VALUE, "failed to create source file, err %lu\n", GetLastError()); + if (src == INVALID_HANDLE_VALUE) + goto cleanup; + + dst = CreateFileW( dst_path, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL ); + ok(dst != INVALID_HANDLE_VALUE, "failed to create dest file, err %lu\n", GetLastError()); + if (dst == INVALID_HANDLE_VALUE) + goto cleanup; + + ret = GetDiskFreeSpaceW( tmp_path, §ors_per_cluster, &bytes_per_sector, NULL, NULL ); + cluster_size = ret ? sectors_per_cluster * bytes_per_sector : 0; + if (!cluster_size) cluster_size = 4096; + + buffer = HeapAlloc( GetProcessHeap(), 0, cluster_size ); + ok(!!buffer, "failed to allocate %lu bytes\n", cluster_size); + if (!buffer) + goto cleanup; + + memset( buffer, 0x5a, cluster_size ); + ret = WriteFile( src, buffer, cluster_size, &written, NULL ); + ok(ret && written == cluster_size, "WriteFile failed, ret %d written %lu err %lu\n", ret, written, GetLastError()); + if (!ret) + goto cleanup; + + dup.FileHandle = src; + dup.SourceFileOffset.QuadPart = 0; + dup.TargetFileOffset.QuadPart = 0; + dup.ByteCount.QuadPart = cluster_size; + + ret = DeviceIoControl( dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &dup, sizeof(dup), + NULL, 0, &bytes_returned, NULL ); + if (!ret) + { + skip("duplicate extents not supported on %s, err %lu\n", debugstr_w(tmp_path), GetLastError()); + goto cleanup; + } + + ret = GetVolumeInformationW( tmp_path, NULL, 0, NULL, NULL, &flags, NULL, 0 ); + ok(ret, "GetVolumeInformationW failed for %s, err %lu\n", debugstr_w(tmp_path), GetLastError()); + if (ret) + ok(flags & FILE_SUPPORTS_BLOCK_REFCOUNTING, + "expected FILE_SUPPORTS_BLOCK_REFCOUNTING for %s, got %#lx\n", debugstr_w(tmp_path), flags); + +cleanup: + if (src != INVALID_HANDLE_VALUE) CloseHandle( src ); + if (dst != INVALID_HANDLE_VALUE) CloseHandle( dst ); + DeleteFileW( src_path ); + DeleteFileW( dst_path ); + HeapFree( GetProcessHeap(), 0, buffer ); +} + static void test_mountmgr_query_points(void) { char input_buffer[64]; @@ -2217,6 +2302,7 @@ START_TEST(volume) test_cdrom_ioctl(); test_mounted_folder(); test_GetVolumeInformationByHandle(); + test_block_refcounting_flag(); test_mountmgr_query_points(); test_GetDiskSpaceInformationA(); } diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c index b6641f31eb9..ad4dadc08fc 100644 --- a/dlls/ntdll/unix/file.c +++ b/dlls/ntdll/unix/file.c @@ -156,6 +156,10 @@ typedef struct /* Case-insensitivity attribute */ #define EXT4_CASEFOLD_FL 0x40000000 +#ifndef BTRFS_SUPER_MAGIC +#define BTRFS_SUPER_MAGIC 0x9123683e +#endif + #ifndef O_DIRECTORY # define O_DIRECTORY 0200000 /* must be directory */ #endif @@ -7465,8 +7469,10 @@ NTSTATUS WINAPI NtQueryVolumeInformationFile( HANDLE handle, IO_STATUS_BLOCK *io static const WCHAR udfW[] = {'U','D','F'}; FILE_FS_ATTRIBUTE_INFORMATION *info = buffer; + struct statfs stfs; struct mountmgr_unix_drive drive; enum mountmgr_fs_type fs_type = MOUNTMGR_FS_TYPE_NTFS; + BOOL supports_block_refcounting = FALSE; if (length < sizeof(FILE_FS_ATTRIBUTE_INFORMATION)) { @@ -7474,11 +7480,32 @@ NTSTATUS WINAPI NtQueryVolumeInformationFile( HANDLE handle, IO_STATUS_BLOCK *io break; } +#if defined(linux) && defined(HAVE_FSTATFS) + if (!fstatfs( fd, &stfs ) && stfs.f_type == BTRFS_SUPER_MAGIC) + supports_block_refcounting = TRUE; +#endif + +#ifdef FICLONERANGE + if (!supports_block_refcounting) + { + struct file_clone_range range = + { + .src_fd = fd, + .src_offset = 0, + .src_length = 0, + .dest_offset = 0, + }; + + if (!ioctl( fd, FICLONERANGE, &range )) + supports_block_refcounting = TRUE; + else if (errno != EOPNOTSUPP && errno != ENOTTY) + supports_block_refcounting = TRUE; + } +#endif + if (!get_mountmgr_fs_info( handle, fd, &drive, sizeof(drive) )) fs_type = drive.fs_type; else { - struct statfs stfs; - if (!fstatfs( fd, &stfs )) { #if defined(linux) && defined(HAVE_FSTATFS) @@ -7541,6 +7568,9 @@ NTSTATUS WINAPI NtQueryVolumeInformationFile( HANDLE handle, IO_STATUS_BLOCK *io break; } + if (supports_block_refcounting) + info->FileSystemAttributes |= FILE_SUPPORTS_BLOCK_REFCOUNTING; + io->Information = offsetof( FILE_FS_ATTRIBUTE_INFORMATION, FileSystemName ) + info->FileSystemNameLength; status = STATUS_SUCCESS; break; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9732
From: strudelll <strudelll@etersoft.ru> --- dlls/ntdll/unix/file.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c index ad4dadc08fc..bab60c5722b 100644 --- a/dlls/ntdll/unix/file.c +++ b/dlls/ntdll/unix/file.c @@ -7473,6 +7473,7 @@ NTSTATUS WINAPI NtQueryVolumeInformationFile( HANDLE handle, IO_STATUS_BLOCK *io struct mountmgr_unix_drive drive; enum mountmgr_fs_type fs_type = MOUNTMGR_FS_TYPE_NTFS; BOOL supports_block_refcounting = FALSE; + BOOL have_stfs = FALSE; if (length < sizeof(FILE_FS_ATTRIBUTE_INFORMATION)) { @@ -7480,8 +7481,12 @@ NTSTATUS WINAPI NtQueryVolumeInformationFile( HANDLE handle, IO_STATUS_BLOCK *io break; } +#ifdef HAVE_FSTATFS + have_stfs = !fstatfs( fd, &stfs ); +#endif + #if defined(linux) && defined(HAVE_FSTATFS) - if (!fstatfs( fd, &stfs ) && stfs.f_type == BTRFS_SUPER_MAGIC) + if (have_stfs && stfs.f_type == BTRFS_SUPER_MAGIC) supports_block_refcounting = TRUE; #endif @@ -7506,7 +7511,7 @@ NTSTATUS WINAPI NtQueryVolumeInformationFile( HANDLE handle, IO_STATUS_BLOCK *io if (!get_mountmgr_fs_info( handle, fd, &drive, sizeof(drive) )) fs_type = drive.fs_type; else { - if (!fstatfs( fd, &stfs )) + if (have_stfs) { #if defined(linux) && defined(HAVE_FSTATFS) switch (stfs.f_type) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9732
Hi Nikita, I see you've made some of the changes I've recommended, but haven't answered my initial question. Is there an application that needs this? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9732#note_125764
_It’s driven by a non‑public app; I can’t share details, but it expects GetCurrentThemeName to return the Aero/NormalColor alias while we load the bundled light theme. I added a regression test to cover the buffer/alias behavior this app relies on (see the updated GetCurrentThemeName tests)._ -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9732#note_125806
This change was motivated by a real (non-public) app that issues FSCTL_DUPLICATE_EXTENTS_TO_FILE to clone file ranges; without it, the app fails during its copy/patch phase. I can’t share the app, but I validated the code path against that workload.
Okay, thanks, that's probably good enough then. We do still need tests, and there's some other comments that haven't been addressed yet. Of course, this won't be committed until after code freeze anyway. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9732#note_125829
On Thu Dec 18 19:30:21 2025 +0000, nikita pushkarev wrote:
This change was motivated by a real (non-public) app that issues FSCTL_DUPLICATE_EXTENTS_TO_FILE to clone file ranges; without it, the app fails during its copy/patch phase. I can’t share the app, but I validated the code path against that workload. As far as I know Windows only supports block cloning on ReFS, does this app require ReFS? Or when run on NTFS, does it handle failure gracefully but was just expecting a different error code than Wine was returning?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9732#note_125835
participants (4)
-
Brendan Shanks (@bshanks) -
Elizabeth Figura (@zfigura) -
nikita pushkarev (@strudelll) -
strudelll