diff --git a/include/my_global.h b/include/my_global.h index 9a2d80519bb52..30a6975fdb321 100644 --- a/include/my_global.h +++ b/include/my_global.h @@ -568,8 +568,13 @@ typedef SOCKET_SIZE_TYPE size_socket; #define O_SHORT_LIVED 0 #endif #ifndef O_NOFOLLOW +#ifdef _WIN32 +/* Not a genuine Windows open() flag, but implemented in my_win_open(). */ +#define O_NOFOLLOW (1 << 28) +#else #define O_NOFOLLOW 0 #endif +#endif #ifndef O_CLOEXEC #define O_CLOEXEC 0 #endif diff --git a/mysys/my_create.c b/mysys/my_create.c index 32cc73a53c476..93da3d0a06399 100644 --- a/mysys/my_create.c +++ b/mysys/my_create.c @@ -41,7 +41,7 @@ File my_create(const char *FileName, mode_t CreateFlags, int access_flags, DBUG_PRINT("my",("Name: '%s' CreateFlags: %u AccessFlags: %d MyFlags: %lu", FileName, CreateFlags, access_flags, MyFlags)); #if defined(_WIN32) - fd= my_win_open(FileName, access_flags | O_CREAT); + fd= my_win_open(FileName, access_flags | O_CREAT, MyFlags); #else fd= open((char *) FileName, access_flags | O_CREAT | O_CLOEXEC, CreateFlags ? CreateFlags : my_umask); diff --git a/mysys/my_delete.c b/mysys/my_delete.c index 6854033f84b2b..40744cdef013a 100644 --- a/mysys/my_delete.c +++ b/mysys/my_delete.c @@ -19,7 +19,6 @@ #ifdef _WIN32 #include /* rmdir */ -static int my_win_unlink(const char *name); #endif CREATE_NOSYMLINK_FUNCTION( @@ -35,7 +34,7 @@ int my_delete(const char *name, myf MyFlags) DBUG_PRINT("my",("name %s MyFlags %lu", name, MyFlags)); #ifdef _WIN32 - err = my_win_unlink(name); + err = my_win_unlink(name, MyFlags); #else if (MyFlags & MY_NOSYMLINKS) err= unlink_nosymlinks(name); @@ -58,148 +57,6 @@ int my_delete(const char *name, myf MyFlags) } /* my_delete */ -#if defined (_WIN32) - -/* - Delete file. - - The function also makes best effort to minimize number of errors, - where another program (or thread in the current program) has the the same file - open. - - We're using several tricks to prevent the errors, such as - - - Windows 10 "posix semantics" delete - - - Avoid the error by using CreateFile() with FILE_FLAG_DELETE_ON_CLOSE, instead - of DeleteFile() - - - If file which is deleted (delete on close) but has not entirely gone, - because it is still opened by some app, an attempt to trcreate file with the - same name would result in yet another error. The workaround here is renaming - a file to unique name. - - Symbolic link are deleted without renaming. Directories are not deleted. -*/ -#include - -static int my_win_unlink(const char *name) -{ - HANDLE handle= INVALID_HANDLE_VALUE; - DWORD attributes; - uint last_error; - char unique_filename[MAX_PATH + 35]; - unsigned long long tsc; /* time stamp counter, for unique filename*/ - int retries; - DBUG_ENTER("my_win_unlink"); - - DBUG_INJECT_FILE_SHARING_VIOLATION(name); - - for (retries= FILE_SHARING_VIOLATION_RETRIES; ; retries--) - { - attributes= GetFileAttributes(name); - if (attributes == INVALID_FILE_ATTRIBUTES) - { - last_error= GetLastError(); - DBUG_PRINT("error", - ("GetFileAttributes(%s) failed with %u\n", name, last_error)); - goto error; - } - - if (attributes & FILE_ATTRIBUTE_DIRECTORY) - { - DBUG_PRINT("error", ("can't remove %s - it is a directory\n", name)); - errno= EINVAL; - DBUG_RETURN(-1); - } - - if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) - { - /* Symbolic link. Delete link, the not target */ - if (!DeleteFile(name)) - { - last_error= GetLastError(); - DBUG_PRINT("error", - ("DeleteFile(%s) failed with %u\n", name, last_error)); - goto error; - } - DBUG_RETURN(0); - } - - /* - Try Windows 10 method, delete with "posix semantics" (file is not - visible, and creating a file with the same name won't fail, even if it - the file was open) - */ - handle= CreateFile(name, DELETE, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - NULL, OPEN_EXISTING, 0, NULL); - if (handle != INVALID_HANDLE_VALUE) - { - /* 0x3 = FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS */ - struct {DWORD _Flags;} disp= {0x3}; - BOOL ok= SetFileInformationByHandle( - handle, (FILE_INFO_BY_HANDLE_CLASS) 21, &disp, sizeof(disp)); - CloseHandle(handle); - if (ok) - DBUG_RETURN(0); - } - - handle= CreateFile(name, DELETE, 0, NULL, OPEN_EXISTING, - FILE_FLAG_DELETE_ON_CLOSE, NULL); - if (handle != INVALID_HANDLE_VALUE) - { - /* - We opened file without sharing flags (exclusive), no one else has this - file opened, thus it is safe to close handle to remove it. No renaming - is necessary. - */ - CloseHandle(handle); - DBUG_RETURN(0); - } - - /* - Can't open file exclusively, hence the file must be already opened by - someone else. Open it for delete (with all FILE_SHARE flags set), - rename to unique name, close. - */ - handle= CreateFile(name, DELETE, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - NULL, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, NULL); - if (handle == INVALID_HANDLE_VALUE) - { - last_error= GetLastError(); - DBUG_PRINT( - "error", - ("CreateFile(%s) with FILE_FLAG_DELETE_ON_CLOSE failed with %u\n", - name, last_error)); - goto error; - } - - tsc= my_timer_cycles(); - my_snprintf(unique_filename, sizeof(unique_filename), "%s.%llx.deleted", - name, tsc); - if (!MoveFile(name, unique_filename)) - { - DBUG_PRINT("warning", - ("moving %s to unique filename failed, error %lu\n", name, - GetLastError())); - } - CloseHandle(handle); - DBUG_RETURN(0); - -error: - if (last_error != ERROR_SHARING_VIOLATION || retries == 0) - { - my_osmaperr(last_error); - DBUG_RETURN(-1); - } - DBUG_CLEAR_FILE_SHARING_VIOLATION(); - Sleep(FILE_SHARING_VIOLATION_DELAY_MS); - } -} -#endif - /* Remove directory recursively. */ diff --git a/mysys/my_open.c b/mysys/my_open.c index e7e3badb5aa33..31caf3361e2b5 100644 --- a/mysys/my_open.c +++ b/mysys/my_open.c @@ -47,10 +47,11 @@ File my_open(const char *FileName, int Flags, myf MyFlags) DBUG_ENTER("my_open"); DBUG_PRINT("my",("Name: '%s' Flags: %d MyFlags: %lu", FileName, Flags, MyFlags)); + DBUG_ASSERT(!(MyFlags & MY_NOSYMLINKS) || !(Flags & (O_CREAT | O_TRUNC))); if (!(MyFlags & (MY_WME | MY_FAE | MY_FFNF))) MyFlags|= my_global_flags; #if defined(_WIN32) - fd= my_win_open(FileName, Flags); + fd= my_win_open(FileName, Flags, MyFlags); #else if (MyFlags & MY_NOSYMLINKS) fd = open_nosymlinks(FileName, Flags | O_CLOEXEC, my_umask); diff --git a/mysys/my_symlink.c b/mysys/my_symlink.c index 8fb7a862352c8..7aa47c53169a8 100644 --- a/mysys/my_symlink.c +++ b/mysys/my_symlink.c @@ -136,9 +136,9 @@ int my_is_symlink(const char *filename __attribute__((unused))) 'filename'. On error returns -1, unless error is file not found, in which case it - is 1; 'to' will then be populated with a fallback value as specified by - my_load_path() with null own_path_prefix; symlinks will not be - resolved. + is 1; 'to' will then be populated with a fallback value (on POSIX, as + specified by my_load_path() with null own_path_prefix; on Windows, see + my_win_realpath()); symlinks will not be resolved. Sets my_errno to specific error number. */ @@ -176,20 +176,7 @@ int my_realpath(char *to, const char *filename, myf MyFlags) } DBUG_RETURN(result); #elif defined(_WIN32) - int ret= GetFullPathName(filename,FN_REFLEN, to, NULL); - if (ret == 0 || ret > FN_REFLEN) - { - my_errno= (ret > FN_REFLEN) ? ENAMETOOLONG : GetLastError(); - if (MyFlags & MY_WME) - my_error(EE_REALPATH, MYF(0), filename, my_errno); - /* - GetFullPathName didn't work : use my_load_path() which is a poor - substitute original name but will at least be able to resolve - paths that starts with '.'. - */ - my_load_path(to, filename, NullS); - return -1; - } + return my_win_realpath(to, filename, MyFlags); #else my_load_path(to, filename, NullS); #endif diff --git a/mysys/my_winfile.cc b/mysys/my_winfile.cc index 5421a1565304e..c06d971c44780 100644 --- a/mysys/my_winfile.cc +++ b/mysys/my_winfile.cc @@ -47,9 +47,12 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ #ifdef _WIN32 #include "mysys_priv.h" +#include "mysys_err.h" #include #include #include +#include +#include extern "C" { @@ -320,11 +323,420 @@ File my_win_sopen(const char *path, int oflag, int shflag, int pmode) } -File my_win_open(const char *path, int flags) +/** + Strip the "\\?\" prefix GetFinalPathNameByHandle() adds (the "\\?\UNC\" + form is rewritten to "\\" instead). + + @param path source path, possibly "\\?\"-prefixed + @param to destination buffer + @param to_size size of 'to', in chars + + @retval 0 stripped path copied to 'to' + @retval -1 'to' too small (errno set to ENAMETOOLONG) +*/ +static int strip_extended_prefix(const char *path, char *to, + size_t to_size) +{ + if (!strncmp(path, "\\\\?\\UNC\\", 8)) + { + size_t len= strlen(path + 8); + if (len + 3 > to_size) + { + errno= ENAMETOOLONG; + return -1; + } + to[0]= to[1]= '\\'; + strmov(to + 2, path + 8); + return 0; + } + if (!strncmp(path, "\\\\?\\", 4)) + path+= 4; + if (strlen(path) >= to_size) + { + errno= ENAMETOOLONG; + return -1; + } + strmov(to, path); + return 0; +} + + +/** Warn once per process that FILE_NAME_NORMALIZED failed and the + weaker FILE_NAME_OPENED fallback is being used instead. */ +static void warn_final_path_fallback_once(const char *name) +{ + static volatile LONG warned= 0; + if (!InterlockedExchange(&warned, 1)) + my_printf_error(EE_REALPATH, "Can't normalize path for '%s': using " + "FILE_NAME_OPENED, which may not fully resolve reparse " + "points", MYF(ME_WARNING | ME_ERROR_LOG_ONLY), name); +} + + +/** + Report the resolved path 'handle' was opened for. Reparse points were + already followed at the CreateFile() that produced 'handle'; this just reads + the result back via GetFinalPathNameByHandle(). + + @param handle handle to query + @param to destination buffer + @param to_size size of 'to', in chars + @param name path being resolved, for the fallback warning message only + + @retval 0 resolved path copied to 'to' + @retval -1 errno set (ENAMETOOLONG, or mapped from GetLastError()) +*/ +static int get_final_path(HANDLE handle, char *to, size_t to_size, + const char *name) +{ + char buf[FN_REFLEN + 8]; + DWORD len= GetFinalPathNameByHandle(handle, buf, (DWORD) sizeof(buf), + FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + DBUG_EXECUTE_IF("force_normalized_path_fail", len= 0;); + if (len == 0) + { + /* NORMALIZED fails outright on some SMB servers; fall back to OPENED + so such shares remain usable, though it may not fully resolve + reparse points, weakening MY_NOSYMLINKS verification. */ + warn_final_path_fallback_once(name); + len= GetFinalPathNameByHandle(handle, buf, (DWORD) sizeof(buf), + FILE_NAME_OPENED | VOLUME_NAME_DOS); + } + if (len == 0 || len >= sizeof(buf)) + { + if (len >= sizeof(buf)) + errno= ENAMETOOLONG; + else + my_osmaperr(GetLastError()); + return -1; + } + return strip_extended_prefix(buf, to, to_size); +} + + +/** + Windows implementation of my_realpath(), see my_symlink.c: open the + canonicalized path and read back CreateFile()'s resolved target via + GetFinalPathNameByHandle(), resolving symlinks, junctions and mount + points along the way. + + @param to destination buffer, FN_REFLEN bytes + @param filename path to resolve; may be empty (current directory), and + may alias 'to' + @param MyFlags MY_WME to report errors via my_error() + + @retval 0 resolved; 'to' holds the resolved path + @retval 1 'filename' doesn't exist; 'to' holds a fallback path + @retval -1 other error; 'to' holds a fallback path +*/ +int my_win_realpath(char *to, const char *filename, myf MyFlags) { + char full_path[FN_REFLEN]; + HANDLE h; + static const char cur_dir[]= {FN_CURLIB, '\0'}; + my_bool enoent= FALSE, have_full_path= TRUE; + DWORD ret; + DBUG_ENTER("my_win_realpath"); + ret= GetFullPathName(filename[0] ? filename : cur_dir, + sizeof(full_path), full_path, NULL); + + if (ret == 0 || ret >= sizeof(full_path)) + { + have_full_path= FALSE; + if (ret >= sizeof(full_path)) + my_errno= ENAMETOOLONG; + else + { + my_osmaperr(GetLastError()); + my_errno= errno; + } + goto err; + } + + h= my_create_file_with_retries(full_path, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (h == INVALID_HANDLE_VALUE) + { + DWORD err= GetLastError(); + enoent= (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND); + if (enoent) + my_errno= ENOENT; + else + { + my_osmaperr(err); + my_errno= errno; + } + goto err; + } + + if (get_final_path(h, to, FN_REFLEN, filename)) + { + my_errno= errno; /* errno set by get_final_path() */ + CloseHandle(h); + goto err; + } + CloseHandle(h); + DBUG_RETURN(0); + +err: + if (MyFlags & MY_WME) + my_error(EE_REALPATH, MYF(0), filename, my_errno); + if (have_full_path) + strmake(to, full_path, FN_REFLEN-1); + else + my_load_path(to, filename, NullS); + DBUG_RETURN(enoent ? 1 : -1); +} + + +/** + Verify that 'handle' (opened for 'path') didn't follow a symlink, junction or + mount point anywhere along the path. + + @param handle handle already opened for path + @param path expected (my_realpath()-resolved) name + + @retval 0 verified + @retval -1 errno set to ENOTDIR on a mismatch, or to whatever + get_final_path() failed with otherwise +*/ +static int verify_nosymlinks(HANDLE handle, const char *path) +{ + char opened_path[FN_REFLEN]; + if (get_final_path(handle, opened_path, sizeof(opened_path), path)) + return -1; /* errno set by get_final_path() */ + if (!strcmp(path, opened_path)) + return 0; + errno= ENOTDIR; + return -1; +} + + +/** + Rename open file through the handle + + @param handle handle + @param target new absolute path for the file + + @retval true renamed + @retval false rename failed; errno not set (check GetLastError()) + + @note handle must have DELETE access, required for renames +*/ +static my_bool rename_via_handle(HANDLE handle, const WCHAR *target) +{ + BYTE buf[sizeof(FILE_RENAME_INFO) + (FN_REFLEN + 40) * sizeof(WCHAR)]; + FILE_RENAME_INFO *ri= (FILE_RENAME_INFO *) buf; + DWORD name_len= (DWORD) wcslen(target) * sizeof(WCHAR); + DWORD bufsize= (DWORD) FIELD_OFFSET(FILE_RENAME_INFO, FileName) + name_len; + + /* FileName must be NUL-terminated beyond FileNameLength; zeroing the + whole buffer (not just the header) provides that. */ + memset(buf, 0, sizeof(buf)); + ri->ReplaceIfExists= FALSE; + ri->RootDirectory= NULL; + ri->FileNameLength= name_len; + memcpy(ri->FileName, target, name_len); + return SetFileInformationByHandle(handle, FileRenameInfo, ri, bufsize) != 0; +} + + +/** + Rename an open file to a unique, throwaway name in the same directory. + + The new name is derived from get_final_path()'s report of where 'handle' + actually is, not from any path string the caller used to open it. + + Failure isn't fatal to the caller (see my_win_unlink()). + + @param handle handle already opened with DELETE access + @param old_name filled with handle's path before the rename (only + meaningful if this returns true); pass back to + rename_via_handle() to undo the rename if a later + step on 'handle' fails (see my_win_unlink()) + @param old_name_size number of WCHARs available in old_name + @param name path being deleted, for the fallback warning + message only + + @retval true renamed; 'old_name' holds the pre-rename path + @retval false not renamed; 'old_name' left untouched +*/ +static my_bool try_rename_to_unique_name(HANDLE handle, WCHAR *old_name, + size_t old_name_size, + const char *name) +{ + char resolved[FN_REFLEN + 8]; + WCHAR unique_name[FN_REFLEN + 40]; + my_bool skip_rename= FALSE; + + if (get_final_path(handle, resolved, sizeof(resolved), name)) + return FALSE; + + if (!MultiByteToWideChar(CP_ACP, 0, resolved, -1, old_name, + (int) old_name_size)) + return FALSE; + + if (_snwprintf(unique_name, array_elements(unique_name), + L"%s.%llx.deleted", old_name, my_timer_cycles()) <= 0) + return FALSE; + + DBUG_EXECUTE_IF("force_rename_fail", skip_rename= TRUE;); + if (skip_rename || !rename_via_handle(handle, unique_name)) + return FALSE; + return TRUE; +} + + +/** + Windows implementation of my_delete(). Tries posix-semantics delete first + (frees 'name' for reuse immediately, even with other open handles); falls + back to renaming to a unique name, then marking for classic delete-on-close. + The fallback is needed for FAT32/ExFAT, which lack "POSIX-semantics" + delete. Renaming to a unique name is needed, otherwise recreating the file + with the same name might fail if another handle on it is still open. + + @param name file to delete; must already be a my_realpath()-resolved + name if MyFlags has MY_NOSYMLINKS (any non-canonical + spelling is rejected with ENOTDIR too) + @param MyFlags MY_NOSYMLINKS to reject (ENOTDIR) a name containing a + symlink or junction anywhere + + @retval 0 deleted + @retval -1 errno set +*/ +int my_win_unlink(const char *name, myf MyFlags) +{ + HANDLE handle; + DWORD last_error; + /* + Without MY_NOSYMLINKS, open the reparse point itself (don't follow a + leaf symlink). With MY_NOSYMLINKS, follow it - otherwise leaf symlink + would go undetected below. + */ + DWORD oflags= (MyFlags & MY_NOSYMLINKS) ? 0 : FILE_FLAG_OPEN_REPARSE_POINT; + DBUG_ENTER("my_win_unlink"); + + handle= my_create_file_with_retries(name, DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, oflags, NULL); + if (handle == INVALID_HANDLE_VALUE) + { + last_error= GetLastError(); + /* + A directory fails here with ERROR_ACCESS_DENIED; report EINVAL + instead, since my_delete() refuses directories regardless. + */ + DWORD attr= GetFileAttributes(name); + if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) + errno= EINVAL; + else + my_osmaperr(last_error); + DBUG_RETURN(-1); + } + + if ((MyFlags & MY_NOSYMLINKS) && verify_nosymlinks(handle, name)) + { + CloseHandle(handle); + DBUG_RETURN(-1); /* errno set by verify_nosymlinks() */ + } + + /* + Try Windows 10 "posix semantics" delete first: the file is unlinked, + and its name freed for reuse, right away -- even while other handles + to it (ours or another process') are still open. + */ + DBUG_EXECUTE_IF("force_posix_delete_fail", goto nonposix_delete;); + FILE_DISPOSITION_INFO_EX disp_ex; + disp_ex.Flags= FILE_DISPOSITION_FLAG_DELETE|FILE_DISPOSITION_FLAG_POSIX_SEMANTICS; + if (SetFileInformationByHandle(handle, FileDispositionInfoEx, &disp_ex, + sizeof(disp_ex))) + { + CloseHandle(handle); + DBUG_RETURN(0); + } + + /* + Only fall back when posix semantics are genuinely unsupported + (e.g FAT/exFAT), not on real errors. + */ + last_error= GetLastError(); + if (last_error != ERROR_INVALID_PARAMETER && + last_error != ERROR_NOT_SUPPORTED && + last_error != ERROR_INVALID_FUNCTION) + { + CloseHandle(handle); + my_osmaperr(last_error); + DBUG_RETURN(-1); + } + +#ifndef DBUG_OFF +nonposix_delete: +#endif + /* Rename to a unique name first, then mark delete-on-close. */ + WCHAR old_name[FN_REFLEN + 8]; + my_bool renamed= try_rename_to_unique_name(handle, old_name, + array_elements(old_name), name); + FILE_DISPOSITION_INFO disp= {TRUE}; + if (SetFileInformationByHandle(handle, FileDispositionInfo, &disp, + sizeof(disp))) + { + CloseHandle(handle); + DBUG_RETURN(0); + } + last_error= GetLastError(); + if (renamed && !rename_via_handle(handle, old_name)) + { + /* Couldn't rename back either: the file is only reachable under the + throwaway name now. Distinguish this from a plain dispose failure, + and log the orphan's name since the caller only sees EIO. */ + char orphan_name[FN_REFLEN + 8]; + CloseHandle(handle); + if (WideCharToMultiByte(CP_ACP, 0, old_name, -1, orphan_name, + sizeof(orphan_name), NULL, NULL)) + my_printf_error(EE_DELETE, "Could not delete or restore '%s' " + "(error %lu); it survives under the temporary name " + "'%s'", MYF(ME_ERROR_LOG_ONLY), name, last_error, + orphan_name); + errno= EIO; + DBUG_RETURN(-1); + } + CloseHandle(handle); + my_osmaperr(last_error); + DBUG_RETURN(-1); +} + + +/** + Windows implementation of my_open(). + + @param path file to open + @param flags O_RDONLY/O_RDWR/O_CREAT/... flags + @param MyFlags if MY_NOSYMLINKS is set, enforce it via + verify_nosymlinks() ('path' must already be a + my_realpath()-resolved name). Checked after the open, so + it can't stop O_CREAT/O_TRUNC acting first; no current + caller combines them with MY_NOSYMLINKS. + + @retval >=0 the open file descriptor + @retval -1 the open failed, or MY_NOSYMLINKS rejected it (errno set) +*/ +File my_win_open(const char *path, int flags, myf MyFlags) +{ + File fd; DBUG_ENTER("my_win_open"); - DBUG_RETURN(my_win_sopen((char *) path, flags | _O_BINARY, _SH_DENYNO, - _S_IREAD | S_IWRITE)); + fd= my_win_sopen((char *) path, flags | _O_BINARY, _SH_DENYNO, + _S_IREAD | S_IWRITE); + if ((MyFlags & MY_NOSYMLINKS) && fd >= 0 && + verify_nosymlinks(my_get_osfhandle(fd), path)) + { + /* preserve errno: my_win_close() overwrites it if CloseHandle() fails */ + int err= errno; + my_win_close(fd); + fd= -1; + errno= err; + } + DBUG_RETURN(fd); } diff --git a/mysys/mysys_priv.h b/mysys/mysys_priv.h index efeb0c65af3bb..3dfa061080333 100644 --- a/mysys/mysys_priv.h +++ b/mysys/mysys_priv.h @@ -181,8 +181,8 @@ static int PROTO { NOSYMLINK_FUNCTION_BODY(AT,NOAT) } #ifdef _WIN32 #include -/* my_winfile.c exports, should not be used outside mysys */ -extern File my_win_open(const char *path, int oflag); +/* my_winfile.cc exports, should not be used outside mysys */ +extern File my_win_open(const char *path, int oflag, myf MyFlags); extern int my_win_close(File fd); extern size_t my_win_read(File fd, uchar *buffer, size_t count); extern size_t my_win_write(File fd, const uchar *buffer, size_t count); @@ -202,6 +202,8 @@ extern int my_win_fsync(File fd); extern File my_win_dup(File fd); extern File my_win_sopen(const char *path, int oflag, int shflag, int perm); extern File my_open_osfhandle(HANDLE handle, int oflag); +extern int my_win_realpath(char *to, const char *filename, myf MyFlags); +extern int my_win_unlink(const char *name, myf MyFlags); /* diff --git a/unittest/mysys/my_delete-t.c b/unittest/mysys/my_delete-t.c index 7bc6cf89de58b..f79e30334ecef 100644 --- a/unittest/mysys/my_delete-t.c +++ b/unittest/mysys/my_delete-t.c @@ -15,8 +15,14 @@ #include #include +#include #include "tap.h" +#ifndef DBUG_OFF +#define FALLBACK_TEST_COUNT 15 +#else +#define FALLBACK_TEST_COUNT 0 +#endif int main(int argc __attribute__((unused)),char *argv[]) { @@ -26,7 +32,7 @@ int main(int argc __attribute__((unused)),char *argv[]) MY_INIT(argv[0]); - plan(6); + plan(16 + FALLBACK_TEST_COUNT); GetTempPathA(MAX_PATH, tmp_dir); ok(GetTempFileNameA(tmp_dir, "foo", 0, tmp_filename) != 0, "create temp file"); @@ -51,6 +57,134 @@ int main(int argc __attribute__((unused)),char *argv[]) CloseHandle(h); CloseHandle(h2); + /* The handle below doesn't grant FILE_SHARE_DELETE, so my_win_unlink()'s + own CreateFile(DELETE) fails outright and my_delete() must fail too. */ + ok(GetTempFileNameA(tmp_dir, "foo", 0, tmp_filename) != 0, + "create temp file 3"); + h = CreateFileA(tmp_filename, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + ok(h != INVALID_HANDLE_VALUE, "open temp file without FILE_SHARE_DELETE"); + ok(my_delete(tmp_filename, MYF(0)) != 0, + "Delete fails when the open itself can't get delete access"); + ok(GetFileAttributesA(tmp_filename) != INVALID_FILE_ATTRIBUTES, + "...and the file is still there under its original name"); + CloseHandle(h); + ok(DeleteFileA(tmp_filename) != 0, "cleanup: delete temp file 3"); + + /* MDEV-39533: posix-semantics delete fails on a read-only file with + ERROR_ACCESS_DENIED, which isn't in the fallback error set, so + my_win_unlink() rejects it immediately -- no rename is attempted. + (See below, under DBUG_OFF, for the rename-then-dispose fallback + also failing on a read-only file.) */ + ok(GetTempFileNameA(tmp_dir, "foo", 0, tmp_filename) != 0, + "create temp file 3b (read-only)"); + ok(SetFileAttributesA(tmp_filename, FILE_ATTRIBUTE_READONLY) != 0, + "mark it read-only"); + ok(my_delete(tmp_filename, MYF(0)) != 0, + "Delete fails on a read-only file instead of silently losing it"); + ok(GetFileAttributesA(tmp_filename) != INVALID_FILE_ATTRIBUTES, + "...and the file is still there under its original name, not " + "renamed to a '*.deleted' orphan"); + SetFileAttributesA(tmp_filename, FILE_ATTRIBUTE_NORMAL); + ok(DeleteFileA(tmp_filename) != 0, "cleanup: delete temp file 3b"); + +#ifndef DBUG_OFF + /* MDEV-39533: force posix-semantics delete to fail (debug hook) and + check the rename-then-dispose fallback, including that a new file + can be created under the original name right away. */ + { + HANDLE h_new; + char final_path[MAX_PATH + 16]; + DWORD final_len; + + ok(GetTempFileNameA(tmp_dir, "foo", 0, tmp_filename) != 0, + "create temp file 4"); + + /* Hold our own handle, so the file survives my_delete()'s own close. */ + h = CreateFileA(tmp_filename, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, 0, NULL); + ok(h != INVALID_HANDLE_VALUE, "open temp file 4"); + + DBUG_SET("+d,force_posix_delete_fail"); + ok(my_delete(tmp_filename, MYF(0)) == 0, + "Delete falls back to rename-then-delete-on-close when " + "posix-semantics delete fails"); + DBUG_SET("-d,force_posix_delete_fail"); + + /* Query via the handle: the new name isn't known until we ask it. */ + final_len= GetFinalPathNameByHandleA(h, final_path, sizeof(final_path), + FILE_NAME_NORMALIZED); + ok(final_len > 0 && final_len < sizeof(final_path) && + strstr(final_path, ".deleted") != NULL, + "...the old file was renamed out of the way: '%s'", final_path); + + h_new = CreateFileA(tmp_filename, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_DELETE, NULL, CREATE_NEW, 0, NULL); + ok(h_new != INVALID_HANDLE_VALUE, + "...and the original name can be recreated right away"); + + CloseHandle(h); + CloseHandle(h_new); + DeleteFileA(tmp_filename); + } + + /* MDEV-39533: force both posix-semantics delete and the rename to fail; + my_delete() should still succeed, under the original name. */ + { + char final_path[MAX_PATH + 16]; + DWORD final_len; + + ok(GetTempFileNameA(tmp_dir, "foo", 0, tmp_filename) != 0, + "create temp file 5"); + + h = CreateFileA(tmp_filename, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, 0, NULL); + ok(h != INVALID_HANDLE_VALUE, "open temp file 5"); + + DBUG_SET("+d,force_posix_delete_fail,force_rename_fail"); + ok(my_delete(tmp_filename, MYF(0)) == 0, + "Delete still succeeds under the original name when the rename " + "itself fails"); + DBUG_SET("-d,force_posix_delete_fail,force_rename_fail"); + + final_len= GetFinalPathNameByHandleA(h, final_path, sizeof(final_path), + FILE_NAME_NORMALIZED); + ok(final_len > 0 && final_len < sizeof(final_path) && + strstr(final_path, ".deleted") == NULL, + "...and was NOT renamed: '%s'", final_path); + + CloseHandle(h); + ok(GetFileAttributesA(tmp_filename) == INVALID_FILE_ATTRIBUTES, + "...and is gone once the last handle closes"); + } + + /* MDEV-39533: force posix-semantics delete to fail (debug hook) on a + read-only file. The classic dispose also fails (read-only), so the + rename must be undone; the file should end up back under its + original name, not orphaned under a "*.deleted" name. */ + { + ok(GetTempFileNameA(tmp_dir, "foo", 0, tmp_filename) != 0, + "create temp file 6 (read-only)"); + ok(SetFileAttributesA(tmp_filename, FILE_ATTRIBUTE_READONLY) != 0, + "mark it read-only"); + + DBUG_SET("+d,force_posix_delete_fail"); + ok(my_delete(tmp_filename, MYF(0)) != 0, + "Delete fails when both posix-semantics and classic dispose are " + "blocked by the read-only attribute"); + DBUG_SET("-d,force_posix_delete_fail"); + + ok(GetFileAttributesA(tmp_filename) != INVALID_FILE_ATTRIBUTES, + "...and the file is back under its original name, not left as a " + "'*.deleted' orphan"); + + SetFileAttributesA(tmp_filename, FILE_ATTRIBUTE_NORMAL); + ok(DeleteFileA(tmp_filename) != 0, "cleanup: delete temp file 6"); + } +#endif + my_end(0); return exit_status(); } diff --git a/unittest/mysys/my_symlink-t.c b/unittest/mysys/my_symlink-t.c index f35c28133d1ae..a4d15fc18fe7d 100644 --- a/unittest/mysys/my_symlink-t.c +++ b/unittest/mysys/my_symlink-t.c @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -73,12 +74,190 @@ static void test_my_realpath() #endif } +#ifdef _WIN32 +#ifndef DBUG_OFF +#define WIN_FALLBACK_TEST_COUNT 2 +#else +#define WIN_FALLBACK_TEST_COUNT 0 +#endif + +/* MDEV-39533: my_realpath()/my_open()/my_delete() and NTFS junctions. */ +#define WIN_TEST_COUNT (13 + WIN_FALLBACK_TEST_COUNT) + +static void test_win_realpath_and_nosymlinks() +{ + char base[FN_REFLEN], realdir[FN_REFLEN], linkdir[FN_REFLEN]; + char file_via_real[FN_REFLEN], file_via_link[FN_REFLEN]; + char resolved[FN_REFLEN], resolved_real[FN_REFLEN]; + char cmd[FN_REFLEN * 2]; + char tmp[FN_REFLEN]; + File fd; + int err; + + if (!GetTempPath(sizeof(tmp), tmp)) + { + diag("GetTempPath() failed: %lu", GetLastError()); + skip(WIN_TEST_COUNT, "GetTempPath() failed"); + return; + } + srand(GetTickCount()); + my_snprintf(base, sizeof(base), "%smdev39533_%lu_%06d", tmp, + GetCurrentProcessId(), rand() % 1000000); + my_snprintf(realdir, sizeof(realdir), "%s\\realdir", base); + my_snprintf(linkdir, sizeof(linkdir), "%s\\linkdir", base); + my_snprintf(file_via_real, sizeof(file_via_real), "%s\\data.txt", realdir); + my_snprintf(file_via_link, sizeof(file_via_link), "%s\\data.txt", linkdir); + + if (!CreateDirectory(base, NULL)) + { + diag("CreateDirectory() failed: %lu", GetLastError()); + skip(WIN_TEST_COUNT, "CreateDirectory() failed"); + return; + } + if (!CreateDirectory(realdir, NULL)) + { + diag("CreateDirectory() failed: %lu", GetLastError()); + skip(WIN_TEST_COUNT, "CreateDirectory() failed"); + goto rmdir_base; + } + + fd= my_create(file_via_real, 0, O_RDWR, MYF(0)); + if (fd < 0) + { + diag("my_create() failed: %d", my_errno); + skip(WIN_TEST_COUNT, "my_create() failed"); + goto rmdir_base; + } + my_close(fd, MYF(0)); + + /* Junctions need no special privilege on NTFS, unlike symlinks. */ + my_snprintf(cmd, sizeof(cmd), "cmd /c mklink /J \"%s\" \"%s\" >nul 2>&1", + linkdir, realdir); + if (system(cmd) != 0) + { + diag("mklink /J failed, skipping (not NTFS, or junctions unsupported?)"); + skip(WIN_TEST_COUNT, "mklink /J failed"); + goto delete_file; + } + + err= my_realpath(resolved, file_via_link, MYF(0)); + ok(err == 0, "my_realpath() resolves through a junction: err=%d", err); + + err= my_realpath(resolved_real, file_via_real, MYF(0)); + ok(err == 0 && !strcmp(resolved, resolved_real), + "my_realpath() output for the junctioned path matches the real path: " + "'%s' vs '%s'", resolved, resolved_real); + +#ifndef DBUG_OFF + /* MDEV-39533: force FILE_NAME_NORMALIZED to fail (debug hook; some SMB + servers fail it for real) and check the FILE_NAME_OPENED fallback. */ + { + char resolved_fallback[FN_REFLEN]; + DBUG_SET("+d,force_normalized_path_fail"); + err= my_realpath(resolved_fallback, file_via_link, MYF(0)); + ok(err == 0 && !strcmp(resolved_fallback, resolved), + "my_realpath() still resolves the junction when " + "FILE_NAME_NORMALIZED fails, via the FILE_NAME_OPENED fallback: " + "err=%d '%s'", err, resolved_fallback); + + fd= my_open(resolved, O_RDONLY, MYF(MY_NOSYMLINKS)); + ok(fd >= 0, + "...and MY_NOSYMLINKS verification still succeeds via the same " + "fallback"); + if (fd >= 0) + my_close(fd, MYF(0)); + DBUG_SET("-d,force_normalized_path_fail"); + } +#endif + + fd= my_open(resolved, O_RDONLY, MYF(MY_NOSYMLINKS)); + ok(fd >= 0, "my_open(MY_NOSYMLINKS) succeeds on an already-resolved name"); + if (fd >= 0) + my_close(fd, MYF(0)); + + fd= my_open(file_via_link, O_RDONLY, MYF(MY_NOSYMLINKS)); + ok(fd < 0 && my_errno == ENOTDIR, + "my_open(MY_NOSYMLINKS) rejects a path still containing a junction: " + "fd=%d my_errno=%d", (int) fd, my_errno); + if (fd >= 0) + my_close(fd, MYF(0)); + + fd= my_open(file_via_link, O_RDONLY, MYF(0)); + ok(fd >= 0, "my_open() without MY_NOSYMLINKS still follows the junction"); + if (fd >= 0) + my_close(fd, MYF(0)); + + { + char missing[FN_REFLEN], missing_resolved[FN_REFLEN]; + my_snprintf(missing, sizeof(missing), "%s\\does_not_exist.txt", realdir); + err= my_realpath(missing_resolved, missing, MYF(0)); + ok(err == 1 && my_errno == ENOENT, + "my_realpath() on a non-existent path returns 1/ENOENT: err=%d " + "my_errno=%d", err, my_errno); + } + + { + char link_file[FN_REFLEN]; + my_snprintf(link_file, sizeof(link_file), "%s\\link.txt", realdir); + if (!CreateSymbolicLink(link_file, file_via_real, + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) && + !CreateSymbolicLink(link_file, file_via_real, 0)) + { + diag("CreateSymbolicLink() failed: %lu (needs Developer Mode or " + "admin)", GetLastError()); + skip(5, "CreateSymbolicLink() failed"); + } + else + { + err= my_delete(link_file, MYF(MY_NOSYMLINKS)); + ok(err != 0 && my_errno == ENOTDIR, + "my_delete(MY_NOSYMLINKS) rejects a name whose leaf is itself a " + "symlink: err=%d my_errno=%d", err, my_errno); + ok(GetFileAttributes(file_via_real) != INVALID_FILE_ATTRIBUTES, + "...and doesn't delete the symlink's target"); + + /* + Without MY_NOSYMLINKS, my_delete() must still behave like unlink(): + remove the link itself, never follow it into the target. + */ + err= my_delete(link_file, MYF(0)); + ok(err == 0, "my_delete() without MY_NOSYMLINKS removes the link " + "itself: err=%d", err); + ok(GetFileAttributes(link_file) == INVALID_FILE_ATTRIBUTES, + "...the link is gone..."); + ok(GetFileAttributes(file_via_real) != INVALID_FILE_ATTRIBUTES, + "...and the target survives"); + } + } + + err= my_delete(file_via_link, MYF(MY_NOSYMLINKS)); + ok(err != 0 && my_errno == ENOTDIR, + "my_delete(MY_NOSYMLINKS) rejects a path still containing a junction: " + "err=%d my_errno=%d", err, my_errno); + + err= my_delete(resolved, MYF(MY_NOSYMLINKS)); + ok(err == 0, "my_delete(MY_NOSYMLINKS) succeeds on an already-resolved " + "name: err=%d", err); + + RemoveDirectory(linkdir); +delete_file: + my_delete(file_via_real, MYF(0)); + RemoveDirectory(realdir); +rmdir_base: + RemoveDirectory(base); +} +#endif /* _WIN32 */ + int main(int argc __attribute__((unused)),char *argv[]) { MY_INIT(argv[0]); - plan(4); + /* test_my_realpath() only emits 2 points on Windows, not 4. */ + plan(IF_WIN(2 + WIN_TEST_COUNT, 4)); test_my_realpath(); +#ifdef _WIN32 + test_win_realpath_and_nosymlinks(); +#endif my_end(0); return exit_status();