diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/usvfs/src/usvfs_dll/hooks | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/usvfs/src/usvfs_dll/hooks')
| -rw-r--r-- | libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h | 263 | ||||
| -rw-r--r-- | libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp | 1860 | ||||
| -rw-r--r-- | libs/usvfs/src/usvfs_dll/hooks/kernel32.h | 116 | ||||
| -rw-r--r-- | libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp | 1520 | ||||
| -rw-r--r-- | libs/usvfs/src/usvfs_dll/hooks/ntdll.h | 57 | ||||
| -rw-r--r-- | libs/usvfs/src/usvfs_dll/hooks/sharedids.h | 9 |
6 files changed, 3825 insertions, 0 deletions
diff --git a/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h new file mode 100644 index 0000000..dbd3597 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h @@ -0,0 +1,263 @@ +#pragma once + +/** + * This header defines function to deal with the various FileInformationClass + * enumeration. + */ + +#include <type_traits> + +#include <ntdll_declarations.h> + +#include <windows.h> + +namespace usvfs::details +{ + +#define DECLARE_HAS_FIELD(Field) \ + template <typename T> \ + constexpr auto HasFieldImpl##Field(int)->decltype(std::declval<T>().Field, void(), \ + std::true_type()) \ + { \ + return {}; \ + } \ + template <typename T> \ + constexpr auto HasFieldImpl##Field(...) \ + { \ + return std::false_type{}; \ + } \ + template <typename T> \ + constexpr auto HasField##Field() \ + { \ + return HasFieldImpl##Field<T>(0); \ + } + +DECLARE_HAS_FIELD(FileName) +DECLARE_HAS_FIELD(NextEntryOffset) +DECLARE_HAS_FIELD(ShortName) + +#undef DECLARE_HAS_FIELD + +template <FILE_INFORMATION_CLASS fileInformationClass, class FileInformationClass> +struct FileInformationClassUtilsImpl +{ + static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName) + { + const FileInformationClass* info = + reinterpret_cast<const FileInformationClass*>(address); + + // default value + offset = sizeof(FileInformationClass); + + if constexpr (HasFieldFileName<FileInformationClass>()) { + if constexpr (HasFieldNextEntryOffset<FileInformationClass>()) { + offset = info->NextEntryOffset; + } + fileName = std::wstring(info->FileName, info->FileNameLength / sizeof(WCHAR)); + } + } + + static void set_offset(LPVOID address, ULONG offset) + { + FileInformationClass* info = reinterpret_cast<FileInformationClass*>(address); + + if constexpr (HasFieldNextEntryOffset<FileInformationClass>()) { + info->NextEntryOffset = offset; + } + } + + static void set_filename(LPVOID address, const std::wstring& fileName) + { + FileInformationClass* info = reinterpret_cast<FileInformationClass*>(address); + + if constexpr (HasFieldFileName<FileInformationClass>()) { + memset(info->FileName, L'\0', info->FileNameLength); + info->FileNameLength = static_cast<ULONG>(fileName.length() * sizeof(WCHAR)); + + // doesn't need to be 0-terminated + memcpy(info->FileName, fileName.c_str(), info->FileNameLength); + + if constexpr (HasFieldShortName<FileInformationClass>()) { + if (info->ShortNameLength > 0) { + info->ShortNameLength = static_cast<CCHAR>( + GetShortPathNameW(fileName.c_str(), info->ShortName, 8)); + } + } + } + } +}; + +template <FILE_INFORMATION_CLASS fileInformationClass> +struct FileInformationClassUtils; + +template <> +struct FileInformationClassUtils<FileDirectoryInformation> + : FileInformationClassUtilsImpl<FileDirectoryInformation, + FILE_DIRECTORY_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileFullDirectoryInformation> + : FileInformationClassUtilsImpl<FileFullDirectoryInformation, + FILE_FULL_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileBothDirectoryInformation, + FILE_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileStandardInformation> + : FileInformationClassUtilsImpl<FileStandardInformation, FILE_STANDARD_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileNameInformation> + : FileInformationClassUtilsImpl<FileNameInformation, FILE_NAME_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileRenameInformation> + : FileInformationClassUtilsImpl<FileRenameInformation, FILE_RENAME_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileNamesInformation> + : FileInformationClassUtilsImpl<FileNamesInformation, FILE_NAMES_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileObjectIdInformation> + : FileInformationClassUtilsImpl<FileObjectIdInformation, FILE_OBJECTID_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileReparsePointInformation> + : FileInformationClassUtilsImpl<FileReparsePointInformation, + FILE_REPARSE_POINT_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdBothDirectoryInformation, + FILE_ID_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdFullDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdFullDirectoryInformation, + FILE_ID_FULL_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileNormalizedNameInformation> + : FileInformationClassUtilsImpl<FileNormalizedNameInformation, + FILE_NAME_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdExtdDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdExtdDirectoryInformation, + FILE_ID_EXTD_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdExtdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdExtdBothDirectoryInformation, + FILE_ID_EXTD_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileId64ExtdDirectoryInformation> + : FileInformationClassUtilsImpl<FileId64ExtdDirectoryInformation, + FILE_ID_64_EXTD_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileId64ExtdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileId64ExtdBothDirectoryInformation, + FILE_ID_64_EXTD_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdAllExtdDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdAllExtdDirectoryInformation, + FILE_ID_ALL_EXTD_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdAllExtdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdAllExtdBothDirectoryInformation, + FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION> +{}; + +// FILE_ALL_INFORMATION needs to be handled differently because it has a field that +// is itself a structure +template <> +struct FileInformationClassUtils<FileAllInformation> +{ + static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName) + { + FileInformationClassUtils<FileNameInformation>::get_data( + &reinterpret_cast<const FILE_ALL_INFORMATION*>(address)->NameInformation, + offset, fileName); + } + + static void set_offset(LPVOID address, ULONG offset) + { + // this is a no-op but it's consistent to do that everywhere + FileInformationClassUtils<FileNameInformation>::set_offset( + &reinterpret_cast<FILE_ALL_INFORMATION*>(address)->NameInformation, offset); + } + + static void set_filename(LPVOID address, const std::wstring& fileName) + { + FileInformationClassUtils<FileNameInformation>::set_filename( + &reinterpret_cast<FILE_ALL_INFORMATION*>(address)->NameInformation, fileName); + } +}; + +} // namespace usvfs::details + +#define _APP_FINFO_CASE(clazz, fn, ...) \ + case clazz: \ + return usvfs::details::FileInformationClassUtils<clazz>::fn(__VA_ARGS__); + +#define _APPLY_FILEINFO_FN(fn, ...) \ + _APP_FINFO_CASE(FileAllInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileFullDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileStandardInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileRenameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNamesInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileObjectIdInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileReparsePointInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdFullDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNormalizedNameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdExtdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileId64ExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileId64ExtdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdAllExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdAllExtdBothDirectoryInformation, fn, __VA_ARGS__) + +void GetFileInformationData(FILE_INFORMATION_CLASS fileInformationClass, + LPCVOID address, ULONG& offset, std::wstring& fileName) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(get_data, address, offset, fileName); + default: + offset = ULONG_MAX; + } +} + +void SetFileInformationOffset(FILE_INFORMATION_CLASS fileInformationClass, + LPVOID address, ULONG offset) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(set_offset, address, offset); + default: + break; + } +} + +void SetFileInformationFileName(FILE_INFORMATION_CLASS fileInformationClass, + LPVOID address, const std::wstring& fileName) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(set_filename, address, fileName); + default: + break; + } +} + +#undef _APP_FINFO_CASE +#undef _APPLY_FILEINFO_FN diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp new file mode 100644 index 0000000..6599eb0 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp @@ -0,0 +1,1860 @@ +#include "kernel32.h" +#include "sharedids.h" + +#include "../hookcallcontext.h" +#include "../hookcontext.h" +#include "../hookmanager.h" +#include "../maptracker.h" +#include <formatters.h> +#include <inject.h> +#include <loghelpers.h> +#include <stringcast.h> +#include <stringutils.h> +#include <usvfs.h> +#include <winapi.h> +#include <winbase.h> + +namespace ush = usvfs::shared; +using ush::CodePage; +using ush::string_cast; + +namespace usvfs +{ +MapTracker k32DeleteTracker; +MapTracker k32FakeDirTracker; +} // namespace usvfs + +class CurrentDirectoryTracker +{ +public: + using wstring = std::wstring; + + bool get(wstring& currentDir, const wchar_t* forRelativePath = nullptr) + { + int index = m_currentDrive; + if (forRelativePath && *forRelativePath && forRelativePath[1] == ':') + if (!getDriveIndex(forRelativePath, index)) + spdlog::get("usvfs")->warn( + "CurrentDirectoryTracker::get() invalid drive letter: {}, will use current " + "drive {}", + string_cast<std::string>(forRelativePath), + static_cast<char>('A' + index)); // prints '@' for m_currentDrive == -1 + if (index < 0) + return false; + + std::shared_lock<std::shared_mutex> lock(m_mutex); + if (m_perDrive[index].empty()) + return false; + else { + currentDir = m_perDrive[index]; + return true; + } + } + + bool set(const wstring& currentDir) + { + int index = -1; + bool good = !currentDir.empty() && getDriveIndex(currentDir.c_str(), index) && + currentDir[1] == ':'; + std::unique_lock<std::shared_mutex> lock(m_mutex); + m_currentDrive = good ? index : -1; + if (good) + m_perDrive[index] = currentDir; + return good; + } + +private: + static bool getDriveIndex(const wchar_t* path, int& index) + { + if (*path >= 'a' && *path <= 'z') + index = *path - 'a'; + else if (*path >= 'A' && *path <= 'Z') + index = *path - 'A'; + else + return false; + return true; + } + + mutable std::shared_mutex m_mutex; + wstring m_perDrive['z' - 'a' + 1]; + int m_currentDrive{-1}; +}; + +CurrentDirectoryTracker k32CurrentDirectoryTracker; + +// attempts to copy source to destination and return the error code +static inline DWORD copyFileDirect(LPCWSTR source, LPCWSTR destination, bool overwrite) +{ + usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::SHELL_FILEOP); + return CopyFileExW(source, destination, NULL, NULL, NULL, + overwrite ? 0 : COPY_FILE_FAIL_IF_EXISTS) + ? ERROR_SUCCESS + : GetLastError(); +} + +static inline WCHAR pathNameDriveLetter(LPCWSTR path) +{ + if (!path || !path[0]) + return 0; + if (path[1] == ':') + return path[0]; + // if path is not ?: or \* then we need to get absolute path: + std::wstring buf; + if (path[0] != '\\') { + buf = winapi::wide::getFullPathName(path).first; + path = buf.c_str(); + if (!path[0] || path[1] == ':') + return path[0]; + } + // check for \??\C: + if (path[1] && path[2] && path[3] && path[4] && path[0] == '\\' && path[3] == '\\' && + path[5] == ':') + return path[4]; + // give up + return 0; +} + +// returns false also in case we fail to determine the drive letter of the path +static inline bool pathsOnDifferentDrives(LPCWSTR path1, LPCWSTR path2) +{ + WCHAR drive1 = pathNameDriveLetter(path1); + WCHAR drive2 = pathNameDriveLetter(path2); + return drive1 && drive2 && towupper(drive1) != towupper(drive2); +} + +HMODULE WINAPI usvfs::hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile, + DWORD dwFlags) +{ + HMODULE res = nullptr; + + HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY) + const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName); + + PRE_REALCALL + res = LoadLibraryExW(fileName.c_str(), hFile, dwFlags); + POST_REALCALL + + HOOK_END + return res; +} + +HMODULE WINAPI usvfs::hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile, + DWORD dwFlags) +{ + HMODULE res = nullptr; + + HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + PRE_REALCALL + res = ::LoadLibraryExW(reroute.fileName(), hFile, dwFlags); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +/// determine name of the binary to run based on parameters for createprocess +std::wstring getBinaryName(LPCWSTR applicationName, LPCWSTR lpCommandLine) +{ + if (applicationName != nullptr) { + std::pair<std::wstring, std::wstring> fullPath = + winapi::wide::getFullPathName(applicationName); + return fullPath.second; + } else { + if (lpCommandLine[0] == '"') { + const wchar_t* endQuote = wcschr(lpCommandLine, '"'); + if (endQuote != nullptr) { + return std::wstring(lpCommandLine + 1, endQuote - 1); + } + } + + // according to the documentation, if the commandline is unquoted and has + // spaces, it will be interpreted in multiple ways, i.e. + // c:\program.exe files\sub dir\program name + // c:\program files\sub.exe dir\program name + // c:\program files\sub dir\program.exe name + // c:\program files\sub dir\program name.exe + LPCWSTR space = wcschr(lpCommandLine, L' '); + while (space != nullptr) { + std::wstring subString(lpCommandLine, space); + bool isDirectory = true; + if (winapi::ex::wide::fileExists(subString.c_str(), &isDirectory) && + !isDirectory) { + return subString; + } else { + space = wcschr(space + 1, L' '); + } + } + return std::wstring(lpCommandLine); + } +} + +BOOL(WINAPI* usvfs::CreateProcessInternalW)( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); + +BOOL WINAPI usvfs::hook_CreateProcessInternalW( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::CREATE_PROCESS) + if (!callContext.active()) { + res = CreateProcessInternalW( + token, lpApplicationName, lpCommandLine, lpProcessAttributes, + lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, + lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken); + callContext.updateLastError(); + return res; + } + + // remember if the caller wanted the process to be suspended. If so, we + // don't resume when we're done + BOOL susp = dwCreationFlags & CREATE_SUSPENDED; + dwCreationFlags |= CREATE_SUSPENDED; + + RerouteW applicationReroute; + RerouteW cmdReroute; + LPWSTR cend = nullptr; + + std::wstring dllPath; + usvfsParameters callParameters; + + { // scope for context lock + auto context = READ_CONTEXT(); + + if (RerouteW::interestingPath(lpCommandLine)) { + // First "argument" in the commandline is the command, we need to identify it and + // reroute it: + if (*lpCommandLine == '"') { + // If the first argument is quoted we trust its is quoted correctly + for (cend = lpCommandLine; *cend && *cend != ' '; ++cend) + if (*cend == '"') { + int escaped = 0; + for (++cend; *cend && (*cend != '"' || escaped % 2 != 0); ++cend) + escaped = *cend == '\\' ? escaped + 1 : 0; + } + + if (*(cend - 1) == '"') + --cend; + auto old_cend = *cend; + *cend = 0; + cmdReroute = RerouteW::create(context, callContext, lpCommandLine + 1); + *cend = old_cend; + if (old_cend == '"') + ++cend; + } else { + // If the first argument we have no choice but to test all the options to quote + // the command as the real CreateProcess will do this: + cend = lpCommandLine; + while (true) { + while (*cend && *cend != ' ') + ++cend; + + auto old_cend = *cend; + *cend = 0; + cmdReroute = RerouteW::create(context, callContext, lpCommandLine); + *cend = old_cend; + if (cmdReroute.wasRerouted() || pathIsFile(cmdReroute.fileName())) + break; + + while (*cend == ' ') + ++cend; + + if (!*cend) { + // if we reached the end of the string we'll just use the whole commandline + // as is: + cend = nullptr; + break; + } + } + } + } + + applicationReroute = RerouteW::create(context, callContext, lpApplicationName); + + dllPath = context->dllPath(); + callParameters = context->callParameters(); + } + + std::wstring cmdline; + if (cend && cmdReroute.fileName()) { + auto fileName = cmdReroute.fileName(); + cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2); + if (*fileName != '"') + cmdline += L"\""; + cmdline += fileName; + if (*fileName != '"') + cmdline += L"\""; + cmdline += cend; + } + + PRE_REALCALL + res = CreateProcessInternalW(token, applicationReroute.fileName(), + cmdline.empty() ? lpCommandLine : &cmdline[0], + lpProcessAttributes, lpThreadAttributes, bInheritHandles, + dwCreationFlags, lpEnvironment, lpCurrentDirectory, + lpStartupInfo, lpProcessInformation, newToken); + POST_REALCALL + + BOOL blacklisted = FALSE; + { // limit scope of context + auto context = READ_CONTEXT(); + blacklisted = context->executableBlacklisted(applicationReroute.fileName(), + cmdReroute.fileName()); + } + + if (res) { + if (!blacklisted) { + try { + injectProcess(dllPath, callParameters, *lpProcessInformation); + } catch (const std::exception& e) { + spdlog::get("hooks")->error("failed to inject into {0}: {1}", + lpApplicationName != nullptr + ? applicationReroute.fileName() + : static_cast<LPCWSTR>(lpCommandLine), + e.what()); + } + } + + // resume unless process is supposed to start suspended + if (!susp && (ResumeThread(lpProcessInformation->hThread) == (DWORD)-1)) { + spdlog::get("hooks")->error("failed to inject into spawned process"); + res = FALSE; + } + } + + LOG_CALL() + .PARAM(lpApplicationName) + .PARAM(applicationReroute.fileName()) + .PARAM(cmdReroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()) + .PARAM(cmdline); + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_GetFileAttributesExA(LPCSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesExA(lpFileName, fInfoLevelId, lpFileInformation); + callContext.updateLastError(); + return res; + } + HOOK_END + + HOOK_START + const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName); + + PRE_REALCALL + res = GetFileAttributesExW(fileName.c_str(), fInfoLevelId, lpFileInformation); + POST_REALCALL + + HOOK_END + return res; +} + +BOOL WINAPI usvfs::hook_GetFileAttributesExW(LPCWSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesExW(lpFileName, fInfoLevelId, lpFileInformation); + callContext.updateLastError(); + return res; + } + + fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str()); + + PRE_REALCALL + res = ::GetFileAttributesExW(reroute.fileName(), fInfoLevelId, lpFileInformation); + POST_REALCALL + + DWORD originalError = callContext.lastError(); + DWORD fixedError = originalError; + // In case the target does not exist the error value varies to differentiate if the + // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND). + // If the original target's parent folder doesn't actually exist it may exist in the + // the virtualized sense, or if we rerouted the query the parent of the original path + // might exist while the parent of the rerouted path might not: + if (!res && fixedError == ERROR_PATH_NOT_FOUND) { + // first query original file parent (if we rerouted it): + fs::path originalParent = canonicalFile.parent_path(); + WIN32_FILE_ATTRIBUTE_DATA parentAttr; + if (reroute.wasRerouted() && + ::GetFileAttributesExW(originalParent.c_str(), GetFileExInfoStandard, + &parentAttr) && + (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + else { + // now query the rerouted path for parent (which can be different from the parent + // of the rerouted path) + RerouteW rerouteParent = + RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str()); + if (rerouteParent.wasRerouted() && + ::GetFileAttributesExW(rerouteParent.fileName(), GetFileExInfoStandard, + &parentAttr) && + (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + } + } + if (fixedError != originalError) + callContext.updateLastError(fixedError); + + if (reroute.wasRerouted() || fixedError != originalError) { + DWORD resAttrib; + if (res && fInfoLevelId == GetFileExInfoStandard && lpFileInformation) + resAttrib = reinterpret_cast<WIN32_FILE_ATTRIBUTE_DATA*>(lpFileInformation) + ->dwFileAttributes; + else + resAttrib = (DWORD)-1; + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(fInfoLevelId) + .PARAMHEX(res) + .PARAMHEX(resAttrib) + .PARAM(originalError) + .PARAM(fixedError); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFileAttributesA(LPCSTR lpFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesA(lpFileName); + callContext.updateLastError(); + return res; + } + HOOK_END + + HOOK_START + const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName); + + PRE_REALCALL + res = GetFileAttributesW(fileName.c_str()); + POST_REALCALL + + HOOK_END + return res; +} + +DWORD WINAPI usvfs::hook_GetFileAttributesW(LPCWSTR lpFileName) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesW(lpFileName); + callContext.updateLastError(); + return res; + } + + fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str()); + + if (reroute.wasRerouted()) + PRE_REALCALL + res = ::GetFileAttributesW(reroute.fileName()); + POST_REALCALL + + DWORD originalError = callContext.lastError(); + DWORD fixedError = originalError; + // In case the target does not exist the error value varies to differentiate if the + // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND). + // If the original target's parent folder doesn't actually exist it may exist in the + // the virtualized sense, or if we rerouted the query the parent of the original path + // might exist while the parent of the rerouted path might not: + if (res == INVALID_FILE_ATTRIBUTES && fixedError == ERROR_PATH_NOT_FOUND) { + // first query original file parent (if we rerouted it): + fs::path originalParent = canonicalFile.parent_path(); + DWORD attr; + if (reroute.wasRerouted() && + (attr = ::GetFileAttributesW(originalParent.c_str())) != + INVALID_FILE_ATTRIBUTES && + (attr & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + else { + // now query the rerouted path for parent (which can be different from the parent + // of the rerouted path) + RerouteW rerouteParent = + RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str()); + if (rerouteParent.wasRerouted() && + (attr = ::GetFileAttributesW(rerouteParent.fileName())) != + INVALID_FILE_ATTRIBUTES && + (attr & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + } + } + if (fixedError != originalError) + callContext.updateLastError(fixedError); + + if (reroute.wasRerouted() || fixedError != originalError) { + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(originalError) + .PARAM(fixedError); + } + + HOOK_ENDP(lpFileName); + + return res; +} + +DWORD WINAPI usvfs::hook_SetFileAttributesW(LPCWSTR lpFileName, DWORD dwFileAttributes) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + PRE_REALCALL + res = ::SetFileAttributesW(reroute.fileName(), dwFileAttributes); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL().PARAM(reroute.fileName()).PARAM(res).PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::DELETE_FILE) + // Why is the usual if (!callContext.active()... check missing? + + const std::wstring path = + RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)).wstring(); + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, path.c_str()); + + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::DeleteFileW(reroute.fileName()); + } else { + res = ::DeleteFileW(path.c_str()); + } + POST_REALCALL + + if (res) { + reroute.removeMapping(READ_CONTEXT()); + } + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +BOOL rewriteChangedDrives(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + const usvfs::RerouteW& readReroute, + const usvfs::CreateRerouter& writeReroute) +{ + return ((readReroute.wasRerouted() || writeReroute.wasRerouted()) && + pathsOnDifferentDrives(readReroute.fileName(), writeReroute.fileName()) && + !pathsOnDifferentDrives(lpExistingFileName, lpNewFileName)); +} + +BOOL WINAPI usvfs::hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileA(lpExistingFileName, lpNewFileName); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const auto& existingFileName = ush::string_cast<std::wstring>(lpExistingFileName); + const auto& newFileName = ush::string_cast<std::wstring>(lpNewFileName); + + PRE_REALCALL + res = MoveFileW(existingFileName.c_str(), newFileName.c_str()); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileW(lpExistingFileName, lpNewFileName); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = 0; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, false, + "hook_MoveFileW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + if (movedDrives) + newFlags |= MOVEFILE_COPY_ALLOWED; + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else if (newFlags) + res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags); + else + res = ::MoveFileW(readReroute.fileName(), writeReroute.fileName()); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + readReroute.removeMapping( + READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted + // file entries should make this okay + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, + DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileExA(lpExistingFileName, lpNewFileName, dwFlags); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const std::wstring existingFileName = + ush::string_cast<std::wstring>(lpExistingFileName); + + // careful: lpNewFileName can be null if dwFlags is + // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure + // the null pointer is forwarded correctly + std::wstring newFileNameWstring; + const wchar_t* newFileName = nullptr; + + if (lpNewFileName) { + newFileNameWstring = ush::string_cast<std::wstring>(lpNewFileName); + newFileName = newFileNameWstring.c_str(); + } + + PRE_REALCALL + res = MoveFileExW(existingFileName.c_str(), newFileName, dwFlags); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileExW(lpExistingFileName, lpNewFileName, dwFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = dwFlags; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, + newFlags & MOVEFILE_REPLACE_EXISTING, + "hook_MoveFileExW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else + res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + readReroute.removeMapping( + READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted + // file entries should make this okay + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(dwFlags) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileWithProgressA(LPCSTR lpExistingFileName, + LPCSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileWithProgressA(lpExistingFileName, lpNewFileName, lpProgressRoutine, + lpData, dwFlags); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const auto& existingFileName = ush::string_cast<std::wstring>(lpExistingFileName); + + // careful: lpNewFileName can be null if dwFlags is + // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure + // the null pointer is forwarded correctly + std::wstring newFileNameWstring; + const wchar_t* newFileName = nullptr; + + if (lpNewFileName) { + newFileNameWstring = ush::string_cast<std::wstring>(lpNewFileName); + newFileName = newFileNameWstring.c_str(); + } + + PRE_REALCALL + res = MoveFileWithProgressW(existingFileName.c_str(), newFileName, lpProgressRoutine, + lpData, dwFlags); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags) +{ + + // TODO: Remove all redundant hooks to moveFile alternatives. + // it would appear that all other moveFile functions end up calling this one with no + // additional code. + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileWithProgressW(lpExistingFileName, lpNewFileName, lpProgressRoutine, + lpData, dwFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + usvfs::CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = dwFlags; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, + newFlags & MOVEFILE_REPLACE_EXISTING, + "hook_MoveFileWithProgressW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + if (movedDrives) + newFlags |= MOVEFILE_COPY_ALLOWED; + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else + res = ::MoveFileWithProgressW(readReroute.fileName(), writeReroute.fileName(), + lpProgressRoutine, lpData, newFlags); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + // TODO: this call causes the node to be removed twice in case of + // MOVEFILE_COPY_ALLOWED as the deleteFile hook lower level already takes care of + // it, but deleteFile can't be disabled since we are relying on it in case of + // MOVEFILE_REPLACE_EXISTING for the destination file. + readReroute.removeMapping( + READ_CONTEXT(), + isDirectory); // Updating the rerouteCreate to check deleted file entries + // should make this okay (not related to comments above) + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(dwFlags) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_CopyFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, LPVOID lpData, + LPBOOL pbCancel, DWORD dwCopyFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = CopyFileExW(lpExistingFileName, lpNewFileName, lpProgressRoutine, lpData, + pbCancel, dwCopyFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + usvfs::CreateRerouter writeReroute; + bool callOriginal = true; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew( + context, callContext, lpNewFileName, + (dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0, "hook_CopyFileExW"); + } + + if (callOriginal) { + PRE_REALCALL + res = ::CopyFileExW(readReroute.fileName(), writeReroute.fileName(), + lpProgressRoutine, lpData, pbCancel, dwCopyFlags); + POST_REALCALL + writeReroute.updateResult(callContext, res); + + if (res && writeReroute.newReroute()) + writeReroute.insertMapping(WRITE_CONTEXT()); + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer) +{ + DWORD res = 0; + + HOOK_START + + std::wstring buffer; + buffer.resize(nBufferLength); + + PRE_REALCALL + res = GetCurrentDirectoryW(nBufferLength, &buffer[0]); + POST_REALCALL + + if (res > 0) { + res = WideCharToMultiByte(CP_ACP, 0, buffer.c_str(), res + 1, lpBuffer, + nBufferLength, nullptr, nullptr); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer) +{ + DWORD res = FALSE; + + HOOK_START + + std::wstring actualCWD; + + if (!k32CurrentDirectoryTracker.get(actualCWD)) { + PRE_REALCALL + res = ::GetCurrentDirectoryW(nBufferLength, lpBuffer); + POST_REALCALL + } else { + ush::wcsncpy_sz(lpBuffer, &actualCWD[0], + std::min(static_cast<size_t>(nBufferLength), actualCWD.size() + 1)); + + // yupp, that's how GetCurrentDirectory actually works... + if (actualCWD.size() < nBufferLength) { + res = static_cast<DWORD>(actualCWD.size()); + } else { + res = static_cast<DWORD>(actualCWD.size() + 1); + } + } + + if (nBufferLength) + LOG_CALL() + .PARAM(std::wstring(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(actualCWD.size()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_SetCurrentDirectoryA(LPCSTR lpPathName) +{ + return SetCurrentDirectoryW(ush::string_cast<std::wstring>(lpPathName).c_str()); +} + +BOOL WINAPI usvfs::hook_SetCurrentDirectoryW(LPCWSTR lpPathName) +{ + BOOL res = FALSE; + + HOOK_START + + const fs::path& realPath = RerouteW::canonizePath(RerouteW::absolutePath(lpPathName)); + const std::wstring& realPathStr = realPath.wstring(); + std::wstring finalRoute; + BOOL found = FALSE; + + if (fs::exists(realPath)) + finalRoute = realPathStr; + else { + WCHAR processDir[MAX_PATH]; + if (::GetModuleFileNameW(NULL, processDir, MAX_PATH) != 0 && + ::PathRemoveFileSpecW(processDir)) { + WCHAR processName[MAX_PATH]; + ::GetModuleFileNameW(NULL, processName, MAX_PATH); + fs::path routedName = realPath / processName; + RerouteW rerouteTest = + RerouteW::create(READ_CONTEXT(), callContext, routedName.wstring().c_str()); + if (rerouteTest.wasRerouted()) { + std::wstring reroutedPath = rerouteTest.fileName(); + if (routedName.wstring().find(processDir) != std::string::npos) { + fs::path finalPath(reroutedPath); + finalRoute = finalPath.parent_path().wstring(); + found = TRUE; + } + } + } + + if (!found) { + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, realPathStr.c_str()); + finalRoute = reroute.fileName(); + } + } + + PRE_REALCALL + res = ::SetCurrentDirectoryW(finalRoute.c_str()); + POST_REALCALL + + if (res) + if (!k32CurrentDirectoryTracker.set(realPathStr)) + spdlog::get("usvfs")->warn("Updating actual current directory failed: {} ?!", + string_cast<std::string>(realPathStr)); + + LOG_CALL() + .PARAM(lpPathName) + .PARAM(realPathStr) + .PARAM(finalRoute) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DLLEXPORT BOOL WINAPI usvfs::hook_CreateDirectoryW( + LPCWSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes) +{ + BOOL res = FALSE; + HOOK_START + + RerouteW reroute = RerouteW::createOrNew(READ_CONTEXT(), callContext, lpPathName); + + PRE_REALCALL + res = ::CreateDirectoryW(reroute.fileName(), lpSecurityAttributes); + POST_REALCALL + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT(), true); + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpPathName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DLLEXPORT BOOL WINAPI usvfs::hook_RemoveDirectoryW(LPCWSTR lpPathName) +{ + + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::DELETE_FILE) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpPathName); + + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::RemoveDirectoryW(reroute.fileName()); + } else { + res = ::RemoveDirectoryW(lpPathName); + } + POST_REALCALL + + if (res) { + reroute.removeMapping(READ_CONTEXT(), true); + } + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpPathName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength, + LPSTR lpBuffer, LPSTR* lpFilePart) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME) + if (!callContext.active()) { + res = GetFullPathNameA(lpFileName, nBufferLength, lpBuffer, lpFilePart); + callContext.updateLastError(); + return res; + } + + std::string resolvedWithCMD; + + std::wstring actualCWD; + fs::path filePath = ush::string_cast<std::wstring>(lpFileName, CodePage::UTF8); + if (k32CurrentDirectoryTracker.get(actualCWD, filePath.wstring().c_str())) { + if (!filePath.is_absolute()) + resolvedWithCMD = ush::string_cast<std::string>( + (actualCWD / filePath.relative_path()).wstring()); + } + + PRE_REALCALL + res = + ::GetFullPathNameA(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(), + nBufferLength, lpBuffer, lpFilePart); + POST_REALCALL + + if (false && nBufferLength) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(resolvedWithCMD) + .PARAM(std::string(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength, + LPWSTR lpBuffer, LPWSTR* lpFilePart) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME) + if (!callContext.active()) { + res = GetFullPathNameW(lpFileName, nBufferLength, lpBuffer, lpFilePart); + callContext.updateLastError(); + return res; + } + + std::wstring resolvedWithCMD; + + std::wstring actualCWD; + if (k32CurrentDirectoryTracker.get(actualCWD, lpFileName)) { + fs::path filePath = lpFileName; + if (!filePath.is_absolute()) + resolvedWithCMD = (actualCWD / filePath.relative_path()).wstring(); + } + + PRE_REALCALL + res = + ::GetFullPathNameW(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(), + nBufferLength, lpBuffer, lpFilePart); + POST_REALCALL + + if (false && nBufferLength) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(resolvedWithCMD) + .PARAM(std::wstring(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename, + DWORD nSize) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + + PRE_REALCALL + res = ::GetModuleFileNameA(hModule, lpFilename, nSize); + POST_REALCALL + if ((res != 0) && callContext.active()) { + std::vector<char> buf; + // If GetModuleFileNameA failed because the buffer is not large enough this + // complicates matters because we are dealing with incomplete information + // (consider for example the case that we have a long real path which will + // be routed to a short virtual so the call should actually succeed in such + // a case). To solve this we simply use our own buffer to find the complete + // module path: + DWORD full_res = res; + size_t buf_size = nSize; + while (full_res == buf_size) { + buf_size = std::max(static_cast<size_t>(MAX_PATH), buf_size * 2); + buf.resize(buf_size); + full_res = ::GetModuleFileNameA(hModule, buf.data(), buf_size); + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, + ush::string_cast<std::wstring>(buf.empty() ? lpFilename : buf.data()).c_str(), + true); + if (reroute.wasRerouted()) { + DWORD reroutedSize = static_cast<DWORD>(wcslen(reroute.fileName())); + if (reroutedSize >= nSize) { + reroutedSize = nSize - 1; + callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER); + res = nSize; + } else + res = reroutedSize; + memcpy(lpFilename, ush::string_cast<std::string>(reroute.fileName()).c_str(), + reroutedSize * sizeof(lpFilename[0])); + lpFilename[reroutedSize] = 0; + + LOG_CALL() + .PARAM(hModule) + .addParam("lpFilename", (res != 0UL) ? lpFilename : "<not set>") + .PARAM(nSize) + .PARAM(res) + .PARAM(callContext.lastError()); + } + } + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename, + DWORD nSize) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + + PRE_REALCALL + res = ::GetModuleFileNameW(hModule, lpFilename, nSize); + POST_REALCALL + if ((res != 0) && callContext.active()) { + std::vector<WCHAR> buf; + // If GetModuleFileNameW failed because the buffer is not large enough this + // complicates matters because we are dealing with incomplete information (consider + // for example the case that we have a long real path which will be routed to a + // short virtual so the call should actually succeed in such a case). To solve this + // we simply use our own buffer to find the complete module path: + DWORD full_res = res; + size_t buf_size = nSize; + while (full_res == buf_size) { + buf_size = std::max(static_cast<size_t>(MAX_PATH), buf_size * 2); + buf.resize(buf_size); + full_res = ::GetModuleFileNameW(hModule, buf.data(), buf_size); + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, + buf.empty() ? lpFilename : buf.data(), true); + if (reroute.wasRerouted()) { + DWORD reroutedSize = static_cast<DWORD>(wcslen(reroute.fileName())); + if (reroutedSize >= nSize) { + reroutedSize = nSize - 1; + callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER); + res = nSize; + } else + res = reroutedSize; + memcpy(lpFilename, reroute.fileName(), reroutedSize * sizeof(lpFilename[0])); + lpFilename[reroutedSize] = 0; + + LOG_CALL() + .PARAM(hModule) + .addParam("lpFilename", (res != 0UL) ? lpFilename : L"<not set>") + .PARAM(nSize) + .PARAM(res) + .PARAM(callContext.lastError()); + } + } + HOOK_END + + return res; +} + +HANDLE WINAPI usvfs::hook_FindFirstFileExW( + LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData, + FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags) +{ + HANDLE res = INVALID_HANDLE_VALUE; + + HOOK_START_GROUP(MutExHookGroup::SEARCH_FILES) + if (!callContext.active()) { + res = FindFirstFileExW(lpFileName, fInfoLevelId, lpFindFileData, fSearchOp, + lpSearchFilter, dwAdditionalFlags); + callContext.updateLastError(); + return res; + } + + // FindFirstFileEx() must fail early if the path ends with a slash + if (lpFileName) { + const auto len = wcslen(lpFileName); + if (len > 0) { + if (lpFileName[len - 1] == L'\\' || lpFileName[len - 1] == L'/') { + spdlog::get("usvfs")->warn( + "hook_FindFirstFileExW(): path '{}' ends with slash, always fails", + fs::path(lpFileName).string()); + return INVALID_HANDLE_VALUE; + } + } + } + + fs::path finalPath; + RerouteW reroute; + fs::path originalPath; + + bool usedRewrite = false; + + // We need to do some trickery here, since we only want to use the hooked + // NtQueryDirectoryFile for rerouted locations we need to check if the Directory path + // has been routed instead of the full path. + originalPath = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + PRE_REALCALL + res = ::FindFirstFileExW(originalPath.c_str(), fInfoLevelId, lpFindFileData, + fSearchOp, lpSearchFilter, dwAdditionalFlags); + POST_REALCALL + + if (res == INVALID_HANDLE_VALUE) { + fs::path searchPath = originalPath.filename(); + fs::path parentPath = originalPath.parent_path(); + std::wstring findPath = parentPath.wstring(); + while (findPath.find(L"*?<>\"", 0, 1) != std::wstring::npos) { + searchPath = parentPath.filename() / searchPath; + parentPath = parentPath.parent_path(); + findPath = parentPath.wstring(); + } + reroute = RerouteW::create(READ_CONTEXT(), callContext, parentPath.c_str()); + if (reroute.wasRerouted()) { + finalPath = reroute.fileName(); + finalPath /= searchPath.wstring(); + } + if (!finalPath.empty()) { + PRE_REALCALL + usedRewrite = true; + res = ::FindFirstFileExW(finalPath.c_str(), fInfoLevelId, lpFindFileData, + fSearchOp, lpSearchFilter, dwAdditionalFlags); + POST_REALCALL + } + } + + if (res != INVALID_HANDLE_VALUE) { + // store the original search path for use during iteration + WRITE_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[res] = lpFileName; + } + + LOG_CALL() + .PARAM(lpFileName) + .PARAM(originalPath.c_str()) + .PARAM(res) + .PARAM(callContext.lastError()); + if (usedRewrite) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(finalPath.c_str()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +HRESULT(WINAPI* usvfs::CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); + +HRESULT WINAPI usvfs::hook_CopyFile2(PCWSTR pwszExistingFileName, + PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters) +{ + HRESULT res = E_FAIL; + + typedef HRESULT(WINAPI * CopyFile2_t)(PCWSTR, PCWSTR, COPYFILE2_EXTENDED_PARAMETERS*); + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = CopyFile2(pwszExistingFileName, pwszNewFileName, pExtendedParameters); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, pwszExistingFileName); + callOriginal = writeReroute.rerouteNew( + context, callContext, pwszNewFileName, + pExtendedParameters && + (pExtendedParameters->dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0, + "hook_CopyFile2"); + } + + if (callOriginal) { + PRE_REALCALL + res = + CopyFile2(readReroute.fileName(), writeReroute.fileName(), pExtendedParameters); + POST_REALCALL + writeReroute.updateResult(callContext, SUCCEEDED(res)); + + if (SUCCEEDED(res) && writeReroute.newReroute()) + writeReroute.insertMapping(WRITE_CONTEXT()); + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpDefault, + LPSTR lpReturnedString, DWORD nSize, + LPCSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileStringA(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str()); + + PRE_REALCALL + res = ::GetPrivateProfileStringA( + lpAppName, lpKeyName, lpDefault, lpReturnedString, nSize, + ush::string_cast<std::string>(reroute.fileName()).c_str()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName, + LPCWSTR lpDefault, + LPWSTR lpReturnedString, DWORD nSize, + LPCWSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + + PRE_REALCALL + res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, reroute.fileName()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileSectionA(LPCSTR lpAppName, + LPSTR lpReturnedString, DWORD nSize, + LPCSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileSectionA(lpAppName, lpReturnedString, nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str()); + + PRE_REALCALL + res = ::GetPrivateProfileSectionA( + lpAppName, lpReturnedString, nSize, + ush::string_cast<std::string>(reroute.fileName()).c_str()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileSectionW(LPCWSTR lpAppName, + LPWSTR lpReturnedString, DWORD nSize, + LPCWSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + + PRE_REALCALL + res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize, + reroute.fileName()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_WritePrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpString, LPCSTR lpFileName) +{ + BOOL res = false; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::WritePrivateProfileStringA(lpAppName, lpKeyName, lpString, lpFileName); + callContext.updateLastError(); + return res; + } + + CreateRerouter reroute; + bool callOriginal = reroute.rerouteNew( + READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str(), + true, "hook_WritePrivateProfileStringA"); + + if (callOriginal) { + PRE_REALCALL + res = ::WritePrivateProfileStringA( + lpAppName, lpKeyName, lpString, + ush::string_cast<std::string>(reroute.fileName()).c_str()); + POST_REALCALL + reroute.updateResult(callContext, res); + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT()); + + if (reroute.wasRerouted() || reroute.changedError()) + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(reroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_WritePrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName, + LPCWSTR lpString, LPCWSTR lpFileName) +{ + BOOL res = false; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString, lpFileName); + callContext.updateLastError(); + return res; + } + + CreateRerouter reroute; + bool callOriginal = reroute.rerouteNew(READ_CONTEXT(), callContext, lpFileName, true, + "hook_WritePrivateProfileStringW"); + + if (callOriginal) { + PRE_REALCALL + res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString, + reroute.fileName()); + POST_REALCALL + reroute.updateResult(callContext, res); + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT()); + + if (reroute.wasRerouted() || reroute.changedError()) + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(reroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +VOID WINAPI usvfs::hook_ExitProcess(UINT exitCode) +{ + HOOK_START + + { + HookContext::Ptr context = WRITE_CONTEXT(); + + std::vector<std::future<int>>& delayed = context->delayed(); + + if (!delayed.empty()) { + // ensure all delayed tasks are completed before we exit the process + for (std::future<int>& delayedOp : delayed) { + delayedOp.get(); + } + delayed.clear(); + } + } + + // exitprocess doesn't return so logging the call after the real call doesn't + // make much sense. + // nor does any pre/post call macro + LOG_CALL().PARAM(exitCode); + + usvfsDisconnectVFS(); + + // HookManager::instance().removeHook("ExitProcess"); + // PRE_REALCALL + ::ExitProcess(exitCode); + // POST_REALCALL + + HOOK_END +} diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.h b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h new file mode 100644 index 0000000..7087b2e --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h @@ -0,0 +1,116 @@ +#pragma once + +#include "ntdll_declarations.h" +#include <dllimport.h> + +namespace usvfs +{ + +DLLEXPORT BOOL WINAPI hook_GetFileAttributesExA(LPCSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation); +DLLEXPORT BOOL WINAPI hook_GetFileAttributesExW(LPCWSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation); +DLLEXPORT DWORD WINAPI hook_GetFileAttributesA(LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetFileAttributesW(LPCWSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_SetFileAttributesW(LPCWSTR lpFileName, + DWORD dwFileAttributes); + +DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer); +DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer); +DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryA(LPCSTR lpPathName); +DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryW(LPCWSTR lpPathName); + +DLLEXPORT DWORD WINAPI hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength, + LPSTR lpBuffer, LPSTR* lpFilePart); +DLLEXPORT DWORD WINAPI hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength, + LPWSTR lpBuffer, LPWSTR* lpFilePart); + +DLLEXPORT BOOL WINAPI hook_CreateDirectoryW(LPCWSTR lpPathName, + LPSECURITY_ATTRIBUTES lpSecurityAttributes); +DLLEXPORT BOOL WINAPI hook_RemoveDirectoryW(LPCWSTR lpPathName); + +DLLEXPORT BOOL WINAPI hook_DeleteFileW(LPCWSTR lpFileName); + +DLLEXPORT BOOL WINAPI hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName); +DLLEXPORT BOOL WINAPI hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName); +DLLEXPORT BOOL WINAPI hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, + DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileExW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressA(LPCSTR lpExistingFileName, + LPCSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags); +; + +DLLEXPORT BOOL WINAPI hook_CopyFileExW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, LPBOOL pbCancel, + DWORD dwCopyFlags); + +extern HRESULT(WINAPI* CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); +DLLEXPORT HRESULT WINAPI +hook_CopyFile2(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); + +DLLEXPORT HMODULE WINAPI hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile, + DWORD dwFlags); +DLLEXPORT HMODULE WINAPI hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile, + DWORD dwFlags); + +extern BOOL(WINAPI* CreateProcessInternalW)( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); +DLLEXPORT BOOL WINAPI hook_CreateProcessInternalW( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); + +DLLEXPORT DWORD WINAPI hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename, + DWORD nSize); +DLLEXPORT DWORD WINAPI hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename, + DWORD nSize); + +DLLEXPORT HANDLE WINAPI hook_FindFirstFileExW( + LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData, + FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags); + +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpDefault, + LPSTR lpReturnedString, + DWORD nSize, LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringW(LPCWSTR lpAppName, + LPCWSTR lpKeyName, + LPCWSTR lpDefault, + LPWSTR lpReturnedString, + DWORD nSize, LPCWSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionA(LPCSTR lpAppName, + LPSTR lpReturnedString, + DWORD nSize, LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionW(LPCWSTR lpAppName, + LPWSTR lpReturnedString, + DWORD nSize, LPCWSTR lpFileName); +DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringA(LPCSTR lpAppName, + LPCSTR lpKeyName, LPCSTR lpString, + LPCSTR lpFileName); +DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringW(LPCWSTR lpAppName, + LPCWSTR lpKeyName, + LPCWSTR lpString, + LPCWSTR lpFileName); + +DLLEXPORT VOID WINAPI hook_ExitProcess(UINT exitCode); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp new file mode 100644 index 0000000..ad53418 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp @@ -0,0 +1,1520 @@ +#include "ntdll.h" + +#include <mutex> +#include <queue> +#include <set> + +#include <boost/filesystem.hpp> + +#include <addrtools.h> +#include <loghelpers.h> +#include <stringcast.h> +#include <stringutils.h> +#include <unicodestring.h> +#include <usvfs.h> + +#include "../hookcallcontext.h" +#include "../hookcontext.h" +#include "../maptracker.h" +#include "../stringcast_boost.h" + +#include "file_information_utils.h" +#include "sharedids.h" + +namespace ulog = usvfs::log; +namespace ush = usvfs::shared; +namespace bfs = boost::filesystem; + +using usvfs::UnicodeString; + +// flag definitions below are copied from winternl.h +#define FILE_SUPERSEDE 0x00000000 +#define FILE_OPEN 0x00000001 +#define FILE_CREATE 0x00000002 +#define FILE_OPEN_IF 0x00000003 +#define FILE_OVERWRITE 0x00000004 +#define FILE_OVERWRITE_IF 0x00000005 +#define FILE_MAXIMUM_DISPOSITION 0x00000005 + +template <typename T> +using unique_ptr_deleter = std::unique_ptr<T, void (*)(T*)>; + +class RedirectionInfo +{ +public: + UnicodeString path; + bool redirected; + + RedirectionInfo() {} + RedirectionInfo(UnicodeString path, bool redirected) + : path(path), redirected(redirected) + {} +}; + +class HandleTracker +{ +public: + using handle_type = HANDLE; + using info_type = UnicodeString; + + HandleTracker() { insert_current_directory(); } + + info_type lookup(handle_type handle) const + { + if (valid_handle(handle)) { + std::shared_lock<std::shared_mutex> lock(m_mutex); + auto find = m_map.find(handle); + if (find != m_map.end()) + return find->second; + } + return info_type(); + } + + void insert(handle_type handle, const info_type& info) + { + if (!valid_handle(handle)) + return; + std::unique_lock<std::shared_mutex> lock(m_mutex); + m_map[handle] = info; + } + + void erase(handle_type handle) + { + if (!valid_handle(handle)) + return; + std::unique_lock<std::shared_mutex> lock(m_mutex); + m_map.erase(handle); + } + +private: + static bool valid_handle(handle_type handle) + { + return handle && handle != INVALID_HANDLE_VALUE; + } + + void insert_current_directory() + { + ntdll_declarations_init(); + + UNICODE_STRING p{0}; + RTL_RELATIVE_NAME r{0}; + NTSTATUS status = + RtlDosPathNameToRelativeNtPathName_U_WithStatus(L"x", &p, nullptr, &r); + if (status >= 0) { + if (r.ContainingDirectory && p.Buffer) { + size_t len = p.Length / sizeof(WCHAR); + size_t trim = strlen("\\x") + (p.Buffer[len - 1] ? 0 : 1); + if (len > trim) + insert(r.ContainingDirectory, info_type(p.Buffer, len - trim)); + } + RtlReleaseRelativeName(&r); + if (p.Buffer) + HeapFree(GetProcessHeap(), 0, p.Buffer); + } + } + + mutable std::shared_mutex m_mutex; + std::unordered_map<handle_type, info_type> m_map; +}; + +HandleTracker ntdllHandleTracker; + +UnicodeString CreateUnicodeString(const OBJECT_ATTRIBUTES* objectAttributes) +{ + UnicodeString result = ntdllHandleTracker.lookup(objectAttributes->RootDirectory); + if (objectAttributes->ObjectName != nullptr) { + result.appendPath(objectAttributes->ObjectName); + } + return result; +} + +std::ostream& operator<<(std::ostream& os, const _UNICODE_STRING& str) +{ + try { + // TODO this does not correctly support surrogate pairs + // since the size used here is the number of 16-bit characters in the buffer + // whereas + // toNarrow expects the actual number of characters. + // It will always underestimate though, so worst case scenario we truncate + // the string + os << ush::string_cast<std::string>(str.Buffer, ush::CodePage::UTF8, + str.Length / sizeof(WCHAR)); + } catch (const std::exception& e) { + os << e.what(); + } + + return os; +} + +std::ostream& operator<<(std::ostream& os, POBJECT_ATTRIBUTES attr) +{ + return operator<<(os, *attr->ObjectName); +} + +RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context, + const usvfs::HookCallContext& callContext, + const UnicodeString& inPath) +{ + RedirectionInfo result; + result.path = inPath; + result.redirected = false; + + if (callContext.active()) { + // see if the file exists in the redirection tree + std::string lookupPath = ush::string_cast<std::string>( + static_cast<LPCWSTR>(result.path) + 4, ush::CodePage::UTF8); + auto node = context->redirectionTable()->findNode(lookupPath.c_str()); + // if so, replace the file name with the path to the mapped file + if ((node.get() != nullptr) && + (!node->data().linkTarget.empty() || node->isDirectory())) { + std::wstring reroutePath; + + if (node->data().linkTarget.length() > 0) { + reroutePath = ush::string_cast<std::wstring>(node->data().linkTarget.c_str(), + ush::CodePage::UTF8); + } else { + reroutePath = + ush::string_cast<std::wstring>(node->path().c_str(), ush::CodePage::UTF8); + } + if ((*reroutePath.rbegin() == L'\\') && (*lookupPath.rbegin() != '\\')) { + reroutePath.resize(reroutePath.size() - 1); + } + std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\'); + if (reroutePath[1] == L'\\') + reroutePath[1] = L'?'; + result.path = LR"(\??\)" + reroutePath; + result.redirected = true; + } + } + return result; +} + +RedirectionInfo applyReroute(const usvfs::CreateRerouter& rerouter) +{ + RedirectionInfo result; + result.path = rerouter.fileName(); + result.redirected = rerouter.wasRerouted(); + + std::wstring reroutePath(static_cast<LPCWSTR>(result.path)); + std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\'); + if (reroutePath[1] == L'\\') + reroutePath[1] = L'?'; + if (!((reroutePath[0] == L'\\') && (reroutePath[1] == L'?') && + (reroutePath[2] == L'?') && (reroutePath[3] == L'\\'))) { + result.path = LR"(\??\)" + reroutePath; + } + + return result; +} + +struct FindCreateTarget +{ + usvfs::RedirectionTree::NodePtrT target; + void operator()(usvfs::RedirectionTree::NodePtrT node) + { + if (node->hasFlag(usvfs::shared::FLAG_CREATETARGET)) { + target = node; + } + } +}; + +std::pair<UnicodeString, UnicodeString> +findCreateTarget(const usvfs::HookContext::ConstPtr& context, + const UnicodeString& inPath) +{ + std::pair<UnicodeString, UnicodeString> result; + result.first = inPath; + result.second = UnicodeString(); + + std::string lookupPath = ush::string_cast<std::string>( + static_cast<LPCWSTR>(result.first) + 4, ush::CodePage::UTF8); + FindCreateTarget visitor; + usvfs::RedirectionTree::VisitorFunction visitorWrapper = + [&](const usvfs::RedirectionTree::NodePtrT& node) { + visitor(node); + }; + context->redirectionTable()->visitPath(lookupPath, visitorWrapper); + if (visitor.target.get() != nullptr) { + bfs::path relativePath = + ush::make_relative(visitor.target->path(), bfs::path(lookupPath)); + + bfs::path target(visitor.target->data().linkTarget.c_str()); + target /= relativePath; + + result.second = UnicodeString(target.wstring().c_str()); + winapi::ex::wide::createPath(target.parent_path()); + } + return result; +} + +RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context, + const usvfs::HookCallContext& callContext, + POBJECT_ATTRIBUTES inAttributes) +{ + return applyReroute(context, callContext, CreateUnicodeString(inAttributes)); +} + +int NextDividableBy(int number, int divider) +{ + return static_cast<int>( + ceilf(static_cast<float>(number) / static_cast<float>(divider)) * divider); +} + +// Something is trying to create a variety of files starting with "\Device\", +// such as "\Device\DeviceApi\Dev\Query", "\Device\MMCSS\MmThread", +// "\Device\DeviceApi\CMNotify", etc. +// +// This used to create a bunch of spurious "Device" and "MMCSS" folders when +// using Explorer++. Some of these names seem to part of PnP, others from the +// "Multimedia Class Scheduler Service". +// +// There's basically zero information on this stuff online. +// +// Regardless, these files ended up being rerouted, so they became something +// like C:\game\Data\Somewhere\Device\MMCSS\MmThread, which ended up being +// rerouted to overwrite and created as a fake path +// "overwrite\Somewhere\Device\MMCSS" +// +// These paths are weird, they feel like pipes or something. It's not clear how +// they should be reliably recognized, so this is a hardcoded check for any +// path that starts with "\Device\". These paths will be forwarded directly +// to NtCreateFile/NtOpenFile +// +bool isDeviceFile(std::wstring_view name) +{ + static const std::wstring_view DevicePrefix(L"\\Device\\"); + + // starts with + if (name.size() < DevicePrefix.size()) { + return false; + } else { + return (_wcsnicmp(name.data(), DevicePrefix.data(), DevicePrefix.size()) == 0); + } +} + +NTSTATUS addNtSearchData(HANDLE hdl, PUNICODE_STRING FileName, + const std::wstring& fakeName, + FILE_INFORMATION_CLASS FileInformationClass, PVOID& buffer, + ULONG& bufferSize, std::set<std::wstring>& foundFiles, + HANDLE event, PIO_APC_ROUTINE apcRoutine, PVOID apcContext, + BOOLEAN returnSingleEntry) +{ + NTSTATUS res = STATUS_NO_SUCH_FILE; + if (hdl != INVALID_HANDLE_VALUE) { + PVOID lastValidRecord = nullptr; + PVOID bufferInit = buffer; + IO_STATUS_BLOCK status; + res = NtQueryDirectoryFile(hdl, event, apcRoutine, apcContext, &status, buffer, + bufferSize, FileInformationClass, returnSingleEntry, + FileName, FALSE); + + if ((res != STATUS_SUCCESS) || (status.Information <= 0)) { + bufferSize = 0UL; + } else { + ULONG totalOffset = 0; + PVOID lastSkipPos = nullptr; + + while (totalOffset < status.Information) { + ULONG offset; + std::wstring fileName; + GetFileInformationData(FileInformationClass, buffer, offset, fileName); + // in case this is a single-file search result and the specified + // filename differs from the file name found, replace it in the + // information structure + if ((totalOffset == 0) && (offset == 0) && (fakeName.length() > 0)) { + // if the fake name is larger than what is in the buffer and there is + // not enough room, that's a buffer overflow + if ((fakeName.length() > fileName.length()) && + ((fakeName.length() - fileName.length()) > + (bufferSize - status.Information))) { + res = STATUS_BUFFER_OVERFLOW; + break; + } + // WARNING for the case where the fake name is longer this needs to + // move back all further results and update the offset first + SetFileInformationFileName(FileInformationClass, buffer, fakeName); + fileName = fakeName; + } + bool add = true; + if (fileName.length() > 0) { + auto insertRes = foundFiles.insert(ush::to_upper(fileName)); + add = insertRes.second; // add only if we didn't find this file before + } + if (!add) { + if (lastSkipPos == nullptr) { + lastSkipPos = buffer; + } + } else { + if (lastSkipPos != nullptr) { + memmove(lastSkipPos, buffer, status.Information - totalOffset); + ULONG delta = static_cast<ULONG>(ush::AddrDiff(buffer, lastSkipPos)); + totalOffset -= delta; + + buffer = lastSkipPos; + lastSkipPos = nullptr; + } + lastValidRecord = buffer; + } + + if (offset == 0) { + offset = static_cast<ULONG>(status.Information) - totalOffset; + } + buffer = ush::AddrAdd(buffer, offset); + totalOffset += offset; + } + + if (lastSkipPos != nullptr) { + buffer = lastSkipPos; + bufferSize = static_cast<ULONG>(ush::AddrDiff(buffer, bufferInit)); + // null out the unused rest if there is some + memset(lastSkipPos, 0, status.Information - bufferSize); + } else { + bufferSize = static_cast<ULONG>(ush::AddrDiff(buffer, bufferInit)); + } + } + if (lastValidRecord != nullptr) { + SetFileInformationOffset(FileInformationClass, lastValidRecord, 0); + } + } + return res; +} + +DATA_ID(SearchInfo); + +struct Searches +{ + struct Info + { + struct VirtualMatch + { + // full path to where the file/directory actually is + std::wstring realPath; + // virtual filename (only filename since it has to be within the searched + // directory) + // this is left empty when a folder with all its content is mapped to the + // search directory + std::wstring virtualName; + }; + + Info() : currentSearchHandle(INVALID_HANDLE_VALUE) {} + std::set<std::wstring> foundFiles; + HANDLE currentSearchHandle; + std::queue<VirtualMatch> virtualMatches; + UnicodeString searchPattern; + bool regularComplete{false}; + }; + + Searches() = default; + + // must provide a special copy constructor because boost::mutex is + // non-copyable + Searches(const Searches& reference) : info(reference.info) {} + + Searches& operator=(const Searches&) = delete; + + std::recursive_mutex queryMutex; + + std::map<HANDLE, Info> info; +}; + +void gatherVirtualEntries(const UnicodeString& dirName, + const usvfs::RedirectionTreeContainer& redir, + PUNICODE_STRING FileName, Searches::Info& info) +{ + LPCWSTR dirNameW = static_cast<LPCWSTR>(dirName); + // fix directory name. I'd love to know why microsoft sometimes uses "\??\" vs + // "\\?\" + if ((wcsncmp(dirNameW, LR"(\\?\)", 4) == 0) || + (wcsncmp(dirNameW, LR"(\??\)", 4) == 0)) { + dirNameW += 4; + } + auto node = redir->findNode(boost::filesystem::path(dirNameW)); + if (node.get() != nullptr) { + std::string searchPattern = + FileName != nullptr + ? ush::string_cast<std::string>(FileName->Buffer, ush::CodePage::UTF8) + : "*.*"; + + boost::replace_all(searchPattern, "\"", "."); + + for (const auto& subNode : node->find(searchPattern)) { + if (((subNode->data().linkTarget.length() > 0) || subNode->isDirectory()) && + !subNode->hasFlag(usvfs::shared::FLAG_DUMMY)) { + std::wstring vName = + ush::string_cast<std::wstring>(subNode->name(), ush::CodePage::UTF8); + + Searches::Info::VirtualMatch m; + if (subNode->data().linkTarget.length() > 0) { + m = {ush::string_cast<std::wstring>(subNode->data().linkTarget.c_str(), + ush::CodePage::UTF8), + vName}; + } else { + m = {ush::string_cast<std::wstring>(subNode->path().c_str(), + ush::CodePage::UTF8), + vName}; + } + + info.virtualMatches.push(m); + info.foundFiles.insert(ush::to_upper(vName)); + } + } + } +} + +/** + * @brief insert a virtual entry into the search result + * @param FileInformation + * @param FileInformationClass + * @param info + * @param realPath path were the actual file resides + * @param virtualName virtual file name (without path). will often be the same + * as the name component of realpath + * @param ReturnSingleEntry + * @param dataRead + * @return true if a virtual result was added, false if the search handle in the + * info object yields no more results + */ +bool addVirtualSearchResult(PVOID& FileInformation, + FILE_INFORMATION_CLASS FileInformationClass, + Searches::Info& info, const std::wstring& realPath, + const std::wstring& virtualName, BOOLEAN ReturnSingleEntry, + ULONG& dataRead) +{ + // this opens a search in the real location, then copies the information about + // files we care about (the ones being mapped) to the result we intend to + // return + bfs::path fullPath(realPath); + if (fullPath.filename().wstring() == L".") { + fullPath = fullPath.parent_path(); + } + if (info.currentSearchHandle == INVALID_HANDLE_VALUE) { + std::wstring dirName = fullPath.parent_path().wstring(); + if (dirName.length() >= MAX_PATH && !ush::startswith(dirName.c_str(), LR"(\\?\)")) + dirName = LR"(\\?\)" + dirName; + info.currentSearchHandle = + CreateFileW(dirName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } + std::wstring fileName = + ush::string_cast<std::wstring>(fullPath.filename().string(), ush::CodePage::UTF8); + NTSTATUS subRes = addNtSearchData( + info.currentSearchHandle, + (fileName != L".") ? static_cast<PUNICODE_STRING>(UnicodeString(fileName.c_str())) + : nullptr, + virtualName, FileInformationClass, FileInformation, dataRead, info.foundFiles, + nullptr, nullptr, nullptr, ReturnSingleEntry); + if (subRes == STATUS_SUCCESS) { + return true; + } else { + // STATUS_NO_MORE_FILES merely means the search ended, everything else is an + // error message. Either way, the search here is finished and we should + // resume in the next mapped directory + if (subRes != STATUS_NO_MORE_FILES) { + spdlog::get("hooks")->warn("error reported listing files in {0}: {1:x}", + fullPath.string(), static_cast<uint32_t>(subRes)); + } + return false; + } +} + +NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFile( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, BOOLEAN RestartScan) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + // this is quite messy... + // first, this will gather the virtual locations mapping to the iterated one + // then we return results from the real location, skipping those that exist + // in the virtual locations, as those take precedence + // finally the virtual results are returned, adding each result to a skip + // list, so they don't get added twice + // + // if we don't add the regular files first, "." and ".." wouldn't be in the + // first search result of wildcard searches which may confuse the caller + NTSTATUS res = STATUS_NO_MORE_FILES; + HOOK_START_GROUP(MutExHookGroup::FIND_FILES) + if (!callContext.active()) { + return ::NtQueryDirectoryFile( + FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, FileInformation, + Length, FileInformationClass, ReturnSingleEntry, FileName, RestartScan); + } + + // std::unique_lock<std::recursive_mutex> queryLock; + std::map<HANDLE, Searches::Info>::iterator infoIter; + bool firstSearch = false; + + { // scope to limit context lifetime + HookContext::ConstPtr context = READ_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // queryLock = std::unique_lock<std::recursive_mutex>(activeSearches.queryMutex); + + if (RestartScan) { + auto iter = activeSearches.info.find(FileHandle); + if (iter != activeSearches.info.end()) { + activeSearches.info.erase(iter); + } + } + + // see if we already have a running search + infoIter = activeSearches.info.find(FileHandle); + firstSearch = (infoIter == activeSearches.info.end()); + } + + if (firstSearch) { + HookContext::Ptr context = WRITE_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // tradeoff time: we store this search status even if no virtual results + // were found. This causes a little extra cost here and in NtClose every + // time a non-virtual dir is being searched. However if we don't, + // whenever NtQueryDirectoryFile is called another time on the same handle, + // this (expensive) block would be run again. + infoIter = + activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first; + infoIter->second.searchPattern.appendPath(FileName); + + SearchHandleMap& searchMap = context->customData<SearchHandleMap>(SearchHandles); + SearchHandleMap::iterator iter = searchMap.find(FileHandle); + + UnicodeString searchPath; + if (iter != searchMap.end()) { + searchPath = UnicodeString(iter->second.c_str()); + infoIter->second.currentSearchHandle = CreateFileW( + iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + searchPath = ntdllHandleTracker.lookup(FileHandle); + } + gatherVirtualEntries(searchPath, context->redirectionTable(), FileName, + infoIter->second); + } + + ULONG dataRead = Length; + PVOID FileInformationCurrent = FileInformation; + + // add regular search results, skipping those files we have in a virtual + // location + bool moreRegular = !infoIter->second.regularComplete; + bool dataReturned = false; + while (moreRegular && !dataReturned) { + dataRead = Length; + + HANDLE handle = infoIter->second.currentSearchHandle; + if (handle == INVALID_HANDLE_VALUE) { + handle = FileHandle; + } + NTSTATUS subRes = addNtSearchData( + handle, FileName, L"", FileInformationClass, FileInformationCurrent, dataRead, + infoIter->second.foundFiles, Event, ApcRoutine, ApcContext, ReturnSingleEntry); + moreRegular = subRes == STATUS_SUCCESS; + if (moreRegular) { + dataReturned = dataRead != 0; + } else { + infoIter->second.regularComplete = true; + infoIter->second.foundFiles.clear(); + if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + if (!moreRegular) { + // add virtual results + while (!dataReturned && infoIter->second.virtualMatches.size() > 0) { + auto match = infoIter->second.virtualMatches.front(); + if (match.realPath.size() != 0) { + dataRead = Length; + if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass, + infoIter->second, match.realPath, match.virtualName, + ReturnSingleEntry, dataRead)) { + // a positive result here means the call returned data and there may + // be further objects to be retrieved by repeating the call + dataReturned = true; + } else { + // proceed to next search handle + + // TODO: doesn't append search results from more than one redirection + // per call. This is bad for performance but otherwise we'd need to + // re-write the offsets between information objects + infoIter->second.virtualMatches.pop(); + CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + } + + if (!dataReturned) { + if (firstSearch) { + res = STATUS_NO_SUCH_FILE; + } else { + res = STATUS_NO_MORE_FILES; + } + } else { + res = STATUS_SUCCESS; + } + IoStatusBlock->Status = res; + IoStatusBlock->Information = dataRead; + + size_t numVirtualFiles = infoIter->second.virtualMatches.size(); + if ((numVirtualFiles > 0)) { + LOG_CALL() + .addParam("path", ntdllHandleTracker.lookup(FileHandle)) + .PARAM(FileInformationClass) + .PARAM(FileName) + .PARAM(numVirtualFiles) + .PARAMWRAP(res); + } + + HOOK_END + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFileEx( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags, + PUNICODE_STRING FileName) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + NTSTATUS res = STATUS_NO_MORE_FILES; + + // this is quite messy... + // first, this will gather the virtual locations mapping to the iterated one + // then we return results from the real location, skipping those that exist + // in the virtual locations, as those take precedence + // finally the virtual results are returned, adding each result to a skip + // list, so they don't get added twice + // + // if we don't add the regular files first, "." and ".." wouldn't be in the + // first search result of wildcard searches which may confuse the caller + HOOK_START_GROUP(MutExHookGroup::FIND_FILES) + if (!callContext.active()) { + return ::NtQueryDirectoryFileEx(FileHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, FileInformation, Length, + FileInformationClass, QueryFlags, FileName); + } + + // std::unique_lock<std::recursive_mutex> queryLock; + std::map<HANDLE, Searches::Info>::iterator infoIter; + bool firstSearch = false; + + { // scope to limit context lifetime + HookContext::ConstPtr context = READ_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // queryLock = std::unique_lock<std::recursive_mutex>(activeSearches.queryMutex); + + if (QueryFlags & SL_RESTART_SCAN) { + auto iter = activeSearches.info.find(FileHandle); + if (iter != activeSearches.info.end()) { + activeSearches.info.erase(iter); + } + } + + // see if we already have a running search + infoIter = activeSearches.info.find(FileHandle); + firstSearch = (infoIter == activeSearches.info.end()); + } + + if (firstSearch) { + HookContext::Ptr context = WRITE_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // tradeoff time: we store this search status even if no virtual results + // were found. This causes a little extra cost here and in NtClose every + // time a non-virtual dir is being searched. However if we don't, + // whenever NtQueryDirectoryFile is called another time on the same handle, + // this (expensive) block would be run again. + infoIter = + activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first; + infoIter->second.searchPattern.appendPath(FileName); + + SearchHandleMap& searchMap = context->customData<SearchHandleMap>(SearchHandles); + SearchHandleMap::iterator iter = searchMap.find(FileHandle); + + UnicodeString searchPath; + if (iter != searchMap.end()) { + searchPath = UnicodeString(iter->second.c_str()); + infoIter->second.currentSearchHandle = CreateFileW( + iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + searchPath = ntdllHandleTracker.lookup(FileHandle); + } + gatherVirtualEntries(searchPath, context->redirectionTable(), FileName, + infoIter->second); + } + + ULONG dataRead = Length; + PVOID FileInformationCurrent = FileInformation; + + // add regular search results, skipping those files we have in a virtual + // location + bool moreRegular = !infoIter->second.regularComplete; + bool dataReturned = false; + while (moreRegular && !dataReturned) { + dataRead = Length; + + HANDLE handle = infoIter->second.currentSearchHandle; + if (handle == INVALID_HANDLE_VALUE) { + handle = FileHandle; + } + NTSTATUS subRes = addNtSearchData(handle, FileName, L"", FileInformationClass, + FileInformationCurrent, dataRead, + infoIter->second.foundFiles, Event, ApcRoutine, + ApcContext, QueryFlags & SL_RETURN_SINGLE_ENTRY); + moreRegular = subRes == STATUS_SUCCESS; + if (moreRegular) { + dataReturned = dataRead != 0; + } else { + infoIter->second.regularComplete = true; + infoIter->second.foundFiles.clear(); + if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + if (!moreRegular) { + // add virtual results + while (!dataReturned && infoIter->second.virtualMatches.size() > 0) { + auto match = infoIter->second.virtualMatches.front(); + if (match.realPath.size() != 0) { + dataRead = Length; + if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass, + infoIter->second, match.realPath, match.virtualName, + QueryFlags & SL_RETURN_SINGLE_ENTRY, dataRead)) { + // a positive result here means the call returned data and there may + // be further objects to be retrieved by repeating the call + dataReturned = true; + } else { + // proceed to next search handle + + // TODO: doesn't append search results from more than one redirection + // per call. This is bad for performance but otherwise we'd need to + // re-write the offsets between information objects + infoIter->second.virtualMatches.pop(); + CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + } + + if (!dataReturned) { + if (firstSearch) { + res = STATUS_NO_SUCH_FILE; + } else { + res = STATUS_NO_MORE_FILES; + } + } else { + res = STATUS_SUCCESS; + } + IoStatusBlock->Status = res; + IoStatusBlock->Information = dataRead; + + size_t numVirtualFiles = infoIter->second.virtualMatches.size(); + if ((numVirtualFiles > 0)) { + LOG_CALL() + .addParam("path", ntdllHandleTracker.lookup(FileHandle)) + .PARAM(FileInformationClass) + .PARAM(FileName) + .PARAM(QueryFlags) + .PARAM(numVirtualFiles) + .PARAMWRAP(res); + } + + HOOK_END + return res; +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryObject( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active()) { + return ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation, + ObjectInformationLength, ReturnLength); + } + + PRE_REALCALL + res = ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation, + ObjectInformationLength, ReturnLength); + POST_REALCALL + + // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be + // smaller than the original one + // + // we only handle STATUS_INFO_LENGTH_MISMATCH if ReturnLength is not NULL since + // this is only returned if the length is too small to hold the structure itself + // (regardless of the name), in which case, we need to compute our own ReturnLength + // + if ((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW || + (res == STATUS_INFO_LENGTH_MISMATCH && ReturnLength)) && + (ObjectInformationClass == ObjectNameInformation)) { + const auto trackerInfo = ntdllHandleTracker.lookup(Handle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + + OBJECT_NAME_INFORMATION* info = + reinterpret_cast<OBJECT_NAME_INFORMATION*>(ObjectInformation); + + if (redir.redirected) { + // https://learn.microsoft.com/en-us/windows/win32/fileio/displaying-volume-paths + // + + // TODO: is that always true? + // path should start with \??\X: - we need to replace this by device name + // + WCHAR deviceName[MAX_PATH]; + std::wstring buffer(static_cast<LPCWSTR>(trackerInfo)); + buffer[6] = L'\0'; + + QueryDosDeviceW(buffer.data() + 4, deviceName, ARRAYSIZE(deviceName)); + + buffer = std::wstring(deviceName) + L'\\' + + std::wstring(buffer.data() + 7, buffer.size() - 7); + + // the name is put in the buffer AFTER the struct, so the required size if + // sizeof(OBJECT_NAME_INFORMATION) + the number of bytes for the name + 2 bytes + // for a wide null character + const auto requiredLength = + sizeof(OBJECT_NAME_INFORMATION) + buffer.size() * 2 + 2; + if (ObjectInformationLength < requiredLength) { + // if the status was info length mismatch, we keep it, we are just going to + // update *ReturnLength + if (res != STATUS_INFO_LENGTH_MISMATCH) { + res = STATUS_BUFFER_OVERFLOW; + } + + if (ReturnLength) { + *ReturnLength = static_cast<ULONG>(requiredLength); + } + } else { + // put the unicode buffer at the end of the object + const USHORT unicodeBufferLength = static_cast<USHORT>(std::min( + static_cast<unsigned long long>(std::numeric_limits<USHORT>::max()), + static_cast<unsigned long long>(ObjectInformationLength - + sizeof(OBJECT_NAME_INFORMATION)))); + LPWSTR unicodeBuffer = reinterpret_cast<LPWSTR>( + static_cast<LPSTR>(ObjectInformation) + sizeof(OBJECT_NAME_INFORMATION)); + + // copy the path into the buffer + wmemcpy_s(unicodeBuffer, unicodeBufferLength, buffer.data(), buffer.size()); + + // set the null character + unicodeBuffer[buffer.size()] = L'\0'; + + // update the actual unicode string + info->Name.Buffer = unicodeBuffer; + info->Name.Length = static_cast<USHORT>(buffer.size() * 2); + info->Name.MaximumLength = unicodeBufferLength; + + res = STATUS_SUCCESS; + } + } + + auto logger = LOG_CALL() + .PARAMWRAP(res) + .PARAM(ObjectInformationLength) + .addParam("return_length", ReturnLength ? *ReturnLength : -1) + .addParam("tracker_path", trackerInfo) + .PARAM(ObjectInformationClass) + .PARAM(redir.redirected) + .PARAM(redir.path); + + if (res == STATUS_SUCCESS) { + logger.addParam("name_info", info->Name); + } else { + logger.addParam("name_info", ""); + } + } + + HOOK_END + return res; +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active()) { + return ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + } + + PRE_REALCALL + res = ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + POST_REALCALL + + // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be + // smaller than the original one + // + // we do not handle STATUS_INFO_LENGTH_MISMATCH because this is only returned if + // the length is too small to old the structure itself (regardless of the name) + // + // TODO: currently, this does not handle STATUS_BUFFER_OVERLOW for ALL information + // because most of the structures would need to be manually filled, which is very + // complicated - this should not pose huge issue + // + if (((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW) && + (FileInformationClass == FileNameInformation || + FileInformationClass == FileNormalizedNameInformation)) || + (res == STATUS_SUCCESS && FileInformationClass == FileAllInformation)) { + + const auto trackerInfo = ntdllHandleTracker.lookup(FileHandle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + + // TODO: difference between FileNameInformation and FileNormalizedNameInformation + + // maximum length in bytes - the required length is + // - for ALL, 100 + the number of bytes in the name (not account for null character) + // - for NAME, 4 + the number of bytes in the name (not accounting for null + // character) + // + // it is close to the sizeof the structure + the number of bytes in the name, except + // for the alignment that gives us a bit more space + // + ULONG prefixStructLength; + FILE_NAME_INFORMATION* info; + if (FileInformationClass == FileAllInformation) { + info = &reinterpret_cast<FILE_ALL_INFORMATION*>(FileInformation)->NameInformation; + prefixStructLength = sizeof(FILE_ALL_INFORMATION) - 4; + } else { + info = reinterpret_cast<FILE_NAME_INFORMATION*>(FileInformation); + prefixStructLength = sizeof(FILE_NAME_INFORMATION) - 4; + } + + if (redir.redirected) { + auto requiredLength = prefixStructLength + 2 * (trackerInfo.size() - 6); + if (Length < requiredLength) { + res = STATUS_BUFFER_OVERFLOW; + } else { + // strip the \??\X: prefix (X being the drive name) + LPCWSTR filenameFixed = static_cast<LPCWSTR>(trackerInfo) + 6; + + // not using SetInfoFilename because the length is not set and we do not need to + // 0-out the memory here + info->FileNameLength = static_cast<ULONG>((trackerInfo.size() - 6) * 2); + wmemcpy(info->FileName, filenameFixed, trackerInfo.size() - 6); + res = STATUS_SUCCESS; + + // update status block, Information is the number of bytes written, basically + // the required length + IoStatusBlock->Status = STATUS_SUCCESS; + IoStatusBlock->Information = static_cast<ULONG_PTR>(requiredLength); + } + } + + LOG_CALL() + .PARAMWRAP(res) + .addParam("tracker_path", trackerInfo) + .PARAM(FileInformationClass) + .PARAM(redir.redirected) + .PARAM(redir.path) + .addParam("name_info", res == STATUS_SUCCESS + ? std::wstring{info->FileName, + info->FileNameLength / sizeof(WCHAR)} + : std::wstring{}); + } + + HOOK_END + return res; +} + +unique_ptr_deleter<OBJECT_ATTRIBUTES> +makeObjectAttributes(RedirectionInfo& redirInfo, POBJECT_ATTRIBUTES attributeTemplate) +{ + if (redirInfo.redirected) { + unique_ptr_deleter<OBJECT_ATTRIBUTES> result(new OBJECT_ATTRIBUTES, + [](OBJECT_ATTRIBUTES* ptr) { + delete ptr; + }); + memcpy(result.get(), attributeTemplate, sizeof(OBJECT_ATTRIBUTES)); + result->RootDirectory = nullptr; + result->ObjectName = static_cast<PUNICODE_STRING>(redirInfo.path); + return result; + } else { + // just reuse the template with a dummy deleter + return unique_ptr_deleter<OBJECT_ATTRIBUTES>(attributeTemplate, + [](OBJECT_ATTRIBUTES*) {}); + } +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationByName( + POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + + if (!callContext.active()) { + res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation, + Length, FileInformationClass); + callContext.updateLastError(); + return res; + } + + RedirectionInfo redir = + applyReroute(READ_CONTEXT(), callContext, CreateUnicodeString(ObjectAttributes)); + + if (redir.redirected) { + auto newObjectAttributes = makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryInformationByName(newObjectAttributes.get(), IoStatusBlock, + FileInformation, Length, FileInformationClass); + POST_REALCALL + + LOG_CALL() + .PARAMWRAP(res) + .addParam("input_path", *ObjectAttributes->ObjectName) + .addParam("reroute_path", redir.path); + } else { + PRE_REALCALL + res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation, + Length, FileInformationClass); + POST_REALCALL + } + + HOOK_END + return res; +} + +NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, ULONG ShareAccess, + ULONG OpenOptions) +{ + using namespace usvfs; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + // Why is the usual if (!callContext.active()... check missing? + + bool storePath = false; + if (((OpenOptions & FILE_DIRECTORY_FILE) != 0UL) && + ((OpenOptions & FILE_OPEN_FOR_BACKUP_INTENT) != 0UL)) { + // this may be an attempt to open a directory handle for iterating. + // If so we need to treat it a little bit differently + /* usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::FILE_ATTRIBUTES); + FILE_BASIC_INFORMATION dummy; + storePath = FAILED(NtQueryAttributesFile(ObjectAttributes, &dummy));*/ + storePath = true; + } + + UnicodeString fullName = CreateUnicodeString(ObjectAttributes); + + if (isDeviceFile(static_cast<LPCWSTR>(fullName))) { + return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + } + + UnicodeString Path = ntdllHandleTracker.lookup(ObjectAttributes->RootDirectory); + + std::wstring checkpath = + ush::string_cast<std::wstring>(static_cast<LPCWSTR>(Path), ush::CodePage::UTF8); + + if ((fullName.size() == 0) || + (GetFileSize(ObjectAttributes->RootDirectory, nullptr) != INVALID_FILE_SIZE)) { + // //relative paths that we don't have permission over will fail here due that we + // can't get the filesize of the root directory + // //We should try again to see if it is a directory using another method + if ((fullName.size() == 0) || + (GetFileAttributesW((LPCWSTR)checkpath.c_str()) == INVALID_FILE_ATTRIBUTES)) { + return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + } + } + + try { + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, fullName); + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtOpenFile(FileHandle, DesiredAccess, adjustedAttributes.get(), + IoStatusBlock, ShareAccess, OpenOptions); + POST_REALCALL + if (SUCCEEDED(res) && storePath) { + // store the original search path for use during iteration + READ_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[*FileHandle] = + static_cast<LPCWSTR>(fullName); +#pragma message("need to clean up this handle in CloseHandle call") + } + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAM(*FileHandle) + .PARAM(OpenOptions) + .PARAMWRAP(res); + } + } catch (const std::exception&) { + PRE_REALCALL + res = ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + POST_REALCALL + } + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, ULONG OpenOptions) +{ + NTSTATUS res = ntdll_mess_NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, + IoStatusBlock, ShareAccess, OpenOptions); + if (res >= 0 && ObjectAttributes && FileHandle && + GetFileType(*FileHandle) == FILE_TYPE_DISK) + ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes)); + return res; +} + +NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, ULONG FileAttributes, + ULONG ShareAccess, ULONG CreateDisposition, + ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength) +{ + using namespace usvfs; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + if (!callContext.active()) { + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + UnicodeString inPath = CreateUnicodeString(ObjectAttributes); + LPCWSTR inPathW = static_cast<LPCWSTR>(inPath); + + if (inPath.size() == 0) { + spdlog::get("hooks")->info( + "failed to set from handle: {0}", + ush::string_cast<std::string>(ObjectAttributes->ObjectName->Buffer)); + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + if (isDeviceFile(inPathW)) { + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + DWORD convertedDisposition = OPEN_EXISTING; + switch (CreateDisposition) { + case FILE_SUPERSEDE: + convertedDisposition = CREATE_ALWAYS; + break; + case FILE_OPEN: + convertedDisposition = OPEN_EXISTING; + break; + case FILE_CREATE: + convertedDisposition = CREATE_NEW; + break; + case FILE_OPEN_IF: + convertedDisposition = OPEN_ALWAYS; + break; + case FILE_OVERWRITE: + convertedDisposition = TRUNCATE_EXISTING; + break; + case FILE_OVERWRITE_IF: + convertedDisposition = CREATE_ALWAYS; + break; + default: + spdlog::get("hooks")->error("invalid disposition: {0}", CreateDisposition); + break; + } + + DWORD convertedAccess = 0; + if ((DesiredAccess & FILE_GENERIC_READ) == FILE_GENERIC_READ) + convertedAccess |= GENERIC_READ; + if ((DesiredAccess & FILE_GENERIC_WRITE) == FILE_GENERIC_WRITE) + convertedAccess |= GENERIC_WRITE; + if ((DesiredAccess & FILE_GENERIC_EXECUTE) == FILE_GENERIC_EXECUTE) + convertedAccess |= GENERIC_EXECUTE; + if ((DesiredAccess & FILE_ALL_ACCESS) == FILE_ALL_ACCESS) + convertedAccess |= GENERIC_ALL; + + ULONG originalDisposition = CreateDisposition; + CreateRerouter rerouter; + if (rerouter.rerouteCreate( + READ_CONTEXT(), callContext, inPathW, convertedDisposition, convertedAccess, + (LPSECURITY_ATTRIBUTES)ObjectAttributes->SecurityDescriptor)) { + switch (convertedDisposition) { + case CREATE_NEW: + CreateDisposition = FILE_CREATE; + break; + case CREATE_ALWAYS: + if (CreateDisposition != FILE_SUPERSEDE) + CreateDisposition = FILE_OVERWRITE_IF; + break; + case OPEN_EXISTING: + CreateDisposition = FILE_OPEN; + break; + case OPEN_ALWAYS: + CreateDisposition = FILE_OPEN_IF; + break; + case TRUNCATE_EXISTING: + CreateDisposition = FILE_OVERWRITE; + break; + } + + RedirectionInfo redir = applyReroute(rerouter); + + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtCreateFile(FileHandle, DesiredAccess, adjustedAttributes.get(), + IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + POST_REALCALL + rerouter.updateResult(callContext, res == STATUS_SUCCESS); + + if (res == STATUS_SUCCESS) { + if (rerouter.newReroute()) + rerouter.insertMapping(WRITE_CONTEXT()); + + if (rerouter.isDir() && rerouter.wasRerouted() && + ((FileAttributes & FILE_OPEN_FOR_BACKUP_INTENT) == + FILE_OPEN_FOR_BACKUP_INTENT)) { + // store the original search path for use during iteration + WRITE_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[*FileHandle] = + inPathW; + } + } + + if (rerouter.wasRerouted() || rerouter.changedError() || + originalDisposition != CreateDisposition) { + LOG_CALL() + .PARAM(inPathW) + .PARAM(rerouter.fileName()) + .PARAMHEX(DesiredAccess) + .PARAMHEX(originalDisposition) + .PARAMHEX(CreateDisposition) + .PARAMHEX(FileAttributes) + .PARAMHEX(res) + .PARAMHEX(*FileHandle) + .PARAM(rerouter.originalError()) + .PARAM(rerouter.error()); + } + } else { + // make the original call to set up the proper errors and return statuses + PRE_REALCALL + res = ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, CreateDisposition, + CreateOptions, EaBuffer, EaLength); + POST_REALCALL + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, + ULONG FileAttributes, ULONG ShareAccess, + ULONG CreateDisposition, ULONG CreateOptions, + PVOID EaBuffer, ULONG EaLength) +{ + NTSTATUS res = ntdll_mess_NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, + IoStatusBlock, AllocationSize, FileAttributes, + ShareAccess, CreateDisposition, CreateOptions, + EaBuffer, EaLength); + if (res >= 0 && ObjectAttributes && FileHandle && + GetFileType(*FileHandle) == FILE_TYPE_DISK) + ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes)); + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtClose(HANDLE Handle) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + bool log = false; + + if ((::GetFileType(Handle) == FILE_TYPE_DISK)) { + HookContext::Ptr context = WRITE_CONTEXT(); + + { // clean up search data associated with this handle part 1 + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // std::lock_guard<std::recursive_mutex> lock(activeSearches.queryMutex); + auto iter = activeSearches.info.find(Handle); + if (iter != activeSearches.info.end()) { + if (iter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(iter->second.currentSearchHandle); + } + + activeSearches.info.erase(iter); + log = true; + } + } + + { + SearchHandleMap& searchHandles = + context->customData<SearchHandleMap>(SearchHandles); + auto iter = searchHandles.find(Handle); + if (iter != searchHandles.end()) { + searchHandles.erase(iter); + log = true; + } + } + } + + if (GetFileType(Handle) == FILE_TYPE_DISK) + ntdllHandleTracker.erase(Handle); + + PRE_REALCALL + res = ::NtClose(Handle); + POST_REALCALL + + if (log) { + LOG_CALL().PARAM(Handle).PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + // Why is the usual if (!callContext.active()... check missing? + + UnicodeString inPath = CreateUnicodeString(ObjectAttributes); + + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath); + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryAttributesFile(adjustedAttributes.get(), FileInformation); + POST_REALCALL + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryFullAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_NETWORK_OPEN_INFORMATION FileInformation) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + + if (!callContext.active()) { + return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation); + } + + UnicodeString inPath; + try { + inPath = CreateUnicodeString(ObjectAttributes); + } catch (const std::exception&) { + return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation); + } + + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath); + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryFullAttributesFile(adjustedAttributes.get(), FileInformation); + POST_REALCALL + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtTerminateProcess(HANDLE ProcessHandle, + NTSTATUS ExitStatus) +{ + // this hook is normally called when terminating another process, in which + // case there's nothing to do + // + // if the current process exits normally, the ExitProcess() hook is called + // and disconnects from the vfs; if the current process crashes, this hook + // is not called, the process just dies + // + // but a process can also terminate itself, bypassing ExitProcess(), and + // ending up here, in which case the vfs should be disconnected + // + // NtTerminateProcess() can be called two different ways to terminate the + // current process: + // - with a valid handle for the current process, or + // - with -1, because that's what GetCurrentProcess() returns + // + // it's unclear what a NULL handle represents, the behaviour is not + // documented anywhere, but looking at ReactOS, it's also interpreted as the + // current process + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START + + const bool isCurrentProcess = ProcessHandle == (HANDLE)-1 || ProcessHandle == 0 || + GetProcessId(ProcessHandle) == GetCurrentProcessId(); + + if (isCurrentProcess) { + usvfsDisconnectVFS(); + } + + res = ::NtTerminateProcess(ProcessHandle, ExitStatus); + + HOOK_END + + return res; +} diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.h b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h new file mode 100644 index 0000000..f3db4a6 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h @@ -0,0 +1,57 @@ +#pragma once + +#include <dllimport.h> +#include <loghelpers.h> +#include <ntdll_declarations.h> + +namespace usvfs +{ + +DLLEXPORT NTSTATUS WINAPI +hook_NtQueryFullAttributesFile(POBJECT_ATTRIBUTES ObjectAttributes, + PFILE_NETWORK_OPEN_INFORMATION FileInformation); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFile( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, BOOLEAN RestartScan); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFileEx( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags, + PUNICODE_STRING FileName); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryObject( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationFile( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationByName( + POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +DLLEXPORT NTSTATUS WINAPI hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, ULONG OpenOptions); + +DLLEXPORT NTSTATUS WINAPI hook_NtCreateFile( + PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, + ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, + ULONG EaLength); + +DLLEXPORT NTSTATUS WINAPI hook_NtClose(HANDLE Handle); + +DLLEXPORT NTSTATUS WINAPI hook_NtTerminateProcess(HANDLE ProcessHandle, + NTSTATUS ExitStatus); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/sharedids.h b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h new file mode 100644 index 0000000..9a7531c --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../hookcontext.h" + +typedef std::map<HANDLE, std::wstring> SearchHandleMap; + +// maps handles opened for searching to the original search path, which is +// necessary if the handle creation was rerouted +DATA_ID(SearchHandles); |
