-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Describe the bug
std::filesystem::copy() with copy_options::overwrite_existing fails with error 87 (ERROR_INVALID_PARAMETER) when the source file resides on a filesystem that does not support 128-bit file IDs, such as FAT32, exFAT, or SMB shares backed by a non-NTFS filesystem.
The failure only occurs when the destination file already exists. In that case, fs::copy() internally calls equivalent() to confirm that source and destination are not the same file. equivalent() queries file identity via GetFileInformationByHandleEx(FileIdInfo), which returns ERROR_INVALID_PARAMETER on any filesystem that does not support FILE_ID_128. The error is propagated back through copy() to the caller.
This is a regression introduced by PR #5434 ("Win8 baseline: Simplify <filesystem> API calls"), merged April 2025, first shipped in v14.50.
Command-line test case
#include <filesystem>
#include <fstream>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
int main(int argc, char *argv[]) {
if (argc < 3) {
std::cerr << "Usage: repro <source-on-mapped-drive> <local-dest>\n"
<< "Example: repro R:\\testfile %TEMP%\\testfile\n";
return 1;
}
fs::path src = argv[1];
fs::path dst = argv[2];
std::cout << "_MSVC_STL_VERSION = " << _MSVC_STL_VERSION
<< ", _MSVC_STL_UPDATE = " << _MSVC_STL_UPDATE << "\n";
{ std::ofstream(dst).flush(); } // pre-create destination
std::cout << "Source: " << src << "\n";
std::cout << "Destination: " << dst << "\n";
std::error_code ec;
fs::copy(src, dst, fs::copy_options::overwrite_existing, ec);
if (ec)
std::cout << "[FAIL] fs::copy() error " << ec.value() << " (" << ec.message() << ")\n";
else
std::cout << "[OK] fs::copy() succeeded\n";
}Build with both toolsets from a VS 2026 installation:
cmd /c "call "C:\Program Files\Microsoft Visual Studio\18\Professional\VC\Auxiliary\Build\vcvars64.bat" -vcvars_ver=14.50 && cl /std:c++17 /EHsc repro.cpp /Fe:repro_v145.exe"
cmd /c "call "C:\Program Files\Microsoft Visual Studio\18\Professional\VC\Auxiliary\Build\vcvars64.bat" -vcvars_ver=14.44 && cl /std:c++17 /EHsc repro.cpp /Fe:repro_v143.exe"Run both against a file on a FAT32 volume (see setup instructions below):
repro_v145.exe R:\testfile %TEMP%\testfile
repro_v143.exe R:\testfile %TEMP%\testfile
Output:
# v145 (regression):
_MSVC_STL_VERSION = 145, _MSVC_STL_UPDATE = 202508
Source: "R:\testfile"
Destination: "C:\Users\...\AppData\Local\Temp\testfile"
[FAIL] fs::copy() error 87 (The parameter is incorrect.)
# v143 (baseline, works correctly):
_MSVC_STL_VERSION = 143, _MSVC_STL_UPDATE = 202503
Source: "R:\testfile"
Destination: "C:\Users\...\AppData\Local\Temp\testfile"
[OK] fs::copy() succeeded
Expected behavior
fs::copy(src, dst, fs::copy_options::overwrite_existing, ec) should succeed (and ec should be clear) regardless of whether the source filesystem supports FILE_ID_128.
STL version
Regression introduced in 14.50. Version 14.44 is not affected.
Additional context
PR #5130 ("Fix false positives by filesystem::equivalent on file systems with transient file IDs") introduced GetFileInformationByHandleEx(FileIdInfo) to query FILE_ID_128 in _Get_file_id_by_handle, but crucially kept a fallback: when FileIdInfo failed with ERROR_INVALID_PARAMETER or ERROR_NOT_SUPPORTED, the code would fall back to GetFileInformationByHandle (BY_HANDLE_FILE_INFORMATION), synthesizing a 128-bit ID from the 32-bit file index. This fallback made v143 work correctly on FAT32.
PR #5434 then removed this fallback as part of collapsing the #ifdef _CRT_APP / non-_CRT_APP bifurcation to a single Win8-API code path. The assumption was that Windows 8+ always supports FileIdInfo. However, FILE_ID_128 support is a filesystem capability, not a Windows version guarantee — FAT32 and exFAT do not support it on any Windows version.
The removed code was in stl/src/filesystem.cpp, _Get_file_id_by_handle:
// Before PR #5434 (v143, works):
switch (_Last_error) {
case __std_win_error::_Not_supported:
case __std_win_error::_Invalid_parameter:
break; // try more things
default:
return _Last_error;
}
// try GetFileInformationByHandle as a fallback
BY_HANDLE_FILE_INFORMATION _Info;
if (GetFileInformationByHandle(_Handle, &_Info)) {
_Id->VolumeSerialNumber = _Info.dwVolumeSerialNumber;
memcpy(&_Id->FileId.Identifier[0], &_Info.nFileIndexHigh, 4);
memcpy(&_Id->FileId.Identifier[4], &_Info.nFileIndexLow, 4);
memset(&_Id->FileId.Identifier[8], 0, 8);
return __std_win_error::_Success;
}
// After PR #5434 (v145, broken):
return __std_win_error{GetLastError()};Test setup: create a FAT32 VHD mounted as R: (requires admin)
# Run as Administrator
$vhdPath = "$PSScriptRoot\repro.vhd"
$disk = New-VHD -Path $vhdPath -SizeBytes 64MB -Fixed |
Mount-VHD -Path $vhdPath -PassThru |
Initialize-Disk -PartitionStyle MBR -PassThru
New-Partition -DiskNumber $disk.DiskNumber -UseMaximumSize -DriveLetter R
Format-Volume -DriveLetter R -FileSystem FAT32 -Confirm:$false
echo test > R:\testfile