From 2d92ed667e766194657d6cbce2b9e928a3f3cc85 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Tue, 9 Dec 2025 23:05:29 +0800 Subject: [PATCH 01/13] Implement Copy-on-Write (COW) for destructive file operations - Implemented detection of destructive file operations (FILE_OVERWRITE, FILE_SUPERSEDE) in NtCreateFile hook. - Added checks for sensitive file extensions (.ini, .json, .txt, etc.) to target configuration files. - Implemented 'Safety Copy' mechanism: - When a destructive write is detected on a file in 'mods_dir' (and not in 'exclude_mods'), the file is automatically copied to 'overwrite_dir'. - The copy preserves the relative path from the mod root (e.g., 'mods/ModA/SKSE/plugins/foo.ini' -> 'overwrite/SKSE/plugins/foo.ini'). - Updated VFS redirection table immediately after the copy to ensure the file handle and subsequent operations point to the new copy in 'overwrite_dir'. - Enhanced 'LoadSettings' to support dynamic 'overwrite_dir' configuration: - Added 'output_directories' map to 'usvfs::settings'. - Parsed '[Output]' section from 'usvfs_redirect.ini'. - If 'current_process' matches an entry in 'output_directories', 'overwrite_dir' is updated to the specified relative path within 'mods_dir'. - Added comprehensive logging for destructive operation detection, file copying, and VFS rerouting. --- src/usvfs_dll/hooks/ntdll.cpp | 135 +++++++++++++++++++++++++++++++-- src/usvfs_dll/hooks/settings.h | 16 ++++ src/usvfs_dll/usvfs.cpp | 126 ++++++++++++++++++++++++++++++ vcpkg-configuration.json | 18 +++-- 4 files changed, 284 insertions(+), 11 deletions(-) create mode 100644 src/usvfs_dll/hooks/settings.h diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index ad534185..26d82d7b 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1,4 +1,5 @@ #include "ntdll.h" +#include "settings.h" #include #include @@ -1199,6 +1200,8 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + auto logger = spdlog::get("hooks"); + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) if (!callContext.active()) { return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, @@ -1209,10 +1212,12 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, UnicodeString inPath = CreateUnicodeString(ObjectAttributes); LPCWSTR inPathW = static_cast(inPath); + logger->debug("NtCreateFile: Original path: {0}", + ush::string_cast(ObjectAttributes->ObjectName->Buffer)); + if (inPath.size() == 0) { - spdlog::get("hooks")->info( - "failed to set from handle: {0}", - ush::string_cast(ObjectAttributes->ObjectName->Buffer)); + logger->info("failed to set from handle: {0}", + ush::string_cast(ObjectAttributes->ObjectName->Buffer)); return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength); @@ -1245,7 +1250,7 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, convertedDisposition = CREATE_ALWAYS; break; default: - spdlog::get("hooks")->error("invalid disposition: {0}", CreateDisposition); + logger->error("invalid disposition: {0}", CreateDisposition); break; } @@ -1283,8 +1288,128 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, break; } - RedirectionInfo redir = applyReroute(rerouter); + RedirectionInfo redir = applyReroute(rerouter); + std::wstring physicalPath = rerouter.fileName(); + logger->debug("NtCreateFile: Rerouted path: {0}", physicalPath); + + bool needReroute = false; + + bool isDestructive = + (CreateDisposition == FILE_SUPERSEDE || CreateDisposition == FILE_OVERWRITE || + CreateDisposition == FILE_OVERWRITE_IF); + + bool isWrite = + (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | + FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0; + + if (isDestructive) + needReroute = true; + if (isWrite && !needReroute) { + bfs::path p(physicalPath); + std::string ext = p.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { + return std::tolower(c); + }); + + static const std::set sensitiveExtensions = { + ".ini", ".json", ".yaml", ".yml", ".txt", + ".log", ".xml", ".cfg", ".conf", ".properties"}; + + if (sensitiveExtensions.count(ext)) + needReroute = true; + else + logger->warn("NtCreateFile: not rerouting write to non-sensitive file: {0}", + physicalPath); + } + + std::wstring overwriteRedirectPath; + if (!usvfs::settings::mods_dir.empty() && needReroute) { + std::wstring modsDirW = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + const wchar_t* pathW = physicalPath.c_str(); + + if (physicalPath.size() >= modsDirW.size() && + _wcsnicmp(pathW, modsDirW.c_str(), modsDirW.size()) == 0) { + wchar_t nextChar = pathW[modsDirW.size()]; + if (nextChar == L'\0' || nextChar == L'\\') { + bool isExcluded = false; + for (const auto& excludedPath : usvfs::settings::exclude_mods) { + std::wstring excludedPathW = + ush::string_cast(excludedPath, ush::CodePage::UTF8); + if (physicalPath.size() >= excludedPathW.size() && + _wcsnicmp(pathW, excludedPathW.c_str(), excludedPathW.size()) == 0) { + wchar_t nextExChar = pathW[excludedPathW.size()]; + if (nextExChar == L'\0' || nextExChar == L'\\') { + isExcluded = true; + break; + } + } + } + + if (!isExcluded) { + logger->warn("NtCreateFile: mod file can be modified - reroute to: {0}", + physicalPath); + + if (!usvfs::settings::overwrite_dir.empty()) { + const wchar_t* relToMods = pathW + modsDirW.size(); + while (*relToMods == L'\\' || *relToMods == L'/') + relToMods++; + + const wchar_t* nextSep = wcschr(relToMods, L'\\'); + const wchar_t* nextSepSlash = wcschr(relToMods, L'/'); + if (nextSepSlash && (!nextSep || nextSepSlash < nextSep)) + nextSep = nextSepSlash; + + if (nextSep) { + const wchar_t* relToModRoot = nextSep + 1; + std::wstring overwriteDirW = ush::string_cast( + usvfs::settings::overwrite_dir, ush::CodePage::UTF8); + bfs::path destPath(overwriteDirW); + destPath /= relToModRoot; + + boost::system::error_code ec; + bfs::create_directories(destPath.parent_path(), ec); + if (!ec) { + if (bfs::exists(destPath, ec)) { + bfs::remove(destPath, ec); + } + bfs::copy_file(physicalPath, destPath, ec); + + if (ec) { + logger->error("Failed to copy to overwrite: {}", ec.message()); + } else { + logger->info("Copied to overwrite: {}", destPath.string()); + overwriteRedirectPath = destPath.wstring(); + } + } else { + logger->error("Failed to create directories for overwrite: {}", + ec.message()); + } + } + } + } + } + } + } + + if (!overwriteRedirectPath.empty()) { + std::wstring ntPath = LR"(\??\)" + overwriteRedirectPath; + redir.path = UnicodeString(ntPath.c_str()); + redir.redirected = true; + + std::wstring lookupPathW = inPathW; + if (lookupPathW.rfind(L"\\??\\", 0) == 0 || + lookupPathW.rfind(L"\\\\?\\", 0) == 0) { + lookupPathW = lookupPathW.substr(4); + } + std::string lookupPath = + ush::string_cast(lookupPathW, ush::CodePage::UTF8); + std::string targetPath = + ush::string_cast(overwriteRedirectPath, ush::CodePage::UTF8); + + WRITE_CONTEXT()->redirectionTable().addFile(lookupPath, targetPath); + } unique_ptr_deleter adjustedAttributes = makeObjectAttributes(redir, ObjectAttributes); diff --git a/src/usvfs_dll/hooks/settings.h b/src/usvfs_dll/hooks/settings.h new file mode 100644 index 00000000..6adbbc8e --- /dev/null +++ b/src/usvfs_dll/hooks/settings.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +#include + +namespace usvfs +{ +namespace settings +{ + inline std::string mods_dir; + inline std::string overwrite_dir; + inline std::vector exclude_mods; + inline std::map output_directories; + inline std::string current_process; +} // namespace settings +} // namespace usvfs diff --git a/src/usvfs_dll/usvfs.cpp b/src/usvfs_dll/usvfs.cpp index 56d9f71d..3f3d672f 100644 --- a/src/usvfs_dll/usvfs.cpp +++ b/src/usvfs_dll/usvfs.cpp @@ -20,6 +20,7 @@ along with usvfs. If not, see . */ #include "usvfs.h" #include "hookmanager.h" +#include "hooks/settings.h" #include "loghelpers.h" #include "redirectiontree.h" #include "usvfs_version.h" @@ -35,6 +36,8 @@ along with usvfs. If not, see . // note that there's a mix of boost and std filesystem stuff in this file and // that they're not completely compatible #include +#include +#include namespace bfs = boost::filesystem; namespace ush = usvfs::shared; @@ -371,12 +374,135 @@ LONG WINAPI VEHandler(PEXCEPTION_POINTERS exceptionPtrs) // Exported functions // +void LoadSettings() +{ + wchar_t modulePath[MAX_PATH]; + if (GetModuleFileNameW(dllModule, modulePath, MAX_PATH) == 0) { + return; + } + std::wstring iniPath = modulePath; + size_t lastSlash = iniPath.find_last_of(L"\\/"); + if (lastSlash != std::wstring::npos) { + iniPath = iniPath.substr(0, lastSlash + 1); + } else { + iniPath = L""; + } + iniPath += L"usvfs_redirect.ini"; + + std::filesystem::path dllDir = std::filesystem::path(modulePath).parent_path(); + + auto makeAbsolute = [&](std::string pathStr) -> std::string { + if (pathStr.empty()) + return ""; + std::filesystem::path p = + ush::string_cast(pathStr, ush::CodePage::UTF8); + if (p.is_relative()) { + p = dllDir / p; + } + return ush::string_cast(p.lexically_normal().wstring(), + ush::CodePage::UTF8); + }; + + // Helper to read string and convert to UTF-8 + auto readString = [&](const wchar_t* section, const wchar_t* key, + const wchar_t* def) { + wchar_t buffer[4096]; + GetPrivateProfileStringW(section, key, def, buffer, 4096, iniPath.c_str()); + return ush::string_cast(buffer, ush::CodePage::UTF8); + }; + + usvfs::settings::mods_dir = makeAbsolute(readString(L"General", L"mods_dir", L"")); + usvfs::settings::overwrite_dir = + makeAbsolute(readString(L"General", L"overwrite_dir", L"")); + + std::string exclude_raw = readString(L"General", L"exclude_dir", L""); + usvfs::settings::exclude_mods.clear(); + if (!exclude_raw.empty()) { + std::stringstream ss(exclude_raw); + std::string item; + while (std::getline(ss, item, ';')) { + if (!item.empty()) { + std::filesystem::path p = + ush::string_cast(item, ush::CodePage::UTF8); + if (p.is_relative() && !usvfs::settings::mods_dir.empty()) { + std::filesystem::path mods = ush::string_cast( + usvfs::settings::mods_dir, ush::CodePage::UTF8); + p = mods / p; + } else if (p.is_relative()) { + p = std::filesystem::path( + ush::string_cast(makeAbsolute(item), ush::CodePage::UTF8)); + } + usvfs::settings::exclude_mods.push_back(ush::string_cast( + p.lexically_normal().wstring(), ush::CodePage::UTF8)); + } + } + } + + // Read Output section + wchar_t buffer[32768]; // 32KB is max for GetPrivateProfileSection + if (GetPrivateProfileSectionW(L"Output", buffer, 32768, iniPath.c_str()) > 0) { + wchar_t* p = buffer; + while (*p) { + std::wstring line = p; + size_t eq = line.find(L'='); + if (eq != std::wstring::npos) { + std::wstring key = line.substr(0, eq); + std::wstring val = line.substr(eq + 1); + usvfs::settings::output_directories[ush::string_cast( + key, ush::CodePage::UTF8)] = + ush::string_cast(val, ush::CodePage::UTF8); + } + p += line.length() + 1; + } + } + + if (!usvfs::settings::current_process.empty()) { + auto it = + usvfs::settings::output_directories.find(usvfs::settings::current_process); + if (it != usvfs::settings::output_directories.end()) { + std::string relativeOverwrite = it->second; + if (!usvfs::settings::mods_dir.empty()) { + std::filesystem::path mods(ush::string_cast( + usvfs::settings::mods_dir, ush::CodePage::UTF8)); + std::filesystem::path rel( + ush::string_cast(relativeOverwrite, ush::CodePage::UTF8)); + std::filesystem::path newOverwrite = mods / rel; + usvfs::settings::overwrite_dir = ush::string_cast( + newOverwrite.lexically_normal().wstring(), ush::CodePage::UTF8); + + auto logger = spdlog::get("usvfs"); + if (logger) { + logger->info("Instance match: '{}' -> Overwrite set to: '{}'", + usvfs::settings::current_process, + usvfs::settings::overwrite_dir); + } + } + } + } + + auto logger = spdlog::get("usvfs"); + logger->info("Settings loaded:"); + logger->info(" mods_dir: {}", usvfs::settings::mods_dir); + logger->info(" overwrite_dir: {}", usvfs::settings::overwrite_dir); + logger->info(" exclude_mods:"); + for (const auto& dir : usvfs::settings::exclude_mods) { + logger->info(" - {}", dir); + } + logger->info(" output_directories:"); + for (const auto& [exe, dir] : usvfs::settings::output_directories) { + logger->info(" - {} -> {}", exe, dir); + } +} + void __cdecl InitHooks(LPVOID parameters, size_t) { InitLoggingInternal(false, true); const usvfsParameters* params = reinterpret_cast(parameters); + usvfs::settings::current_process = "SkyrimSE.exe"; + LoadSettings(); + // there is already a wait in the constructor of HookManager, but this one is useful // to debug code here (from experience... ), should not wait twice since the second // will return true immediately diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 723deeed..b29ed986 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,21 +1,27 @@ { "default-registry": { "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", + "repository": "git@github.com:Microsoft/vcpkg.git", "baseline": "294f76666c3000630d828703e675814c05a4fd43" }, "registries": [ { "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", + "repository": "git@github.com:Microsoft/vcpkg.git", "baseline": "294f76666c3000630d828703e675814c05a4fd43", - "packages": ["boost*", "boost-*"] + "packages": [ + "boost*", + "boost-*" + ] }, { "kind": "git", - "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "repository": "git@github.com:ModOrganizer2/vcpkg-registry.git", "baseline": "27d8adbfe9e4ce88a875be3a45fadab69869eb60", - "packages": ["asmjit", "spdlog"] + "packages": [ + "asmjit", + "spdlog" + ] } ] -} +} \ No newline at end of file From f52591fc8d6e0c0cae338960dc6ebbded544b2f2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:19:19 +0000 Subject: [PATCH 02/13] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- vcpkg-configuration.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index b29ed986..d021d49e 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -24,4 +24,4 @@ ] } ] -} \ No newline at end of file +} From ef7e670ac9f24dbc6b6038c72ba2ce228c8b21d3 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Wed, 10 Dec 2025 20:57:38 +0800 Subject: [PATCH 03/13] hooks(ntdll): implement Copy-on-Write (CoW) for mod files --- src/shared/ntdll_declarations.cpp | 4 + src/shared/ntdll_declarations.h | 17 ++ src/usvfs_dll/hookmanager.cpp | 2 + src/usvfs_dll/hooks/ntdll.cpp | 433 ++++++++++++++++++++++++------ src/usvfs_dll/hooks/ntdll.h | 12 + src/usvfs_dll/hooks/sharedids.h | 12 + 6 files changed, 398 insertions(+), 82 deletions(-) diff --git a/src/shared/ntdll_declarations.cpp b/src/shared/ntdll_declarations.cpp index b4bf0f56..43da5cb6 100644 --- a/src/shared/ntdll_declarations.cpp +++ b/src/shared/ntdll_declarations.cpp @@ -41,6 +41,8 @@ RtlDosPathNameToRelativeNtPathName_U_WithStatus_type RtlReleaseRelativeName_type RtlReleaseRelativeName; RtlGetVersion_type RtlGetVersion; NtTerminateProcess_type NtTerminateProcess; +NtReadFile_type NtReadFile; +NtWriteFile_type NtWriteFile; static bool ntdll_initialized; @@ -64,6 +66,8 @@ void ntdll_declarations_init() LOAD_EXT(ntDLLMod, RtlReleaseRelativeName); LOAD_EXT(ntDLLMod, RtlGetVersion); LOAD_EXT(ntDLLMod, NtTerminateProcess); + LOAD_EXT(ntDLLMod, NtReadFile); + LOAD_EXT(ntDLLMod, NtWriteFile); ntdll_initialized = true; } diff --git a/src/shared/ntdll_declarations.h b/src/shared/ntdll_declarations.h index 813ad8c9..9a394546 100644 --- a/src/shared/ntdll_declarations.h +++ b/src/shared/ntdll_declarations.h @@ -358,6 +358,8 @@ typedef struct _FILE_REPARSE_POINT_INFORMATION #define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L) #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) #define STATUS_NO_SUCH_FILE ((NTSTATUS)0xC000000FL) +#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L) +#define STATUS_CANNOT_DELETE ((NTSTATUS)0xC0000121L) #define SL_RESTART_SCAN 0x01 #define SL_RETURN_SINGLE_ENTRY 0x02 @@ -579,6 +581,18 @@ using NtClose_type = NTSTATUS(WINAPI*)(HANDLE); using NtTerminateProcess_type = NTSTATUS(WINAPI*)(HANDLE ProcessHandle, NTSTATUS ExitStatus); +using NtReadFile_type = NTSTATUS(WINAPI*)(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + +using NtWriteFile_type = NTSTATUS(WINAPI*)(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + // Rtl using RtlDoesFileExists_U_type = NTSYSAPI BOOLEAN(NTAPI*)(PCWSTR); @@ -599,6 +613,9 @@ extern NtOpenFile_type NtOpenFile; extern NtCreateFile_type NtCreateFile; extern NtClose_type NtClose; extern NtTerminateProcess_type NtTerminateProcess; +extern NtReadFile_type NtReadFile; +extern NtWriteFile_type NtWriteFile; + extern RtlDoesFileExists_U_type RtlDoesFileExists_U; extern RtlDosPathNameToRelativeNtPathName_U_WithStatus_type RtlDosPathNameToRelativeNtPathName_U_WithStatus; diff --git a/src/usvfs_dll/hookmanager.cpp b/src/usvfs_dll/hookmanager.cpp index 731b8560..61fbe953 100644 --- a/src/usvfs_dll/hookmanager.cpp +++ b/src/usvfs_dll/hookmanager.cpp @@ -302,6 +302,8 @@ void HookManager::initHooks() installHook(ntdllMod, nullptr, "NtCreateFile", hook_NtCreateFile); installHook(ntdllMod, nullptr, "NtClose", hook_NtClose); installHook(ntdllMod, nullptr, "NtTerminateProcess", hook_NtTerminateProcess); + installHook(ntdllMod, nullptr, "NtReadFile", hook_NtReadFile); + installHook(ntdllMod, nullptr, "NtWriteFile", hook_NtWriteFile); installHook(kbaseMod, k32Mod, "LoadLibraryExA", hook_LoadLibraryExA); installHook(kbaseMod, k32Mod, "LoadLibraryExW", hook_LoadLibraryExW); diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index 26d82d7b..a0a454bd 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1144,6 +1144,62 @@ NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, unique_ptr_deleter adjustedAttributes = makeObjectAttributes(redir, ObjectAttributes); + auto logger = spdlog::get("hooks"); + + bool isWrite = + (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | + FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0; + if (isWrite) { + std::wstring physicalPath = adjustedAttributes->ObjectName->Buffer; + + // Clean up NT path prefix if present + if (physicalPath.rfind(L"\\??\\", 0) == 0 || + physicalPath.rfind(L"\\\\?\\", 0) == 0) { + physicalPath = physicalPath.substr(4); + } + + // Check if file is in mods directory + bool isInModsDir = false; + std::wstring modsDirW; + if (!usvfs::settings::mods_dir.empty()) { + modsDirW = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + if (physicalPath.size() >= modsDirW.size() && + _wcsnicmp(physicalPath.c_str(), modsDirW.c_str(), modsDirW.size()) == 0) { + wchar_t nextChar = physicalPath.c_str()[modsDirW.size()]; + if (nextChar == L'\0' || nextChar == L'\\') { + isInModsDir = true; + } + } + } + + // Check if file is in excluded directory + bool isExcluded = false; + if (isInModsDir) { + for (const auto& excludedPath : usvfs::settings::exclude_mods) { + std::wstring excludedPathW = + ush::string_cast(excludedPath, ush::CodePage::UTF8); + if (physicalPath.size() >= excludedPathW.size() && + _wcsnicmp(physicalPath.c_str(), excludedPathW.c_str(), + excludedPathW.size()) == 0) { + wchar_t nextExChar = physicalPath.c_str()[excludedPathW.size()]; + if (nextExChar == L'\0' || nextExChar == L'\\') { + isExcluded = true; + break; + } + } + } + } + + // Only track for CoW if in mods directory and not excluded + if (isInModsDir && !isExcluded) { + logger->debug("NtOpenFile: write access detected for file: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + WRITE_CONTEXT()->customData( + WriteAccessHandles)[*FileHandle] = {nullptr, physicalPath}; + } + } + PRE_REALCALL res = ::NtOpenFile(FileHandle, DesiredAccess, adjustedAttributes.get(), IoStatusBlock, ShareAccess, OpenOptions); @@ -1216,7 +1272,7 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, ush::string_cast(ObjectAttributes->ObjectName->Buffer)); if (inPath.size() == 0) { - logger->info("failed to set from handle: {0}", + logger->info("NtCreateFile: failed to set from handle: {0}", ush::string_cast(ObjectAttributes->ObjectName->Buffer)); return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, @@ -1250,7 +1306,7 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, convertedDisposition = CREATE_ALWAYS; break; default: - logger->error("invalid disposition: {0}", CreateDisposition); + logger->error("NtCreateFile: invalid disposition: {0}", CreateDisposition); break; } @@ -1290,9 +1346,15 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, RedirectionInfo redir = applyReroute(rerouter); std::wstring physicalPath = rerouter.fileName(); - logger->debug("NtCreateFile: Rerouted path: {0}", physicalPath); - bool needReroute = false; + // Clean up NT path prefix if present + if (physicalPath.rfind(L"\\??\\", 0) == 0 || + physicalPath.rfind(L"\\\\?\\", 0) == 0) { + physicalPath = physicalPath.substr(4); + } + + logger->debug("NtCreateFile: Rerouted path: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); bool isDestructive = (CreateDisposition == FILE_SUPERSEDE || CreateDisposition == FILE_OVERWRITE || @@ -1302,95 +1364,110 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0; - if (isDestructive) - needReroute = true; - - if (isWrite && !needReroute) { - bfs::path p(physicalPath); - std::string ext = p.extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { - return std::tolower(c); - }); - - static const std::set sensitiveExtensions = { - ".ini", ".json", ".yaml", ".yml", ".txt", - ".log", ".xml", ".cfg", ".conf", ".properties"}; - - if (sensitiveExtensions.count(ext)) - needReroute = true; - else - logger->warn("NtCreateFile: not rerouting write to non-sensitive file: {0}", - physicalPath); - } + logger->debug("NtCreateFile: isWrite: {}, isDestructive: {}", isWrite, + isDestructive); std::wstring overwriteRedirectPath; - if (!usvfs::settings::mods_dir.empty() && needReroute) { - std::wstring modsDirW = ush::string_cast(usvfs::settings::mods_dir, - ush::CodePage::UTF8); - const wchar_t* pathW = physicalPath.c_str(); + // Check if file is in mods directory + bool isInModsDir = false; + std::wstring modsDirW; + if (!usvfs::settings::mods_dir.empty() && (isDestructive || isWrite)) { + modsDirW = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); if (physicalPath.size() >= modsDirW.size() && - _wcsnicmp(pathW, modsDirW.c_str(), modsDirW.size()) == 0) { - wchar_t nextChar = pathW[modsDirW.size()]; + _wcsnicmp(physicalPath.c_str(), modsDirW.c_str(), modsDirW.size()) == 0) { + wchar_t nextChar = physicalPath.c_str()[modsDirW.size()]; if (nextChar == L'\0' || nextChar == L'\\') { - bool isExcluded = false; - for (const auto& excludedPath : usvfs::settings::exclude_mods) { - std::wstring excludedPathW = - ush::string_cast(excludedPath, ush::CodePage::UTF8); - if (physicalPath.size() >= excludedPathW.size() && - _wcsnicmp(pathW, excludedPathW.c_str(), excludedPathW.size()) == 0) { - wchar_t nextExChar = pathW[excludedPathW.size()]; - if (nextExChar == L'\0' || nextExChar == L'\\') { - isExcluded = true; - break; - } - } + isInModsDir = true; + } + } + } + + // Check if file is in excluded directory + bool isExcluded = false; + if (isInModsDir) { + for (const auto& excludedPath : usvfs::settings::exclude_mods) { + std::wstring excludedPathW = + ush::string_cast(excludedPath, ush::CodePage::UTF8); + if (physicalPath.size() >= excludedPathW.size() && + _wcsnicmp(physicalPath.c_str(), excludedPathW.c_str(), + excludedPathW.size()) == 0) { + wchar_t nextExChar = physicalPath.c_str()[excludedPathW.size()]; + if (nextExChar == L'\0' || nextExChar == L'\\') { + isExcluded = true; + logger->debug( + "NtCreateFile: File is in excluded directory: {}", + ush::string_cast(excludedPathW, ush::CodePage::UTF8)); + break; } + } + } + } - if (!isExcluded) { - logger->warn("NtCreateFile: mod file can be modified - reroute to: {0}", - physicalPath); - - if (!usvfs::settings::overwrite_dir.empty()) { - const wchar_t* relToMods = pathW + modsDirW.size(); - while (*relToMods == L'\\' || *relToMods == L'/') - relToMods++; - - const wchar_t* nextSep = wcschr(relToMods, L'\\'); - const wchar_t* nextSepSlash = wcschr(relToMods, L'/'); - if (nextSepSlash && (!nextSep || nextSepSlash < nextSep)) - nextSep = nextSepSlash; - - if (nextSep) { - const wchar_t* relToModRoot = nextSep + 1; - std::wstring overwriteDirW = ush::string_cast( - usvfs::settings::overwrite_dir, ush::CodePage::UTF8); - bfs::path destPath(overwriteDirW); - destPath /= relToModRoot; - - boost::system::error_code ec; - bfs::create_directories(destPath.parent_path(), ec); - if (!ec) { - if (bfs::exists(destPath, ec)) { - bfs::remove(destPath, ec); - } - bfs::copy_file(physicalPath, destPath, ec); - - if (ec) { - logger->error("Failed to copy to overwrite: {}", ec.message()); - } else { - logger->info("Copied to overwrite: {}", destPath.string()); - overwriteRedirectPath = destPath.wstring(); - } - } else { - logger->error("Failed to create directories for overwrite: {}", - ec.message()); - } - } + logger->debug("NtCreateFile: isInModsDir: {}, isExcluded: {}", isInModsDir, + isExcluded); + + // Handle destructive operations on mods files + if (isInModsDir && !isExcluded && isDestructive) { + logger->warn("NtCreateFile: mod file will be truncated - original path: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + + if (!usvfs::settings::overwrite_dir.empty()) { + const wchar_t* relToMods = physicalPath.c_str() + modsDirW.size(); + while (*relToMods == L'\\' || *relToMods == L'/') + relToMods++; + + const wchar_t* nextSep = wcschr(relToMods, L'\\'); + const wchar_t* nextSepSlash = wcschr(relToMods, L'/'); + if (nextSepSlash && (!nextSep || nextSepSlash < nextSep)) + nextSep = nextSepSlash; + + if (nextSep) { + const wchar_t* relToModRoot = nextSep + 1; + std::wstring overwriteDirW = ush::string_cast( + usvfs::settings::overwrite_dir, ush::CodePage::UTF8); + bfs::path destPath(overwriteDirW); + destPath /= relToModRoot; + + boost::system::error_code ec; + bfs::create_directories(destPath.parent_path(), ec); + if (!ec) { + if (bfs::exists(destPath, ec)) { + bfs::remove(destPath, ec); } + bfs::copy_file(physicalPath, destPath, ec); + + if (ec) { + logger->error( + "NtCreateFile: Failed to copy to overwrite: {} (source: {}, dest: " + "{})", + ush::string_cast( + ush::string_cast(ec.message(), ush::CodePage::UTF8), + ush::CodePage::UTF8), + ush::string_cast(physicalPath, ush::CodePage::UTF8), + destPath.string()); + } else { + logger->info("NtCreateFile: Copied to overwrite: {}", destPath.string()); + overwriteRedirectPath = destPath.wstring(); + } + } else { + logger->error( + "NtCreateFile: Failed to create directories for overwrite: {} (dest: " + "{})", + ush::string_cast( + ush::string_cast(ec.message(), ush::CodePage::UTF8), + ush::CodePage::UTF8), + ush::string_cast(overwriteDirW, ush::CodePage::UTF8)); } } } + } else if (isInModsDir && !isExcluded && isWrite) { + // File is in mods directory and has write access, prepare for CoW + logger->debug("NtCreateFile: write access detected for file: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + WRITE_CONTEXT()->customData( + WriteAccessHandles)[*FileHandle] = {nullptr, physicalPath}; } if (!overwriteRedirectPath.empty()) { @@ -1516,6 +1593,15 @@ NTSTATUS WINAPI usvfs::hook_NtClose(HANDLE Handle) } } + WriteAccessHandleMap& writeAccessHandles = + WRITE_CONTEXT()->customData(WriteAccessHandles); + auto writeIter = writeAccessHandles.find(Handle); + if (writeIter != writeAccessHandles.end()) { + if (writeIter->second.RerouteHandle != nullptr) + ::CloseHandle(writeIter->second.RerouteHandle); + writeAccessHandles.erase(writeIter); + } + if (GetFileType(Handle) == FILE_TYPE_DISK) ntdllHandleTracker.erase(Handle); @@ -1643,3 +1729,186 @@ NTSTATUS WINAPI usvfs::hook_NtTerminateProcess(HANDLE ProcessHandle, return res; } + +NTSTATUS WINAPI usvfs::hook_NtReadFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key) +{ + using namespace usvfs; + + NTSTATUS res = STATUS_SUCCESS; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + auto logger = spdlog::get("hooks"); + + HOOK_START_GROUP(MutExHookGroup::NO_GROUP) + if (!callContext.active()) { + return ::NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + } + + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + + auto writeIter = writeAccessMap.find(FileHandle); + if (writeIter != writeAccessMap.end()) { + if (writeIter->second.RerouteHandle != nullptr) { + // File has been copied, read from the rerouted file + logger->debug("NtReadFile: Reading from rerouted file: {}", + writeIter->second.ReroutePath); + PRE_REALCALL + res = ::NtReadFile(writeIter->second.RerouteHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, Buffer, Length, ByteOffset, Key); + POST_REALCALL + } else { + // File is in CoW state but not yet copied, read from original file + logger->debug("NtReadFile: Reading from original file (CoW pending): {}", + writeIter->second.ReroutePath); + PRE_REALCALL + res = ::NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + POST_REALCALL + } + } else { + // File not in write access map, read normally + PRE_REALCALL + res = ::NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, Buffer, + Length, ByteOffset, Key); + POST_REALCALL + } + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key) +{ + using namespace usvfs; + + NTSTATUS res = STATUS_ACCESS_DENIED; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + auto logger = spdlog::get("hooks"); + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + if (!callContext.active()) { + return ::NtWriteFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + } + + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + + auto writeIter = writeAccessMap.find(FileHandle); + if (writeIter != writeAccessMap.end()) { + // handle write access + WriteAccessHandle& writeAccessInfo = writeIter->second; + if (writeAccessInfo.RerouteHandle != nullptr) { + logger->debug("NtWriteFile: Write operation detected for file: {}", + writeAccessInfo.ReroutePath); + PRE_REALCALL + res = ::NtWriteFile(writeAccessInfo.RerouteHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, Buffer, Length, ByteOffset, Key); + POST_REALCALL + } else if (writeAccessInfo.RerouteHandle == nullptr) { + logger->info("NtWriteFile: Copy on Write for file: {}", + writeAccessInfo.ReroutePath); + + // CoW: Copy the original file to the reroute path first + std::wstring reroutePath = writeAccessInfo.ReroutePath; + if (!usvfs::settings::overwrite_dir.empty()) { + std::wstring overwriteDirW = ush::string_cast( + usvfs::settings::overwrite_dir, ush::CodePage::UTF8); + + // Extract the relative path from the mods directory + std::wstring modsDirW = ush::string_cast( + usvfs::settings::mods_dir, ush::CodePage::UTF8); + const wchar_t* pathW = writeAccessInfo.ReroutePath.c_str(); + + if (writeAccessInfo.ReroutePath.size() >= modsDirW.size() && + _wcsnicmp(pathW, modsDirW.c_str(), modsDirW.size()) == 0) { + const wchar_t* relToMods = pathW + modsDirW.size(); + while (*relToMods == L'\\' || *relToMods == L'/') + relToMods++; + + const wchar_t* nextSep = wcschr(relToMods, L'\\'); + const wchar_t* nextSepSlash = wcschr(relToMods, L'/'); + if (nextSepSlash && (!nextSep || nextSepSlash < nextSep)) + nextSep = nextSepSlash; + + if (nextSep) { + const wchar_t* relToModRoot = nextSep + 1; + bfs::path destPath(overwriteDirW); + destPath /= relToModRoot; + + boost::system::error_code ec; + bfs::create_directories(destPath.parent_path(), ec); + if (!ec) { + if (bfs::exists(destPath, ec)) { + bfs::remove(destPath, ec); + } + bfs::copy_file(writeAccessInfo.ReroutePath, destPath, ec); + + if (!ec) { + logger->info("NtWriteFile: CoW - Copied file to overwrite: {}", + destPath.string()); + reroutePath = destPath.wstring(); + + // Open the rerouted file for writing + HANDLE rerouteHandle = + CreateFileW(reroutePath.c_str(), GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (rerouteHandle != INVALID_HANDLE_VALUE) { + writeAccessInfo.RerouteHandle = rerouteHandle; + writeAccessInfo.ReroutePath = reroutePath; + PRE_REALCALL + res = ::NtWriteFile(rerouteHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, Buffer, Length, ByteOffset, Key); + POST_REALCALL + } else { + logger->error("NtWriteFile: CoW - Failed to open rerouted file: {}", + reroutePath); + res = STATUS_CANNOT_DELETE; + } + } else { + logger->error("NtWriteFile: CoW - Failed to copy file: {}", + ec.message()); + res = STATUS_CANNOT_DELETE; + } + } else { + logger->error("NtWriteFile: CoW - Failed to create directories: {}", + ec.message()); + res = STATUS_CANNOT_DELETE; + } + } else { + logger->warn("NtWriteFile: CoW - Could not determine relative path"); + res = STATUS_INVALID_PARAMETER; + } + } else { + logger->warn("NtWriteFile: CoW - File not in mods directory"); + res = STATUS_INVALID_PARAMETER; + } + } else { + logger->warn("NtWriteFile: CoW - overwrite_dir not configured"); + res = STATUS_INVALID_PARAMETER; + } + } + } else { + PRE_REALCALL + res = ::NtWriteFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + POST_REALCALL + } + HOOK_END + + return res; +} \ No newline at end of file diff --git a/src/usvfs_dll/hooks/ntdll.h b/src/usvfs_dll/hooks/ntdll.h index f3db4a68..7ef522cd 100644 --- a/src/usvfs_dll/hooks/ntdll.h +++ b/src/usvfs_dll/hooks/ntdll.h @@ -54,4 +54,16 @@ DLLEXPORT NTSTATUS WINAPI hook_NtClose(HANDLE Handle); DLLEXPORT NTSTATUS WINAPI hook_NtTerminateProcess(HANDLE ProcessHandle, NTSTATUS ExitStatus); +DLLEXPORT NTSTATUS WINAPI hook_NtReadFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + +DLLEXPORT NTSTATUS WINAPI hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + } // namespace usvfs diff --git a/src/usvfs_dll/hooks/sharedids.h b/src/usvfs_dll/hooks/sharedids.h index 9a7531c2..a2a18889 100644 --- a/src/usvfs_dll/hooks/sharedids.h +++ b/src/usvfs_dll/hooks/sharedids.h @@ -7,3 +7,15 @@ typedef std::map SearchHandleMap; // maps handles opened for searching to the original search path, which is // necessary if the handle creation was rerouted DATA_ID(SearchHandles); + +// RerouteHandle init as nullptr +// ReroutePath init as physical path +// When Write Operation occurs, RerouteHandle is set to a new handle at overwrite +// ReroutePath is updated to the new path +typedef struct +{ + HANDLE RerouteHandle; + std::wstring ReroutePath; +} WriteAccessHandle; +typedef std::map WriteAccessHandleMap; +DATA_ID(WriteAccessHandles); \ No newline at end of file From 14e3559ebf214994f44127491d6c1b8d943e92f8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:59:57 +0000 Subject: [PATCH 04/13] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- src/usvfs_dll/hooks/ntdll.cpp | 2 +- src/usvfs_dll/hooks/sharedids.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index a0a454bd..1b62c0a9 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1911,4 +1911,4 @@ NTSTATUS WINAPI usvfs::hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, HOOK_END return res; -} \ No newline at end of file +} diff --git a/src/usvfs_dll/hooks/sharedids.h b/src/usvfs_dll/hooks/sharedids.h index a2a18889..d96e8399 100644 --- a/src/usvfs_dll/hooks/sharedids.h +++ b/src/usvfs_dll/hooks/sharedids.h @@ -18,4 +18,4 @@ typedef struct std::wstring ReroutePath; } WriteAccessHandle; typedef std::map WriteAccessHandleMap; -DATA_ID(WriteAccessHandles); \ No newline at end of file +DATA_ID(WriteAccessHandles); From 6c7ca0040c22bdacc5ebe1699d94f0e9a7c5dd0a Mon Sep 17 00:00:00 2001 From: Acook1e Date: Thu, 11 Dec 2025 01:26:33 +0800 Subject: [PATCH 05/13] Delete mutex of NtReadFile, may improve performance --- src/usvfs_dll/hooks/ntdll.cpp | 2 +- vcpkg-configuration.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index 1b62c0a9..0594f4fa 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1744,7 +1744,7 @@ NTSTATUS WINAPI usvfs::hook_NtReadFile(HANDLE FileHandle, HANDLE Event, auto logger = spdlog::get("hooks"); - HOOK_START_GROUP(MutExHookGroup::NO_GROUP) + HOOK_START if (!callContext.active()) { return ::NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, Buffer, Length, ByteOffset, Key); diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index d021d49e..c8d97226 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,13 +1,13 @@ { "default-registry": { "kind": "git", - "repository": "git@github.com:Microsoft/vcpkg.git", + "repository": "https://github.com/Microsoft/vcpkg", "baseline": "294f76666c3000630d828703e675814c05a4fd43" }, "registries": [ { "kind": "git", - "repository": "git@github.com:Microsoft/vcpkg.git", + "repository": "https://github.com/Microsoft/vcpkg", "baseline": "294f76666c3000630d828703e675814c05a4fd43", "packages": [ "boost*", @@ -16,7 +16,7 @@ }, { "kind": "git", - "repository": "git@github.com:ModOrganizer2/vcpkg-registry.git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", "baseline": "27d8adbfe9e4ce88a875be3a45fadab69869eb60", "packages": [ "asmjit", @@ -24,4 +24,4 @@ ] } ] -} +} \ No newline at end of file From 8a344e37d7e49a461c1796da02550409ad1077d7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:26:49 +0000 Subject: [PATCH 06/13] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- vcpkg-configuration.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index c8d97226..df8d809d 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -24,4 +24,4 @@ ] } ] -} \ No newline at end of file +} From 7d34ed7b296598bd416e5ed49ede1ae96a8c084d Mon Sep 17 00:00:00 2001 From: Acook1e Date: Thu, 11 Dec 2025 01:42:09 +0800 Subject: [PATCH 07/13] Update github action --- .github/workflows/build.yml | 140 ++++++++++++++++++++++------------ .github/workflows/linting.yml | 2 +- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2acd5357..4a419746 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,9 +8,6 @@ on: pull_request: types: [opened, synchronize, reopened] -env: - VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite - jobs: build: name: Build USVFS @@ -19,44 +16,66 @@ jobs: arch: [x86, x64] config: [Debug, Release] runs-on: windows-2022 + steps: - # set VCPKG Root - - name: "Set environmental variables" + # Set vcpkg root directory + - name: "Set environment variables" shell: bash run: | echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV - - # checkout USVFS and vcpkg + + # Checkout code - uses: actions/checkout@v4 - - # configure - - run: cmake --preset vs2022-windows-${{ matrix.arch }} -B build_${{ matrix.arch }} "-DCMAKE_INSTALL_PREFIX=install/${{ matrix.config }}" - - # build - - run: cmake --build build_${{ matrix.arch }} --config ${{ matrix.config }} --target INSTALL - - # package install - - uses: actions/upload-artifact@master + + # Setup vcpkg binary caching (GitHub Actions cache) + - name: Configure vcpkg cache + uses: actions/cache@v4 + id: cache-vcpkg + with: + path: | + ${{ env.VCPKG_ROOT }}/downloads + ${{ env.VCPKG_ROOT }}/installed + ${{ env.VCPKG_ROOT }}/buildtrees + key: ${{ runner.os }}-vcpkg-${{ matrix.arch }}-${{ matrix.config }}-${{ hashFiles('**/vcpkg.json', '**/CMakeLists.txt', '**/vcpkg-configuration.json') }} + restore-keys: | + ${{ runner.os }}-vcpkg-${{ matrix.arch }}-${{ matrix.config }}- + ${{ runner.os }}-vcpkg- + + # Configure + - name: Configure CMake + run: cmake --preset vs2022-windows-${{ matrix.arch }} -B build_${{ matrix.arch }} "-DCMAKE_INSTALL_PREFIX=install/${{ matrix.config }}" + + # Build + - name: Build project + run: cmake --build build_${{ matrix.arch }} --config ${{ matrix.config }} --target INSTALL + + # Package install files + - name: Upload install files + uses: actions/upload-artifact@v4 with: name: usvfs_${{ matrix.config }}_${{ matrix.arch }} path: ./install/${{ matrix.config }} - - # package test/dlls/etc. for tests - - - uses: actions/upload-artifact@master + + # Package test files, etc. + - name: Upload library files + uses: actions/upload-artifact@v4 with: name: usvfs-libs_${{ matrix.config }}_${{ matrix.arch }} path: ./lib - - uses: actions/upload-artifact@master + + - name: Upload binary files + uses: actions/upload-artifact@v4 with: name: usvfs-bins_${{ matrix.config }}_${{ matrix.arch }} path: ./bin - - uses: actions/upload-artifact@master + + - name: Upload test files + uses: actions/upload-artifact@v4 with: name: usvfs-tests_${{ matrix.config }}_${{ matrix.arch }} path: ./test/bin - # merge x86 / x64 artifacts for tests (root bin/lib and test folder) + # Merge x86/x64 test artifacts merge-artifacts-for-tests: runs-on: ubuntu-latest name: Merge Test Artifacts @@ -65,23 +84,25 @@ jobs: matrix: config: [Debug, Release] steps: - - name: Merge USVFS libs + - name: Merge USVFS library files uses: actions/upload-artifact/merge@v4 with: name: usvfs-libs_${{ matrix.config }} pattern: usvfs-libs_${{ matrix.config }}_* - - name: Merge USVFS bins + + - name: Merge USVFS binary files uses: actions/upload-artifact/merge@v4 with: name: usvfs-bins_${{ matrix.config }} pattern: usvfs-bins_${{ matrix.config }}_* - - name: Merge USVFS tests + + - name: Merge USVFS test files uses: actions/upload-artifact/merge@v4 with: name: usvfs-tests_${{ matrix.config }} pattern: usvfs-tests_${{ matrix.config }}_* - # merge x86 / x64 artifacts (install folder) + # Merge x86/x64 install artifacts merge-artifacts-for-release: runs-on: ubuntu-latest name: Merge Install Artifacts @@ -90,7 +111,7 @@ jobs: matrix: config: [Debug, Release] steps: - - name: Merge USVFS install + - name: Merge USVFS install files uses: actions/upload-artifact/merge@v4 with: name: usvfs_${{ matrix.config }} @@ -106,33 +127,52 @@ jobs: arch: [x86, x64] steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@master + + - uses: actions/download-artifact@v4 with: name: usvfs-libs_${{ matrix.config }} path: ./lib - - uses: actions/download-artifact@master + + - uses: actions/download-artifact@v4 with: name: usvfs-bins_${{ matrix.config }} path: ./bin - - uses: actions/download-artifact@master + + - uses: actions/download-artifact@v4 with: name: usvfs-tests_${{ matrix.config }} path: ./test/bin - - run: ./test/bin/shared_test_${{ matrix.arch }}.exe + + - name: Run shared tests + run: ./test/bin/shared_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/testinject_bin_${{ matrix.arch }}.exe + + - name: Run injection tests + run: ./test/bin/testinject_bin_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/thooklib_test_${{ matrix.arch }}.exe + + - name: Run hook library tests + run: ./test/bin/thooklib_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/tinjectlib_test_${{ matrix.arch }}.exe + + - name: Run inject library tests + run: ./test/bin/tinjectlib_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/tvfs_test_${{ matrix.arch }}.exe + + - name: Run VFS tests + run: ./test/bin/tvfs_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/usvfs_test_runner_${{ matrix.arch }}.exe + + - name: Run USVFS test runner + run: ./test/bin/usvfs_test_runner_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/usvfs_global_test_runner_${{ matrix.arch }}.exe + + - name: Run USVFS global test runner + run: ./test/bin/usvfs_global_test_runner_${{ matrix.arch }}.exe if: always() - - uses: actions/upload-artifact@v4 + + - name: Upload test outputs + uses: actions/upload-artifact@v4 if: always() with: name: tests-outputs_${{ matrix.config }}_${{ matrix.arch }} @@ -148,26 +188,26 @@ jobs: permissions: contents: write steps: - # USVFS does not use different names for debug and release so we are going to + # USVFS does not use different names for Debug and Release so we are going to # retrieve both install artifacts and put them under install/ and install/debug/ - - - name: Download Release Artifact - uses: actions/download-artifact@master + + - name: Download Release artifact + uses: actions/download-artifact@v4 with: name: usvfs_Release path: ./install - - - name: Download Debug Artifact - uses: actions/download-artifact@master + + - name: Download Debug artifact + uses: actions/download-artifact@v4 with: name: usvfs_Debug path: ./install/debug - - - name: Create USVFS Base archive + + - name: Create USVFS base archive run: 7z a usvfs_${{ github.ref_name }}.7z ./install/* - + - name: Publish Release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - run: gh release create --draft=false --notes="Release ${{ github.ref_name }}" "${{ github.ref_name }}" ./usvfs_${{ github.ref_name }}.7z + run: gh release create --draft=false --notes="Release ${{ github.ref_name }}" "${{ github.ref_name }}" ./usvfs_${{ github.ref_name }}.7z \ No newline at end of file diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index d5e3f4b9..c8319166 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -9,7 +9,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Check format uses: ModOrganizer2/check-formatting-action@master with: From 53da7525312fd6ff85415befbce1190fba6d8266 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Thu, 11 Dec 2025 14:09:44 +0800 Subject: [PATCH 08/13] Complete CoW: Set Correct File Pointer --- src/shared/ntdll_declarations.cpp | 2 + src/shared/ntdll_declarations.h | 5 + src/usvfs_dll/hookmanager.cpp | 1 + src/usvfs_dll/hooks/file_information_utils.h | 4 +- src/usvfs_dll/hooks/ntdll.cpp | 165 ++++++++++++++----- src/usvfs_dll/hooks/ntdll.h | 4 + 6 files changed, 140 insertions(+), 41 deletions(-) diff --git a/src/shared/ntdll_declarations.cpp b/src/shared/ntdll_declarations.cpp index 43da5cb6..b4c68660 100644 --- a/src/shared/ntdll_declarations.cpp +++ b/src/shared/ntdll_declarations.cpp @@ -31,6 +31,7 @@ NtQueryFullAttributesFile_type NtQueryFullAttributesFile; NtQueryAttributesFile_type NtQueryAttributesFile; NtQueryObject_type NtQueryObject; NtQueryInformationFile_type NtQueryInformationFile; +NtSetInformationFile_type NtSetInformationFile; NtQueryInformationByName_type NtQueryInformationByName; NtOpenFile_type NtOpenFile; NtCreateFile_type NtCreateFile; @@ -57,6 +58,7 @@ void ntdll_declarations_init() LOAD_EXT(ntDLLMod, NtQueryAttributesFile); LOAD_EXT(ntDLLMod, NtQueryObject); LOAD_EXT(ntDLLMod, NtQueryInformationFile); + LOAD_EXT(ntDLLMod, NtSetInformationFile); LOAD_EXT(ntDLLMod, NtQueryInformationByName); LOAD_EXT(ntDLLMod, NtCreateFile); LOAD_EXT(ntDLLMod, NtOpenFile); diff --git a/src/shared/ntdll_declarations.h b/src/shared/ntdll_declarations.h index 9a394546..e8e9583f 100644 --- a/src/shared/ntdll_declarations.h +++ b/src/shared/ntdll_declarations.h @@ -377,6 +377,7 @@ typedef enum _FILE_INFORMATION_CLASS FileNameInformation = 9, FileRenameInformation = 10, FileNamesInformation = 12, + FilePositionInformation = 14, FileAllInformation = 18, FileObjectIdInformation = 29, FileReparsePointInformation = 33, @@ -565,6 +566,9 @@ using NtQueryObject_type = NTSTATUS(WINAPI*)( using NtQueryInformationFile_type = NTSTATUS(WINAPI*)( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); +using NtSetInformationFile_type = NTSTATUS(WINAPI*)( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); using NtQueryInformationByName_type = NTSTATUS(WINAPI*)( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, @@ -608,6 +612,7 @@ extern NtQueryFullAttributesFile_type NtQueryFullAttributesFile; extern NtQueryAttributesFile_type NtQueryAttributesFile; extern NtQueryObject_type NtQueryObject; extern NtQueryInformationFile_type NtQueryInformationFile; +extern NtSetInformationFile_type NtSetInformationFile; extern NtQueryInformationByName_type NtQueryInformationByName; extern NtOpenFile_type NtOpenFile; extern NtCreateFile_type NtCreateFile; diff --git a/src/usvfs_dll/hookmanager.cpp b/src/usvfs_dll/hookmanager.cpp index 61fbe953..123391be 100644 --- a/src/usvfs_dll/hookmanager.cpp +++ b/src/usvfs_dll/hookmanager.cpp @@ -296,6 +296,7 @@ void HookManager::initHooks() installHook(ntdllMod, nullptr, "NtQueryDirectoryFileEx", hook_NtQueryDirectoryFileEx); installHook(ntdllMod, nullptr, "NtQueryObject", hook_NtQueryObject); installHook(ntdllMod, nullptr, "NtQueryInformationFile", hook_NtQueryInformationFile); + installHook(ntdllMod, nullptr, "NtSetInformationFile", hook_NtSetInformationFile); installHook(ntdllMod, nullptr, "NtQueryInformationByName", hook_NtQueryInformationByName); installHook(ntdllMod, nullptr, "NtOpenFile", hook_NtOpenFile); diff --git a/src/usvfs_dll/hooks/file_information_utils.h b/src/usvfs_dll/hooks/file_information_utils.h index dbd35974..6d11f095 100644 --- a/src/usvfs_dll/hooks/file_information_utils.h +++ b/src/usvfs_dll/hooks/file_information_utils.h @@ -16,8 +16,8 @@ namespace usvfs::details #define DECLARE_HAS_FIELD(Field) \ template \ - constexpr auto HasFieldImpl##Field(int)->decltype(std::declval().Field, void(), \ - std::true_type()) \ + constexpr auto HasFieldImpl##Field(int) \ + -> decltype(std::declval().Field, void(), std::true_type()) \ { \ return {}; \ } \ diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index 0594f4fa..e9387a4d 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -950,10 +950,22 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( FileInformationClass); } - PRE_REALCALL - res = ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, - FileInformationClass); - POST_REALCALL + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + auto WriteIter = writeAccessMap.find(FileHandle); + if (WriteIter != writeAccessMap.end() && + WriteIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { + auto WriteInfo = WriteIter->second; + PRE_REALCALL + res = ::NtQueryInformationFile(WriteInfo.RerouteHandle, IoStatusBlock, + FileInformation, Length, FileInformationClass); + POST_REALCALL + } else { + 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 @@ -970,8 +982,13 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( FileInformationClass == FileNormalizedNameInformation)) || (res == STATUS_SUCCESS && FileInformationClass == FileAllInformation)) { - const auto trackerInfo = ntdllHandleTracker.lookup(FileHandle); - const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + HandleTracker::info_type trackerInfo; + if (WriteIter != writeAccessMap.end() && + WriteIter->second.RerouteHandle != INVALID_HANDLE_VALUE) + trackerInfo = ntdllHandleTracker.lookup(WriteIter->second.RerouteHandle); + else + trackerInfo = ntdllHandleTracker.lookup(FileHandle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); // TODO: difference between FileNameInformation and FileNormalizedNameInformation @@ -1030,6 +1047,42 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( return res; } +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtSetInformationFile( + 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()) { + res = ::NtSetInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + callContext.updateLastError(); + return res; + } + + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + auto WriteIter = writeAccessMap.find(FileHandle); + if (WriteIter != writeAccessMap.end() && + WriteIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { + auto WriteInfo = WriteIter->second; + PRE_REALCALL + res = ::NtSetInformationFile(WriteInfo.RerouteHandle, IoStatusBlock, + FileInformation, Length, FileInformationClass); + POST_REALCALL + } else { + PRE_REALCALL + res = ::NtSetInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + POST_REALCALL + } + + HOOK_END + return res; +} + unique_ptr_deleter makeObjectAttributes(RedirectionInfo& redirInfo, POBJECT_ATTRIBUTES attributeTemplate) { @@ -1146,11 +1199,11 @@ NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, auto logger = spdlog::get("hooks"); - bool isWrite = - (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | - FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0; - if (isWrite) { - std::wstring physicalPath = adjustedAttributes->ObjectName->Buffer; + bool needReroute = false; + std::wstring physicalPath; + if ((DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | + FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0) { + physicalPath = adjustedAttributes->ObjectName->Buffer; // Clean up NT path prefix if present if (physicalPath.rfind(L"\\??\\", 0) == 0 || @@ -1192,12 +1245,8 @@ NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, } // Only track for CoW if in mods directory and not excluded - if (isInModsDir && !isExcluded) { - logger->debug("NtOpenFile: write access detected for file: {}", - ush::string_cast(physicalPath, ush::CodePage::UTF8)); - WRITE_CONTEXT()->customData( - WriteAccessHandles)[*FileHandle] = {nullptr, physicalPath}; - } + if (isInModsDir && !isExcluded) + needReroute = true; } PRE_REALCALL @@ -1211,6 +1260,13 @@ NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, #pragma message("need to clean up this handle in CloseHandle call") } + if (needReroute) { + logger->debug("NtOpenFile: write access detected for file: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + WRITE_CONTEXT()->customData( + WriteAccessHandles)[*FileHandle] = {nullptr, physicalPath}; + } + if (redir.redirected) { LOG_CALL() .addParam("source", ObjectAttributes) @@ -1442,9 +1498,9 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, logger->error( "NtCreateFile: Failed to copy to overwrite: {} (source: {}, dest: " "{})", - ush::string_cast( - ush::string_cast(ec.message(), ush::CodePage::UTF8), - ush::CodePage::UTF8), + ush::string_cast(ush::string_cast( + ec.message(), ush::CodePage::LOCAL), + ush::CodePage::UTF8), ush::string_cast(physicalPath, ush::CodePage::UTF8), destPath.string()); } else { @@ -1456,18 +1512,12 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, "NtCreateFile: Failed to create directories for overwrite: {} (dest: " "{})", ush::string_cast( - ush::string_cast(ec.message(), ush::CodePage::UTF8), + ush::string_cast(ec.message(), ush::CodePage::LOCAL), ush::CodePage::UTF8), ush::string_cast(overwriteDirW, ush::CodePage::UTF8)); } } } - } else if (isInModsDir && !isExcluded && isWrite) { - // File is in mods directory and has write access, prepare for CoW - logger->debug("NtCreateFile: write access detected for file: {}", - ush::string_cast(physicalPath, ush::CodePage::UTF8)); - WRITE_CONTEXT()->customData( - WriteAccessHandles)[*FileHandle] = {nullptr, physicalPath}; } if (!overwriteRedirectPath.empty()) { @@ -1508,6 +1558,15 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, WRITE_CONTEXT()->customData(SearchHandles)[*FileHandle] = inPathW; } + + // Register write access tracking AFTER successful file creation when we have a + // valid handle + if (isInModsDir && !isExcluded && !isDestructive && isWrite) { + logger->debug("NtCreateFile: write access detected for file: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + WRITE_CONTEXT()->customData( + WriteAccessHandles)[*FileHandle] = {INVALID_HANDLE_VALUE, physicalPath}; + } } if (rerouter.wasRerouted() || rerouter.changedError() || @@ -1597,8 +1656,11 @@ NTSTATUS WINAPI usvfs::hook_NtClose(HANDLE Handle) WRITE_CONTEXT()->customData(WriteAccessHandles); auto writeIter = writeAccessHandles.find(Handle); if (writeIter != writeAccessHandles.end()) { - if (writeIter->second.RerouteHandle != nullptr) - ::CloseHandle(writeIter->second.RerouteHandle); + if (writeIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { + spdlog::get("hooks")->debug("NtClose: CoW - Closing reroute file: {}", + writeIter->second.ReroutePath); + ::NtClose(writeIter->second.RerouteHandle); + } writeAccessHandles.erase(writeIter); } @@ -1755,7 +1817,7 @@ NTSTATUS WINAPI usvfs::hook_NtReadFile(HANDLE FileHandle, HANDLE Event, auto writeIter = writeAccessMap.find(FileHandle); if (writeIter != writeAccessMap.end()) { - if (writeIter->second.RerouteHandle != nullptr) { + if (writeIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { // File has been copied, read from the rerouted file logger->debug("NtReadFile: Reading from rerouted file: {}", writeIter->second.ReroutePath); @@ -1811,14 +1873,14 @@ NTSTATUS WINAPI usvfs::hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, if (writeIter != writeAccessMap.end()) { // handle write access WriteAccessHandle& writeAccessInfo = writeIter->second; - if (writeAccessInfo.RerouteHandle != nullptr) { + if (writeAccessInfo.RerouteHandle != INVALID_HANDLE_VALUE) { logger->debug("NtWriteFile: Write operation detected for file: {}", writeAccessInfo.ReroutePath); PRE_REALCALL res = ::NtWriteFile(writeAccessInfo.RerouteHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, Buffer, Length, ByteOffset, Key); POST_REALCALL - } else if (writeAccessInfo.RerouteHandle == nullptr) { + } else if (writeAccessInfo.RerouteHandle == INVALID_HANDLE_VALUE) { logger->info("NtWriteFile: Copy on Write for file: {}", writeAccessInfo.ReroutePath); @@ -1862,10 +1924,30 @@ NTSTATUS WINAPI usvfs::hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, destPath.string()); reroutePath = destPath.wstring(); - // Open the rerouted file for writing - HANDLE rerouteHandle = - CreateFileW(reroutePath.c_str(), GENERIC_WRITE, 0, nullptr, - OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + FILE_POSITION_INFORMATION FilePointer; + IO_STATUS_BLOCK iosbQuery, iosbSet; + HANDLE rerouteHandle = INVALID_HANDLE_VALUE; + NTSTATUS queryStatus = ::NtQueryInformationFile( + FileHandle, &iosbQuery, &FilePointer, sizeof(FilePointer), + FilePositionInformation); + + if (queryStatus == STATUS_SUCCESS) { + rerouteHandle = + CreateFileW(reroutePath.c_str(), GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (rerouteHandle != INVALID_HANDLE_VALUE) { + NTSTATUS setStatus = ::NtSetInformationFile( + rerouteHandle, &iosbSet, &FilePointer, sizeof(FilePointer), + FilePositionInformation); + if (setStatus != STATUS_SUCCESS) { + logger->error("NtWriteFile: CoW - Failed to set file pointer for " + "rerouted file: {}", + reroutePath); + CloseHandle(rerouteHandle); + rerouteHandle = INVALID_HANDLE_VALUE; + } + } + } if (rerouteHandle != INVALID_HANDLE_VALUE) { writeAccessInfo.RerouteHandle = rerouteHandle; @@ -1880,13 +1962,18 @@ NTSTATUS WINAPI usvfs::hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, res = STATUS_CANNOT_DELETE; } } else { - logger->error("NtWriteFile: CoW - Failed to copy file: {}", - ec.message()); + std::wstring errorMsgW = ush::string_cast( + ush::string_cast(ec.message(), ush::CodePage::LOCAL), + ush::CodePage::UTF8); + logger->error("NtWriteFile: CoW - Failed to copy file: {}", errorMsgW); res = STATUS_CANNOT_DELETE; } } else { + std::wstring errorMsgW = ush::string_cast( + ush::string_cast(ec.message(), ush::CodePage::LOCAL), + ush::CodePage::UTF8); logger->error("NtWriteFile: CoW - Failed to create directories: {}", - ec.message()); + errorMsgW); res = STATUS_CANNOT_DELETE; } } else { diff --git a/src/usvfs_dll/hooks/ntdll.h b/src/usvfs_dll/hooks/ntdll.h index 7ef522cd..57abb0aa 100644 --- a/src/usvfs_dll/hooks/ntdll.h +++ b/src/usvfs_dll/hooks/ntdll.h @@ -34,6 +34,10 @@ DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationFile( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); +DLLEXPORT NTSTATUS WINAPI hook_NtSetInformationFile( + 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); From 994ca4ca928a915f1830a5c818f1fead5bb193a6 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Fri, 12 Dec 2025 15:58:18 +0800 Subject: [PATCH 09/13] DeleteFile Changed: when enable CoW, just remove vfs mapping --- src/usvfs_dll/hooks/kernel32.cpp | 104 ++++++++++++++++++++++++++++--- src/usvfs_dll/hooks/ntdll.cpp | 6 -- src/usvfs_dll/hooks/settings.h | 3 +- src/usvfs_dll/maptracker.h | 49 ++++++++++++--- src/usvfs_dll/usvfs.cpp | 23 ++++--- 5 files changed, 151 insertions(+), 34 deletions(-) diff --git a/src/usvfs_dll/hooks/kernel32.cpp b/src/usvfs_dll/hooks/kernel32.cpp index 6599eb02..821c1878 100644 --- a/src/usvfs_dll/hooks/kernel32.cpp +++ b/src/usvfs_dll/hooks/kernel32.cpp @@ -5,6 +5,7 @@ #include "../hookcontext.h" #include "../hookmanager.h" #include "../maptracker.h" +#include "settings.h" #include #include #include @@ -58,8 +59,6 @@ class CurrentDirectoryTracker currentDir[1] == ':'; std::unique_lock lock(m_mutex); m_currentDrive = good ? index : -1; - if (good) - m_perDrive[index] = currentDir; return good; } @@ -296,20 +295,57 @@ BOOL WINAPI usvfs::hook_CreateProcessInternalW( } std::wstring cmdline; + bool useCmdline = false; if (cend && cmdReroute.fileName()) { - auto fileName = cmdReroute.fileName(); - cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2); - if (*fileName != '"') + std::wstring fileName = cmdReroute.fileName(); + if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\\\?\\") == 0) { + fileName = fileName.substr(4); + } else if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\??\\") == 0) { + fileName = fileName.substr(4); + } + + cmdline.reserve(fileName.length() + wcslen(cend) + 2); + if (fileName.length() > 0 && fileName[0] != '"') cmdline += L"\""; cmdline += fileName; - if (*fileName != '"') + if (fileName.length() > 0 && fileName[0] != '"') cmdline += L"\""; cmdline += cend; + useCmdline = true; + } else if (lpCommandLine) { + cmdline = lpCommandLine; + useCmdline = true; + } + + if (useCmdline) { + size_t pos = 0; + while ((pos = cmdline.find(L"\\\\?\\", pos)) != std::wstring::npos) { + cmdline.replace(pos, 4, L""); + } + pos = 0; + while ((pos = cmdline.find(L"\\??\\", pos)) != std::wstring::npos) { + cmdline.replace(pos, 4, L""); + } + } + + std::wstring appName; + LPCWSTR lpAppName = applicationReroute.fileName(); + if (lpAppName) { + if (wcsncmp(lpAppName, L"\\\\?\\", 4) == 0) { + appName = lpAppName + 4; + lpAppName = appName.c_str(); + } else if (wcsncmp(lpAppName, L"\\??\\", 4) == 0) { + appName = lpAppName + 4; + lpAppName = appName.c_str(); + } } + spdlog::get("hooks")->info("CreateProcessInternalW: app={}, cmd={}", + lpAppName ? string_cast(lpAppName) : "null", + useCmdline ? string_cast(cmdline) : "null"); + PRE_REALCALL - res = CreateProcessInternalW(token, applicationReroute.fileName(), - cmdline.empty() ? lpCommandLine : &cmdline[0], + res = CreateProcessInternalW(token, lpAppName, useCmdline ? &cmdline[0] : nullptr, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken); @@ -576,6 +612,58 @@ BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, path.c_str()); + if (usvfs::settings::enableCoW) { + auto logger = spdlog::get("usvfs_hooks"); + logger->info("DeleteFileW: requested path: {}", string_cast(path)); + logger->info("DeleteFileW: rerouted path: {}", + string_cast(reroute.fileName())); + + std::wstring physicalPath = reroute.fileName(); + if (physicalPath.rfind(L"\\??\\", 0) == 0 || + physicalPath.rfind(L"\\\\?\\", 0) == 0) { + physicalPath = physicalPath.substr(4); + } + + bool isInModsDir = false; + std::wstring modsDirW; + if (!usvfs::settings::mods_dir.empty()) { + modsDirW = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + if (physicalPath.size() >= modsDirW.size() && + _wcsnicmp(physicalPath.c_str(), modsDirW.c_str(), modsDirW.size()) == 0) { + wchar_t nextChar = physicalPath.c_str()[modsDirW.size()]; + if (nextChar == L'\0' || nextChar == L'\\') { + isInModsDir = true; + } + } + } + + // Check if file is in excluded directory + bool isExcluded = false; + if (isInModsDir) { + for (const auto& excludedPath : usvfs::settings::exclude_mods) { + std::wstring excludedPathW = + ush::string_cast(excludedPath, ush::CodePage::UTF8); + if (physicalPath.size() >= excludedPathW.size() && + _wcsnicmp(physicalPath.c_str(), excludedPathW.c_str(), + excludedPathW.size()) == 0) { + wchar_t nextExChar = physicalPath.c_str()[excludedPathW.size()]; + if (nextExChar == L'\0' || nextExChar == L'\\') { + isExcluded = true; + break; + } + } + } + } + + if (isInModsDir && !isExcluded) { + logger->info("DeleteFileW: CoW - Remove mapping instead of deleting file: {}", + physicalPath); + reroute.removeMapping(READ_CONTEXT(), false); + return true; + } + } + PRE_REALCALL if (reroute.wasRerouted()) { res = ::DeleteFileW(reroute.fileName()); diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index e9387a4d..1d937e05 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1420,9 +1420,6 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0; - logger->debug("NtCreateFile: isWrite: {}, isDestructive: {}", isWrite, - isDestructive); - std::wstring overwriteRedirectPath; // Check if file is in mods directory @@ -1461,9 +1458,6 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, } } - logger->debug("NtCreateFile: isInModsDir: {}, isExcluded: {}", isInModsDir, - isExcluded); - // Handle destructive operations on mods files if (isInModsDir && !isExcluded && isDestructive) { logger->warn("NtCreateFile: mod file will be truncated - original path: {}", diff --git a/src/usvfs_dll/hooks/settings.h b/src/usvfs_dll/hooks/settings.h index 6adbbc8e..df931cd5 100644 --- a/src/usvfs_dll/hooks/settings.h +++ b/src/usvfs_dll/hooks/settings.h @@ -7,10 +7,11 @@ namespace usvfs { namespace settings { + inline bool enableCoW = false; + inline std::string current_process; inline std::string mods_dir; inline std::string overwrite_dir; inline std::vector exclude_mods; inline std::map output_directories; - inline std::string current_process; } // namespace settings } // namespace usvfs diff --git a/src/usvfs_dll/maptracker.h b/src/usvfs_dll/maptracker.h index 6ddf471b..4a746fb0 100644 --- a/src/usvfs_dll/maptracker.h +++ b/src/usvfs_dll/maptracker.h @@ -358,10 +358,11 @@ class RerouteW static fs::path absolutePath(const wchar_t* inPath) { + fs::path p; if (shared::startswith(inPath, LR"(\\?\)") || shared::startswith(inPath, LR"(\??\)")) { inPath += 4; - return inPath; + p = inPath; } else if ((shared::startswith(inPath, LR"(\\localhost\)") || shared::startswith(inPath, LR"(\\127.0.0.1\)")) && inPath[13] == L'$') { @@ -369,17 +370,47 @@ class RerouteW newPath += towupper(inPath[12]); newPath += L':'; newPath += &inPath[14]; - return newPath; + p = newPath; } else if (inPath[0] == L'\0' || inPath[1] == L':') { - return inPath; + p = inPath; } else if (inPath[0] == L'\\' || inPath[0] == L'/') { - return fs::path(winapi::wide::getFullPathName(inPath).first); + p = fs::path(winapi::wide::getFullPathName(inPath).first); + } else { + WCHAR currentDirectory[MAX_PATH]; + ::GetCurrentDirectoryW(MAX_PATH, currentDirectory); + p = fs::path(currentDirectory) / inPath; + } + + std::wstring pathStr = p.wstring(); + if (pathStr.find(L'~') != std::wstring::npos) { + std::vector buffer(MAX_PATH); + DWORD res = GetLongPathNameW(pathStr.c_str(), buffer.data(), buffer.size()); + if (res > buffer.size()) { + buffer.resize(res); + res = GetLongPathNameW(pathStr.c_str(), buffer.data(), buffer.size()); + } + if (res > 0) { + return fs::path(std::wstring(buffer.data(), res)); + } + + fs::path existing = p; + fs::path remainder; + while (!existing.empty() && existing != existing.root_path()) { + std::wstring eStr = existing.wstring(); + res = GetLongPathNameW(eStr.c_str(), buffer.data(), buffer.size()); + if (res > buffer.size()) { + buffer.resize(res); + res = GetLongPathNameW(eStr.c_str(), buffer.data(), buffer.size()); + } + if (res > 0) { + fs::path longExisting(std::wstring(buffer.data(), res)); + return longExisting / remainder; + } + remainder = existing.filename() / remainder; + existing = existing.parent_path(); + } } - WCHAR currentDirectory[MAX_PATH]; - ::GetCurrentDirectoryW(MAX_PATH, currentDirectory); - fs::path finalPath = fs::path(currentDirectory) / inPath; - return finalPath; - // winapi::wide::getFullPathName(inPath).first; + return p; } static fs::path canonizePath(const fs::path& inPath) diff --git a/src/usvfs_dll/usvfs.cpp b/src/usvfs_dll/usvfs.cpp index 3f3d672f..f6e763a9 100644 --- a/src/usvfs_dll/usvfs.cpp +++ b/src/usvfs_dll/usvfs.cpp @@ -389,6 +389,8 @@ void LoadSettings() } iniPath += L"usvfs_redirect.ini"; + auto logger = spdlog::get("usvfs"); + std::filesystem::path dllDir = std::filesystem::path(modulePath).parent_path(); auto makeAbsolute = [&](std::string pathStr) -> std::string { @@ -411,7 +413,8 @@ void LoadSettings() return ush::string_cast(buffer, ush::CodePage::UTF8); }; - usvfs::settings::mods_dir = makeAbsolute(readString(L"General", L"mods_dir", L"")); + usvfs::settings::enableCoW = readString(L"General", L"enable_cow", L"1") != "0"; + usvfs::settings::mods_dir = makeAbsolute(readString(L"General", L"mods_dir", L"")); usvfs::settings::overwrite_dir = makeAbsolute(readString(L"General", L"overwrite_dir", L"")); @@ -420,7 +423,7 @@ void LoadSettings() if (!exclude_raw.empty()) { std::stringstream ss(exclude_raw); std::string item; - while (std::getline(ss, item, ';')) { + while (std::getline(ss, item, '|')) { if (!item.empty()) { std::filesystem::path p = ush::string_cast(item, ush::CodePage::UTF8); @@ -470,18 +473,16 @@ void LoadSettings() usvfs::settings::overwrite_dir = ush::string_cast( newOverwrite.lexically_normal().wstring(), ush::CodePage::UTF8); - auto logger = spdlog::get("usvfs"); - if (logger) { - logger->info("Instance match: '{}' -> Overwrite set to: '{}'", - usvfs::settings::current_process, - usvfs::settings::overwrite_dir); - } + logger->info("Instance match: '{}' -> Overwrite set to: '{}'", + usvfs::settings::current_process, usvfs::settings::overwrite_dir); } } } + usvfs::settings::exclude_mods.push_back(usvfs::settings::overwrite_dir); - auto logger = spdlog::get("usvfs"); logger->info("Settings loaded:"); + logger->info(" enable_cow: {}", usvfs::settings::enableCoW); + logger->info(" current_process: {}", usvfs::settings::current_process); logger->info(" mods_dir: {}", usvfs::settings::mods_dir); logger->info(" overwrite_dir: {}", usvfs::settings::overwrite_dir); logger->info(" exclude_mods:"); @@ -500,7 +501,9 @@ void __cdecl InitHooks(LPVOID parameters, size_t) const usvfsParameters* params = reinterpret_cast(parameters); - usvfs::settings::current_process = "SkyrimSE.exe"; + // get process name from path + usvfs::settings::current_process = + bfs::path(winapi::ansi::getModuleFileName(nullptr)).filename().string(); LoadSettings(); // there is already a wait in the constructor of HookManager, but this one is useful From e48260c950950a244602a054aec096bdc3649f54 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Fri, 12 Dec 2025 21:56:54 +0800 Subject: [PATCH 10/13] Fix DeleteFileW and LpProcessInformation --- src/usvfs_dll/hooks/kernel32.cpp | 69 +++++++++++--------------------- 1 file changed, 24 insertions(+), 45 deletions(-) diff --git a/src/usvfs_dll/hooks/kernel32.cpp b/src/usvfs_dll/hooks/kernel32.cpp index 821c1878..bdedffdc 100644 --- a/src/usvfs_dll/hooks/kernel32.cpp +++ b/src/usvfs_dll/hooks/kernel32.cpp @@ -59,6 +59,8 @@ class CurrentDirectoryTracker currentDir[1] == ':'; std::unique_lock lock(m_mutex); m_currentDrive = good ? index : -1; + if (good) + m_perDrive[index] = currentDir; return good; } @@ -295,57 +297,38 @@ BOOL WINAPI usvfs::hook_CreateProcessInternalW( } std::wstring cmdline; - bool useCmdline = false; if (cend && cmdReroute.fileName()) { - std::wstring fileName = cmdReroute.fileName(); - if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\\\?\\") == 0) { - fileName = fileName.substr(4); - } else if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\??\\") == 0) { - fileName = fileName.substr(4); - } - - cmdline.reserve(fileName.length() + wcslen(cend) + 2); - if (fileName.length() > 0 && fileName[0] != '"') + auto fileName = cmdReroute.fileName(); + cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2); + if (*fileName != '"') cmdline += L"\""; cmdline += fileName; - if (fileName.length() > 0 && fileName[0] != '"') + if (*fileName != '"') cmdline += L"\""; cmdline += cend; - useCmdline = true; - } else if (lpCommandLine) { - cmdline = lpCommandLine; - useCmdline = true; } - if (useCmdline) { - size_t pos = 0; - while ((pos = cmdline.find(L"\\\\?\\", pos)) != std::wstring::npos) { - cmdline.replace(pos, 4, L""); - } - pos = 0; - while ((pos = cmdline.find(L"\\??\\", pos)) != std::wstring::npos) { - cmdline.replace(pos, 4, L""); + if (!cmdline.empty()) { + // remove \??\ or \\?\ prefix added by RerouteW for CreateProcess + auto pos = cmdline.find(L"\\??\\"); + while (pos != std::wstring::npos) { + cmdline = cmdline.replace(pos, 4, L""); + pos = cmdline.find(L"\\??\\"); } - } - - std::wstring appName; - LPCWSTR lpAppName = applicationReroute.fileName(); - if (lpAppName) { - if (wcsncmp(lpAppName, L"\\\\?\\", 4) == 0) { - appName = lpAppName + 4; - lpAppName = appName.c_str(); - } else if (wcsncmp(lpAppName, L"\\??\\", 4) == 0) { - appName = lpAppName + 4; - lpAppName = appName.c_str(); + pos = cmdline.find(L"\\\\?\\"); + while (pos != std::wstring::npos) { + cmdline = cmdline.replace(pos, 4, L""); + pos = cmdline.find(L"\\\\?\\"); } } - spdlog::get("hooks")->info("CreateProcessInternalW: app={}, cmd={}", - lpAppName ? string_cast(lpAppName) : "null", - useCmdline ? string_cast(cmdline) : "null"); + spdlog::get("hooks")->info( + "CreateProcessInternalW: application='{}', commandline='{}'", + applicationReroute.fileName(), cmdline.empty() ? lpCommandLine : cmdline); PRE_REALCALL - res = CreateProcessInternalW(token, lpAppName, useCmdline ? &cmdline[0] : nullptr, + res = CreateProcessInternalW(token, applicationReroute.fileName(), + cmdline.empty() ? lpCommandLine : &cmdline[0], lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken); @@ -613,11 +596,6 @@ BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, path.c_str()); if (usvfs::settings::enableCoW) { - auto logger = spdlog::get("usvfs_hooks"); - logger->info("DeleteFileW: requested path: {}", string_cast(path)); - logger->info("DeleteFileW: rerouted path: {}", - string_cast(reroute.fileName())); - std::wstring physicalPath = reroute.fileName(); if (physicalPath.rfind(L"\\??\\", 0) == 0 || physicalPath.rfind(L"\\\\?\\", 0) == 0) { @@ -657,8 +635,9 @@ BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) } if (isInModsDir && !isExcluded) { - logger->info("DeleteFileW: CoW - Remove mapping instead of deleting file: {}", - physicalPath); + spdlog::get("hooks")->info( + "DeleteFileW: CoW - Remove mapping instead of deleting file: {}", + physicalPath); reroute.removeMapping(READ_CONTEXT(), false); return true; } From 38a40a8101b6f8c613884147dcb7eff543523db6 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Sat, 13 Dec 2025 00:45:44 +0800 Subject: [PATCH 11/13] Add a Variable to switch CoW Feature --- src/usvfs_dll/hooks/ntdll.cpp | 6 +++-- src/usvfs_dll/usvfs.cpp | 51 +++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index 1d937e05..cd5badf6 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1201,7 +1201,8 @@ NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, bool needReroute = false; std::wstring physicalPath; - if ((DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | + if (usvfs::settings::enableCoW && + (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0) { physicalPath = adjustedAttributes->ObjectName->Buffer; @@ -1425,7 +1426,8 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, // Check if file is in mods directory bool isInModsDir = false; std::wstring modsDirW; - if (!usvfs::settings::mods_dir.empty() && (isDestructive || isWrite)) { + if (usvfs::settings::enableCoW && !usvfs::settings::mods_dir.empty() && + (isDestructive || isWrite)) { modsDirW = ush::string_cast(usvfs::settings::mods_dir, ush::CodePage::UTF8); if (physicalPath.size() >= modsDirW.size() && diff --git a/src/usvfs_dll/usvfs.cpp b/src/usvfs_dll/usvfs.cpp index f6e763a9..485e325f 100644 --- a/src/usvfs_dll/usvfs.cpp +++ b/src/usvfs_dll/usvfs.cpp @@ -391,13 +391,12 @@ void LoadSettings() auto logger = spdlog::get("usvfs"); - std::filesystem::path dllDir = std::filesystem::path(modulePath).parent_path(); + bfs::path dllDir = bfs::path(modulePath).parent_path(); auto makeAbsolute = [&](std::string pathStr) -> std::string { if (pathStr.empty()) return ""; - std::filesystem::path p = - ush::string_cast(pathStr, ush::CodePage::UTF8); + bfs::path p = ush::string_cast(pathStr, ush::CodePage::UTF8); if (p.is_relative()) { p = dllDir / p; } @@ -418,6 +417,15 @@ void LoadSettings() usvfs::settings::overwrite_dir = makeAbsolute(readString(L"General", L"overwrite_dir", L"")); + if (!bfs::exists(bfs::path(usvfs::settings::mods_dir)) || + !bfs::is_directory(bfs::path(usvfs::settings::mods_dir))) + logger->error("Mods directory does not exist or is not a directory: '{}'", + usvfs::settings::mods_dir); + if (!bfs::exists(bfs::path(usvfs::settings::overwrite_dir)) || + !bfs::is_directory(bfs::path(usvfs::settings::overwrite_dir))) + logger->error("Overwrite directory does not exist or is not a directory: '{}'", + usvfs::settings::overwrite_dir); + std::string exclude_raw = readString(L"General", L"exclude_dir", L""); usvfs::settings::exclude_mods.clear(); if (!exclude_raw.empty()) { @@ -425,18 +433,19 @@ void LoadSettings() std::string item; while (std::getline(ss, item, '|')) { if (!item.empty()) { - std::filesystem::path p = - ush::string_cast(item, ush::CodePage::UTF8); + bfs::path p = ush::string_cast(item, ush::CodePage::UTF8); if (p.is_relative() && !usvfs::settings::mods_dir.empty()) { - std::filesystem::path mods = ush::string_cast( - usvfs::settings::mods_dir, ush::CodePage::UTF8); - p = mods / p; + bfs::path mods = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + p = mods / p; } else if (p.is_relative()) { - p = std::filesystem::path( + p = bfs::path( ush::string_cast(makeAbsolute(item), ush::CodePage::UTF8)); } - usvfs::settings::exclude_mods.push_back(ush::string_cast( - p.lexically_normal().wstring(), ush::CodePage::UTF8)); + // only add if directory exists + if (bfs::exists(p) && bfs::is_directory(p)) + usvfs::settings::exclude_mods.push_back(ush::string_cast( + p.lexically_normal().wstring(), ush::CodePage::UTF8)); } } } @@ -465,16 +474,18 @@ void LoadSettings() if (it != usvfs::settings::output_directories.end()) { std::string relativeOverwrite = it->second; if (!usvfs::settings::mods_dir.empty()) { - std::filesystem::path mods(ush::string_cast( - usvfs::settings::mods_dir, ush::CodePage::UTF8)); - std::filesystem::path rel( + bfs::path mods(ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8)); + bfs::path rel( ush::string_cast(relativeOverwrite, ush::CodePage::UTF8)); - std::filesystem::path newOverwrite = mods / rel; - usvfs::settings::overwrite_dir = ush::string_cast( - newOverwrite.lexically_normal().wstring(), ush::CodePage::UTF8); - - logger->info("Instance match: '{}' -> Overwrite set to: '{}'", - usvfs::settings::current_process, usvfs::settings::overwrite_dir); + bfs::path newOverwrite = mods / rel; + if (bfs::exists(newOverwrite) && bfs::is_directory(newOverwrite)) { + usvfs::settings::overwrite_dir = ush::string_cast( + newOverwrite.lexically_normal().wstring(), ush::CodePage::UTF8); + logger->info("Instance match: '{}' -> Overwrite set to: '{}'", + usvfs::settings::current_process, + usvfs::settings::overwrite_dir); + } } } } From fdc738440685f1e7d0c8af8bc890775aece5ff84 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Sat, 13 Dec 2025 01:17:59 +0800 Subject: [PATCH 12/13] Fix LpProcessInformation Aagin And Resovle Mutex Release --- src/usvfs_dll/hooks/kernel32.cpp | 79 ++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/usvfs_dll/hooks/kernel32.cpp b/src/usvfs_dll/hooks/kernel32.cpp index bdedffdc..15f5997c 100644 --- a/src/usvfs_dll/hooks/kernel32.cpp +++ b/src/usvfs_dll/hooks/kernel32.cpp @@ -297,38 +297,57 @@ BOOL WINAPI usvfs::hook_CreateProcessInternalW( } std::wstring cmdline; + bool useCmdline = false; if (cend && cmdReroute.fileName()) { - auto fileName = cmdReroute.fileName(); - cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2); - if (*fileName != '"') + std::wstring fileName = cmdReroute.fileName(); + if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\\\?\\") == 0) { + fileName = fileName.substr(4); + } else if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\??\\") == 0) { + fileName = fileName.substr(4); + } + + cmdline.reserve(fileName.length() + wcslen(cend) + 2); + if (fileName.length() > 0 && fileName[0] != '"') cmdline += L"\""; cmdline += fileName; - if (*fileName != '"') + if (fileName.length() > 0 && fileName[0] != '"') cmdline += L"\""; cmdline += cend; + useCmdline = true; + } else if (lpCommandLine) { + cmdline = lpCommandLine; + useCmdline = true; } - if (!cmdline.empty()) { - // remove \??\ or \\?\ prefix added by RerouteW for CreateProcess - auto pos = cmdline.find(L"\\??\\"); - while (pos != std::wstring::npos) { - cmdline = cmdline.replace(pos, 4, L""); - pos = cmdline.find(L"\\??\\"); + if (useCmdline) { + size_t pos = 0; + while ((pos = cmdline.find(L"\\\\?\\", pos)) != std::wstring::npos) { + cmdline.replace(pos, 4, L""); + } + pos = 0; + while ((pos = cmdline.find(L"\\??\\", pos)) != std::wstring::npos) { + cmdline.replace(pos, 4, L""); } - pos = cmdline.find(L"\\\\?\\"); - while (pos != std::wstring::npos) { - cmdline = cmdline.replace(pos, 4, L""); - pos = cmdline.find(L"\\\\?\\"); + } + + std::wstring appName; + LPCWSTR lpAppName = applicationReroute.fileName(); + if (lpAppName) { + if (wcsncmp(lpAppName, L"\\\\?\\", 4) == 0) { + appName = lpAppName + 4; + lpAppName = appName.c_str(); + } else if (wcsncmp(lpAppName, L"\\??\\", 4) == 0) { + appName = lpAppName + 4; + lpAppName = appName.c_str(); } } - spdlog::get("hooks")->info( - "CreateProcessInternalW: application='{}', commandline='{}'", - applicationReroute.fileName(), cmdline.empty() ? lpCommandLine : cmdline); + spdlog::get("hooks")->info("CreateProcessInternalW: app={}, cmd={}", + lpAppName ? string_cast(lpAppName) : "null", + useCmdline ? string_cast(cmdline) : "null"); PRE_REALCALL - res = CreateProcessInternalW(token, applicationReroute.fileName(), - cmdline.empty() ? lpCommandLine : &cmdline[0], + res = CreateProcessInternalW(token, lpAppName, useCmdline ? &cmdline[0] : nullptr, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken); @@ -639,20 +658,20 @@ BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) "DeleteFileW: CoW - Remove mapping instead of deleting file: {}", physicalPath); reroute.removeMapping(READ_CONTEXT(), false); - return true; + res = true; } - } - - PRE_REALCALL - if (reroute.wasRerouted()) { - res = ::DeleteFileW(reroute.fileName()); } else { - res = ::DeleteFileW(path.c_str()); - } - POST_REALCALL + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::DeleteFileW(reroute.fileName()); + } else { + res = ::DeleteFileW(path.c_str()); + } + POST_REALCALL - if (res) { - reroute.removeMapping(READ_CONTEXT()); + if (res) { + reroute.removeMapping(READ_CONTEXT()); + } } if (reroute.wasRerouted()) From 2de5ec9b2ad24723cc39cab892618874a71217b4 Mon Sep 17 00:00:00 2001 From: Acook1e Date: Sat, 13 Dec 2025 01:52:41 +0800 Subject: [PATCH 13/13] Fix DeleteW --- src/usvfs_dll/hooks/kernel32.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/usvfs_dll/hooks/kernel32.cpp b/src/usvfs_dll/hooks/kernel32.cpp index 15f5997c..7bf44900 100644 --- a/src/usvfs_dll/hooks/kernel32.cpp +++ b/src/usvfs_dll/hooks/kernel32.cpp @@ -660,18 +660,18 @@ BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) reroute.removeMapping(READ_CONTEXT(), false); res = true; } + } + + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::DeleteFileW(reroute.fileName()); } else { - PRE_REALCALL - if (reroute.wasRerouted()) { - res = ::DeleteFileW(reroute.fileName()); - } else { - res = ::DeleteFileW(path.c_str()); - } - POST_REALCALL + res = ::DeleteFileW(path.c_str()); + } + POST_REALCALL - if (res) { - reroute.removeMapping(READ_CONTEXT()); - } + if (res) { + reroute.removeMapping(READ_CONTEXT()); } if (reroute.wasRerouted())