diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-08 11:06:06 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-08 11:06:06 -0500 |
| commit | 6391164071b859e901283bec098974ef6b104f50 (patch) | |
| tree | 52ded084a17437b225382a01f1f293696de3029a | |
| parent | f9fbaf60cdb8e242554227eb41b163c12591fdf2 (diff) | |
Add PE-side VFS bridge acceleration
32 files changed, 8300 insertions, 14 deletions
diff --git a/docker/Dockerfile b/docker/Dockerfile index 643074c..13a67d5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ zip unzip \ # Static analysis clang-tidy \ + # mingw-w64 cross-toolchain for fluorine_vfs.dll (PE-side VFS injector, + # loaded into Wine processes via AppInit_DLLs). + gcc-mingw-w64-x86-64-posix \ && rm -rf /var/lib/apt/lists/* # ── Qt 6.10 via aqtinstall ── diff --git a/docker/build-inner.sh b/docker/build-inner.sh index 24e965a..8a4f9cb 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -149,6 +149,18 @@ cp -f build/libs/uibase/src/libuibase.so "${OUT_DIR}/lib/" cp -f build/libs/libbsarch/liblibbsarch.so "${OUT_DIR}/lib/" cp -f build/libs/archive/src/libarchive.so "${OUT_DIR}/lib/" cp -f build/libs/plugin_python/src/runner/librunner.so "${OUT_DIR}/lib/" +[ -f build/src/src/libfluorine_vfs_preload.so ] && \ + cp -f build/src/src/libfluorine_vfs_preload.so "${OUT_DIR}/lib/" +# PE-side VFS injector: cross-built with mingw, copied into wine/ where +# Fluorine picks it up at prefix init and stages into c:\\windows\\system32\\. +if [ -f build/src/src/fluorine_vfs.dll ]; then + mkdir -p "${OUT_DIR}/wine" + cp -f build/src/src/fluorine_vfs.dll "${OUT_DIR}/wine/" +fi +if [ -f build/src/src/fluorine_vfs_hid.dll ]; then + mkdir -p "${OUT_DIR}/wine" + cp -f build/src/src/fluorine_vfs_hid.dll "${OUT_DIR}/wine/" +fi if [ -f "libs/bsa_ffi/target/release/libbsa_ffi.so" ]; then cp -f libs/bsa_ffi/target/release/libbsa_ffi.so "${OUT_DIR}/lib/" fi diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index 70da406..ef9f696 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -74,6 +74,87 @@ add_executable(organizer ${ORGANIZER_UI} ${ORGANIZER_QRC}) +add_library(fluorine_vfs_preload SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/vfs/vfsbridgepreload.c) + +set_target_properties(fluorine_vfs_preload PROPERTIES + OUTPUT_NAME "fluorine_vfs_preload") + +target_link_libraries(fluorine_vfs_preload PRIVATE dl) + +# ── PE-side VFS injector (fluorine_vfs.dll) ────────────────────────────── +# Cross-compiled with mingw-w64 and loaded into Wine PE processes via the +# AppInit_DLLs registry mechanism set up at prefix init. A game-directory +# hid.dll proxy is also built for Wine paths that do not honor AppInit_DLLs. +find_program(MINGW_CC NAMES x86_64-w64-mingw32-gcc-posix x86_64-w64-mingw32-gcc) +if(MINGW_CC) + set(FL_VFS_DLL_SRC ${CMAKE_CURRENT_SOURCE_DIR}/vfs/fluorine_vfs_inject.c) + set(FL_VFS_HOOKS_SRC ${CMAKE_CURRENT_SOURCE_DIR}/vfs/fluorine_vfs_hooks.c) + set(FL_VFS_MINHOOK_SRC + ${CMAKE_SOURCE_DIR}/third_party/minhook/src/buffer.c + ${CMAKE_SOURCE_DIR}/third_party/minhook/src/hook.c + ${CMAKE_SOURCE_DIR}/third_party/minhook/src/trampoline.c + ${CMAKE_SOURCE_DIR}/third_party/minhook/src/hde/hde64.c) + set(FL_VFS_MINGW_INCLUDES + -I${CMAKE_SOURCE_DIR}/third_party/minhook/include + -I${CMAKE_SOURCE_DIR}/third_party/minhook/src + -I${CMAKE_SOURCE_DIR}/third_party/minhook/src/hde) + set(FL_VFS_DLL ${CMAKE_CURRENT_BINARY_DIR}/fluorine_vfs.dll) + set(FL_VFS_HID_DLL ${CMAKE_CURRENT_BINARY_DIR}/fluorine_vfs_hid.dll) + add_custom_command( + OUTPUT ${FL_VFS_DLL} + COMMAND ${MINGW_CC} + -O2 -Wall -Wextra + ${FL_VFS_MINGW_INCLUDES} + -shared -fno-asynchronous-unwind-tables + -static -static-libgcc + -Wl,--subsystem,windows -Wl,--enable-stdcall-fixup + -Wl,--exclude-all-symbols + -o ${FL_VFS_DLL} + ${FL_VFS_DLL_SRC} + ${FL_VFS_HOOKS_SRC} + ${FL_VFS_MINHOOK_SRC} + # kernel32 only. AppInit_DLLs DLLs run inside user32's + # DllMain under the loader lock — calling into user32 (or any + # other DLL whose DllMain hasn't run yet) wedges every PE + # process in the prefix. + -lkernel32 + DEPENDS ${FL_VFS_DLL_SRC} + ${FL_VFS_HOOKS_SRC} + ${FL_VFS_MINHOOK_SRC} + COMMENT "[mingw] fluorine_vfs.dll" + VERBATIM) + add_custom_command( + OUTPUT ${FL_VFS_HID_DLL} + COMMAND ${MINGW_CC} + -O2 -Wall -Wextra -Wno-cast-function-type + ${FL_VFS_MINGW_INCLUDES} + -shared -fno-asynchronous-unwind-tables + -static -static-libgcc + -DFLUORINE_VFS_PROXY_HID + -Wl,--subsystem,windows -Wl,--enable-stdcall-fixup + -Wl,--exclude-all-symbols + -o ${FL_VFS_HID_DLL} + ${FL_VFS_DLL_SRC} + ${FL_VFS_HOOKS_SRC} + ${FL_VFS_MINHOOK_SRC} + # Game-dir proxy fallback for Wine builds that ignore + # AppInit_DLLs. DllMain still stays kernel32-only; the + # hid exports forward to the real system DLL after load. + -lkernel32 + DEPENDS ${FL_VFS_DLL_SRC} + ${FL_VFS_HOOKS_SRC} + ${FL_VFS_MINHOOK_SRC} + COMMENT "[mingw] fluorine_vfs_hid.dll" + VERBATIM) + add_custom_target(fluorine_vfs_dll ALL DEPENDS ${FL_VFS_DLL} ${FL_VFS_HID_DLL}) + message(STATUS "[Fluorine] mingw-w64 found at ${MINGW_CC} - building fluorine_vfs.dll") +else() + message(WARNING + "[Fluorine] mingw-w64 (x86_64-w64-mingw32-gcc) not found - " + "skipping fluorine_vfs.dll. PE-side VFS injection will be unavailable.") +endif() + set_target_properties(organizer PROPERTIES OUTPUT_NAME "ModOrganizer" AUTOMOC ON diff --git a/src/src/fluorinepaths.cpp b/src/src/fluorinepaths.cpp index 6c52ec4..e00f11a 100644 --- a/src/src/fluorinepaths.cpp +++ b/src/src/fluorinepaths.cpp @@ -1,6 +1,7 @@ #include "fluorinepaths.h" #include "fluorineconfig.h" +#include <QCoreApplication> #include <QDir> #include <QFile> #include <QFileInfo> @@ -8,6 +9,7 @@ #include <QTextStream> #include <cstdio> +#include <cstdlib> static const QString OldFlatpakRoot = QDir::homePath() + "/.var/app/com.fluorine.manager"; @@ -22,6 +24,52 @@ QString fluorineVfsCacheDir() return fluorineDataDir() + "/vfs_cache"; } +QString fluorineVfsBridgeDir() +{ + return fluorineDataDir() + "/vfs_bridge"; +} + +QString fluorineVfsInjectDllPath() +{ + static const QString filename = QStringLiteral("fluorine_vfs.dll"); + + // Allow MO2_LIBS_DIR override for ad-hoc test builds; mirrors the + // logic in fluorineVfsPreloadPath(). Falls back to the bundled + // location next to the binary. + if (const char* env = std::getenv("MO2_LIBS_DIR")) { + if (env[0] != '\0') { + const QString candidate = QDir(QString::fromLocal8Bit(env)).filePath(filename); + if (QFileInfo::exists(candidate)) { + return candidate; + } + } + } + + const QString candidate = + QDir(QCoreApplication::applicationDirPath()).filePath( + QStringLiteral("wine/") + filename); + return QFileInfo::exists(candidate) ? candidate : QString(); +} + +QString fluorineVfsHidProxyDllPath() +{ + static const QString filename = QStringLiteral("fluorine_vfs_hid.dll"); + + if (const char* env = std::getenv("MO2_LIBS_DIR")) { + if (env[0] != '\0') { + const QString candidate = QDir(QString::fromLocal8Bit(env)).filePath(filename); + if (QFileInfo::exists(candidate)) { + return candidate; + } + } + } + + const QString candidate = + QDir(QCoreApplication::applicationDirPath()).filePath( + QStringLiteral("wine/") + filename); + return QFileInfo::exists(candidate) ? candidate : QString(); +} + void fluorineMigrateDataDir() { const QString oldRoot = OldFlatpakRoot; diff --git a/src/src/fluorinepaths.h b/src/src/fluorinepaths.h index eef60b2..b4928f5 100644 --- a/src/src/fluorinepaths.h +++ b/src/src/fluorinepaths.h @@ -10,6 +10,21 @@ QString fluorineDataDir(); /// Created on demand by the cache writer. QString fluorineVfsCacheDir(); +/// Returns the VFS bridge index directory: ~/.local/share/fluorine/vfs_bridge +/// Created on demand by the bridge index writer. +QString fluorineVfsBridgeDir(); + +/// Returns the path to the bundled PE-side VFS injector +/// (`fluorine_vfs.dll`), or an empty string if it was not built (e.g. +/// mingw missing in the build image). The DLL is staged to the +/// prefix's `c:\\windows\\system32\\` and registered in AppInit_DLLs at +/// prefix init. +QString fluorineVfsInjectDllPath(); + +/// Returns the bundled low-collision `hid.dll` game-directory proxy for +/// Wine builds that ignore AppInit_DLLs, or an empty string if it was not built. +QString fluorineVfsHidProxyDllPath(); + /// One-time migration from ~/.var/app/com.fluorine.manager/ to /// ~/.local/share/fluorine/. Call before initLogging(). void fluorineMigrateDataDir(); diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp index f66d1bb..59ec92a 100644 --- a/src/src/fuseconnector.cpp +++ b/src/src/fuseconnector.cpp @@ -2,6 +2,7 @@ #include "settings.h" #include "vfs/scancache.h" +#include "vfs/vfsbridge.h" #include "vfs/vfstree.h" #include <QCoreApplication> @@ -55,6 +56,19 @@ namespace { namespace fs = std::filesystem; +bool envFlagEnabled(const char* name) +{ + const QString value = qEnvironmentVariable(name).trimmed(); + return value == "1" || value.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || + value.compare(QStringLiteral("yes"), Qt::CaseInsensitive) == 0; +} + +bool vfsBridgeExportRequested() +{ + return !envFlagEnabled("FLUORINE_DISABLE_VFS_BRIDGE") && + !envFlagEnabled("FLUORINE_DISABLE_VFS_PRELOAD"); +} + std::string decodeProcMountField(const std::string& in) { std::string out; @@ -446,6 +460,8 @@ bool FuseConnector::mount( stampPluginTimestamps(*tree, m_pluginLoadOrder); } + exportVfsBridgeIndex(*tree); + { const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::steady_clock::now() - treeStart).count(); @@ -652,6 +668,48 @@ std::shared_ptr<TrackedWrites> FuseConnector::trackedWrites() const return m_trackedWrites; } +QString FuseConnector::vfsBridgeIndexPath() const +{ + return QString::fromStdString(m_vfsBridgeIndexPath); +} + +QString FuseConnector::vfsBridgeDataDir() const +{ + return QString::fromStdString(m_dataDirPath); +} + +QString FuseConnector::vfsBridgeMountPoint() const +{ + return QString::fromStdString(m_mountPoint); +} + +void FuseConnector::exportVfsBridgeIndex(const VfsTree& tree) +{ + if (!vfsBridgeExportRequested()) { + m_vfsBridgeIndexPath.clear(); + return; + } + + const auto exportStart = std::chrono::steady_clock::now(); + const auto path = ::vfsBridgeIndexPath(m_dataDirPath, m_overwriteDir, m_lastMods); + auto result = ::exportVfsBridgeIndex(tree, path, m_dataDirPath, m_overwriteDir, + m_mountPoint); + + const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - exportStart).count(); + if (result.ok) { + m_vfsBridgeIndexPath = result.path.string(); + std::fprintf(stderr, + "[VFS] [bridge] exported %zu records to '%s' in %lldms\n", + result.records_written, m_vfsBridgeIndexPath.c_str(), + static_cast<long long>(ms)); + } else { + m_vfsBridgeIndexPath.clear(); + std::fprintf(stderr, "[VFS] [bridge] export failed for '%s': %s\n", + result.path.string().c_str(), result.error.c_str()); + } +} + void FuseConnector::rebuild( const std::vector<std::pair<std::string, std::string>>& mods, const QString& overwrite_dir, const QString& data_dir_name) @@ -691,6 +749,8 @@ void FuseConnector::rebuild( stampPluginTimestamps(*newTree, m_pluginLoadOrder); } + exportVfsBridgeIndex(*newTree); + { std::unique_lock const lock(m_context->tree_mutex); m_context->tree.swap(newTree); diff --git a/src/src/fuseconnector.h b/src/src/fuseconnector.h index 1f6d1fa..e67a498 100644 --- a/src/src/fuseconnector.h +++ b/src/src/fuseconnector.h @@ -43,6 +43,9 @@ public: void setPluginLoadOrder(const std::vector<std::string>& load_order); void setTrackingFilePath(const std::string& path); std::shared_ptr<TrackedWrites> trackedWrites() const; + QString vfsBridgeIndexPath() const; + QString vfsBridgeDataDir() const; + QString vfsBridgeMountPoint() const; void unmount(); void discardStagingOnUnmount(); @@ -70,6 +73,7 @@ public: void clearRootFiles(); private: + void exportVfsBridgeIndex(const VfsTree& tree); void flushStaging(); void deployExternalMappings(const MappingType& mapping, const QString& dataDir); void cleanupExternalMappings(); @@ -81,6 +85,7 @@ private: std::string m_gameDir; std::string m_dataDirName; std::string m_dataDirPath; + std::string m_vfsBridgeIndexPath; int m_backingFd = -1; std::vector<CachedBaseFile> m_baseFileCache; std::string m_cachedDataDirPath; diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 3707d58..449ff75 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -2473,11 +2473,16 @@ bool OrganizerCore::beforeRun( const QFileInfo& binary, const QDir& cwd, const QString& arguments, const QString& profileName, const QString& customOverwrite, const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries, - QString* saveBindMountSource, QString* saveBindMountTarget) + QString* saveBindMountSource, QString* saveBindMountTarget, + QString* vfsBridgeIndexPath, QString* vfsBridgeDataDir, + QString* vfsBridgeMountPoint) { saveCurrentProfile(); if (saveBindMountSource) saveBindMountSource->clear(); if (saveBindMountTarget) saveBindMountTarget->clear(); + if (vfsBridgeIndexPath) vfsBridgeIndexPath->clear(); + if (vfsBridgeDataDir) vfsBridgeDataDir->clear(); + if (vfsBridgeMountPoint) vfsBridgeMountPoint->clear(); // need to wait until directory structure is ready if (m_DirectoryUpdate) { @@ -2517,6 +2522,15 @@ bool OrganizerCore::beforeRun( try { m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); m_USVFS.updateForcedLibraries(forcedLibraries); + if (vfsBridgeIndexPath) { + *vfsBridgeIndexPath = m_USVFS.vfsBridgeIndexPath(); + } + if (vfsBridgeDataDir) { + *vfsBridgeDataDir = m_USVFS.vfsBridgeDataDir(); + } + if (vfsBridgeMountPoint) { + *vfsBridgeMountPoint = m_USVFS.vfsBridgeMountPoint(); + } } catch (const FuseConnectorException& e) { log::error("VFS mount failed: {}", e.what()); return false; diff --git a/src/src/organizercore.h b/src/src/organizercore.h index 7ef7713..d786113 100644 --- a/src/src/organizercore.h +++ b/src/src/organizercore.h @@ -311,11 +311,14 @@ public: ProcessRunner processRunner();
- bool beforeRun(const QFileInfo& binary, const QDir& cwd, const QString& arguments,
- const QString& profileName, const QString& customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries,
- QString* saveBindMountSource = nullptr,
- QString* saveBindMountTarget = nullptr);
+ bool beforeRun(const QFileInfo& binary, const QDir& cwd, const QString& arguments, + const QString& profileName, const QString& customOverwrite, + const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries, + QString* saveBindMountSource = nullptr, + QString* saveBindMountTarget = nullptr, + QString* vfsBridgeIndexPath = nullptr, + QString* vfsBridgeDataDir = nullptr, + QString* vfsBridgeMountPoint = nullptr); bool checkGameRegistryKey();
diff --git a/src/src/prefixsetuprunner.cpp b/src/src/prefixsetuprunner.cpp index a3f782f..fef590b 100644 --- a/src/src/prefixsetuprunner.cpp +++ b/src/src/prefixsetuprunner.cpp @@ -181,6 +181,8 @@ static const char* WINE_SETTINGS_REG = R"(Windows Registry Editor Version 5.00 "dinput.dll"="native,builtin" "dinput8"="native,builtin" "dinput8.dll"="native,builtin" +"hid"="native,builtin" +"hid.dll"="native,builtin" "d3dcompiler_42"="native" "d3dcompiler_43"="native" "d3dcompiler_47"="native" @@ -453,6 +455,13 @@ void PrefixSetupRunner::buildStepList() addStep("post_setup", "Post-Setup (symlinks, dxvk)", [this] { return stepPostSetup(); }); + + // PE-side VFS injector: stage fluorine_vfs.dll into system32 and + // register it in AppInit_DLLs so every Wine PE process loads it. + // Skipped silently when the DLL is not bundled (mingw missing in + // build image). + addStep("vfs_inject", "PE-side VFS Injector", + [this] { return stepVfsInject(); }); } // ============================================================================ @@ -1523,6 +1532,89 @@ bool PrefixSetupRunner::stepPostSetup() return true; } +bool PrefixSetupRunner::stepVfsInject() +{ + // Stage fluorine_vfs.dll into c:\windows\system32 and register it in + // AppInit_DLLs so every PE process in this prefix loads it. Replaces + // the old libfluorine_vfs_preload.so LD_PRELOAD path, which couldn't + // intercept Wine's NTDLL syscall path. + const QString srcDll = fluorineVfsInjectDllPath(); + if (srcDll.isEmpty()) { + emit logMessage("fluorine_vfs.dll not bundled (mingw missing in build " + "image); skipping AppInit_DLLs registration"); + m_steps.last().status = SetupStep::Skipped; + return true; + } + + const QString system32 = m_prefixPath + "/drive_c/windows/system32"; + if (!QDir(system32).exists()) { + m_steps.last().errorMessage = + QStringLiteral("system32 not found at '%1'").arg(system32); + return false; + } + + const QString dstDll = system32 + "/fluorine_vfs.dll"; + QFile::remove(dstDll); + if (!QFile::copy(srcDll, dstDll)) { + m_steps.last().errorMessage = + QStringLiteral("failed to copy fluorine_vfs.dll into '%1'").arg(system32); + return false; + } + emit logMessage(QStringLiteral("Staged fluorine_vfs.dll -> %1").arg(dstDll)); + + // AppInit_DLLs is read by user32 on init. REG_SZ of the DLL basename; + // LoadAppInit_DLLs=1 enables the mechanism; RequireSignedAppInit_DLLs=0 + // disables Vista+ signing requirement (Wine ignores it, but set it + // anyway for defence-in-depth). + const QString tmpDir = fluorineTmpDir(); + QDir().mkpath(tmpDir); + const QString regFile = tmpDir + "/fluorine_vfs_appinit.reg"; + { + QFile f(regFile); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + m_steps.last().errorMessage = "failed to write registry import file"; + return false; + } + // Register only in the 64-bit hive. fluorine_vfs.dll is built as + // 64-bit PE; if Wow6432Node also pointed at it, 32-bit Wine helpers + // (services.exe, winedevice.exe, etc.) would attempt to LoadLibrary + // the missing syswow64\fluorine_vfs.dll and the loader would abort + // EXE init, killing every 32-bit process in the prefix. Also + // explicitly delete any pre-existing Wow6432Node values left over + // from older Fluorine builds that registered both views. + f.write( + "Windows Registry Editor Version 5.00\n\n" + "[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT" + "\\CurrentVersion\\Windows]\n" + "\"AppInit_DLLs\"=\"fluorine_vfs.dll\"\n" + "\"LoadAppInit_DLLs\"=dword:00000001\n" + "\"RequireSignedAppInit_DLLs\"=dword:00000000\n\n" + "[HKEY_LOCAL_MACHINE\\Software\\Wow6432Node\\Microsoft\\Windows NT" + "\\CurrentVersion\\Windows]\n" + "\"AppInit_DLLs\"=-\n" + "\"LoadAppInit_DLLs\"=-\n" + "\"RequireSignedAppInit_DLLs\"=-\n"); + f.close(); + } + + QMap<QString, QString> env = baseWineEnv(); + env["WINEDLLOVERRIDES"] = "mshtml=d"; + env["PROTON_USE_XALIA"] = "0"; + // Don't load our own DLL into regedit during the registration call. + env["FLUORINE_DISABLE_VFS_INJECT"] = "1"; + + const int rc = runProcess(m_wineBin, {"regedit", regFile}, env); + QFile::remove(regFile); + if (rc != 0) { + m_steps.last().errorMessage = + QStringLiteral("wine regedit failed for AppInit_DLLs (exit %1)").arg(rc); + return false; + } + + emit logMessage("AppInit_DLLs registered: fluorine_vfs.dll"); + return true; +} + // ============================================================================ // Tool management // ============================================================================ diff --git a/src/src/prefixsetuprunner.h b/src/src/prefixsetuprunner.h index 3797c40..be80462 100644 --- a/src/src/prefixsetuprunner.h +++ b/src/src/prefixsetuprunner.h @@ -104,6 +104,7 @@ private: bool stepWineRegistry(); bool stepWin11Mode(); bool stepPostSetup(); + bool stepVfsInject(); // -- DirectX cab extraction helpers ---------------------------------------- bool ensureDirectXRedist(QString& redistPath); diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index ac3ab67..4fb8a2c 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -1101,7 +1101,9 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary() // if a plugin doesn't want the program to run. if (!m_core.beforeRun(m_sp.binary, m_sp.currentDirectory, m_sp.arguments, m_profileName, m_customOverwrite, m_forcedLibraries, - &m_sp.saveBindMountSource, &m_sp.saveBindMountTarget)) { + &m_sp.saveBindMountSource, &m_sp.saveBindMountTarget, + &m_sp.vfsBridgeIndexPath, &m_sp.vfsBridgeDataDir, + &m_sp.vfsBridgeMountPoint)) { return Error; } diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index 532dbe8..8976632 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -1,9 +1,14 @@ #include "protonlauncher.h" +#include "fluorinepaths.h" #include "steamdetection.h" #include "slrmanager.h" #include <cstdio> +#include <QByteArray> +#include <QCoreApplication> #include <QDir> +#include <QDirIterator> +#include <QElapsedTimer> #include <QFile> #include <QFileInfo> #include <QProcess> @@ -12,6 +17,8 @@ #include <QSet> #include <log.h> +#include <fcntl.h> +#include <unistd.h> namespace { @@ -71,6 +78,355 @@ void cleanFluorineEnv(QProcessEnvironment& env) env.value("LD_LIBRARY_PATH", "<unset>")); } +QString fluorineVfsPreloadPath() +{ + const QString libName = QStringLiteral("libfluorine_vfs_preload.so"); + const QString envLibDir = qEnvironmentVariable("MO2_LIBS_DIR"); + if (!envLibDir.isEmpty()) { + const QString candidate = QDir(envLibDir).filePath(libName); + if (QFileInfo::exists(candidate)) { + return candidate; + } + } + + const QString candidate = + QDir(QCoreApplication::applicationDirPath()).filePath( + QStringLiteral("lib/") + libName); + return QFileInfo::exists(candidate) ? candidate : QString(); +} + +void prependLdPreload(QProcessEnvironment& env, const QString& library) +{ + if (library.isEmpty()) { + return; + } + + const QString existing = env.value("LD_PRELOAD"); + env.insert("LD_PRELOAD", existing.isEmpty() ? library : library + ":" + existing); +} + +void prependWineDllOverride(QProcessEnvironment& env, const QString& override) +{ + if (override.isEmpty()) { + return; + } + + const QString existing = env.value("WINEDLLOVERRIDES"); + if (existing.split(';').contains(override)) { + return; + } + env.insert("WINEDLLOVERRIDES", + existing.isEmpty() ? override : override + ";" + existing); +} + +bool vfsBridgeIndexPresent(const QMap<QString, QString>& envVars) +{ + return envVars.contains("FLUORINE_VFS_INDEX") && + !envVars.value("FLUORINE_VFS_INDEX").isEmpty(); +} + +struct StagedProxy +{ + QString path; + QByteArray expectedBytes; + + explicit operator bool() const + { + return !path.isEmpty() && !expectedBytes.isEmpty(); + } +}; + +QByteArray readSmallFile(const QString& path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + return {}; + } + return file.readAll(); +} + +void cleanupStagedProxy(const StagedProxy& proxy) +{ + if (!proxy) { + return; + } + + const QByteArray current = readSmallFile(proxy.path); + if (!current.isEmpty() && current == proxy.expectedBytes) { + if (QFile::remove(proxy.path)) { + MOBase::log::info("VFS HID proxy removed: {}", proxy.path); + } else { + MOBase::log::warn("VFS HID proxy cleanup failed: {}", proxy.path); + } + } +} + +StagedProxy stageVfsHidProxy(const QString& binary) +{ + const QString src = fluorineVfsHidProxyDllPath(); + const QByteArray srcBytes = readSmallFile(src); + if (src.isEmpty() || binary.isEmpty()) { + return {}; + } + if (srcBytes.isEmpty()) { + MOBase::log::warn("VFS HID proxy not staged: failed to read '{}'", src); + return {}; + } + + const QString gameDir = QFileInfo(binary).absolutePath(); + const QString dst = QDir(gameDir).filePath(QStringLiteral("hid.dll")); + const QFileInfo dstInfo(dst); + if (dstInfo.exists()) { + const QByteArray dstBytes = readSmallFile(dst); + if (srcBytes == dstBytes) { + return {dst, srcBytes}; + } + if (dstBytes.contains("fluorine_vfs")) { + if (!QFile::remove(dst)) { + MOBase::log::warn("VFS HID proxy update failed: could not remove '{}'", dst); + return {}; + } + } else { + MOBase::log::warn("VFS HID proxy not staged: '{}' already exists", dst); + return {}; + } + } + + if (!QFile::copy(src, dst)) { + MOBase::log::warn("VFS HID proxy copy failed: '{}' -> '{}'", src, dst); + return {}; + } + + MOBase::log::info("VFS HID proxy staged: {}", dst); + return {dst, srcBytes}; +} + +bool vfsBridgePreloadRequested() +{ + // Default OFF. The libc-side LD_PRELOAD bridge cannot intercept Wine + // NTDLL's directory reads (Wine emits inline `syscall` instructions or + // routes via ntdll-internal vDSO thunks; neither path resolves through + // glibc symbols). When dir synthesis was on it caused empty-dir hangs + // (>60s) and the canonicalizer overhead made even file-only redirect + // net-negative versus baseline (~43s vs ~25s). The replacement is a + // PE-side `fluorine_vfs.dll` injected via AppInit_DLLs (in progress). + // Set FLUORINE_ENABLE_VFS_PRELOAD=1 to opt back in for diagnostics. + const QString enable = qEnvironmentVariable("FLUORINE_ENABLE_VFS_PRELOAD").trimmed(); + return enable == "1" || + enable.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || + enable.compare(QStringLiteral("yes"), Qt::CaseInsensitive) == 0; +} + +bool wineLoaderTraceRequested() +{ + const QString enable = qEnvironmentVariable("FLUORINE_WINE_LOADER_TRACE").trimmed(); + return enable == "1" || + enable.compare(QStringLiteral("true"), Qt::CaseInsensitive) == 0 || + enable.compare(QStringLiteral("yes"), Qt::CaseInsensitive) == 0 || + QFileInfo::exists(QStringLiteral("/tmp/fluorine_enable_wine_loader_trace")); +} + +bool vfsPrewarmRequested(const QMap<QString, QString>& envVars) +{ + QString enable = envVars.value(QStringLiteral("FLUORINE_VFS_PREWARM")).trimmed(); + if (enable.isEmpty()) { + enable = qEnvironmentVariable("FLUORINE_VFS_PREWARM").trimmed(); + } + + if (enable.isEmpty()) { + return true; + } + + return !(enable == "0" || + enable.compare(QStringLiteral("false"), Qt::CaseInsensitive) == 0 || + enable.compare(QStringLiteral("no"), Qt::CaseInsensitive) == 0); +} + +bool isBethesdaPrewarmPath(const QString& path) +{ + const QString lower = path.toLower(); + return lower.endsWith(QStringLiteral(".dll")) || + lower.endsWith(QStringLiteral(".esp")) || + lower.endsWith(QStringLiteral(".esm")) || + lower.endsWith(QStringLiteral(".esl")) || + lower.endsWith(QStringLiteral(".bsa")) || + lower.endsWith(QStringLiteral(".ba2")); +} + +void addPrewarmPath(QStringList& paths, QSet<QString>& seen, const QString& path) +{ + if (path.isEmpty() || !isBethesdaPrewarmPath(path)) { + return; + } + + const QFileInfo info(path); + if (!info.exists() || !info.isFile()) { + return; + } + + const QString clean = QDir::cleanPath(info.absoluteFilePath()); + if (!seen.contains(clean)) { + seen.insert(clean); + paths.append(clean); + } +} + +QString jsonStringField(const QByteArray& line, const char* name) +{ + const QByteArray needle = QByteArray("\"") + name + "\":\""; + const int start = line.indexOf(needle); + if (start < 0) { + return {}; + } + + QByteArray out; + bool escaped = false; + for (int i = start + needle.size(); i < line.size(); ++i) { + const char c = line.at(i); + if (escaped) { + switch (c) { + case '"': + case '\\': + case '/': + out.append(c); + break; + case 'b': + out.append('\b'); + break; + case 'f': + out.append('\f'); + break; + case 'n': + out.append('\n'); + break; + case 'r': + out.append('\r'); + break; + case 't': + out.append('\t'); + break; + default: + out.append(c); + break; + } + escaped = false; + continue; + } + if (c == '\\') { + escaped = true; + continue; + } + if (c == '"') { + return QString::fromUtf8(out); + } + out.append(c); + } + + return {}; +} + +void collectStockPrewarmPaths(QStringList& paths, QSet<QString>& seen, + const QString& binary) +{ + const QString gameDir = QFileInfo(binary).absolutePath(); + if (gameDir.isEmpty() || !QFileInfo(gameDir).isDir()) { + return; + } + + QDirIterator it(gameDir, QDir::Files | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories); + while (it.hasNext()) { + addPrewarmPath(paths, seen, it.next()); + } +} + +void collectIndexPrewarmPaths(QStringList& paths, QSet<QString>& seen, + const QString& indexPath) +{ + QFile file(indexPath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + MOBase::log::warn("VFS prewarm skipped index paths: failed to open '{}'", + indexPath); + return; + } + + while (!file.atEnd()) { + const QByteArray line = file.readLine(); + if (!line.contains("\"record\":\"entry\"") || + !line.contains("\"type\":\"file\"")) { + continue; + } + + const QString realPath = jsonStringField(line, "real_path"); + if (realPath.startsWith('/')) { + addPrewarmPath(paths, seen, realPath); + } + } +} + +struct PrewarmStats +{ + int files = 0; + int failed = 0; + qint64 bytes = 0; +}; + +PrewarmStats prewarmFiles(const QStringList& paths, int begin, int end) +{ + PrewarmStats stats; + for (int i = begin; i < end; ++i) { + const QFileInfo info(paths.at(i)); + const QByteArray nativePath = QFile::encodeName(info.absoluteFilePath()); + const int fd = ::open(nativePath.constData(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { + ++stats.failed; + continue; + } + +#ifdef POSIX_FADV_WILLNEED + if (::posix_fadvise(fd, 0, 0, POSIX_FADV_WILLNEED) != 0) { + ++stats.failed; + ::close(fd); + continue; + } +#endif + + ++stats.files; + stats.bytes += info.size(); + ::close(fd); + } + return stats; +} + +void prewarmVfsBridgeFiles(const QString& binary, const QString& indexPath) +{ + QElapsedTimer timer; + timer.start(); + + QStringList paths; + QSet<QString> seen; + collectStockPrewarmPaths(paths, seen, binary); + const int stockEnd = paths.size(); + collectIndexPrewarmPaths(paths, seen, indexPath); + + const PrewarmStats stock = prewarmFiles(paths, 0, stockEnd); + const PrewarmStats indexed = prewarmFiles(paths, stockEnd, paths.size()); + + const qint64 elapsedMs = timer.elapsed(); + MOBase::log::info( + "VFS prewarm: stock_files={} stock_bytes={} index_files={} " + "index_bytes={} failed={} elapsed_ms={}", + stock.files, stock.bytes, indexed.files, indexed.bytes, + stock.failed + indexed.failed, elapsedMs); + std::fprintf(stderr, + "[Fluorine] VFS prewarm: stock_files=%d stock_bytes=%lld " + "index_files=%d index_bytes=%lld failed=%d elapsed_ms=%lld\n", + stock.files, static_cast<long long>(stock.bytes), + indexed.files, static_cast<long long>(indexed.bytes), + stock.failed + indexed.failed, + static_cast<long long>(elapsedMs)); +} + QString decodeMountInfoPath(const QByteArray& encoded) { QByteArray decoded; @@ -455,7 +811,8 @@ void wrapInTerminal(QString& program, QStringList& arguments) bool startWithEnv(const QString& program, const QStringList& arguments, const QString& workingDir, - const QProcessEnvironment& environment, qint64& pid) + const QProcessEnvironment& environment, qint64& pid, + const StagedProxy& stagedProxy) { auto* process = new QProcess(); process->setProgram(program); @@ -468,10 +825,28 @@ bool startWithEnv(const QString& program, const QStringList& arguments, process->setProcessEnvironment(environment); process->setProcessChannelMode(QProcess::ForwardedOutputChannel); + QFile* wineTraceLog = nullptr; + const QString wineTracePath = + environment.value(QStringLiteral("FLUORINE_WINE_LOADER_TRACE_LOG")); + if (!wineTracePath.isEmpty()) { + wineTraceLog = new QFile(wineTracePath, process); + if (!wineTraceLog->open(QIODevice::WriteOnly | QIODevice::Truncate)) { + MOBase::log::warn("Wine loader trace log could not be opened: {}", + wineTracePath); + wineTraceLog = nullptr; + } else { + MOBase::log::info("Wine loader trace log: {}", wineTracePath); + } + } + // Filter noisy Wine/Proton stderr (GStreamer warnings, etc.) while // forwarding everything else to our stderr. - QObject::connect(process, &QProcess::readyReadStandardError, process, [process]() { + QObject::connect(process, &QProcess::readyReadStandardError, process, [process, wineTraceLog]() { const QByteArray data = process->readAllStandardError(); + if (wineTraceLog != nullptr && wineTraceLog->isOpen()) { + wineTraceLog->write(data); + wineTraceLog->flush(); + } for (const QByteArray& line : data.split('\n')) { if (line.isEmpty()) continue; @@ -487,6 +862,9 @@ bool startWithEnv(const QString& program, const QStringList& arguments, QObject::connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), process, &QProcess::deleteLater); + QObject::connect(process, + QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), + process, [stagedProxy]() { cleanupStagedProxy(stagedProxy); }); process->start(); if (!process->waitForStarted(5000)) { @@ -723,6 +1101,16 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const QStringList pressureVesselImportantPaths; pressureVesselImportantPaths << m_binary << m_workingDir << m_prefixPath << m_bindMountSource << m_bindMountTarget; + const bool useVfsBridgeIndex = vfsBridgeIndexPresent(m_envVars); + const StagedProxy vfsHidProxy = + useVfsBridgeIndex ? stageVfsHidProxy(m_binary) : StagedProxy{}; + const bool useVfsBridgePreload = + vfsBridgePreloadRequested() && useVfsBridgeIndex; + const QString vfsBridgePreload = + useVfsBridgePreload ? fluorineVfsPreloadPath() : QString(); + if (!vfsBridgePreload.isEmpty()) { + pressureVesselImportantPaths << vfsBridgePreload; + } // If SLR is enabled, wrap the whole proton invocation inside the // pressure-vessel container provided by SteamLinuxRuntime_sniper. @@ -990,6 +1378,26 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const for (auto it = m_envVars.cbegin(); it != m_envVars.cend(); ++it) { env.insert(it.key(), it.value()); } + if (useVfsBridgeIndex && wineLoaderTraceRequested()) { + const QString tracePath = + QStringLiteral("/tmp/fluorine_wine_loader_trace.log"); + QFile::remove(tracePath); + env.insert("WINEDEBUG", "+timestamp,+pid,+tid,+loaddll"); + env.insert("FLUORINE_WINE_LOADER_TRACE_LOG", tracePath); + MOBase::log::info("Wine loader tracing enabled: WINEDEBUG='{}', log='{}'", + env.value("WINEDEBUG"), tracePath); + } + if (vfsHidProxy) { + prependWineDllOverride(env, QStringLiteral("hid=n,b")); + } + if (useVfsBridgePreload && !vfsBridgePreload.isEmpty()) { + prependLdPreload(env, vfsBridgePreload); + env.insert("FLUORINE_VFS_PRELOAD_STATS", "1"); + MOBase::log::info("VFS bridge preload enabled: {}", vfsBridgePreload); + } else if (useVfsBridgePreload) { + MOBase::log::warn("VFS bridge index is present but libfluorine_vfs_preload.so " + "was not found; launching without preload acceleration"); + } if (m_useSLR) { appendPressureVesselFilesystems( @@ -1033,11 +1441,15 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const env.insert("PWD", m_workingDir); } + if (useVfsBridgeIndex && vfsPrewarmRequested(m_envVars)) { + prewarmVfsBridgeFiles(m_binary, m_envVars.value("FLUORINE_VFS_INDEX")); + } + if (m_useTerminal) { wrapInTerminal(program, arguments); } - return startWithEnv(program, arguments, m_workingDir, env, pid); + return startWithEnv(program, arguments, m_workingDir, env, pid, vfsHidProxy); } bool ProtonLauncher::launchDirect(qint64& pid) const @@ -1059,12 +1471,42 @@ bool ProtonLauncher::launchDirect(qint64& pid) const for (auto it = m_envVars.cbegin(); it != m_envVars.cend(); ++it) { env.insert(it.key(), it.value()); } + const bool useVfsBridgeIndex = vfsBridgeIndexPresent(m_envVars); + const StagedProxy vfsHidProxy = + useVfsBridgeIndex ? stageVfsHidProxy(m_binary) : StagedProxy{}; + if (useVfsBridgeIndex && wineLoaderTraceRequested()) { + const QString tracePath = + QStringLiteral("/tmp/fluorine_wine_loader_trace.log"); + QFile::remove(tracePath); + env.insert("WINEDEBUG", "+timestamp,+pid,+tid,+loaddll"); + env.insert("FLUORINE_WINE_LOADER_TRACE_LOG", tracePath); + MOBase::log::info("Wine loader tracing enabled: WINEDEBUG='{}', log='{}'", + env.value("WINEDEBUG"), tracePath); + } + if (vfsHidProxy) { + prependWineDllOverride(env, QStringLiteral("hid=n,b")); + } + if (vfsBridgePreloadRequested() && useVfsBridgeIndex) { + const QString vfsBridgePreload = fluorineVfsPreloadPath(); + if (!vfsBridgePreload.isEmpty()) { + prependLdPreload(env, vfsBridgePreload); + env.insert("FLUORINE_VFS_PRELOAD_STATS", "1"); + MOBase::log::info("VFS bridge preload enabled: {}", vfsBridgePreload); + } else { + MOBase::log::warn("VFS bridge index is present but libfluorine_vfs_preload.so " + "was not found; launching without preload acceleration"); + } + } + + if (useVfsBridgeIndex && vfsPrewarmRequested(m_envVars)) { + prewarmVfsBridgeFiles(m_binary, m_envVars.value("FLUORINE_VFS_INDEX")); + } if (m_useTerminal) { wrapInTerminal(program, arguments); } - return startWithEnv(program, arguments, m_workingDir, env, pid); + return startWithEnv(program, arguments, m_workingDir, env, pid, vfsHidProxy); } bool ProtonLauncher::ensureSteamRunning() diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 76d4e0e..5ed3db1 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -476,6 +476,16 @@ int spawn(const SpawnParameters& sp, pid_t& processId) MOBase::log::info("Proton disabled for this executable, launching directly"); } + if (!sp.vfsBridgeIndexPath.isEmpty()) { + launcher.addEnvVar("FLUORINE_VFS_INDEX", sp.vfsBridgeIndexPath); + } + if (!sp.vfsBridgeDataDir.isEmpty()) { + launcher.addEnvVar("FLUORINE_VFS_DATA_DIR", sp.vfsBridgeDataDir); + } + if (!sp.vfsBridgeMountPoint.isEmpty()) { + launcher.addEnvVar("FLUORINE_VFS_MOUNT", sp.vfsBridgeMountPoint); + } + launcher.setUseTerminal(sp.useTerminal); const auto [ok, pid] = launcher.launch(); diff --git a/src/src/spawn.h b/src/src/spawn.h index 40207c3..77ad204 100644 --- a/src/src/spawn.h +++ b/src/src/spawn.h @@ -60,9 +60,12 @@ struct SpawnParameters // of `saveBindMountSource` for the duration of the game process tree.
// Used to redirect `<prefix>/__MO_Saves` to the profile's saves dir
// without symlinks, which Wine can accidentally replace.
- QString saveBindMountSource;
- QString saveBindMountTarget;
-};
+ QString saveBindMountSource; + QString saveBindMountTarget; + QString vfsBridgeIndexPath; + QString vfsBridgeDataDir; + QString vfsBridgeMountPoint; +}; bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory,
const QString& steamAppID, const Settings& settings);
diff --git a/src/src/vfs/fluorine_vfs_hooks.c b/src/src/vfs/fluorine_vfs_hooks.c new file mode 100644 index 0000000..b056cf3 --- /dev/null +++ b/src/src/vfs/fluorine_vfs_hooks.c @@ -0,0 +1,2507 @@ +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +#include <stdint.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <MinHook.h> + +typedef LONG NTSTATUS; + +typedef struct _IO_STATUS_BLOCK_FL +{ + union { + NTSTATUS Status; + PVOID Pointer; + } u; + ULONG_PTR Information; +} IO_STATUS_BLOCK_FL, *PIO_STATUS_BLOCK_FL; + +typedef struct _UNICODE_STRING_FL +{ + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} UNICODE_STRING_FL, *PUNICODE_STRING_FL; + +typedef struct _OBJECT_ATTRIBUTES_FL +{ + ULONG Length; + HANDLE RootDirectory; + PUNICODE_STRING_FL ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; + PVOID SecurityQualityOfService; +} OBJECT_ATTRIBUTES_FL, *POBJECT_ATTRIBUTES_FL; + +typedef VOID(NTAPI* PIO_APC_ROUTINE_FL)(PVOID, PIO_STATUS_BLOCK_FL, ULONG); + +typedef NTSTATUS(NTAPI* NtCreateFile_t)(PHANDLE, ACCESS_MASK, + POBJECT_ATTRIBUTES_FL, + PIO_STATUS_BLOCK_FL, PLARGE_INTEGER, + ULONG, ULONG, ULONG, ULONG, PVOID, + ULONG); +typedef NTSTATUS(NTAPI* NtOpenFile_t)(PHANDLE, ACCESS_MASK, + POBJECT_ATTRIBUTES_FL, + PIO_STATUS_BLOCK_FL, ULONG, ULONG); +typedef NTSTATUS(NTAPI* NtClose_t)(HANDLE); +typedef NTSTATUS(NTAPI* NtQueryDirectoryFile_t)( + HANDLE, HANDLE, PIO_APC_ROUTINE_FL, PVOID, PIO_STATUS_BLOCK_FL, PVOID, + ULONG, ULONG, BOOLEAN, PUNICODE_STRING_FL, BOOLEAN); +typedef NTSTATUS(NTAPI* NtQueryDirectoryFileEx_t)( + HANDLE, HANDLE, PIO_APC_ROUTINE_FL, PVOID, PIO_STATUS_BLOCK_FL, PVOID, + ULONG, ULONG, ULONG, PUNICODE_STRING_FL); +typedef NTSTATUS(NTAPI* NtQueryAttributesFile_t)(POBJECT_ATTRIBUTES_FL, + PVOID); +typedef NTSTATUS(NTAPI* NtQueryFullAttributesFile_t)(POBJECT_ATTRIBUTES_FL, + PVOID); +typedef NTSTATUS(NTAPI* NtReadFile_t)(HANDLE, HANDLE, PIO_APC_ROUTINE_FL, + PVOID, PIO_STATUS_BLOCK_FL, PVOID, + ULONG, PLARGE_INTEGER, PULONG); +typedef NTSTATUS(NTAPI* NtSetInformationFile_t)(HANDLE, + PIO_STATUS_BLOCK_FL, PVOID, + ULONG, ULONG); +typedef NTSTATUS(NTAPI* NtQueryInformationFile_t)(HANDLE, + PIO_STATUS_BLOCK_FL, + PVOID, ULONG, ULONG); +typedef BOOL(WINAPI* GetFileAttributesExW_t)(LPCWSTR, GET_FILEEX_INFO_LEVELS, + LPVOID); +typedef BOOL(WINAPI* GetFileAttributesExA_t)(LPCSTR, GET_FILEEX_INFO_LEVELS, + LPVOID); + +#ifndef STATUS_SUCCESS +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +#endif +#ifndef STATUS_NO_MORE_FILES +#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L) +#endif +#ifndef STATUS_BUFFER_OVERFLOW +#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) +#endif +#ifndef STATUS_INFO_LENGTH_MISMATCH +#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) +#endif +#ifndef STATUS_OBJECT_NAME_NOT_FOUND +#define STATUS_OBJECT_NAME_NOT_FOUND ((NTSTATUS)0xC0000034L) +#endif +#ifndef STATUS_END_OF_FILE +#define STATUS_END_OF_FILE ((NTSTATUS)0xC0000011L) +#endif + +#define FL_FilePositionInformation 14u +#define FL_FileStandardInformation 5u +#define FL_FileEndOfFileInformation 20u + +typedef struct _FL_FILE_POSITION_INFORMATION { + LARGE_INTEGER CurrentByteOffset; +} FL_FILE_POSITION_INFORMATION; + +typedef struct _FL_FILE_STANDARD_INFORMATION { + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG NumberOfLinks; + BOOLEAN DeletePending; + BOOLEAN Directory; +} FL_FILE_STANDARD_INFORMATION; + +#define FL_FileDirectoryInformation 1u +#define FL_FileFullDirectoryInformation 2u +#define FL_FileBothDirectoryInformation 3u +#define FL_FileNamesInformation 12u +#define FL_FileIdBothDirectoryInformation 37u +#define FL_FileIdFullDirectoryInformation 38u +#define FL_SL_RESTART_SCAN 0x00000001u +#define FL_SL_RETURN_SINGLE_ENTRY 0x00000002u + +typedef struct _FL_FILE_DIRECTORY_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + WCHAR FileName[1]; +} FL_FILE_DIRECTORY_INFORMATION; + +typedef struct _FL_FILE_FULL_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + WCHAR FileName[1]; +} FL_FILE_FULL_DIR_INFORMATION; + +typedef struct _FL_FILE_BOTH_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FL_FILE_BOTH_DIR_INFORMATION; + +typedef struct _FL_FILE_ID_BOTH_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + LARGE_INTEGER FileId; + WCHAR FileName[1]; +} FL_FILE_ID_BOTH_DIR_INFORMATION; + +typedef struct _FL_FILE_ID_FULL_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + LARGE_INTEGER FileId; + WCHAR FileName[1]; +} FL_FILE_ID_FULL_DIR_INFORMATION; + +typedef struct _FL_FILE_NAMES_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + ULONG FileNameLength; + WCHAR FileName[1]; +} FL_FILE_NAMES_INFORMATION; + +typedef struct _FL_FILE_BASIC_INFORMATION { + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + ULONG FileAttributes; +} FL_FILE_BASIC_INFORMATION; + +typedef struct _FL_FILE_NETWORK_OPEN_INFORMATION { + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG FileAttributes; +} FL_FILE_NETWORK_OPEN_INFORMATION; + +static NtCreateFile_t Real_NtCreateFile = NULL; +static NtOpenFile_t Real_NtOpenFile = NULL; +static NtClose_t Real_NtClose = NULL; +static NtQueryDirectoryFile_t Real_NtQueryDirectoryFile = NULL; +static NtQueryDirectoryFileEx_t Real_NtQueryDirectoryFileEx = NULL; +static NtQueryAttributesFile_t Real_NtQueryAttributesFile = NULL; +static NtQueryFullAttributesFile_t Real_NtQueryFullAttributesFile = NULL; +static NtReadFile_t Real_NtReadFile = NULL; +static NtSetInformationFile_t Real_NtSetInformationFile = NULL; +static NtQueryInformationFile_t Real_NtQueryInformationFile = NULL; +static GetFileAttributesExW_t Real_GetFileAttributesExW = NULL; +static GetFileAttributesExA_t Real_GetFileAttributesExA = NULL; + +static volatile LONG g_started = 0; +static volatile LONG g_installed = 0; +static volatile LONG g_stats_started = 0; +static volatile LONG g_first_create_logged = 0; +static volatile LONG g_first_open_logged = 0; +static volatile LONG g_first_mmap_logged = 0; +static ULONGLONG g_phase_start_tick = 0; +static volatile LONG64 g_nt_create = 0; +static volatile LONG64 g_nt_open = 0; +static volatile LONG64 g_nt_close = 0; +static volatile LONG64 g_nt_qdf = 0; +static volatile LONG64 g_nt_qdfex = 0; +static volatile LONG64 g_nt_qattr = 0; +static volatile LONG64 g_nt_qfullattr = 0; +static volatile LONG64 g_redirect_file = 0; +static volatile LONG64 g_redirect_miss = 0; +static volatile LONG64 g_redirect_skip = 0; +static volatile LONG64 g_redirect_dir = 0; +static volatile LONG64 g_dir_query_served = 0; +static volatile LONG64 g_dir_query_empty = 0; +static volatile LONG64 g_dir_query_bypass = 0; +static volatile LONG64 g_attr_hit = 0; +static volatile LONG64 g_attr_miss = 0; +static volatile LONG64 g_attr_neg = 0; +static volatile LONG64 g_attr_passthru = 0; +static volatile LONG64 g_attr_cache_hit = 0; +static volatile LONG64 g_attr_cache_insert = 0; +static volatile LONG64 g_attr_cache_full = 0; +static volatile LONG64 g_k32_gfaexw = 0; +static volatile LONG64 g_k32_gfaexa = 0; +static volatile LONG64 g_k32_attr_hit = 0; +static volatile LONG64 g_k32_cache_hit = 0; +static volatile LONG64 g_k32_passthru = 0; +static volatile LONG64 g_k32_neg = 0; +static volatile LONG64 g_k32_fast_path = 0; +static volatile LONG64 g_k32_full_path = 0; +static volatile LONG64 g_live_insert = 0; +static volatile LONG64 g_open_neg = 0; +static volatile LONG64 g_mmap_attached = 0; +static volatile LONG64 g_mmap_skip_size = 0; +static volatile LONG64 g_mmap_skip_zero = 0; +static volatile LONG64 g_mmap_map_failed = 0; +static volatile LONG64 g_mmap_view_failed = 0; +static volatile LONG64 g_mmap_reads = 0; +static volatile LONG64 g_mmap_bytes = 0; +static volatile LONG64 g_mmap_passthru = 0; +static volatile LONG64 g_mmap_detached = 0; + +typedef struct VfsEntry +{ + char* key; + WCHAR* real_nt; + ULONG attrs; + unsigned long long size; + FILETIME mtime; +} VfsEntry; + +typedef struct ChildEntry +{ + char* key; + WCHAR* name; + ULONG attrs; + unsigned long long size; + FILETIME mtime; +} ChildEntry; + +typedef struct DirBucket +{ + char* key; + ChildEntry* children; + size_t child_count; + size_t child_cap; + struct DirBucket* next; +} DirBucket; + +typedef struct DirHandleState +{ + HANDLE handle; + DirBucket* bucket; + size_t cursor; + WCHAR* filter; + size_t filter_len; + int filter_set; +} DirHandleState; + +static VfsEntry* g_entries = NULL; +static size_t g_entry_count = 0; +static DirBucket** g_dir_hash = NULL; +static size_t g_dir_count = 0; +static size_t g_child_count = 0; +static DirHandleState* g_dir_handles = NULL; +static size_t g_dir_handle_count = 0; +static size_t g_dir_handle_cap = 0; +static CRITICAL_SECTION g_dir_lock; +static int g_dir_lock_ready = 0; +static HANDLE g_dummy_dir = INVALID_HANDLE_VALUE; +static char* g_mount = NULL; +static volatile LONG g_index_ready = 0; +static __thread int t_inside_redirect = 0; +static __thread WCHAR t_redirect_path[32768]; + +static DWORD copy_str(char* dst, DWORD pos, DWORD cap, const char* src); + +static char ascii_lower(char c) +{ + return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; +} + +static int ascii_eq_ci(const char* a, const char* b) +{ + while (*a && *b) { + char ca = ascii_lower(*a); + char cb = ascii_lower(*b); + if (ca != cb) return 0; + ++a; + ++b; + } + return *a == '\0' && *b == '\0'; +} + +static char* heap_strndup(const char* s, size_t n) +{ + char* out = (char*)malloc(n + 1); + if (out == NULL) return NULL; + memcpy(out, s, n); + out[n] = '\0'; + return out; +} + +static char* normalize_utf8_path(char* s) +{ + char* r = s; + char* w = s; + while (*r == '/' || *r == '\\') ++r; + while (*r) { + char c = *r++; + if (c == '\\') c = '/'; + *w++ = ascii_lower(c); + } + while (w > s && w[-1] == '/') --w; + *w = '\0'; + return s; +} + +static char* json_string_field(const char* line, const char* key) +{ + char needle[96]; + DWORD pos = 0; + pos = copy_str(needle, pos, sizeof(needle), "\""); + pos = copy_str(needle, pos, sizeof(needle), key); + pos = copy_str(needle, pos, sizeof(needle), "\":"); + needle[pos] = '\0'; + + const char* p = strstr(line, needle); + if (p == NULL) return NULL; + p = strchr(p + strlen(needle), '"'); + if (p == NULL) return NULL; + ++p; + + char* out = (char*)malloc(strlen(p) + 1); + if (out == NULL) return NULL; + size_t n = 0; + while (*p) { + char c = *p++; + if (c == '"') { + out[n] = '\0'; + return out; + } + if (c == '\\' && *p) { + char e = *p++; + switch (e) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = e; break; + } + } + out[n++] = c; + } + free(out); + return NULL; +} + +static WCHAR* unix_path_to_nt_wide(const char* path) +{ + if (path == NULL || path[0] != '/') return NULL; + size_t len = strlen(path); + char* nt = (char*)malloc(len + 8); + if (nt == NULL) return NULL; + strcpy(nt, "\\??\\Z:"); + strcpy(nt + 6, path); + for (char* p = nt; *p; ++p) { + if (*p == '/') *p = '\\'; + } + + int wlen = MultiByteToWideChar(CP_UTF8, 0, nt, -1, NULL, 0); + if (wlen <= 0) { + free(nt); + return NULL; + } + WCHAR* out = (WCHAR*)malloc((size_t)wlen * sizeof(WCHAR)); + if (out == NULL) { + free(nt); + return NULL; + } + MultiByteToWideChar(CP_UTF8, 0, nt, -1, out, wlen); + free(nt); + return out; +} + +static WCHAR* utf8_to_wide_n(const char* s, size_t n) +{ + int wlen = MultiByteToWideChar(CP_UTF8, 0, s, (int)n, NULL, 0); + if (wlen <= 0) return NULL; + WCHAR* out = (WCHAR*)malloc(((size_t)wlen + 1) * sizeof(WCHAR)); + if (out == NULL) return NULL; + MultiByteToWideChar(CP_UTF8, 0, s, (int)n, out, wlen); + out[wlen] = 0; + return out; +} + +static unsigned long long json_u64_field(const char* line, const char* key) +{ + char needle[96]; + DWORD pos = 0; + pos = copy_str(needle, pos, sizeof(needle), "\""); + pos = copy_str(needle, pos, sizeof(needle), key); + pos = copy_str(needle, pos, sizeof(needle), "\":"); + needle[pos] = '\0'; + + const char* p = strstr(line, needle); + if (p == NULL) return 0; + p += strlen(needle); + while (*p == ' ') ++p; + unsigned long long v = 0; + while (*p >= '0' && *p <= '9') { + v = (v * 10ULL) + (unsigned long long)(*p - '0'); + ++p; + } + return v; +} + +static FILETIME filetime_from_unix_ns(unsigned long long ns) +{ + unsigned long long ft = 116444736000000000ULL + (ns / 100ULL); + FILETIME out; + out.dwLowDateTime = (DWORD)(ft & 0xffffffffULL); + out.dwHighDateTime = (DWORD)(ft >> 32); + return out; +} + +static unsigned long hash_str(const char* s) +{ + unsigned long h = 2166136261u; + while (*s) { + h ^= (unsigned char)*s++; + h *= 16777619u; + } + return h; +} + +static DirBucket* get_dir_bucket(const char* key, int create) +{ + if (g_dir_hash == NULL) { + if (!create) return NULL; + g_dir_hash = (DirBucket**)calloc(65536, sizeof(DirBucket*)); + if (g_dir_hash == NULL) return NULL; + } + unsigned long slot = hash_str(key) & 65535u; + for (DirBucket* b = g_dir_hash[slot]; b != NULL; b = b->next) { + if (strcmp(b->key, key) == 0) return b; + } + if (!create) return NULL; + + DirBucket* b = (DirBucket*)calloc(1, sizeof(DirBucket)); + if (b == NULL) return NULL; + b->key = heap_strndup(key, strlen(key)); + if (b->key == NULL) { + free(b); + return NULL; + } + b->next = g_dir_hash[slot]; + g_dir_hash[slot] = b; + ++g_dir_count; + return b; +} + +static int child_cmp(const void* a, const void* b) +{ + const ChildEntry* ca = (const ChildEntry*)a; + const ChildEntry* cb = (const ChildEntry*)b; + return strcmp(ca->key, cb->key); +} + +static void sort_dir_children(void) +{ + if (g_dir_hash == NULL) return; + for (size_t i = 0; i < 65536; ++i) { + for (DirBucket* b = g_dir_hash[i]; b != NULL; b = b->next) { + if (b->child_count > 1) { + qsort(b->children, b->child_count, sizeof(ChildEntry), child_cmp); + } + } + } +} + +static int bucket_has_child(DirBucket* b, const char* child_key) +{ + for (size_t i = 0; i < b->child_count; ++i) { + if (strcmp(b->children[i].key, child_key) == 0) return 1; + } + return 0; +} + +static int add_child(const char* parent_key, const char* child_key, + const char* name, size_t name_len, ULONG attrs, + unsigned long long size, FILETIME mtime) +{ + DirBucket* b = get_dir_bucket(parent_key, 1); + if (b == NULL) return 0; + if (bucket_has_child(b, child_key)) return 1; + + if (b->child_count == b->child_cap) { + size_t next_cap = b->child_cap ? b->child_cap * 2 : 8; + ChildEntry* next = + (ChildEntry*)realloc(b->children, next_cap * sizeof(ChildEntry)); + if (next == NULL) return 0; + b->children = next; + b->child_cap = next_cap; + } + + ChildEntry* e = &b->children[b->child_count]; + memset(e, 0, sizeof(*e)); + e->key = heap_strndup(child_key, strlen(child_key)); + e->name = utf8_to_wide_n(name, name_len); + if (e->key == NULL || e->name == NULL) { + if (e->key != NULL) free(e->key); + if (e->name != NULL) free(e->name); + memset(e, 0, sizeof(*e)); + return 0; + } + e->attrs = attrs; + e->size = size; + e->mtime = mtime; + ++b->child_count; + ++g_child_count; + return 1; +} + +static void append_segment_key(char* dst, size_t cap, const char* parent, + const char* seg) +{ + if (parent[0] != '\0') { + snprintf(dst, cap, "%s/%s", parent, seg); + } else { + snprintf(dst, cap, "%s", seg); + } + dst[cap - 1] = '\0'; +} + +static void add_path_to_dirs(const char* original_path, + unsigned long long size, + FILETIME mtime) +{ + char parent[2048]; + parent[0] = '\0'; + get_dir_bucket("", 1); + + const char* p = original_path; + while (*p == '/' || *p == '\\') ++p; + while (*p) { + const char* start = p; + while (*p && *p != '/' && *p != '\\') ++p; + size_t n = (size_t)(p - start); + while (*p == '/' || *p == '\\') ++p; + if (n == 0) continue; + + char* seg = heap_strndup(start, n); + if (seg == NULL) return; + normalize_utf8_path(seg); + + char child_key[2048]; + append_segment_key(child_key, sizeof(child_key), parent, seg); + int is_file = (*p == '\0'); + add_child(parent, seg, start, n, + is_file ? FILE_ATTRIBUTE_ARCHIVE : FILE_ATTRIBUTE_DIRECTORY, + is_file ? size : 0, is_file ? mtime : (FILETIME){0, 0}); + if (!is_file) { + get_dir_bucket(child_key, 1); + strncpy(parent, child_key, sizeof(parent) - 1); + parent[sizeof(parent) - 1] = '\0'; + } + free(seg); + } +} + +static int entry_cmp(const void* a, const void* b) +{ + const VfsEntry* ea = (const VfsEntry*)a; + const VfsEntry* eb = (const VfsEntry*)b; + return strcmp(ea->key, eb->key); +} + +static VfsEntry* lookup_entry(const char* key) +{ + size_t lo = 0; + size_t hi = g_entry_count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int cmp = strcmp(key, g_entries[mid].key); + if (cmp == 0) return &g_entries[mid]; + if (cmp < 0) hi = mid; + else lo = mid + 1; + } + return NULL; +} + +static void append_log(const char* buf, DWORD len) +{ + HANDLE err = GetStdHandle(STD_ERROR_HANDLE); + if (err != NULL && err != INVALID_HANDLE_VALUE) { + DWORD wrote = 0; + WriteFile(err, buf, len, &wrote, NULL); + } + + HANDLE f = CreateFileA("Z:\\tmp\\fluorine_vfs_appinit.log", + FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | + FILE_SHARE_DELETE, + NULL, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (f != INVALID_HANDLE_VALUE) { + DWORD wrote = 0; + WriteFile(f, buf, len, &wrote, NULL); + CloseHandle(f); + } +} + +static DWORD copy_str(char* dst, DWORD pos, DWORD cap, const char* src) +{ + while (*src && pos + 1 < cap) dst[pos++] = *src++; + return pos; +} + +static DWORD copy_u64(char* dst, DWORD pos, DWORD cap, unsigned long long v) +{ + char rev[32]; + int n = 0; + if (v == 0) { + rev[n++] = '0'; + } else { + while (v > 0 && n < (int)sizeof(rev)) { + rev[n++] = (char)('0' + (v % 10)); + v /= 10; + } + } + while (n > 0 && pos + 1 < cap) dst[pos++] = rev[--n]; + return pos; +} + +static void log_msg(const char* msg) +{ + char line[512]; + DWORD pos = 0; + pos = copy_str(line, pos, sizeof(line), "[fluorine_vfs] "); + pos = copy_str(line, pos, sizeof(line), msg); + pos = copy_str(line, pos, sizeof(line), "\n"); + append_log(line, pos); +} + +static void log_hook_status(const char* name, MH_STATUS st) +{ + char line[512]; + DWORD pos = 0; + pos = copy_str(line, pos, sizeof(line), "[fluorine_vfs] hook "); + pos = copy_str(line, pos, sizeof(line), name); + pos = copy_str(line, pos, sizeof(line), " status="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)(int)st); + pos = copy_str(line, pos, sizeof(line), "\n"); + append_log(line, pos); +} + +static void log_phase(const char* name) +{ + ULONGLONG now = GetTickCount64(); + ULONGLONG start = g_phase_start_tick; + char line[512]; + DWORD pos = 0; + pos = copy_str(line, pos, sizeof(line), "[fluorine_vfs] phase "); + pos = copy_str(line, pos, sizeof(line), name); + pos = copy_str(line, pos, sizeof(line), " tick_ms="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)now); + if (start != 0) { + pos = copy_str(line, pos, sizeof(line), " since_hook_worker_ms="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)(now - start)); + } + pos = copy_str(line, pos, sizeof(line), " tid="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)GetCurrentThreadId()); + pos = copy_str(line, pos, sizeof(line), "\n"); + append_log(line, pos); +} + +static void log_stats(void) +{ + char line[512]; + DWORD pos = 0; + pos = copy_str(line, pos, sizeof(line), "[fluorine_vfs] nt_stats create="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_nt_create); + pos = copy_str(line, pos, sizeof(line), " open="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_nt_open); + pos = copy_str(line, pos, sizeof(line), " close="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_nt_close); + pos = copy_str(line, pos, sizeof(line), " qdf="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_nt_qdf); + pos = copy_str(line, pos, sizeof(line), " qdfex="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_nt_qdfex); + pos = copy_str(line, pos, sizeof(line), " qattr="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_nt_qattr); + pos = copy_str(line, pos, sizeof(line), " qfullattr="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_nt_qfullattr); + pos = copy_str(line, pos, sizeof(line), " redir_file="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_redirect_file); + pos = copy_str(line, pos, sizeof(line), " redir_miss="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_redirect_miss); + pos = copy_str(line, pos, sizeof(line), " redir_skip="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_redirect_skip); + pos = copy_str(line, pos, sizeof(line), " redir_dir="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_redirect_dir); + pos = copy_str(line, pos, sizeof(line), " dir_served="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_dir_query_served); + pos = copy_str(line, pos, sizeof(line), " dir_empty="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_dir_query_empty); + pos = copy_str(line, pos, sizeof(line), " dir_bypass="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_dir_query_bypass); + pos = copy_str(line, pos, sizeof(line), " attr_hit="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_attr_hit); + pos = copy_str(line, pos, sizeof(line), " attr_miss="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_attr_miss); + pos = copy_str(line, pos, sizeof(line), " attr_neg="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_attr_neg); + pos = copy_str(line, pos, sizeof(line), " attr_pass="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_attr_passthru); + pos = copy_str(line, pos, sizeof(line), " cache_hit="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_attr_cache_hit); + pos = copy_str(line, pos, sizeof(line), " cache_ins="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_attr_cache_insert); + pos = copy_str(line, pos, sizeof(line), " cache_full="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_attr_cache_full); + pos = copy_str(line, pos, sizeof(line), " k32w="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_gfaexw); + pos = copy_str(line, pos, sizeof(line), " k32a="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_gfaexa); + pos = copy_str(line, pos, sizeof(line), " k32_hit="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_attr_hit); + pos = copy_str(line, pos, sizeof(line), " k32_cache="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_cache_hit); + pos = copy_str(line, pos, sizeof(line), " k32_pass="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_passthru); + pos = copy_str(line, pos, sizeof(line), " k32_neg="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_neg); + pos = copy_str(line, pos, sizeof(line), " k32_fast="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_fast_path); + pos = copy_str(line, pos, sizeof(line), " k32_full="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_k32_full_path); + pos = copy_str(line, pos, sizeof(line), " live_ins="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_live_insert); + pos = copy_str(line, pos, sizeof(line), " open_neg="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_open_neg); + pos = copy_str(line, pos, sizeof(line), " mm_att="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_mmap_attached); + pos = copy_str(line, pos, sizeof(line), " mm_det="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_mmap_detached); + pos = copy_str(line, pos, sizeof(line), " mm_rd="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_mmap_reads); + pos = copy_str(line, pos, sizeof(line), " mm_by="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_mmap_bytes); + pos = copy_str(line, pos, sizeof(line), " mm_pt="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_mmap_passthru); + pos = copy_str(line, pos, sizeof(line), " mm_skz="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_mmap_skip_size); + pos = copy_str(line, pos, sizeof(line), " mm_mf="); + pos = copy_u64(line, pos, sizeof(line), + (unsigned long long)g_mmap_map_failed); + pos = copy_str(line, pos, sizeof(line), "\n"); + append_log(line, pos); +} + +static DWORD WINAPI stats_worker(void* unused) +{ + (void)unused; + for (int i = 0; i < 120; ++i) { + Sleep(1000); + if (g_installed) log_stats(); + } + log_phase("stats_worker_exit"); + return 0; +} + +static void start_stats_worker(void) +{ + if (InterlockedExchange(&g_stats_started, 1) != 0) return; + HANDLE th = CreateThread(NULL, 0, stats_worker, NULL, 0, NULL); + if (th != NULL) CloseHandle(th); +} + +static void log_index_loaded(void) +{ + char line[256]; + DWORD pos = 0; + pos = copy_str(line, pos, sizeof(line), "[fluorine_vfs] index files="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_entry_count); + pos = copy_str(line, pos, sizeof(line), " dirs="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_dir_count); + pos = copy_str(line, pos, sizeof(line), " children="); + pos = copy_u64(line, pos, sizeof(line), (unsigned long long)g_child_count); + pos = copy_str(line, pos, sizeof(line), "\n"); + append_log(line, pos); +} + +static int read_file_all(const char* path, char** out, size_t* out_len) +{ + *out = NULL; + *out_len = 0; + HANDLE f = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (f == INVALID_HANDLE_VALUE) return 0; + LARGE_INTEGER sz; + if (!GetFileSizeEx(f, &sz) || sz.QuadPart <= 0 || + sz.QuadPart > 512LL * 1024LL * 1024LL) { + CloseHandle(f); + return 0; + } + + size_t len = (size_t)sz.QuadPart; + char* buf = (char*)malloc(len + 1); + if (buf == NULL) { + CloseHandle(f); + return 0; + } + + size_t off = 0; + while (off < len) { + DWORD want = (DWORD)((len - off) > (1024 * 1024) ? (1024 * 1024) : (len - off)); + DWORD got = 0; + if (!ReadFile(f, buf + off, want, &got, NULL) || got == 0) { + free(buf); + CloseHandle(f); + return 0; + } + off += got; + } + CloseHandle(f); + buf[len] = '\0'; + *out = buf; + *out_len = len; + return 1; +} + +static int append_entry(char* key, WCHAR* real_nt, ULONG attrs, + unsigned long long size, FILETIME mtime) +{ + VfsEntry* next = + (VfsEntry*)realloc(g_entries, (g_entry_count + 1) * sizeof(VfsEntry)); + if (next == NULL) return 0; + g_entries = next; + g_entries[g_entry_count].key = key; + g_entries[g_entry_count].real_nt = real_nt; + g_entries[g_entry_count].attrs = attrs; + g_entries[g_entry_count].size = size; + g_entries[g_entry_count].mtime = mtime; + ++g_entry_count; + return 1; +} + +static int load_index(void) +{ + char index_path[1024]; + DWORD n = GetEnvironmentVariableA("FLUORINE_VFS_INDEX", index_path, + (DWORD)sizeof(index_path)); + if (n == 0 || n >= sizeof(index_path)) { + log_msg("index load skipped: FLUORINE_VFS_INDEX unset"); + return 0; + } + + char mount[1024]; + n = GetEnvironmentVariableA("FLUORINE_VFS_MOUNT", mount, (DWORD)sizeof(mount)); + if (n == 0 || n >= sizeof(mount)) { + log_msg("index load skipped: FLUORINE_VFS_MOUNT unset"); + return 0; + } + g_mount = heap_strndup(mount, strlen(mount)); + if (g_mount == NULL) return 0; + normalize_utf8_path(g_mount); + + char* file = NULL; + size_t file_len = 0; + if (!read_file_all(index_path, &file, &file_len)) { + log_msg("index read failed"); + return 0; + } + + char* line = file; + char* end = file + file_len; + while (line < end) { + char* nl = (char*)memchr(line, '\n', (size_t)(end - line)); + if (nl != NULL) *nl = '\0'; + if (strstr(line, "\"record\":\"entry\"") != NULL) { + char* type = json_string_field(line, "type"); + if (type != NULL && ascii_eq_ci(type, "file")) { + char* vp = json_string_field(line, "virtual_path"); + char* rp = json_string_field(line, "real_path"); + if (vp != NULL && rp != NULL && rp[0] == '/') { + unsigned long long size = json_u64_field(line, "size"); + FILETIME mtime = + filetime_from_unix_ns(json_u64_field(line, "mtime_ns")); + add_path_to_dirs(vp, size, mtime); + normalize_utf8_path(vp); + WCHAR* real_nt = unix_path_to_nt_wide(rp); + if (real_nt == NULL || + !append_entry(vp, real_nt, FILE_ATTRIBUTE_ARCHIVE, size, + mtime)) { + free(vp); + if (real_nt != NULL) free(real_nt); + } + vp = NULL; + } + if (vp != NULL) free(vp); + if (rp != NULL) free(rp); + } + if (type != NULL) free(type); + } + if (nl == NULL) break; + line = nl + 1; + } + free(file); + + if (g_entry_count == 0) { + log_msg("index load failed: no files"); + return 0; + } + qsort(g_entries, g_entry_count, sizeof(VfsEntry), entry_cmp); + sort_dir_children(); + InterlockedExchange(&g_index_ready, 1); + log_index_loaded(); + return 1; +} + +static int nt_path_to_key(PUNICODE_STRING_FL us, char* out, size_t cap) +{ + if (us == NULL || us->Buffer == NULL || cap == 0) return 0; + int chars = us->Length / sizeof(WCHAR); + if (chars <= 0) return 0; + int need = WideCharToMultiByte(CP_UTF8, 0, us->Buffer, chars, NULL, 0, + NULL, NULL); + if (need <= 0 || (size_t)need + 1 >= cap) return 0; + WideCharToMultiByte(CP_UTF8, 0, us->Buffer, chars, out, (int)cap, NULL, + NULL); + out[need] = '\0'; + + char* p = out; + if (strncmp(p, "\\??\\", 4) == 0 || strncmp(p, "\\\\?\\", 4) == 0) p += 4; + if ((p[0] == 'Z' || p[0] == 'z') && p[1] == ':') { + p += 2; + while (*p == '\\' || *p == '/') ++p; + memmove(out, "/", 1); + memmove(out + 1, p, strlen(p) + 1); + } else { + if (p != out) memmove(out, p, strlen(p) + 1); + } + for (char* q = out; *q; ++q) { + if (*q == '\\') *q = '/'; + else *q = ascii_lower(*q); + } + while (strlen(out) > 1 && out[strlen(out) - 1] == '/') { + out[strlen(out) - 1] = '\0'; + } + + const char* cmp = out; + if (g_mount != NULL && g_mount[0] != '/' && cmp[0] == '/') ++cmp; + + size_t ml = g_mount ? strlen(g_mount) : 0; + if (ml == 0 || strncmp(cmp, g_mount, ml) != 0) return 0; + if (cmp[ml] == '\0') { + out[0] = '\0'; + return 1; + } + if (cmp[ml] != '/') return 0; + memmove(out, cmp + ml + 1, strlen(cmp + ml + 1) + 1); + return out[0] != '\0'; +} + +static int dos_wide_path_to_key(LPCWSTR path, char* out, size_t cap) +{ + if (path == NULL || out == NULL || cap == 0 || !g_index_ready) return 0; + out[0] = '\0'; + + int raw_bytes = WideCharToMultiByte(CP_UTF8, 0, path, -1, NULL, 0, + NULL, NULL); + if (raw_bytes > 0 && (size_t)raw_bytes < cap) { + WideCharToMultiByte(CP_UTF8, 0, path, -1, out, (int)cap, NULL, NULL); + + char* p = out; + if (strncmp(p, "\\\\?\\", 4) == 0 || strncmp(p, "\\??\\", 4) == 0) p += 4; + if ((p[0] == 'Z' || p[0] == 'z') && p[1] == ':') { + p += 2; + while (*p == '\\' || *p == '/') ++p; + memmove(out, "/", 1); + memmove(out + 1, p, strlen(p) + 1); + } else if (p != out) { + memmove(out, p, strlen(p) + 1); + } + + for (char* q = out; *q; ++q) { + if (*q == '\\') *q = '/'; + else *q = ascii_lower(*q); + } + while (strlen(out) > 1 && out[strlen(out) - 1] == '/') { + out[strlen(out) - 1] = '\0'; + } + + const char* cmp = out; + if (g_mount != NULL && g_mount[0] != '/' && cmp[0] == '/') ++cmp; + size_t ml = g_mount ? strlen(g_mount) : 0; + if (ml != 0 && strncmp(cmp, g_mount, ml) == 0) { + if (cmp[ml] == '\0') { + out[0] = '\0'; + return 1; + } + if (cmp[ml] == '/') { + memmove(out, cmp + ml + 1, strlen(cmp + ml + 1) + 1); + if (out[0] != '\0') { + InterlockedIncrement64(&g_k32_fast_path); + return 1; + } + } + } + + if (strncmp(out, "data/", 5) == 0 && out[5] != '\0') { + memmove(out, out + 5, strlen(out + 5) + 1); + InterlockedIncrement64(&g_k32_fast_path); + return 1; + } + } + + DWORD need = GetFullPathNameW(path, 0, NULL, NULL); + if (need == 0 || need > 32760) return 0; + + WCHAR* full = (WCHAR*)malloc(((size_t)need + 1) * sizeof(WCHAR)); + if (full == NULL) return 0; + DWORD got = GetFullPathNameW(path, need + 1, full, NULL); + if (got == 0 || got >= need + 1) { + free(full); + return 0; + } + + int bytes = WideCharToMultiByte(CP_UTF8, 0, full, -1, NULL, 0, NULL, NULL); + if (bytes <= 0 || (size_t)bytes >= cap) { + free(full); + return 0; + } + WideCharToMultiByte(CP_UTF8, 0, full, -1, out, (int)cap, NULL, NULL); + free(full); + + char* p = out; + if (strncmp(p, "\\\\?\\", 4) == 0) p += 4; + if ((p[0] == 'Z' || p[0] == 'z') && p[1] == ':') { + p += 2; + while (*p == '\\' || *p == '/') ++p; + memmove(out, "/", 1); + memmove(out + 1, p, strlen(p) + 1); + } else { + if (p != out) memmove(out, p, strlen(p) + 1); + } + for (char* q = out; *q; ++q) { + if (*q == '\\') *q = '/'; + else *q = ascii_lower(*q); + } + while (strlen(out) > 1 && out[strlen(out) - 1] == '/') { + out[strlen(out) - 1] = '\0'; + } + + const char* cmp = out; + if (g_mount != NULL && g_mount[0] != '/' && cmp[0] == '/') ++cmp; + size_t ml = g_mount ? strlen(g_mount) : 0; + if (ml == 0 || strncmp(cmp, g_mount, ml) != 0) return 0; + if (cmp[ml] == '\0') { + out[0] = '\0'; + return 1; + } + if (cmp[ml] != '/') return 0; + memmove(out, cmp + ml + 1, strlen(cmp + ml + 1) + 1); + InterlockedIncrement64(&g_k32_full_path); + return out[0] != '\0'; +} + +/* Module-handle-sensitive: Windows registers loaded PE modules under + * the string used to open them. Redirecting those breaks + * GetModuleHandleW lookups, script-extender trampoline systems, and + * compiled-script (.pex) version probes. Archives (BSA/BA2/ESP/ESM/ + * ESL) carry no path-string semantics on Linux because reads stream + * from the resulting file handle, so they redirect freely. */ +static int protected_ext_module(const char* key) +{ + const char* dot = strrchr(key, '.'); + if (dot == NULL || dot[1] == '\0') return 0; + ++dot; + return strcmp(dot, "dll") == 0 || strcmp(dot, "exe") == 0 || + strcmp(dot, "pex") == 0; +} + +static int build_redirect_oa(POBJECT_ATTRIBUTES_FL src, + UNICODE_STRING_FL* out_us, + OBJECT_ATTRIBUTES_FL* out_oa, + const WCHAR* real_nt) +{ + size_t len = lstrlenW(real_nt); + if (len == 0 || len >= (sizeof(t_redirect_path) / sizeof(t_redirect_path[0]))) + return 0; + memcpy(t_redirect_path, real_nt, (len + 1) * sizeof(WCHAR)); + out_us->Buffer = t_redirect_path; + out_us->Length = (USHORT)(len * sizeof(WCHAR)); + out_us->MaximumLength = out_us->Length; + *out_oa = *src; + out_oa->RootDirectory = NULL; + out_oa->ObjectName = out_us; + return 1; +} + +static int ensure_dummy_dir(void) +{ + if (g_dummy_dir != INVALID_HANDLE_VALUE) return 1; + g_dummy_dir = CreateFileW(L"Z:\\tmp", GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | + FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (g_dummy_dir == INVALID_HANDLE_VALUE) { + log_msg("dummy dir open failed"); + return 0; + } + return 1; +} + +static int duplicate_dummy_dir(PHANDLE out) +{ + if (out == NULL || !ensure_dummy_dir()) return 0; + HANDLE dup = INVALID_HANDLE_VALUE; + if (!DuplicateHandle(GetCurrentProcess(), g_dummy_dir, GetCurrentProcess(), + &dup, 0, FALSE, DUPLICATE_SAME_ACCESS)) { + return 0; + } + *out = dup; + return 1; +} + +static int is_write_or_create(ACCESS_MASK access, ULONG disp, int is_create) +{ + if (is_create && disp != 1u) return 1; + return (access & (0x40000000u | 0x00010000u | 0x00000002u | + 0x00000004u | 0x00000100u)) != 0; +} + +static int register_dir_handle(HANDLE h, DirBucket* bucket) +{ + if (h == NULL || h == INVALID_HANDLE_VALUE || bucket == NULL || + !g_dir_lock_ready) { + return 0; + } + EnterCriticalSection(&g_dir_lock); + if (g_dir_handle_count == g_dir_handle_cap) { + size_t next_cap = g_dir_handle_cap ? g_dir_handle_cap * 2 : 64; + DirHandleState* next = + (DirHandleState*)realloc(g_dir_handles, + next_cap * sizeof(DirHandleState)); + if (next == NULL) { + LeaveCriticalSection(&g_dir_lock); + return 0; + } + g_dir_handles = next; + g_dir_handle_cap = next_cap; + } + DirHandleState* st = &g_dir_handles[g_dir_handle_count++]; + memset(st, 0, sizeof(*st)); + st->handle = h; + st->bucket = bucket; + LeaveCriticalSection(&g_dir_lock); + return 1; +} + +static void unregister_dir_handle(HANDLE h) +{ + if (!g_dir_lock_ready || h == NULL || h == INVALID_HANDLE_VALUE) return; + EnterCriticalSection(&g_dir_lock); + for (size_t i = 0; i < g_dir_handle_count; ++i) { + if (g_dir_handles[i].handle == h) { + if (g_dir_handles[i].filter != NULL) free(g_dir_handles[i].filter); + g_dir_handles[i] = g_dir_handles[g_dir_handle_count - 1]; + --g_dir_handle_count; + break; + } + } + LeaveCriticalSection(&g_dir_lock); +} + +static DirHandleState* find_dir_handle_locked(HANDLE h) +{ + if (h == NULL || h == INVALID_HANDLE_VALUE) return NULL; + for (size_t i = 0; i < g_dir_handle_count; ++i) { + if (g_dir_handles[i].handle == h) return &g_dir_handles[i]; + } + return NULL; +} + +static WCHAR wlower(WCHAR c) +{ + return (c >= L'A' && c <= L'Z') ? (WCHAR)(c + 32) : c; +} + +static int wildcard_match_w(const WCHAR* name, size_t nlen, + const WCHAR* pat, size_t plen) +{ + size_t i = 0, j = 0; + size_t star_i = (size_t)-1, star_j = 0; + while (i < nlen) { + if (j < plen) { + WCHAR p = pat[j]; + if (p == L'<') p = L'*'; + else if (p == L'>') p = L'?'; + else if (p == L'"') p = L'.'; + if (p == L'*') { + star_j = j + 1; + star_i = i; + ++j; + continue; + } + if (p == L'?' || wlower(name[i]) == p) { + ++i; + ++j; + continue; + } + } + if (star_i != (size_t)-1) { + j = star_j; + ++star_i; + i = star_i; + continue; + } + return 0; + } + while (j < plen) { + WCHAR p = pat[j++]; + if (p == L'<') p = L'*'; + if (p != L'*') return 0; + } + return 1; +} + +static int child_matches_filter(DirHandleState* st, const ChildEntry* e) +{ + if (st->filter == NULL || st->filter_len == 0 || + (st->filter_len == 1 && st->filter[0] == L'*')) { + return 1; + } + size_t n = lstrlenW(e->name); + return wildcard_match_w(e->name, n, st->filter, st->filter_len); +} + +static size_t dir_info_header_size(ULONG info_class) +{ + switch (info_class) { + case FL_FileDirectoryInformation: + return offsetof(FL_FILE_DIRECTORY_INFORMATION, FileName); + case FL_FileFullDirectoryInformation: + return offsetof(FL_FILE_FULL_DIR_INFORMATION, FileName); + case FL_FileBothDirectoryInformation: + return offsetof(FL_FILE_BOTH_DIR_INFORMATION, FileName); + case FL_FileIdBothDirectoryInformation: + return offsetof(FL_FILE_ID_BOTH_DIR_INFORMATION, FileName); + case FL_FileIdFullDirectoryInformation: + return offsetof(FL_FILE_ID_FULL_DIR_INFORMATION, FileName); + case FL_FileNamesInformation: + return offsetof(FL_FILE_NAMES_INFORMATION, FileName); + default: + return 0; + } +} + +static size_t write_dir_entry(unsigned char* buf, size_t cap, size_t off, + ULONG info_class, const ChildEntry* e) +{ + size_t hdr = dir_info_header_size(info_class); + if (hdr == 0) return 0; + size_t name_chars = lstrlenW(e->name); + size_t name_bytes = name_chars * sizeof(WCHAR); + size_t total_raw = hdr + name_bytes; + size_t total = (total_raw + 7u) & ~(size_t)7u; + if (off + total_raw > cap) return 0; + + unsigned char* p = buf + off; + memset(p, 0, total); + if (info_class != FL_FileNamesInformation) { + FL_FILE_DIRECTORY_INFORMATION* base = + (FL_FILE_DIRECTORY_INFORMATION*)p; + base->CreationTime.LowPart = e->mtime.dwLowDateTime; + base->CreationTime.HighPart = (LONG)e->mtime.dwHighDateTime; + base->LastAccessTime = base->CreationTime; + base->LastWriteTime = base->CreationTime; + base->ChangeTime = base->CreationTime; + base->EndOfFile.QuadPart = (LONGLONG)e->size; + base->AllocationSize.QuadPart = + (LONGLONG)((e->size + 4095ULL) & ~4095ULL); + base->FileAttributes = e->attrs; + base->FileNameLength = (ULONG)name_bytes; + } else { + FL_FILE_NAMES_INFORMATION* base = (FL_FILE_NAMES_INFORMATION*)p; + base->FileNameLength = (ULONG)name_bytes; + } + memcpy(p + hdr, e->name, name_bytes); + return total; +} + +static void patch_next(unsigned char* buf, size_t prev, ULONG delta) +{ + memcpy(buf + prev, &delta, sizeof(delta)); +} + +static void fill_basic_info(FL_FILE_BASIC_INFORMATION* out, ULONG attrs, + FILETIME mtime) +{ + memset(out, 0, sizeof(*out)); + out->CreationTime.LowPart = mtime.dwLowDateTime; + out->CreationTime.HighPart = (LONG)mtime.dwHighDateTime; + out->LastAccessTime = out->CreationTime; + out->LastWriteTime = out->CreationTime; + out->ChangeTime = out->CreationTime; + out->FileAttributes = attrs; +} + +static void fill_network_info(FL_FILE_NETWORK_OPEN_INFORMATION* out, + ULONG attrs, unsigned long long size, + FILETIME mtime) +{ + memset(out, 0, sizeof(*out)); + out->CreationTime.LowPart = mtime.dwLowDateTime; + out->CreationTime.HighPart = (LONG)mtime.dwHighDateTime; + out->LastAccessTime = out->CreationTime; + out->LastWriteTime = out->CreationTime; + out->ChangeTime = out->CreationTime; + out->EndOfFile.QuadPart = (LONGLONG)size; + out->AllocationSize.QuadPart = (LONGLONG)((size + 4095ULL) & ~4095ULL); + out->FileAttributes = attrs; +} + +static void fill_win32_attr_data(WIN32_FILE_ATTRIBUTE_DATA* out, ULONG attrs, + unsigned long long size, FILETIME mtime) +{ + memset(out, 0, sizeof(*out)); + out->dwFileAttributes = attrs; + out->ftCreationTime = mtime; + out->ftLastAccessTime = mtime; + out->ftLastWriteTime = mtime; + out->nFileSizeHigh = (DWORD)(size >> 32); + out->nFileSizeLow = (DWORD)(size & 0xffffffffULL); +} + +static int attr_path_key(POBJECT_ATTRIBUTES_FL oa, char* key, size_t key_cap) +{ + if (oa == NULL || oa->ObjectName == NULL || oa->RootDirectory != NULL || + !g_index_ready) { + return 0; + } + return nt_path_to_key(oa->ObjectName, key, key_cap); +} + +static int query_index_attrs_key(const char* key, ULONG* attrs, + unsigned long long* size, FILETIME* mtime) +{ + VfsEntry* e = lookup_entry(key); + if (e != NULL) { + *attrs = e->attrs; + *size = e->size; + *mtime = e->mtime; + return 1; + } + + DirBucket* b = get_dir_bucket(key, 0); + if (b != NULL) { + *attrs = FILE_ATTRIBUTE_DIRECTORY; + *size = 0; + mtime->dwLowDateTime = 0; + mtime->dwHighDateTime = 0; + return 1; + } + return 0; +} + +static int should_neg_attr_miss(const char* key) +{ + static LONG cached = -1; + LONG v = cached; + if (v < 0) { + char buf[16]; + DWORD n = GetEnvironmentVariableA("FLUORINE_VFS_NEG_ATTR_MISS", buf, + (DWORD)sizeof(buf)); + v = (n > 0 && n < sizeof(buf) && buf[0] == '1') ? 1 : 0; + InterlockedCompareExchange(&cached, v, -1); + v = cached; + } + if (!v) return 0; + if (key == NULL || key[0] == '\0') return 0; + return !protected_ext_module(key); +} + +/* ----- Path cache (PathCache equivalent) ----- + * + * For paths under the VFS mount that miss our static index, the first + * Real_NtQuery* roundtrip to Wine -> Z: -> FUSE answers definitively + * (FUSE is authoritative for the live tree, including Overwrite + * writes). We memoize the answer so subsequent probes for the same + * path skip the Real_ syscall entirely. Game-agnostic — no subtree + * carve-outs needed because we always real-probe before answering. + * + * Negatives are cached too. Mid-boot writes that would invalidate a + * negative cache entry are caught by attr_cache_invalidate() called + * from Hook_NtCreateFile on create-success. */ + +typedef struct AttrCacheEntry +{ + char* key; + ULONG attrs; + unsigned long long size; + FILETIME mtime; + unsigned char exists; + struct AttrCacheEntry* next; +} AttrCacheEntry; + +#define FL_ATTR_CACHE_BUCKETS 8192u +#define FL_ATTR_CACHE_MAX 32768u +static AttrCacheEntry** g_attr_cache = NULL; +static volatile LONG64 g_attr_cache_count = 0; +static CRITICAL_SECTION g_attr_cache_lock; +static volatile LONG g_attr_cache_ready = 0; + +static unsigned long key_hash_djb2(const char* s) +{ + unsigned long h = 5381u; + while (*s) h = (h * 33u) ^ (unsigned char)*s++; + return h; +} + +static void attr_cache_init(void) +{ + if (InterlockedExchange(&g_attr_cache_ready, 1) != 0) return; + g_attr_cache = (AttrCacheEntry**)calloc(FL_ATTR_CACHE_BUCKETS, + sizeof(AttrCacheEntry*)); + InitializeCriticalSection(&g_attr_cache_lock); +} + +static int attr_cache_lookup(const char* key, ULONG* attrs, + unsigned long long* size, FILETIME* mtime, + int* exists) +{ + if (!g_attr_cache_ready || g_attr_cache == NULL || key == NULL || + key[0] == '\0') + return 0; + unsigned long h = key_hash_djb2(key) % FL_ATTR_CACHE_BUCKETS; + EnterCriticalSection(&g_attr_cache_lock); + for (AttrCacheEntry* e = g_attr_cache[h]; e != NULL; e = e->next) { + if (strcmp(e->key, key) == 0) { + *attrs = e->attrs; + *size = e->size; + *mtime = e->mtime; + *exists = e->exists ? 1 : 0; + LeaveCriticalSection(&g_attr_cache_lock); + return 1; + } + } + LeaveCriticalSection(&g_attr_cache_lock); + return 0; +} + +static void attr_cache_insert(const char* key, ULONG attrs, + unsigned long long size, FILETIME mtime, + int exists) +{ + if (!g_attr_cache_ready || g_attr_cache == NULL || key == NULL || + key[0] == '\0') + return; + if (g_attr_cache_count >= (LONG64)FL_ATTR_CACHE_MAX) { + InterlockedIncrement64(&g_attr_cache_full); + return; + } + size_t klen = strlen(key); + unsigned long h = key_hash_djb2(key) % FL_ATTR_CACHE_BUCKETS; + EnterCriticalSection(&g_attr_cache_lock); + for (AttrCacheEntry* e = g_attr_cache[h]; e != NULL; e = e->next) { + if (strcmp(e->key, key) == 0) { + e->attrs = attrs; + e->size = size; + e->mtime = mtime; + e->exists = (unsigned char)(exists ? 1 : 0); + LeaveCriticalSection(&g_attr_cache_lock); + return; + } + } + AttrCacheEntry* ne = + (AttrCacheEntry*)malloc(sizeof(AttrCacheEntry)); + if (ne == NULL) { + LeaveCriticalSection(&g_attr_cache_lock); + return; + } + ne->key = heap_strndup(key, klen); + if (ne->key == NULL) { + free(ne); + LeaveCriticalSection(&g_attr_cache_lock); + return; + } + ne->attrs = attrs; + ne->size = size; + ne->mtime = mtime; + ne->exists = (unsigned char)(exists ? 1 : 0); + ne->next = g_attr_cache[h]; + g_attr_cache[h] = ne; + InterlockedIncrement64(&g_attr_cache_count); + InterlockedIncrement64(&g_attr_cache_insert); + LeaveCriticalSection(&g_attr_cache_lock); +} + +/* Promote a path to "exists" after a successful create/write. + * Uses caller-supplied attrs/size/mtime if available, otherwise + * stamps a defensive default of FILE_ATTRIBUTE_NORMAL with zero + * timestamp so subsequent attr probes return STATUS_SUCCESS instead + * of stale ENOENT. */ +static void attr_cache_mark_exists(const char* key) +{ + if (!g_attr_cache_ready || key == NULL || key[0] == '\0') return; + FILETIME ft = {0, 0}; + attr_cache_insert(key, 0x00000080u /* FILE_ATTRIBUTE_NORMAL */, 0, ft, + 1); + InterlockedIncrement64(&g_live_insert); +} + +/* ----- mmap shim ----- + * + * Engine ReadFile loops on plugin/archive loads cost ~20-50us per + * call in pure Wine syscall overhead, dwarfing the ~50ns memcpy that + * actually moves the bytes. For redirected handles (which point at a + * real backing file under the mount) we opportunistically map the + * whole file into our process address space and serve subsequent + * NtReadFile calls from memcpy. NtSetInformationFile/ + * NtQueryInformationFile keep the tracked cursor in sync with what + * the engine sets explicitly. + * + * Skipped: files >32 MB (RAM pressure for full map; BSAs handled in + * a later phase via header-window mapping), zero-byte files, + * overlapped reads, write-intent opens (write paths never reach + * mmap_attach because they bail before redirect). */ + +typedef struct MmapEntry +{ + HANDLE h_file; + HANDLE h_section; + void* base; + LONGLONG size; + volatile LONGLONG pos; + struct MmapEntry* next; +} MmapEntry; + +#define FL_MMAP_BUCKETS 4096u +/* Bumped to cover DXVK pipeline cache (.bin/.lut) and similar big + * read-only files. Linux kernel handles physical page eviction; we + * only consume virtual address space, of which 64-bit has plenty. */ +#define FL_MMAP_MAX_SIZE (1024LL * 1024LL * 1024LL) + +static MmapEntry** g_mmap_table = NULL; +static SRWLOCK g_mmap_lock = SRWLOCK_INIT; +static volatile LONG g_mmap_ready = 0; + +static unsigned long handle_hash(HANDLE h) +{ + uintptr_t v = (uintptr_t)h; + v ^= v >> 33; + v *= 0xff51afd7ed558ccdULL; + v ^= v >> 33; + return (unsigned long)v; +} + +static void mmap_init(void) +{ + if (InterlockedExchange(&g_mmap_ready, 1) != 0) return; + g_mmap_table = + (MmapEntry**)calloc(FL_MMAP_BUCKETS, sizeof(MmapEntry*)); + /* SRWLOCK already statically initialized via SRWLOCK_INIT. */ +} + +static MmapEntry* mmap_lookup(HANDLE h) +{ + if (!g_mmap_ready || g_mmap_table == NULL || h == NULL || + h == INVALID_HANDLE_VALUE) + return NULL; + unsigned long b = handle_hash(h) % FL_MMAP_BUCKETS; + AcquireSRWLockShared(&g_mmap_lock); + for (MmapEntry* e = g_mmap_table[b]; e != NULL; e = e->next) { + if (e->h_file == h) { + ReleaseSRWLockShared(&g_mmap_lock); + return e; + } + } + ReleaseSRWLockShared(&g_mmap_lock); + return NULL; +} + +static int mmap_disabled(void) +{ + static volatile LONG cached = -1; + LONG v = cached; + if (v < 0) { + char buf[16]; + DWORD n = GetEnvironmentVariableA("FLUORINE_VFS_NO_MMAP", buf, + (DWORD)sizeof(buf)); + v = (n > 0 && n < sizeof(buf) && buf[0] == '1') ? 1 : 0; + InterlockedCompareExchange(&cached, v, -1); + v = cached; + } + return v; +} + +static void mmap_attach(HANDLE h, const char* key) +{ + if (mmap_disabled()) return; + if (!g_mmap_ready || h == NULL || h == INVALID_HANDLE_VALUE) return; + /* Avoid double-attach in unlikely re-open of same kernel handle. */ + if (mmap_lookup(h) != NULL) return; + + IO_STATUS_BLOCK_FL iosb; + FL_FILE_STANDARD_INFORMATION fsi; + memset(&fsi, 0, sizeof(fsi)); + if (Real_NtQueryInformationFile == NULL) return; + NTSTATUS qs = + Real_NtQueryInformationFile(h, &iosb, &fsi, (ULONG)sizeof(fsi), + FL_FileStandardInformation); + if (qs != STATUS_SUCCESS) return; + if (fsi.Directory) return; + LONGLONG size = fsi.EndOfFile.QuadPart; + if (size <= 0) { + InterlockedIncrement64(&g_mmap_skip_zero); + return; + } + if (size > FL_MMAP_MAX_SIZE) { + InterlockedIncrement64(&g_mmap_skip_size); + return; + } + + HANDLE sec = CreateFileMappingW(h, NULL, PAGE_READONLY, 0, 0, NULL); + if (sec == NULL) { + InterlockedIncrement64(&g_mmap_map_failed); + return; + } + void* base = MapViewOfFile(sec, FILE_MAP_READ, 0, 0, 0); + if (base == NULL) { + CloseHandle(sec); + InterlockedIncrement64(&g_mmap_view_failed); + return; + } + + MmapEntry* ne = (MmapEntry*)malloc(sizeof(MmapEntry)); + if (ne == NULL) { + UnmapViewOfFile(base); + CloseHandle(sec); + return; + } + ne->h_file = h; + ne->h_section = sec; + ne->base = base; + ne->size = size; + ne->pos = 0; + unsigned long b = handle_hash(h) % FL_MMAP_BUCKETS; + AcquireSRWLockExclusive(&g_mmap_lock); + ne->next = g_mmap_table[b]; + g_mmap_table[b] = ne; + ReleaseSRWLockExclusive(&g_mmap_lock); + InterlockedIncrement64(&g_mmap_attached); + if (key != NULL && + InterlockedCompareExchange(&g_first_mmap_logged, 1, 0) == 0) { + log_phase("first_indexed_mmap_attach"); + } +} + +static void mmap_detach(HANDLE h) +{ + if (!g_mmap_ready || g_mmap_table == NULL || h == NULL || + h == INVALID_HANDLE_VALUE) + return; + unsigned long b = handle_hash(h) % FL_MMAP_BUCKETS; + AcquireSRWLockExclusive(&g_mmap_lock); + MmapEntry** pp = &g_mmap_table[b]; + while (*pp != NULL) { + if ((*pp)->h_file == h) { + MmapEntry* victim = *pp; + *pp = victim->next; + ReleaseSRWLockExclusive(&g_mmap_lock); + UnmapViewOfFile(victim->base); + CloseHandle(victim->h_section); + free(victim); + InterlockedIncrement64(&g_mmap_detached); + return; + } + pp = &(*pp)->next; + } + ReleaseSRWLockExclusive(&g_mmap_lock); +} + +/* NtReadFile ByteOffset sentinels (per MSDN/ReactOS): + * If ByteOffset is NULL or its value == FILE_USE_FILE_POINTER_POSITION + * (-2 == 0xFFFFFFFFFFFFFFFE), reads from current file pointer. */ +static int byte_offset_use_pointer(PLARGE_INTEGER bo) +{ + if (bo == NULL) return 1; + if (bo->QuadPart == (LONGLONG)0xFFFFFFFFFFFFFFFEULL) return 1; + if (bo->QuadPart < 0) return 1; + return 0; +} + +static NTSTATUS NTAPI Hook_NtReadFile(HANDLE h, HANDLE event, + PIO_APC_ROUTINE_FL apc, PVOID apc_ctx, + PIO_STATUS_BLOCK_FL iosb, + PVOID buf, ULONG len, + PLARGE_INTEGER bo, PULONG key_arg) +{ + /* Pass overlapped reads through; engine plugin loads are sync. */ + if (event != NULL || apc != NULL || apc_ctx != NULL || buf == NULL) { + return Real_NtReadFile(h, event, apc, apc_ctx, iosb, buf, len, bo, + key_arg); + } + MmapEntry* e = mmap_lookup(h); + if (e == NULL) { + return Real_NtReadFile(h, event, apc, apc_ctx, iosb, buf, len, bo, + key_arg); + } + LONGLONG start; + int advance_pos; + if (byte_offset_use_pointer(bo)) { + start = e->pos; + advance_pos = 1; + } else { + start = bo->QuadPart; + advance_pos = 0; + } + if (start >= e->size) { + /* File may have grown through this handle since we mapped it. + * Sync Wine's kernel-side file pointer to our tracked position so + * the fall-through Real_NtReadFile (which uses bo or the kernel + * pointer) reads from the correct offset. Without this sync, our + * mmap-served reads advance e->pos but leave the kernel's pointer + * stale at the position of the last NtSetInformationFile call, + * and the fall-through reads garbage. */ + if (advance_pos && Real_NtSetInformationFile != NULL) { + FL_FILE_POSITION_INFORMATION pi; + pi.CurrentByteOffset.QuadPart = e->pos; + IO_STATUS_BLOCK_FL temp_iosb; + Real_NtSetInformationFile(h, &temp_iosb, &pi, + (ULONG)sizeof(pi), + FL_FilePositionInformation); + } + InterlockedIncrement64(&g_mmap_passthru); + return Real_NtReadFile(h, event, apc, apc_ctx, iosb, buf, len, bo, + key_arg); + } + LONGLONG remain = e->size - start; + ULONG to_copy = (remain < (LONGLONG)len) ? (ULONG)remain : len; + memcpy(buf, (const char*)e->base + start, to_copy); + if (advance_pos) { + e->pos = start + (LONGLONG)to_copy; + } + if (iosb != NULL) { + iosb->u.Status = STATUS_SUCCESS; + iosb->Information = to_copy; + } + InterlockedIncrement64(&g_mmap_reads); + InterlockedAdd64(&g_mmap_bytes, (LONG64)to_copy); + return STATUS_SUCCESS; +} + +static NTSTATUS NTAPI Hook_NtSetInformationFile(HANDLE h, + PIO_STATUS_BLOCK_FL iosb, + PVOID info, ULONG len, + ULONG info_class) +{ + NTSTATUS rs = Real_NtSetInformationFile(h, iosb, info, len, info_class); + if (rs == STATUS_SUCCESS && info_class == FL_FilePositionInformation && + info != NULL && len >= sizeof(FL_FILE_POSITION_INFORMATION)) { + MmapEntry* e = mmap_lookup(h); + if (e != NULL) { + FL_FILE_POSITION_INFORMATION* pi = + (FL_FILE_POSITION_INFORMATION*)info; + e->pos = pi->CurrentByteOffset.QuadPart; + } + } + return rs; +} + +static NTSTATUS NTAPI Hook_NtQueryInformationFile(HANDLE h, + PIO_STATUS_BLOCK_FL iosb, + PVOID info, ULONG len, + ULONG info_class) +{ + /* Most callers use this for size/position. Serve from our state to + * avoid a redundant kernel transition when we have it. */ + MmapEntry* e = mmap_lookup(h); + if (e != NULL && info != NULL) { + if (info_class == FL_FileStandardInformation && + len >= sizeof(FL_FILE_STANDARD_INFORMATION)) { + FL_FILE_STANDARD_INFORMATION* fsi = + (FL_FILE_STANDARD_INFORMATION*)info; + memset(fsi, 0, sizeof(*fsi)); + fsi->EndOfFile.QuadPart = e->size; + fsi->AllocationSize.QuadPart = + (e->size + 4095LL) & ~4095LL; + fsi->NumberOfLinks = 1; + if (iosb != NULL) { + iosb->u.Status = STATUS_SUCCESS; + iosb->Information = sizeof(*fsi); + } + return STATUS_SUCCESS; + } + if (info_class == FL_FilePositionInformation && + len >= sizeof(FL_FILE_POSITION_INFORMATION)) { + FL_FILE_POSITION_INFORMATION* pi = + (FL_FILE_POSITION_INFORMATION*)info; + pi->CurrentByteOffset.QuadPart = e->pos; + if (iosb != NULL) { + iosb->u.Status = STATUS_SUCCESS; + iosb->Information = sizeof(*pi); + } + return STATUS_SUCCESS; + } + } + return Real_NtQueryInformationFile(h, iosb, info, len, info_class); +} + +static NTSTATUS serve_dir_query_locked(DirHandleState* st, PVOID info, + ULONG len, ULONG info_class, + int restart, int single, + PUNICODE_STRING_FL file_name, + PIO_STATUS_BLOCK_FL iosb) +{ + if (iosb == NULL) return STATUS_INFO_LENGTH_MISMATCH; + if (restart) { + st->cursor = 0; + st->filter_set = 0; + if (st->filter != NULL) { + free(st->filter); + st->filter = NULL; + } + st->filter_len = 0; + } + if (!st->filter_set) { + if (file_name != NULL && file_name->Buffer != NULL && + file_name->Length > 0) { + st->filter_len = file_name->Length / sizeof(WCHAR); + st->filter = + (WCHAR*)malloc((st->filter_len + 1) * sizeof(WCHAR)); + if (st->filter != NULL) { + for (size_t i = 0; i < st->filter_len; ++i) { + st->filter[i] = wlower(file_name->Buffer[i]); + } + st->filter[st->filter_len] = 0; + } else { + st->filter_len = 0; + } + } + st->filter_set = 1; + } + + iosb->u.Status = STATUS_SUCCESS; + iosb->Information = 0; + if (info == NULL || len < 16) { + iosb->u.Status = STATUS_INFO_LENGTH_MISMATCH; + return STATUS_INFO_LENGTH_MISMATCH; + } + + unsigned char* out = (unsigned char*)info; + size_t off = 0; + size_t prev = (size_t)-1; + size_t emitted = 0; + while (st->cursor < st->bucket->child_count) { + ChildEntry* e = &st->bucket->children[st->cursor]; + if (!child_matches_filter(st, e)) { + ++st->cursor; + continue; + } + size_t wrote = write_dir_entry(out, len, off, info_class, e); + if (wrote == 0) { + if (emitted > 0) break; + iosb->u.Status = STATUS_BUFFER_OVERFLOW; + return STATUS_BUFFER_OVERFLOW; + } + if (prev != (size_t)-1) patch_next(out, prev, (ULONG)(off - prev)); + prev = off; + off += wrote; + ++emitted; + ++st->cursor; + if (single) break; + } + + if (emitted == 0) { + iosb->u.Status = STATUS_NO_MORE_FILES; + iosb->Information = 0; + InterlockedIncrement64(&g_dir_query_empty); + return STATUS_NO_MORE_FILES; + } + iosb->u.Status = STATUS_SUCCESS; + iosb->Information = off; + InterlockedIncrement64(&g_dir_query_served); + return STATUS_SUCCESS; +} + +static int should_skip_redirect(ACCESS_MASK access, ULONG disp, ULONG opts, + int is_create) +{ + if (t_inside_redirect > 0) return 1; + if (!g_index_ready) return 1; + if (is_create && disp != 1u) return 1; + if (opts & 0x00000001u) return 1; /* FILE_DIRECTORY_FILE */ + if (access & (0x40000000u | 0x10000000u | 0x00010000u | + 0x00000002u | 0x00000004u | 0x00000010u | + 0x00000100u)) { + return 1; + } + return 0; +} + +static NTSTATUS NTAPI Hook_NtCreateFile(PHANDLE out, ACCESS_MASK access, + POBJECT_ATTRIBUTES_FL oa, + PIO_STATUS_BLOCK_FL iosb, + PLARGE_INTEGER alloc_sz, + ULONG file_attrs, ULONG share, + ULONG disp, ULONG opts, PVOID ea, + ULONG ea_len) +{ + InterlockedIncrement64(&g_nt_create); + char key[2048]; + key[0] = '\0'; + int have_key = 0; + int write_intent = 0; + if (oa != NULL && oa->ObjectName != NULL && oa->RootDirectory == NULL && + t_inside_redirect == 0 && g_index_ready) { + if (nt_path_to_key(oa->ObjectName, key, sizeof(key))) { + have_key = 1; + if (InterlockedCompareExchange(&g_first_create_logged, 1, 0) == 0) { + log_phase("first_vfs_ntcreatefile"); + } + write_intent = is_write_or_create(access, disp, 1); + if (!write_intent) { + DirBucket* dir = get_dir_bucket(key, 0); + if (dir != NULL && duplicate_dummy_dir(out)) { + if (iosb != NULL) { + iosb->u.Status = STATUS_SUCCESS; + iosb->Information = 1; + } + register_dir_handle(out ? *out : INVALID_HANDLE_VALUE, dir); + InterlockedIncrement64(&g_redirect_dir); + return STATUS_SUCCESS; + } + } + + if (!should_skip_redirect(access, disp, opts, 1) && + !protected_ext_module(key)) { + VfsEntry* e = lookup_entry(key); + if (e != NULL && e->real_nt != NULL) { + UNICODE_STRING_FL new_us; + OBJECT_ATTRIBUTES_FL new_oa; + if (build_redirect_oa(oa, &new_us, &new_oa, e->real_nt)) { + ++t_inside_redirect; + NTSTATUS st = Real_NtCreateFile(out, access, &new_oa, iosb, + alloc_sz, file_attrs, share, disp, + opts, ea, ea_len); + --t_inside_redirect; + InterlockedIncrement64(&g_redirect_file); + if (st == STATUS_SUCCESS && out != NULL && *out != NULL && + *out != INVALID_HANDLE_VALUE) { + mmap_attach(*out, key); + } + return st; + } + } else { + InterlockedIncrement64(&g_redirect_miss); + /* Aggressive open negcache: index is authoritative for the + * VFS mount tree (it walks FUSE which already composes mods + + * Overwrite). If the path is not in the index and not + * recently created via our hooks (attr_cache positive), it + * does not exist. Skip the Real_ -> Wine -> FUSE round-trip. */ + ULONG cattrs = 0; + unsigned long long csize = 0; + FILETIME cmtime; + int cexists = 0; + if (!attr_cache_lookup(key, &cattrs, &csize, &cmtime, &cexists) + || !cexists) { + if (iosb != NULL) { + iosb->u.Status = STATUS_OBJECT_NAME_NOT_FOUND; + iosb->Information = 0; + } + InterlockedIncrement64(&g_open_neg); + return STATUS_OBJECT_NAME_NOT_FOUND; + } + } + } else { + InterlockedIncrement64(&g_redirect_skip); + } + } else { + InterlockedIncrement64(&g_redirect_skip); + } + } + NTSTATUS rs = Real_NtCreateFile(out, access, oa, iosb, alloc_sz, + file_attrs, share, disp, opts, ea, ea_len); + int rs_write_intent = is_write_or_create(access, disp, 1); + if (have_key && rs_write_intent && rs == STATUS_SUCCESS) { + attr_cache_mark_exists(key); + } + if (rs == STATUS_SUCCESS && out != NULL && *out != NULL && + *out != INVALID_HANDLE_VALUE && !rs_write_intent && + !(opts & 0x00000001u /* FILE_DIRECTORY_FILE */)) { + mmap_attach(*out, have_key ? key : NULL); + } + return rs; +} + +static NTSTATUS NTAPI Hook_NtOpenFile(PHANDLE out, ACCESS_MASK access, + POBJECT_ATTRIBUTES_FL oa, + PIO_STATUS_BLOCK_FL iosb, ULONG share, + ULONG opts) +{ + InterlockedIncrement64(&g_nt_open); + char ofkey[2048]; + ofkey[0] = '\0'; + int of_have_key = 0; + int of_write_intent = is_write_or_create(access, 1u, 0); + if (oa != NULL && oa->ObjectName != NULL && oa->RootDirectory == NULL && + t_inside_redirect == 0 && g_index_ready) { + char* key = ofkey; + if (nt_path_to_key(oa->ObjectName, key, sizeof(ofkey))) { + of_have_key = 1; + if (InterlockedCompareExchange(&g_first_open_logged, 1, 0) == 0) { + log_phase("first_vfs_ntopenfile"); + } + if (!of_write_intent) { + DirBucket* dir = get_dir_bucket(key, 0); + if (dir != NULL && duplicate_dummy_dir(out)) { + if (iosb != NULL) { + iosb->u.Status = STATUS_SUCCESS; + iosb->Information = 1; + } + register_dir_handle(out ? *out : INVALID_HANDLE_VALUE, dir); + InterlockedIncrement64(&g_redirect_dir); + return STATUS_SUCCESS; + } + } + + if (!should_skip_redirect(access, 1u, opts, 0) && + !protected_ext_module(key)) { + VfsEntry* e = lookup_entry(key); + if (e != NULL && e->real_nt != NULL) { + UNICODE_STRING_FL new_us; + OBJECT_ATTRIBUTES_FL new_oa; + if (build_redirect_oa(oa, &new_us, &new_oa, e->real_nt)) { + ++t_inside_redirect; + NTSTATUS st = + Real_NtOpenFile(out, access, &new_oa, iosb, share, opts); + --t_inside_redirect; + InterlockedIncrement64(&g_redirect_file); + if (st == STATUS_SUCCESS && out != NULL && *out != NULL && + *out != INVALID_HANDLE_VALUE) { + mmap_attach(*out, key); + } + return st; + } + } else { + InterlockedIncrement64(&g_redirect_miss); + ULONG cattrs = 0; + unsigned long long csize = 0; + FILETIME cmtime; + int cexists = 0; + if (!attr_cache_lookup(key, &cattrs, &csize, &cmtime, &cexists) + || !cexists) { + if (iosb != NULL) { + iosb->u.Status = STATUS_OBJECT_NAME_NOT_FOUND; + iosb->Information = 0; + } + InterlockedIncrement64(&g_open_neg); + return STATUS_OBJECT_NAME_NOT_FOUND; + } + } + } else { + InterlockedIncrement64(&g_redirect_skip); + } + } else { + InterlockedIncrement64(&g_redirect_skip); + } + } + NTSTATUS rs = Real_NtOpenFile(out, access, oa, iosb, share, opts); + if (of_have_key && of_write_intent && rs == STATUS_SUCCESS) { + attr_cache_mark_exists(ofkey); + } + if (rs == STATUS_SUCCESS && out != NULL && *out != NULL && + *out != INVALID_HANDLE_VALUE && !of_write_intent && + !(opts & 0x00000001u /* FILE_DIRECTORY_FILE */)) { + mmap_attach(*out, of_have_key ? ofkey : NULL); + } + return rs; +} + +static NTSTATUS NTAPI Hook_NtClose(HANDLE h) +{ + InterlockedIncrement64(&g_nt_close); + unregister_dir_handle(h); + mmap_detach(h); + return Real_NtClose(h); +} + +static NTSTATUS NTAPI Hook_NtQueryDirectoryFile( + HANDLE h, HANDLE ev, PIO_APC_ROUTINE_FL apc, PVOID apc_ctx, + PIO_STATUS_BLOCK_FL iosb, PVOID info, ULONG len, ULONG info_class, + BOOLEAN single, PUNICODE_STRING_FL file_name, BOOLEAN restart) +{ + InterlockedIncrement64(&g_nt_qdf); + if (g_dir_lock_ready) { + EnterCriticalSection(&g_dir_lock); + DirHandleState* st = find_dir_handle_locked(h); + if (st != NULL) { + NTSTATUS r = serve_dir_query_locked(st, info, len, info_class, + restart ? 1 : 0, + single ? 1 : 0, file_name, iosb); + LeaveCriticalSection(&g_dir_lock); + return r; + } + LeaveCriticalSection(&g_dir_lock); + } + InterlockedIncrement64(&g_dir_query_bypass); + return Real_NtQueryDirectoryFile(h, ev, apc, apc_ctx, iosb, info, len, + info_class, single, file_name, restart); +} + +static NTSTATUS NTAPI Hook_NtQueryDirectoryFileEx( + HANDLE h, HANDLE ev, PIO_APC_ROUTINE_FL apc, PVOID apc_ctx, + PIO_STATUS_BLOCK_FL iosb, PVOID info, ULONG len, ULONG info_class, + ULONG query_flags, PUNICODE_STRING_FL file_name) +{ + InterlockedIncrement64(&g_nt_qdfex); + if (g_dir_lock_ready) { + EnterCriticalSection(&g_dir_lock); + DirHandleState* st = find_dir_handle_locked(h); + if (st != NULL) { + NTSTATUS r = serve_dir_query_locked( + st, info, len, info_class, (query_flags & FL_SL_RESTART_SCAN) != 0, + (query_flags & FL_SL_RETURN_SINGLE_ENTRY) != 0, file_name, iosb); + LeaveCriticalSection(&g_dir_lock); + return r; + } + LeaveCriticalSection(&g_dir_lock); + } + InterlockedIncrement64(&g_dir_query_bypass); + return Real_NtQueryDirectoryFileEx(h, ev, apc, apc_ctx, iosb, info, len, + info_class, query_flags, file_name); +} + +static NTSTATUS NTAPI Hook_NtQueryAttributesFile(POBJECT_ATTRIBUTES_FL oa, + PVOID info) +{ + InterlockedIncrement64(&g_nt_qattr); + if (info != NULL && t_inside_redirect == 0) { + ULONG attrs = 0; + unsigned long long size = 0; + FILETIME mtime; + char key[2048]; + key[0] = '\0'; + if (attr_path_key(oa, key, sizeof(key)) && + query_index_attrs_key(key, &attrs, &size, &mtime)) { + (void)size; + fill_basic_info((FL_FILE_BASIC_INFORMATION*)info, attrs, mtime); + InterlockedIncrement64(&g_attr_hit); + return STATUS_SUCCESS; + } + if (key[0] != '\0') { + ULONG cattrs = 0; + unsigned long long csize = 0; + FILETIME cmtime; + int cexists = 0; + if (attr_cache_lookup(key, &cattrs, &csize, &cmtime, &cexists)) { + InterlockedIncrement64(&g_attr_cache_hit); + if (cexists) { + fill_basic_info((FL_FILE_BASIC_INFORMATION*)info, cattrs, + cmtime); + return STATUS_SUCCESS; + } + return STATUS_OBJECT_NAME_NOT_FOUND; + } + if (should_neg_attr_miss(key)) { + InterlockedIncrement64(&g_attr_miss); + InterlockedIncrement64(&g_attr_neg); + return STATUS_OBJECT_NAME_NOT_FOUND; + } + InterlockedIncrement64(&g_attr_passthru); + NTSTATUS rs = Real_NtQueryAttributesFile(oa, info); + if (rs == STATUS_SUCCESS) { + FL_FILE_BASIC_INFORMATION* bi = + (FL_FILE_BASIC_INFORMATION*)info; + FILETIME ft; + ft.dwLowDateTime = (DWORD)bi->LastWriteTime.LowPart; + ft.dwHighDateTime = (DWORD)bi->LastWriteTime.HighPart; + attr_cache_insert(key, bi->FileAttributes, 0, ft, 1); + } else if (rs == STATUS_OBJECT_NAME_NOT_FOUND) { + FILETIME zft = {0, 0}; + attr_cache_insert(key, 0, 0, zft, 0); + } + return rs; + } + InterlockedIncrement64(&g_attr_passthru); + } + return Real_NtQueryAttributesFile(oa, info); +} + +static NTSTATUS NTAPI Hook_NtQueryFullAttributesFile(POBJECT_ATTRIBUTES_FL oa, + PVOID info) +{ + InterlockedIncrement64(&g_nt_qfullattr); + if (info != NULL && t_inside_redirect == 0) { + ULONG attrs = 0; + unsigned long long size = 0; + FILETIME mtime; + char key[2048]; + key[0] = '\0'; + if (attr_path_key(oa, key, sizeof(key)) && + query_index_attrs_key(key, &attrs, &size, &mtime)) { + fill_network_info((FL_FILE_NETWORK_OPEN_INFORMATION*)info, attrs, size, + mtime); + InterlockedIncrement64(&g_attr_hit); + return STATUS_SUCCESS; + } + if (key[0] != '\0') { + ULONG cattrs = 0; + unsigned long long csize = 0; + FILETIME cmtime; + int cexists = 0; + if (attr_cache_lookup(key, &cattrs, &csize, &cmtime, &cexists)) { + InterlockedIncrement64(&g_attr_cache_hit); + if (cexists) { + fill_network_info( + (FL_FILE_NETWORK_OPEN_INFORMATION*)info, cattrs, csize, + cmtime); + return STATUS_SUCCESS; + } + return STATUS_OBJECT_NAME_NOT_FOUND; + } + if (should_neg_attr_miss(key)) { + InterlockedIncrement64(&g_attr_miss); + InterlockedIncrement64(&g_attr_neg); + return STATUS_OBJECT_NAME_NOT_FOUND; + } + InterlockedIncrement64(&g_attr_passthru); + NTSTATUS rs = Real_NtQueryFullAttributesFile(oa, info); + if (rs == STATUS_SUCCESS) { + FL_FILE_NETWORK_OPEN_INFORMATION* ni = + (FL_FILE_NETWORK_OPEN_INFORMATION*)info; + FILETIME ft; + ft.dwLowDateTime = (DWORD)ni->LastWriteTime.LowPart; + ft.dwHighDateTime = (DWORD)ni->LastWriteTime.HighPart; + attr_cache_insert(key, ni->FileAttributes, + (unsigned long long)ni->EndOfFile.QuadPart, ft, + 1); + } else if (rs == STATUS_OBJECT_NAME_NOT_FOUND) { + FILETIME zft = {0, 0}; + attr_cache_insert(key, 0, 0, zft, 0); + } + return rs; + } + InterlockedIncrement64(&g_attr_passthru); + } + return Real_NtQueryFullAttributesFile(oa, info); +} + +static int try_kernel32_attr_hit(LPCWSTR path, GET_FILEEX_INFO_LEVELS level, + LPVOID info) +{ + if (info == NULL || level != GetFileExInfoStandard || + t_inside_redirect != 0 || !g_index_ready) { + return 0; + } + + char key[2048]; + key[0] = '\0'; + if (!dos_wide_path_to_key(path, key, sizeof(key)) || key[0] == '\0') { + return 0; + } + + ULONG attrs = 0; + unsigned long long size = 0; + FILETIME mtime; + if (query_index_attrs_key(key, &attrs, &size, &mtime)) { + fill_win32_attr_data((WIN32_FILE_ATTRIBUTE_DATA*)info, attrs, size, + mtime); + InterlockedIncrement64(&g_k32_attr_hit); + return 1; + } + + ULONG cattrs = 0; + unsigned long long csize = 0; + FILETIME cmtime; + int cexists = 0; + if (attr_cache_lookup(key, &cattrs, &csize, &cmtime, &cexists)) { + InterlockedIncrement64(&g_k32_cache_hit); + if (cexists) { + fill_win32_attr_data((WIN32_FILE_ATTRIBUTE_DATA*)info, cattrs, csize, + cmtime); + return 1; + } + SetLastError(ERROR_FILE_NOT_FOUND); + InterlockedIncrement64(&g_k32_neg); + return -1; + } + + return 0; +} + +static BOOL WINAPI Hook_GetFileAttributesExW(LPCWSTR path, + GET_FILEEX_INFO_LEVELS level, + LPVOID info) +{ + InterlockedIncrement64(&g_k32_gfaexw); + int hit = try_kernel32_attr_hit(path, level, info); + if (hit > 0) return TRUE; + if (hit < 0) return FALSE; + InterlockedIncrement64(&g_k32_passthru); + return Real_GetFileAttributesExW(path, level, info); +} + +static BOOL WINAPI Hook_GetFileAttributesExA(LPCSTR path, + GET_FILEEX_INFO_LEVELS level, + LPVOID info) +{ + InterlockedIncrement64(&g_k32_gfaexa); + if (path != NULL && info != NULL && level == GetFileExInfoStandard && + t_inside_redirect == 0 && g_index_ready) { + int wlen = MultiByteToWideChar(CP_ACP, 0, path, -1, NULL, 0); + if (wlen > 0 && wlen < 32760) { + WCHAR* wpath = (WCHAR*)malloc((size_t)wlen * sizeof(WCHAR)); + if (wpath != NULL) { + MultiByteToWideChar(CP_ACP, 0, path, -1, wpath, wlen); + int hit = try_kernel32_attr_hit(wpath, level, info); + free(wpath); + if (hit > 0) return TRUE; + if (hit < 0) return FALSE; + } + } + } + InterlockedIncrement64(&g_k32_passthru); + return Real_GetFileAttributesExA(path, level, info); +} + +static int hook_api(const wchar_t* module, const char* name, void* hook, + void** real, int required) +{ + MH_STATUS st = MH_CreateHookApi(module, name, hook, real); + log_hook_status(name, st); + if (st != MH_OK && st != MH_ERROR_ALREADY_CREATED) { + if (!required) return 1; + return 0; + } + return 1; +} + +static DWORD WINAPI hook_worker(void* unused) +{ + (void)unused; + g_phase_start_tick = GetTickCount64(); + log_phase("hook_worker_entry"); + if (!load_index()) { + log_msg("ntdll hooks skipped: index unavailable"); + return 0; + } + log_phase("index_loaded"); + InitializeCriticalSection(&g_dir_lock); + g_dir_lock_ready = 1; + attr_cache_init(); + mmap_init(); + ensure_dummy_dir(); + + MH_STATUS st = MH_Initialize(); + if (st != MH_OK && st != MH_ERROR_ALREADY_INITIALIZED) { + log_msg("MinHook init failed"); + return 0; + } + + int ok = 1; + ok &= hook_api(L"ntdll.dll", "NtCreateFile", Hook_NtCreateFile, + (void**)&Real_NtCreateFile, 1); + ok &= hook_api(L"ntdll.dll", "NtOpenFile", Hook_NtOpenFile, + (void**)&Real_NtOpenFile, 1); + ok &= hook_api(L"ntdll.dll", "NtClose", Hook_NtClose, + (void**)&Real_NtClose, 1); + ok &= hook_api(L"ntdll.dll", "NtQueryDirectoryFile", + Hook_NtQueryDirectoryFile, + (void**)&Real_NtQueryDirectoryFile, 1); + ok &= hook_api(L"ntdll.dll", "NtQueryDirectoryFileEx", + Hook_NtQueryDirectoryFileEx, + (void**)&Real_NtQueryDirectoryFileEx, 0); + ok &= hook_api(L"ntdll.dll", "NtQueryAttributesFile", + Hook_NtQueryAttributesFile, + (void**)&Real_NtQueryAttributesFile, 1); + ok &= hook_api(L"ntdll.dll", "NtQueryFullAttributesFile", + Hook_NtQueryFullAttributesFile, + (void**)&Real_NtQueryFullAttributesFile, 1); + ok &= hook_api(L"ntdll.dll", "NtReadFile", Hook_NtReadFile, + (void**)&Real_NtReadFile, 1); + ok &= hook_api(L"ntdll.dll", "NtSetInformationFile", + Hook_NtSetInformationFile, + (void**)&Real_NtSetInformationFile, 1); + ok &= hook_api(L"ntdll.dll", "NtQueryInformationFile", + Hook_NtQueryInformationFile, + (void**)&Real_NtQueryInformationFile, 1); + ok &= hook_api(L"kernel32.dll", "GetFileAttributesExW", + Hook_GetFileAttributesExW, + (void**)&Real_GetFileAttributesExW, 0); + ok &= hook_api(L"kernel32.dll", "GetFileAttributesExA", + Hook_GetFileAttributesExA, + (void**)&Real_GetFileAttributesExA, 0); + if (!ok) { + log_msg("ntdll hook create failed"); + return 0; + } + + st = MH_EnableHook(MH_ALL_HOOKS); + if (st != MH_OK) { + log_msg("ntdll hook enable failed"); + return 0; + } + log_phase("hooks_enabled"); + + InterlockedExchange(&g_installed, 1); + log_msg("ntdll hooks installed (file+dir+attr redirect)"); + start_stats_worker(); + return 0; +} + +void fluorine_vfs_start_hooks(void) +{ + if (InterlockedExchange(&g_started, 1) != 0) return; + HANDLE th = CreateThread(NULL, 0, hook_worker, NULL, 0, NULL); + if (th != NULL) CloseHandle(th); +} + +void fluorine_vfs_report_hooks(void) +{ + if (g_installed) log_stats(); +} diff --git a/src/src/vfs/fluorine_vfs_inject.c b/src/src/vfs/fluorine_vfs_inject.c new file mode 100644 index 0000000..8c7b2d1 --- /dev/null +++ b/src/src/vfs/fluorine_vfs_inject.c @@ -0,0 +1,310 @@ +// PE-side VFS injector for Wine/Proton. +// +// Loaded into PE game processes via Wine's AppInit_DLLs registry mechanism +// when available, or through the game-directory hid.dll proxy fallback. The +// hook implementation short-circuits selected NTDLL file, directory, read, and +// metadata calls against Fluorine's exported VFS bridge index. +// +// Design constraints: +// - Hot path runs in every PE process spawned in the prefix. +// Keep DllMain cheap; bail early in known non-game host procs. +// - AppInit_DLLs DLLs are loaded *from inside user32's DllMain*, so +// the Windows loader lock is held when our DllMain runs. Calling +// into user32 (e.g. wsprintfA) or any other DLL whose own DllMain +// hasn't run yet causes ShellExecuteEx-style "General failure" +// across every PE process in the prefix. Stick to kernel32-only +// APIs and inline-format anything we need to print. +// - AppInit_DLLs DLLs also load before MSVCRT stdio is initialized. +// Use Win32 (GetEnvironmentVariableA / WriteFile / GetStdHandle) +// instead of fprintf/getenv so we don't crash on uninitialized +// CRT state in early-stage processes. +// - Talk to Fluorine via env vars set by ProtonLauncher +// (FLUORINE_VFS_INDEX, FLUORINE_VFS_DATA_DIR, FLUORINE_VFS_MOUNT). + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +void fluorine_vfs_start_hooks(void); +void fluorine_vfs_report_hooks(void); + +static int ascii_eq_ci(const char* a, const char* b) +{ + while (*a && *b) { + char ca = *a, cb = *b; + if (ca >= 'A' && ca <= 'Z') ca = (char)(ca + 32); + if (cb >= 'A' && cb <= 'Z') cb = (char)(cb + 32); + if (ca != cb) return 0; + ++a; ++b; + } + return *a == '\0' && *b == '\0'; +} + +static int env_truthy(const char* name) +{ + char buf[16]; + DWORD n = GetEnvironmentVariableA(name, buf, (DWORD)sizeof(buf)); + if (n == 0 || n >= sizeof(buf)) return 0; + if (buf[0] == '0' && buf[1] == '\0') return 0; + if (ascii_eq_ci(buf, "false")) return 0; + if (ascii_eq_ci(buf, "no")) return 0; + return 1; +} + +static DWORD env_get(const char* name, char* out, DWORD cap) +{ + DWORD n = GetEnvironmentVariableA(name, out, cap); + if (n == 0 || n >= cap) { + if (cap > 0) out[0] = '\0'; + return 0; + } + return n; +} + +static const char* basename_of(const char* path) +{ + const char* p = path; + const char* last = path; + while (*p) { + if (*p == '\\' || *p == '/') last = p + 1; + ++p; + } + return last; +} + +// Skip non-game host processes that fork/exec dozens of times during a +// Wine/Proton session. Hooking them costs more than it saves and +// occasionally breaks pipes (services.exe spawns PE helpers inline). +static int is_skipped_host_proc(const char* exe_basename) +{ + static const char* const skip[] = { + "services.exe", "plugplay.exe", "winedevice.exe", "rpcss.exe", + "explorer.exe", "svchost.exe", "conhost.exe", "wineboot.exe", + "winemenubuilder.exe", "tabtip.exe", "start.exe", "regedit.exe", + "rundll32.exe", "msiexec.exe", + NULL, + }; + for (size_t i = 0; skip[i] != NULL; ++i) { + if (ascii_eq_ci(exe_basename, skip[i])) return 1; + } + return 0; +} + +static void log_line(const char* buf, DWORD len) +{ + HANDLE h = GetStdHandle(STD_ERROR_HANDLE); + if (h != NULL && h != INVALID_HANDLE_VALUE) { + DWORD wrote = 0; + WriteFile(h, buf, len, &wrote, NULL); + } + + // Stderr can disappear under some Proton launch paths. Keep this + // kernel32-only fallback so Phase 1 can distinguish "not loaded" from + // "loaded, but the launch log swallowed stderr". + HANDLE f = CreateFileA("Z:\\tmp\\fluorine_vfs_appinit.log", + FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | + FILE_SHARE_DELETE, + NULL, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (f != INVALID_HANDLE_VALUE) { + DWORD wrote = 0; + WriteFile(f, buf, len, &wrote, NULL); + CloseHandle(f); + } +} + +static DWORD copy_str(char* dst, DWORD dst_pos, DWORD dst_cap, const char* src) +{ + while (*src && dst_pos + 1 < dst_cap) dst[dst_pos++] = *src++; + return dst_pos; +} + +#ifdef FLUORINE_VFS_PROXY_HID +static HMODULE real_hid(void) +{ + static HMODULE mod = NULL; + if (mod == NULL) { + mod = LoadLibraryA("C:\\windows\\system32\\hid.dll"); + } + return mod; +} + +static FARPROC real_hid_proc(const char* name) +{ + HMODULE mod = real_hid(); + if (mod == NULL) return NULL; + return GetProcAddress(mod, name); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_FreePreparsedData(void* preparsed) +{ + typedef BOOLEAN(WINAPI * Fn)(void*); + Fn fn = (Fn)real_hid_proc("HidD_FreePreparsedData"); + if (fn == NULL) return FALSE; + return fn(preparsed); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_GetAttributes(HANDLE file, void* attrs) +{ + typedef BOOLEAN(WINAPI * Fn)(HANDLE, void*); + Fn fn = (Fn)real_hid_proc("HidD_GetAttributes"); + if (fn == NULL) return FALSE; + return fn(file, attrs); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_GetFeature(HANDLE file, + void* report, + ULONG len) +{ + typedef BOOLEAN(WINAPI * Fn)(HANDLE, void*, ULONG); + Fn fn = (Fn)real_hid_proc("HidD_GetFeature"); + if (fn == NULL) return FALSE; + return fn(file, report, len); +} + +__declspec(dllexport) void WINAPI HidD_GetHidGuid(GUID* guid) +{ + typedef void(WINAPI * Fn)(GUID*); + Fn fn = (Fn)real_hid_proc("HidD_GetHidGuid"); + if (fn != NULL) fn(guid); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_GetManufacturerString(HANDLE file, + void* buf, + ULONG len) +{ + typedef BOOLEAN(WINAPI * Fn)(HANDLE, void*, ULONG); + Fn fn = (Fn)real_hid_proc("HidD_GetManufacturerString"); + if (fn == NULL) return FALSE; + return fn(file, buf, len); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_GetPreparsedData(HANDLE file, + void** preparsed) +{ + typedef BOOLEAN(WINAPI * Fn)(HANDLE, void**); + Fn fn = (Fn)real_hid_proc("HidD_GetPreparsedData"); + if (fn == NULL) return FALSE; + return fn(file, preparsed); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_GetProductString(HANDLE file, + void* buf, + ULONG len) +{ + typedef BOOLEAN(WINAPI * Fn)(HANDLE, void*, ULONG); + Fn fn = (Fn)real_hid_proc("HidD_GetProductString"); + if (fn == NULL) return FALSE; + return fn(file, buf, len); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_GetSerialNumberString(HANDLE file, + void* buf, + ULONG len) +{ + typedef BOOLEAN(WINAPI * Fn)(HANDLE, void*, ULONG); + Fn fn = (Fn)real_hid_proc("HidD_GetSerialNumberString"); + if (fn == NULL) return FALSE; + return fn(file, buf, len); +} + +__declspec(dllexport) BOOLEAN WINAPI HidD_SetFeature(HANDLE file, + void* report, + ULONG len) +{ + typedef BOOLEAN(WINAPI * Fn)(HANDLE, void*, ULONG); + Fn fn = (Fn)real_hid_proc("HidD_SetFeature"); + if (fn == NULL) return FALSE; + return fn(file, report, len); +} + +__declspec(dllexport) LONG WINAPI HidP_GetCaps(void* preparsed, void* caps) +{ + typedef LONG(WINAPI * Fn)(void*, void*); + Fn fn = (Fn)real_hid_proc("HidP_GetCaps"); + if (fn == NULL) return (LONG)0xc0110001; + return fn(preparsed, caps); +} + +__declspec(dllexport) LONG WINAPI HidP_GetValueCaps(int report_type, + void* caps, + ULONG* len, + void* preparsed) +{ + typedef LONG(WINAPI * Fn)(int, void*, ULONG*, void*); + Fn fn = (Fn)real_hid_proc("HidP_GetValueCaps"); + if (fn == NULL) return (LONG)0xc0110001; + return fn(report_type, caps, len, preparsed); +} +#endif + +BOOL APIENTRY DllMain(HINSTANCE hinst, DWORD reason, LPVOID reserved) +{ + (void)reserved; + if (reason == DLL_PROCESS_DETACH) { + fluorine_vfs_report_hooks(); + return TRUE; + } + if (reason != DLL_PROCESS_ATTACH) return TRUE; + DisableThreadLibraryCalls(hinst); + + if (env_truthy("FLUORINE_DISABLE_VFS_INJECT")) return TRUE; + + char proc_path[MAX_PATH]; + DWORD pn = GetModuleFileNameA(NULL, proc_path, (DWORD)sizeof(proc_path)); + if (pn == 0 || pn >= sizeof(proc_path)) { + proc_path[0] = '\0'; + } else { + proc_path[pn] = '\0'; + } + const char* exe = basename_of(proc_path); + + if (is_skipped_host_proc(exe)) return TRUE; + + char idx[1024]; + env_get("FLUORINE_VFS_INDEX", idx, (DWORD)sizeof(idx)); + char mount[1024]; + env_get("FLUORINE_VFS_MOUNT", mount, (DWORD)sizeof(mount)); + + // Log process/env wiring before hook startup. This also leaves a marker in + // /tmp/fluorine_vfs_appinit.log when Proton swallows stderr. + if (idx[0] != '\0' || env_truthy("FLUORINE_VFS_INJECT_DEBUG")) { + char line[2048]; + DWORD pos = 0; + pos = copy_str(line, pos, sizeof(line), "[fluorine_vfs] pid="); + // Format PID without calling into user32 (wsprintfA) — see header + // comment about loader-lock constraint when running from + // AppInit_DLLs. + char num[16]; + DWORD pid = GetCurrentProcessId(); + int ndigits = 0; + if (pid == 0) { + num[ndigits++] = '0'; + } else { + char rev[16]; + int rn = 0; + while (pid > 0 && rn < (int)sizeof(rev)) { + rev[rn++] = (char)('0' + (pid % 10)); + pid /= 10; + } + while (rn > 0) num[ndigits++] = rev[--rn]; + } + for (int i = 0; i < ndigits && pos + 1 < sizeof(line); ++i) line[pos++] = num[i]; + pos = copy_str(line, pos, sizeof(line), " proc='"); + pos = copy_str(line, pos, sizeof(line), exe[0] ? exe : "?"); + pos = copy_str(line, pos, sizeof(line), "' index='"); + pos = copy_str(line, pos, sizeof(line), idx[0] ? idx : "<unset>"); + pos = copy_str(line, pos, sizeof(line), "' mount='"); + pos = copy_str(line, pos, sizeof(line), mount[0] ? mount : "<unset>"); + pos = copy_str(line, pos, sizeof(line), "'\n"); + log_line(line, pos); + } + + if (idx[0] != '\0') { + fluorine_vfs_start_hooks(); + } + + return TRUE; +} diff --git a/src/src/vfs/vfsbridge.cpp b/src/src/vfs/vfsbridge.cpp new file mode 100644 index 0000000..c74259f --- /dev/null +++ b/src/src/vfs/vfsbridge.cpp @@ -0,0 +1,226 @@ +#include "vfsbridge.h" + +#include "../fluorinepaths.h" + +#include <algorithm> +#include <chrono> +#include <cstdint> +#include <cstdio> +#include <fstream> +#include <iomanip> +#include <ostream> +#include <sstream> +#include <system_error> + +namespace +{ +namespace fs = std::filesystem; + +uint64_t hash64(const std::string& s) +{ + uint64_t h = 1469598103934665603ULL; + for (unsigned char c : s) { + h ^= c; + h *= 1099511628211ULL; + } + return h; +} + +std::string jsonEscape(const std::string& s) +{ + std::ostringstream out; + out << '"'; + for (unsigned char c : s) { + switch (c) { + case '"': + out << "\\\""; + break; + case '\\': + out << "\\\\"; + break; + case '\b': + out << "\\b"; + break; + case '\f': + out << "\\f"; + break; + case '\n': + out << "\\n"; + break; + case '\r': + out << "\\r"; + break; + case '\t': + out << "\\t"; + break; + default: + if (c < 0x20) { + out << "\\u" << std::hex << std::setw(4) << std::setfill('0') + << static_cast<int>(c) << std::dec; + } else { + out << static_cast<char>(c); + } + break; + } + } + out << '"'; + return out.str(); +} + +int64_t timeToNs(std::chrono::system_clock::time_point t) +{ + return std::chrono::duration_cast<std::chrono::nanoseconds>( + t.time_since_epoch()) + .count(); +} + +int64_t nowToNs() +{ + return timeToNs(std::chrono::system_clock::now()); +} + +void writeMeta(std::ostream& out, const VfsTree& tree, + const std::string& data_dir, const std::string& overwrite_dir, + const std::string& mount_point) +{ + out << "{\"record\":\"meta\"," + << "\"schema\":\"fluorine-vfs-index-v1\"," + << "\"data_dir\":" << jsonEscape(data_dir) << ',' + << "\"overwrite_dir\":" << jsonEscape(overwrite_dir) << ',' + << "\"mount_point\":" << jsonEscape(mount_point) << ',' + << "\"file_count\":" << tree.file_count << ',' + << "\"dir_count\":" << tree.dir_count << ',' + << "\"export_time_ns\":" << nowToNs() << "}\n"; +} + +void writeEntry(std::ostream& out, const std::string& virtual_path, + const VfsNode& node) +{ + out << "{\"record\":\"entry\"," + << "\"type\":" << jsonEscape(node.is_directory ? "directory" : "file") + << ",\"virtual_path\":" << jsonEscape(virtual_path); + + if (!node.is_directory) { + const auto& fi = node.file_info; + out << ",\"real_path\":" << jsonEscape(fi.real_path) + << ",\"real_path_kind\":" + << jsonEscape(fi.is_backing ? "backing_relative" : "absolute") + << ",\"origin\":" << jsonEscape(fi.origin) + << ",\"size\":" << fi.size + << ",\"mtime_ns\":" << timeToNs(fi.mtime) + << ",\"is_backing\":" << (fi.is_backing ? "true" : "false"); + } + + out << "}\n"; +} + +std::size_t writeNode(std::ostream& out, const VfsNode& node, + const std::string& virtual_path) +{ + std::size_t written = 0; + if (!virtual_path.empty()) { + writeEntry(out, virtual_path, node); + ++written; + } + + if (!node.is_directory) { + return written; + } + + auto children = node.listChildren(); + std::sort(children.begin(), children.end(), + [](const auto& a, const auto& b) { + return normalizeForLookup(a.first) < normalizeForLookup(b.first); + }); + + for (const auto& [displayName, child] : children) { + if (child == nullptr) { + continue; + } + std::string childPath = virtual_path; + if (!childPath.empty()) { + childPath += '/'; + } + childPath += displayName; + written += writeNode(out, *child, childPath); + } + + return written; +} + +} // namespace + +fs::path vfsBridgeIndexPath( + const std::string& data_dir, const std::string& overwrite_dir, + const std::vector<std::pair<std::string, std::string>>& mods) +{ + std::string fp; + fp.reserve(4096); + fp += data_dir; + fp += '\0'; + fp += overwrite_dir; + fp += '\0'; + for (const auto& [name, path] : mods) { + fp += name; + fp += '\0'; + fp += path; + fp += '\0'; + } + + char buf[32]; + std::snprintf(buf, sizeof(buf), "%016llx.jsonl", + static_cast<unsigned long long>(hash64(fp))); + + return fs::path(fluorineVfsBridgeDir().toStdString()) / buf; +} + +VfsBridgeExportResult exportVfsBridgeIndex( + const VfsTree& tree, const fs::path& path, const std::string& data_dir, + const std::string& overwrite_dir, const std::string& mount_point) +{ + VfsBridgeExportResult result; + result.path = path; + + std::error_code ec; + fs::create_directories(path.parent_path(), ec); + if (ec) { + result.error = ec.message(); + return result; + } + + fs::path tmp = path; + tmp += ".tmp"; + + { + std::ofstream out(tmp, std::ios::trunc); + if (!out) { + result.error = "failed to open temporary index file"; + return result; + } + + writeMeta(out, tree, data_dir, overwrite_dir, mount_point); + result.records_written = writeNode(out, tree.root, {}); + + out.flush(); + if (!out.good()) { + result.error = "failed while writing index file"; + fs::remove(tmp, ec); + return result; + } + } + + fs::rename(tmp, path, ec); + if (ec) { + fs::remove(path, ec); + ec.clear(); + fs::rename(tmp, path, ec); + if (ec) { + result.error = ec.message(); + fs::remove(tmp, ec); + return result; + } + } + + result.ok = true; + return result; +} diff --git a/src/src/vfs/vfsbridge.h b/src/src/vfs/vfsbridge.h new file mode 100644 index 0000000..34b6880 --- /dev/null +++ b/src/src/vfs/vfsbridge.h @@ -0,0 +1,28 @@ +#ifndef VFS_VFSBRIDGE_H +#define VFS_VFSBRIDGE_H + +#include "vfstree.h" + +#include <filesystem> +#include <string> +#include <utility> +#include <vector> + +struct VfsBridgeExportResult +{ + bool ok = false; + std::filesystem::path path; + std::size_t records_written = 0; + std::string error; +}; + +std::filesystem::path vfsBridgeIndexPath( + const std::string& data_dir, const std::string& overwrite_dir, + const std::vector<std::pair<std::string, std::string>>& mods); + +VfsBridgeExportResult exportVfsBridgeIndex( + const VfsTree& tree, const std::filesystem::path& path, + const std::string& data_dir, const std::string& overwrite_dir, + const std::string& mount_point); + +#endif // VFS_VFSBRIDGE_H diff --git a/src/src/vfs/vfsbridgepreload.c b/src/src/vfs/vfsbridgepreload.c new file mode 100644 index 0000000..daaa5d9 --- /dev/null +++ b/src/src/vfs/vfsbridgepreload.c @@ -0,0 +1,1872 @@ +#define _GNU_SOURCE + +#include <ctype.h> +#include <dirent.h> +#include <dlfcn.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <linux/stat.h> +#include <pthread.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <unistd.h> + +typedef struct Entry +{ + char* key; + char* real_path; + int directory; + int backing; + uint64_t size; + int64_t mtime_ns; + uint32_t child_start; + uint32_t child_count; +} Entry; + +static Entry* g_entries = NULL; +static size_t g_entry_count = 0; +static size_t g_entry_cap = 0; +static uint32_t* g_children = NULL; +static size_t g_children_count = 0; +static char* g_mount_point = NULL; +static char* g_data_dir = NULL; +static int g_loaded = 0; +static int g_load_failed = 0; +static int g_debug = 0; +static int g_stats = 0; +// Directory synthesis (libc opendir + open(O_DIRECTORY) → synthetic FlDir fd). +// Default ON. Set FLUORINE_VFS_PRELOAD_DIRS=0 to disable when debugging Wine +// readdir issues (currently NTDLL bypasses our syscall() hook for getdents64 +// so synthetic dirs read empty there). +static int g_dirs_enabled = 1; +static pthread_once_t g_once = PTHREAD_ONCE_INIT; +static __thread int g_inside = 0; +static uint64_t g_resolve_hits = 0; +static uint64_t g_resolve_misses = 0; +static uint64_t g_open_hits = 0; +static uint64_t g_open_fallbacks = 0; +static uint64_t g_stat_hits = 0; +static uint64_t g_access_hits = 0; +static uint64_t g_open_calls = 0; +static uint64_t g_stat_calls = 0; +static uint64_t g_access_calls = 0; +static uint64_t g_statx_calls = 0; +static uint64_t g_syscall_calls = 0; +static uint64_t g_path_mismatches = 0; +static uint64_t g_path_samples = 0; +static uint64_t g_canon_attempts = 0; +static uint64_t g_canon_mount_hits = 0; +static uint64_t g_canon_outside = 0; +static uint64_t g_canon_failed = 0; +static uint64_t g_opendir_calls = 0; +static uint64_t g_opendir_hits = 0; +static uint64_t g_readdir_calls = 0; +static uint64_t g_readdir_hits = 0; +static uint64_t g_getdents_calls = 0; +static uint64_t g_getdents_hits = 0; +// libc getdents64() symbol path. Wine 9+ / glibc 2.30+ may call this directly +// instead of going through syscall(SYS_getdents64, ...). Keeping the counters +// separate so we can tell which path NTDLL actually uses. +static uint64_t g_getdents_libc_calls = 0; +static uint64_t g_getdents_libc_hits = 0; + +#define FL_DIR_MAGIC ((uint64_t)0xF10DD1ECDEADC0DEULL) + +typedef struct FlDir +{ + uint64_t magic; // first field: type discrimination + size_t pos; + size_t entry_idx; // directory entry in g_entries + int real_fd; // backing /dev/null fd or -1 + struct dirent dirent_buf; + struct dirent64 dirent64_buf; +} FlDir; + +// fd → FlDir* map for getdents64 syscall hook. Open-addressed linear-probe +// hash table. Sized at first insert; bounded by /proc/sys/fs/file-max but in +// practice this stays under ~50 entries per process during a launch. +typedef struct FdSlot +{ + int fd; // -1 = empty + FlDir* dir; +} FdSlot; + +static FdSlot* g_fd_map = NULL; +static size_t g_fd_map_cap = 0; +static size_t g_fd_map_count = 0; +static pthread_mutex_t g_fd_map_mutex = PTHREAD_MUTEX_INITIALIZER; + +static void ensure_loaded(void); + +static void* next_symbol(const char* name) +{ + return dlsym(RTLD_NEXT, name); +} + +static int flag_enabled(const char* name) +{ + const char* v = getenv(name); + if (v == NULL) return 0; + return strcmp(v, "1") == 0 || strcasecmp(v, "true") == 0 || + strcasecmp(v, "yes") == 0; +} + +static char* xstrdup_range(const char* start, size_t len) +{ + char* out = (char*)malloc(len + 1); + if (out == NULL) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +static char* trim_trailing_slash_dup(const char* s) +{ + if (s == NULL) return NULL; + size_t len = strlen(s); + while (len > 1 && s[len - 1] == '/') --len; + return xstrdup_range(s, len); +} + +static char* normalized_slashes_dup(const char* s) +{ + char* out = trim_trailing_slash_dup(s); + if (out == NULL) return NULL; + for (char* p = out; *p; ++p) { + if (*p == '\\') *p = '/'; + } + while (out[0] == '/' && out[1] == '/') { + memmove(out, out + 1, strlen(out)); + } + return out; +} + +static char* wine_dos_path_to_unix_dup(const char* s) +{ + if (s == NULL) return NULL; + const char* p = s; + if (strncmp(p, "\\\\??\\", 5) == 0) p += 5; + if (strncmp(p, "\\??\\", 4) == 0) p += 4; + if (strncmp(p, "\\\\?\\", 4) == 0) p += 4; + + if (isalpha((unsigned char)p[0]) && p[1] == ':') { + const char drive = (char)tolower((unsigned char)p[0]); + p += 2; + while (*p == '\\' || *p == '/') ++p; + + if (drive == 'z') { + const size_t len = strlen(p) + 2; + char* out = (char*)malloc(len); + if (out == NULL) return NULL; + out[0] = '/'; + strcpy(out + 1, p); + for (char* c = out; *c; ++c) { + if (*c == '\\') *c = '/'; + } + while (strlen(out) > 1 && out[strlen(out) - 1] == '/') { + out[strlen(out) - 1] = '\0'; + } + return out; + } + } + + return NULL; +} + +static void lowercase_vfs_path(char* s) +{ + char* r = s; + char* w = s; + while (*r == '/') ++r; + while (*r) { + char c = *r++; + if (c == '\\') c = '/'; + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + *w++ = c; + } + while (w > s && w[-1] == '/') --w; + *w = '\0'; +} + +static char* json_string_field(const char* line, const char* key) +{ + char needle[128]; + snprintf(needle, sizeof(needle), "\"%s\":", key); + const char* p = strstr(line, needle); + if (p == NULL) return NULL; + p = strchr(p + strlen(needle), '"'); + if (p == NULL) return NULL; + ++p; + + size_t cap = strlen(p) + 1; + char* out = (char*)malloc(cap); + if (out == NULL) return NULL; + size_t n = 0; + + while (*p) { + char c = *p++; + if (c == '"') { + out[n] = '\0'; + return out; + } + if (c == '\\' && *p) { + char e = *p++; + switch (e) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = e; break; + } + } + out[n++] = c; + } + + free(out); + return NULL; +} + +static int json_int_field(const char* line, const char* key, int64_t* out) +{ + char needle[128]; + snprintf(needle, sizeof(needle), "\"%s\":", key); + const char* p = strstr(line, needle); + if (p == NULL) return 0; + p += strlen(needle); + char* end = NULL; + long long v = strtoll(p, &end, 10); + if (end == p) return 0; + *out = (int64_t)v; + return 1; +} + +static int json_bool_field(const char* line, const char* key) +{ + char needle[128]; + snprintf(needle, sizeof(needle), "\"%s\":", key); + const char* p = strstr(line, needle); + if (p == NULL) return 0; + p += strlen(needle); + return strncmp(p, "true", 4) == 0; +} + +static int add_entry(Entry e) +{ + if (g_entry_count == g_entry_cap) { + size_t next_cap = g_entry_cap == 0 ? 4096 : g_entry_cap * 2; + Entry* next = (Entry*)realloc(g_entries, next_cap * sizeof(Entry)); + if (next == NULL) return 0; + g_entries = next; + g_entry_cap = next_cap; + } + g_entries[g_entry_count++] = e; + return 1; +} + +static int entry_cmp(const void* a, const void* b) +{ + const Entry* ea = (const Entry*)a; + const Entry* eb = (const Entry*)b; + return strcmp(ea->key, eb->key); +} + +static Entry* find_entry(const char* key) +{ + Entry probe; + probe.key = (char*)key; + return (Entry*)bsearch(&probe, g_entries, g_entry_count, sizeof(Entry), entry_cmp); +} + +// Returns index in g_entries of the parent of `key`, or SIZE_MAX if none. +// Only valid after qsort. `key` must not contain trailing slash. +static size_t parent_index(const char* key) +{ + if (key == NULL || key[0] == '\0') return SIZE_MAX; + const char* slash = strrchr(key, '/'); + if (slash == NULL) { + // Top-level key: parent is root entry "". + Entry probe; + probe.key = (char*)""; + Entry* p = (Entry*)bsearch(&probe, g_entries, g_entry_count, sizeof(Entry), entry_cmp); + if (p == NULL) return SIZE_MAX; + return (size_t)(p - g_entries); + } + size_t parent_len = (size_t)(slash - key); + char* parent_key = (char*)malloc(parent_len + 1); + if (parent_key == NULL) return SIZE_MAX; + memcpy(parent_key, key, parent_len); + parent_key[parent_len] = '\0'; + + Entry probe; + probe.key = parent_key; + Entry* p = (Entry*)bsearch(&probe, g_entries, g_entry_count, sizeof(Entry), entry_cmp); + free(parent_key); + if (p == NULL) return SIZE_MAX; + return (size_t)(p - g_entries); +} + +static void build_child_index(void) +{ + if (g_entry_count == 0) return; + + // Pass 1: count children per parent. + for (size_t i = 0; i < g_entry_count; ++i) { + size_t pi = parent_index(g_entries[i].key); + if (pi == SIZE_MAX) continue; + ++g_entries[pi].child_count; + } + + // Pass 2: prefix-sum to compute child_start. + size_t total = 0; + for (size_t i = 0; i < g_entry_count; ++i) { + g_entries[i].child_start = (uint32_t)total; + total += g_entries[i].child_count; + g_entries[i].child_count = 0; // refilled in pass 3 + } + + if (total == 0) return; + g_children = (uint32_t*)malloc(total * sizeof(uint32_t)); + if (g_children == NULL) { + g_load_failed = 1; + return; + } + g_children_count = total; + + // Pass 3: append child indices. + for (size_t i = 0; i < g_entry_count; ++i) { + size_t pi = parent_index(g_entries[i].key); + if (pi == SIZE_MAX) continue; + Entry* parent = &g_entries[pi]; + g_children[parent->child_start + parent->child_count++] = (uint32_t)i; + } +} + +static void load_index(void) +{ + g_inside = 1; + g_debug = flag_enabled("FLUORINE_VFS_BRIDGE_DEBUG"); + g_stats = flag_enabled("FLUORINE_VFS_PRELOAD_STATS"); + // Default-on: disable only when env explicitly says 0/false/no. + const char* dirs_env = getenv("FLUORINE_VFS_PRELOAD_DIRS"); + if (dirs_env != NULL && + (strcmp(dirs_env, "0") == 0 || strcasecmp(dirs_env, "false") == 0 || + strcasecmp(dirs_env, "no") == 0)) { + g_dirs_enabled = 0; + } + + const char* index = getenv("FLUORINE_VFS_INDEX"); + if (index == NULL || index[0] == '\0') { + g_inside = 0; + return; + } + + FILE* f = fopen(index, "r"); + if (f == NULL) { + g_load_failed = 1; + g_inside = 0; + return; + } + + char* line = NULL; + size_t line_cap = 0; + while (getline(&line, &line_cap, f) > 0) { + char* record = json_string_field(line, "record"); + if (record == NULL) continue; + + if (strcmp(record, "meta") == 0) { + char* mount = json_string_field(line, "mount_point"); + char* data = json_string_field(line, "data_dir"); + free(g_mount_point); + free(g_data_dir); + g_mount_point = trim_trailing_slash_dup(mount); + g_data_dir = trim_trailing_slash_dup(data); + free(mount); + free(data); + free(record); + continue; + } + + if (strcmp(record, "entry") != 0) { + free(record); + continue; + } + free(record); + + char* type = json_string_field(line, "type"); + char* key = json_string_field(line, "virtual_path"); + if (type == NULL || key == NULL) { + free(type); + free(key); + continue; + } + lowercase_vfs_path(key); + + Entry e; + memset(&e, 0, sizeof(e)); + e.key = key; + e.directory = strcmp(type, "directory") == 0; + free(type); + + if (!e.directory) { + int64_t size = 0; + e.real_path = json_string_field(line, "real_path"); + e.backing = json_bool_field(line, "is_backing"); + if (json_int_field(line, "size", &size) && size >= 0) { + e.size = (uint64_t)size; + } + json_int_field(line, "mtime_ns", &e.mtime_ns); + } + + if (!add_entry(e)) { + free(e.key); + free(e.real_path); + g_load_failed = 1; + break; + } + } + + free(line); + fclose(f); + + if (g_mount_point != NULL) { + Entry root; + memset(&root, 0, sizeof(root)); + root.key = strdup(""); + root.directory = 1; + if (root.key != NULL) add_entry(root); + } + + qsort(g_entries, g_entry_count, sizeof(Entry), entry_cmp); + build_child_index(); + g_loaded = g_mount_point != NULL && !g_load_failed; + if (g_debug) { + fprintf(stderr, + "[fluorine-vfs-preload] pid=%ld loaded=%d entries=%zu children=%zu index='%s' mount='%s'\n", + (long)getpid(), g_loaded, g_entry_count, g_children_count, index, + g_mount_point ? g_mount_point : ""); + } + g_inside = 0; +} + +static void ensure_loaded(void) +{ + pthread_once(&g_once, load_index); +} + +static void sample_path_mismatch(const char* reason, int dirfd, const char* raw, + const char* clean, const char* canon) +{ + __sync_fetch_and_add(&g_path_mismatches, 1); + if (!g_stats && !g_debug) return; + const uint64_t slot = __sync_fetch_and_add(&g_path_samples, 1); + if (slot >= 24) return; + + fprintf(stderr, + "[fluorine-vfs-preload] path_miss pid=%ld reason=%s dirfd=%d raw='%s' clean='%s' canon='%s' mount='%s'\n", + (long)getpid(), reason ? reason : "", dirfd, + raw ? raw : "", clean ? clean : "", canon ? canon : "", + g_mount_point ? g_mount_point : ""); +} + +// realpath() existing prefix; preserve trailing suffix that does not yet exist. +// Returns malloc'd canonical path or NULL when no parent component resolves. +static char* canonicalize_existing_prefix_dup(const char* path) +{ + if (path == NULL || path[0] != '/') return NULL; + + char* canon = realpath(path, NULL); + if (canon != NULL) return canon; + + size_t len = strlen(path); + char* work = (char*)malloc(len + 1); + if (work == NULL) return NULL; + memcpy(work, path, len + 1); + + for (;;) { + char* slash = strrchr(work, '/'); + if (slash == NULL || slash == work) { + free(work); + return NULL; + } + size_t suffix_off = (size_t)(slash - work); // includes leading '/' + *slash = '\0'; + + canon = realpath(work, NULL); + if (canon != NULL) { + size_t parent_len = strlen(canon); + size_t suffix_len = len - suffix_off; + char* out = (char*)malloc(parent_len + suffix_len + 1); + if (out != NULL) { + memcpy(out, canon, parent_len); + memcpy(out + parent_len, path + suffix_off, suffix_len); + out[parent_len + suffix_len] = '\0'; + } + free(canon); + free(work); + return out; + } + // continue walking up + } +} + +static int starts_with_mount(const char* abs) +{ + if (abs == NULL || g_mount_point == NULL) return 0; + size_t mp_len = strlen(g_mount_point); + if (strncmp(abs, g_mount_point, mp_len) != 0) return 0; + return abs[mp_len] == '\0' || abs[mp_len] == '/'; +} + +// Skip canonicalization for paths that obviously cannot reach the mount, and +// avoid burning a realpath() syscall per stat for system libs / Wine internals. +static int looks_like_system_path(const char* abs) +{ + if (abs == NULL) return 1; + static const char* const skip[] = { + "/proc/", "/sys/", "/dev/", "/run/", "/tmp/", "/usr/", + "/lib/", "/lib64/", "/etc/", "/var/", "/opt/", "/bin/", + "/sbin/", "/boot/", + }; + for (size_t i = 0; i < sizeof(skip) / sizeof(skip[0]); ++i) { + size_t n = strlen(skip[i]); + if (strncmp(abs, skip[i], n) == 0) return 1; + } + return 0; +} + +static char* absolute_path_from_dirfd(int dirfd, const char* path) +{ + if (path == NULL) return NULL; + + char* wine = wine_dos_path_to_unix_dup(path); + if (wine != NULL) return wine; + + if (path[0] == '/' || path[0] == '\\') return normalized_slashes_dup(path); + + char base[4096]; + if (dirfd == AT_FDCWD) { + if (getcwd(base, sizeof(base)) == NULL) return NULL; + } else { + char proc[64]; + snprintf(proc, sizeof(proc), "/proc/self/fd/%d", dirfd); + ssize_t n = readlink(proc, base, sizeof(base) - 1); + if (n < 0) return NULL; + base[n] = '\0'; + } + + size_t len = strlen(base) + 1 + strlen(path) + 1; + char* out = (char*)malloc(len); + if (out == NULL) return NULL; + snprintf(out, len, "%s/%s", base, path); + char* trimmed = normalized_slashes_dup(out); + free(out); + return trimmed; +} + +static int rel_from_mount_at(int dirfd, const char* path, char** rel) +{ + if (path == NULL) return 0; + ensure_loaded(); + if (!g_loaded || g_mount_point == NULL) return 0; + + char* clean = absolute_path_from_dirfd(dirfd, path); + if (clean == NULL) { + sample_path_mismatch("normalize-failed", dirfd, path, NULL, NULL); + return 0; + } + + // Fast path: clean already under mount. + const char* abs = NULL; + char* canon = NULL; + + if (starts_with_mount(clean)) { + abs = clean; + } else if (!looks_like_system_path(clean)) { + __sync_fetch_and_add(&g_canon_attempts, 1); + canon = canonicalize_existing_prefix_dup(clean); + if (canon == NULL) { + __sync_fetch_and_add(&g_canon_failed, 1); + sample_path_mismatch("canon-failed", dirfd, path, clean, NULL); + free(clean); + return 0; + } + if (starts_with_mount(canon)) { + __sync_fetch_and_add(&g_canon_mount_hits, 1); + abs = canon; + } else { + __sync_fetch_and_add(&g_canon_outside, 1); + sample_path_mismatch("outside-mount", dirfd, path, clean, canon); + free(clean); + free(canon); + return 0; + } + } else { + // System paths flood every process; count, do not sample. + __sync_fetch_and_add(&g_path_mismatches, 1); + free(clean); + return 0; + } + + size_t mp_len = strlen(g_mount_point); + if (abs[mp_len] == '\0') { + free(clean); + free(canon); + *rel = strdup(""); + return *rel != NULL; + } + + char* out = strdup(abs + mp_len + 1); + free(clean); + free(canon); + if (out == NULL) return 0; + lowercase_vfs_path(out); + *rel = out; + return 1; +} + +static Entry* resolve_entry_at(int dirfd, const char* path) +{ + char* rel = NULL; + if (!rel_from_mount_at(dirfd, path, &rel)) return NULL; + Entry* e = find_entry(rel); + free(rel); + if (e == NULL) { + __sync_fetch_and_add(&g_resolve_misses, 1); + return NULL; + } + __sync_fetch_and_add(&g_resolve_hits, 1); + return e; +} + +static Entry* resolve_entry(const char* path) +{ + return resolve_entry_at(AT_FDCWD, path); +} + +static char* resolved_real_path(const Entry* e) +{ + if (e == NULL || e->directory || e->real_path == NULL) return NULL; + if (!e->backing || g_data_dir == NULL) return strdup(e->real_path); + + size_t len = strlen(g_data_dir) + 1 + strlen(e->real_path) + 1; + char* out = (char*)malloc(len); + if (out == NULL) return NULL; + snprintf(out, len, "%s/%s", g_data_dir, e->real_path); + return out; +} + +static int read_only_flags(int flags) +{ + return (flags & O_ACCMODE) == O_RDONLY && + (flags & (O_CREAT | O_TRUNC | O_APPEND)) == 0; +} + +static void fill_synthetic_stat(struct stat* st, const Entry* e) +{ + memset(st, 0, sizeof(*st)); + st->st_uid = getuid(); + st->st_gid = getgid(); + st->st_nlink = e->directory ? 2 : 1; + st->st_mode = e->directory ? (S_IFDIR | 0755) : (S_IFREG | 0644); + st->st_size = (off_t)e->size; + st->st_mtim.tv_sec = e->mtime_ns / 1000000000LL; + st->st_mtim.tv_nsec = e->mtime_ns % 1000000000LL; + st->st_atim = st->st_mtim; + st->st_ctim = st->st_mtim; +} + +static void fill_synthetic_statx(struct statx* stx, const Entry* e) +{ + memset(stx, 0, sizeof(*stx)); + stx->stx_mask = STATX_TYPE | STATX_MODE | STATX_NLINK | STATX_UID | + STATX_GID | STATX_SIZE | STATX_MTIME | STATX_ATIME | + STATX_CTIME; + stx->stx_uid = getuid(); + stx->stx_gid = getgid(); + stx->stx_nlink = e->directory ? 2 : 1; + stx->stx_mode = e->directory ? (S_IFDIR | 0755) : (S_IFREG | 0644); + stx->stx_size = e->size; + stx->stx_mtime.tv_sec = e->mtime_ns / 1000000000LL; + stx->stx_mtime.tv_nsec = e->mtime_ns % 1000000000LL; + stx->stx_atime = stx->stx_mtime; + stx->stx_ctime = stx->stx_mtime; +} + +// ---- Directory listing path --------------------------------------------- + +// Last component of a virtual key. "a/b/c" -> "c"; root key "" -> "". +static const char* basename_of(const char* key) +{ + const char* slash = strrchr(key, '/'); + return slash != NULL ? slash + 1 : key; +} + +static FlDir* fl_dir_alloc(size_t entry_idx) +{ + FlDir* dir = (FlDir*)calloc(1, sizeof(FlDir)); + if (dir == NULL) return NULL; + dir->magic = FL_DIR_MAGIC; + dir->entry_idx = entry_idx; + dir->real_fd = -1; + return dir; +} + +static int is_fl_dir(const void* p) +{ + if (p == NULL) return 0; + uint64_t magic; + memcpy(&magic, p, sizeof(magic)); + return magic == FL_DIR_MAGIC; +} + +static void fd_map_register(int fd, FlDir* dir) +{ + if (fd < 0 || dir == NULL) return; + pthread_mutex_lock(&g_fd_map_mutex); + if (g_fd_map_count + 1 >= (g_fd_map_cap >> 1)) { + size_t new_cap = g_fd_map_cap == 0 ? 32 : g_fd_map_cap * 2; + FdSlot* new_map = (FdSlot*)calloc(new_cap, sizeof(FdSlot)); + if (new_map == NULL) { + pthread_mutex_unlock(&g_fd_map_mutex); + return; + } + for (size_t i = 0; i < new_cap; ++i) new_map[i].fd = -1; + for (size_t i = 0; i < g_fd_map_cap; ++i) { + if (g_fd_map[i].fd < 0) continue; + size_t h = ((size_t)g_fd_map[i].fd * 2654435761u) & (new_cap - 1); + while (new_map[h].fd >= 0) h = (h + 1) & (new_cap - 1); + new_map[h] = g_fd_map[i]; + } + free(g_fd_map); + g_fd_map = new_map; + g_fd_map_cap = new_cap; + } + size_t h = ((size_t)fd * 2654435761u) & (g_fd_map_cap - 1); + while (g_fd_map[h].fd >= 0 && g_fd_map[h].fd != fd) { + h = (h + 1) & (g_fd_map_cap - 1); + } + if (g_fd_map[h].fd < 0) ++g_fd_map_count; + g_fd_map[h].fd = fd; + g_fd_map[h].dir = dir; + pthread_mutex_unlock(&g_fd_map_mutex); +} + +static FlDir* fd_map_lookup(int fd) +{ + if (fd < 0 || g_fd_map_cap == 0) return NULL; + pthread_mutex_lock(&g_fd_map_mutex); + size_t h = ((size_t)fd * 2654435761u) & (g_fd_map_cap - 1); + FlDir* found = NULL; + for (size_t probes = 0; probes < g_fd_map_cap; ++probes) { + if (g_fd_map[h].fd < 0) break; + if (g_fd_map[h].fd == fd) { found = g_fd_map[h].dir; break; } + h = (h + 1) & (g_fd_map_cap - 1); + } + pthread_mutex_unlock(&g_fd_map_mutex); + return found; +} + +static FlDir* fd_map_take(int fd) +{ + if (fd < 0 || g_fd_map_cap == 0) return NULL; + pthread_mutex_lock(&g_fd_map_mutex); + size_t h = ((size_t)fd * 2654435761u) & (g_fd_map_cap - 1); + FlDir* found = NULL; + for (size_t probes = 0; probes < g_fd_map_cap; ++probes) { + if (g_fd_map[h].fd < 0) break; + if (g_fd_map[h].fd == fd) { + found = g_fd_map[h].dir; + g_fd_map[h].fd = -1; + g_fd_map[h].dir = NULL; + --g_fd_map_count; + // Re-insert any clustered entries to repair probe chain. + size_t j = (h + 1) & (g_fd_map_cap - 1); + while (g_fd_map[j].fd >= 0) { + FdSlot moved = g_fd_map[j]; + g_fd_map[j].fd = -1; + g_fd_map[j].dir = NULL; + --g_fd_map_count; + size_t k = ((size_t)moved.fd * 2654435761u) & (g_fd_map_cap - 1); + while (g_fd_map[k].fd >= 0) k = (k + 1) & (g_fd_map_cap - 1); + g_fd_map[k] = moved; + ++g_fd_map_count; + j = (j + 1) & (g_fd_map_cap - 1); + } + break; + } + h = (h + 1) & (g_fd_map_cap - 1); + } + pthread_mutex_unlock(&g_fd_map_mutex); + return found; +} + +static FlDir* try_open_synthetic_dir(const char* path) +{ + ensure_loaded(); + if (!g_loaded) return NULL; + Entry* e = resolve_entry(path); + if (e == NULL || !e->directory) return NULL; + size_t idx = (size_t)(e - g_entries); + return fl_dir_alloc(idx); +} + +static FlDir* try_open_synthetic_dir_at(int dirfd, const char* path) +{ + ensure_loaded(); + if (!g_loaded) return NULL; + Entry* e = resolve_entry_at(dirfd, path); + if (e == NULL || !e->directory) return NULL; + size_t idx = (size_t)(e - g_entries); + return fl_dir_alloc(idx); +} + +// /dev/null fd to back DIR* when callers ask for dirfd(). +static int open_devnull_fd(void) +{ + int (*real_open)(const char*, int, ...) = + (int (*)(const char*, int, ...))next_symbol("open"); + if (real_open == NULL) return -1; + g_inside = 1; + int fd = real_open("/dev/null", O_RDONLY | O_CLOEXEC, 0); + g_inside = 0; + return fd; +} + +static unsigned char dirent_type_for(const Entry* child) +{ + return child->directory ? DT_DIR : DT_REG; +} + +// Returns 1 if a record was filled, 0 when we're past the end. +static int fl_dir_next_dirent(FlDir* dir, struct dirent* out) +{ + if (dir == NULL) return 0; + Entry* parent = &g_entries[dir->entry_idx]; + size_t total = (size_t)parent->child_count; + // Synthetic '.' at pos 0, '..' at pos 1, then children. + while (dir->pos < 2 + total) { + size_t p = dir->pos++; + memset(out, 0, sizeof(*out)); + out->d_off = (off_t)dir->pos; + out->d_reclen = sizeof(struct dirent); + if (p == 0) { + out->d_ino = 1; + out->d_type = DT_DIR; + strcpy(out->d_name, "."); + return 1; + } + if (p == 1) { + out->d_ino = 1; + out->d_type = DT_DIR; + strcpy(out->d_name, ".."); + return 1; + } + uint32_t cidx = g_children[parent->child_start + (p - 2)]; + Entry* child = &g_entries[cidx]; + const char* name = basename_of(child->key); + size_t nlen = strlen(name); + if (nlen >= sizeof(out->d_name)) continue; // skip oversize + out->d_ino = (ino_t)(cidx + 2); + out->d_type = dirent_type_for(child); + memcpy(out->d_name, name, nlen + 1); + return 1; + } + return 0; +} + +static int fl_dir_next_dirent64(FlDir* dir, struct dirent64* out) +{ + if (dir == NULL) return 0; + Entry* parent = &g_entries[dir->entry_idx]; + size_t total = (size_t)parent->child_count; + while (dir->pos < 2 + total) { + size_t p = dir->pos++; + memset(out, 0, sizeof(*out)); + out->d_off = (off_t)dir->pos; + out->d_reclen = sizeof(struct dirent64); + if (p == 0) { + out->d_ino = 1; + out->d_type = DT_DIR; + strcpy(out->d_name, "."); + return 1; + } + if (p == 1) { + out->d_ino = 1; + out->d_type = DT_DIR; + strcpy(out->d_name, ".."); + return 1; + } + uint32_t cidx = g_children[parent->child_start + (p - 2)]; + Entry* child = &g_entries[cidx]; + const char* name = basename_of(child->key); + size_t nlen = strlen(name); + if (nlen >= sizeof(out->d_name)) continue; + out->d_ino = (ino64_t)(cidx + 2); + out->d_type = dirent_type_for(child); + memcpy(out->d_name, name, nlen + 1); + return 1; + } + return 0; +} + +// Pack getdents64-style records (struct linux_dirent64). The kernel layout +// matches glibc struct dirent64. Returns bytes written. +static long fl_dir_fill_getdents64(FlDir* dir, void* buf, size_t buf_len) +{ + if (dir == NULL) return 0; + Entry* parent = &g_entries[dir->entry_idx]; + size_t total = (size_t)parent->child_count; + + size_t off = 0; + char* out = (char*)buf; + + while (dir->pos < 2 + total) { + size_t p = dir->pos; + const char* name; + char dot[] = "."; + char dotdot[] = ".."; + Entry* child = NULL; + uint64_t ino = 1; + unsigned char dtype = DT_DIR; + + if (p == 0) { + name = dot; + } else if (p == 1) { + name = dotdot; + } else { + uint32_t cidx = g_children[parent->child_start + (p - 2)]; + child = &g_entries[cidx]; + name = basename_of(child->key); + ino = (uint64_t)(cidx + 2); + dtype = dirent_type_for(child); + } + + size_t nlen = strlen(name); + // struct linux_dirent64 layout: u64 ino, s64 off, u16 reclen, u8 type, char[] name (NUL). + // Aligned to 8. + size_t reclen = 19 + nlen + 1; + reclen = (reclen + 7u) & ~(size_t)7u; + if (off + reclen > buf_len) { + if (off == 0) { + errno = EINVAL; + return -1; + } + break; + } + char* rec = out + off; + memset(rec, 0, reclen); + memcpy(rec, &ino, 8); + int64_t doff = (int64_t)(p + 1); + memcpy(rec + 8, &doff, 8); + uint16_t rl = (uint16_t)reclen; + memcpy(rec + 16, &rl, 2); + rec[18] = (char)dtype; + memcpy(rec + 19, name, nlen + 1); + off += reclen; + ++dir->pos; + } + return (long)off; +} + +// Synthesize a /dev/null-backed fd for an already-resolved directory entry. +// Returns the fd, or -1 on failure. The fd is registered in g_fd_map so +// getdents64 syscalls and fdopendir() can find the FlDir state. +static int synthesize_dir_fd_for(const Entry* e) +{ + if (e == NULL || !e->directory) return -1; + FlDir* dir = fl_dir_alloc((size_t)(e - g_entries)); + if (dir == NULL) return -1; + int fd = open_devnull_fd(); + if (fd < 0) { + free(dir); + return -1; + } + dir->real_fd = fd; + fd_map_register(fd, dir); + __sync_fetch_and_add(&g_opendir_hits, 1); + return fd; +} + +DIR* opendir(const char* name) +{ + __sync_fetch_and_add(&g_opendir_calls, 1); + if (g_dirs_enabled && !g_inside && name != NULL) { + ensure_loaded(); + if (g_loaded) { + Entry* e = resolve_entry(name); + if (e != NULL && e->directory) { + // Allocate FlDir + register backing fd, then return the FlDir cast as + // DIR*. dirfd() will then return the registered /dev/null fd. + FlDir* dir = fl_dir_alloc((size_t)(e - g_entries)); + if (dir != NULL) { + dir->real_fd = open_devnull_fd(); + if (dir->real_fd >= 0) fd_map_register(dir->real_fd, dir); + __sync_fetch_and_add(&g_opendir_hits, 1); + return (DIR*)dir; + } + } + } + } + DIR* (*real_opendir)(const char*) = + (DIR* (*)(const char*))next_symbol("opendir"); + return real_opendir(name); +} + +DIR* fdopendir(int fd) +{ + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + __sync_fetch_and_add(&g_opendir_hits, 1); + return (DIR*)dir; + } + DIR* (*real_fdopendir)(int) = (DIR* (*)(int))next_symbol("fdopendir"); + return real_fdopendir(fd); +} + +struct dirent* readdir(DIR* dir) +{ + __sync_fetch_and_add(&g_readdir_calls, 1); + if (is_fl_dir(dir)) { + FlDir* fl = (FlDir*)dir; + if (fl_dir_next_dirent(fl, &fl->dirent_buf)) { + __sync_fetch_and_add(&g_readdir_hits, 1); + return &fl->dirent_buf; + } + errno = 0; + return NULL; + } + struct dirent* (*real_readdir)(DIR*) = + (struct dirent* (*)(DIR*))next_symbol("readdir"); + return real_readdir(dir); +} + +struct dirent64* readdir64(DIR* dir) +{ + __sync_fetch_and_add(&g_readdir_calls, 1); + if (is_fl_dir(dir)) { + FlDir* fl = (FlDir*)dir; + if (fl_dir_next_dirent64(fl, &fl->dirent64_buf)) { + __sync_fetch_and_add(&g_readdir_hits, 1); + return &fl->dirent64_buf; + } + errno = 0; + return NULL; + } + struct dirent64* (*real_readdir64)(DIR*) = + (struct dirent64* (*)(DIR*))next_symbol("readdir64"); + if (real_readdir64 != NULL) return real_readdir64(dir); + return (struct dirent64*)readdir(dir); +} + +int closedir(DIR* dir) +{ + if (is_fl_dir(dir)) { + FlDir* fl = (FlDir*)dir; + int real_fd = fl->real_fd; + if (real_fd >= 0) fd_map_take(real_fd); + free(fl); + if (real_fd >= 0) { + int (*real_close)(int) = (int (*)(int))next_symbol("close"); + return real_close(real_fd); + } + return 0; + } + int (*real_closedir)(DIR*) = (int (*)(DIR*))next_symbol("closedir"); + return real_closedir(dir); +} + +int dirfd(DIR* dir) +{ + if (is_fl_dir(dir)) return ((FlDir*)dir)->real_fd; + int (*real_dirfd)(DIR*) = (int (*)(DIR*))next_symbol("dirfd"); + return real_dirfd(dir); +} + +void rewinddir(DIR* dir) +{ + if (is_fl_dir(dir)) { + ((FlDir*)dir)->pos = 0; + return; + } + void (*real_rewinddir)(DIR*) = (void (*)(DIR*))next_symbol("rewinddir"); + real_rewinddir(dir); +} + +long telldir(DIR* dir) +{ + if (is_fl_dir(dir)) return (long)((FlDir*)dir)->pos; + long (*real_telldir)(DIR*) = (long (*)(DIR*))next_symbol("telldir"); + return real_telldir(dir); +} + +void seekdir(DIR* dir, long loc) +{ + if (is_fl_dir(dir)) { + ((FlDir*)dir)->pos = (size_t)(loc < 0 ? 0 : loc); + return; + } + void (*real_seekdir)(DIR*, long) = + (void (*)(DIR*, long))next_symbol("seekdir"); + real_seekdir(dir, loc); +} + +int close(int fd) +{ + int (*real_close)(int) = (int (*)(int))next_symbol("close"); + FlDir* dir = fd_map_take(fd); + if (dir != NULL) { + free(dir); + } + return real_close(fd); +} + +// fstat / fstat64: callers (Wine NTDLL especially) frequently fstat a freshly +// opened directory fd to verify it is a directory before issuing getdents64. +// /dev/null would report S_IFCHR and fail that check, so for any fd we +// synthesised we substitute a synthetic directory stat from the index. +int fstat(int fd, struct stat* st) +{ + if (st != NULL) { + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + fill_synthetic_stat(st, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + int (*real_fstat)(int, struct stat*) = + (int (*)(int, struct stat*))next_symbol("fstat"); + return real_fstat(fd, st); +} + +int fstat64(int fd, struct stat64* st) +{ + if (st != NULL) { + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + // struct stat64 layout matches struct stat on x86_64 glibc. + fill_synthetic_stat((struct stat*)st, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + int (*real_fstat64)(int, struct stat64*) = + (int (*)(int, struct stat64*))next_symbol("fstat64"); + if (real_fstat64 == NULL) return fstat(fd, (struct stat*)st); + return real_fstat64(fd, st); +} + +// Old glibc compat (still used by some packaged binaries). +int __fxstat(int ver, int fd, struct stat* st) +{ + if (st != NULL) { + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + fill_synthetic_stat(st, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + int (*real)(int, int, struct stat*) = + (int (*)(int, int, struct stat*))next_symbol("__fxstat"); + if (real == NULL) return fstat(fd, st); + return real(ver, fd, st); +} + +int __fxstat64(int ver, int fd, struct stat64* st) +{ + if (st != NULL) { + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + fill_synthetic_stat((struct stat*)st, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + int (*real)(int, int, struct stat64*) = + (int (*)(int, int, struct stat64*))next_symbol("__fxstat64"); + if (real == NULL) return fstat64(fd, st); + return real(ver, fd, st); +} + +// fcntl(F_GETFL) on the synthetic /dev/null fd reports O_RDONLY which matches +// what NTDLL wants to see for an O_DIRECTORY|O_RDONLY open, so leave that +// alone. lseek(fd, 0, SEEK_SET) is the rewind probe NTDLL emits before +// re-reading entries — translate it to a pos reset on our FlDir. +off_t lseek(int fd, off_t offset, int whence) +{ + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + if (whence == SEEK_SET) { + dir->pos = (size_t)(offset < 0 ? 0 : offset); + return offset; + } + if (whence == SEEK_CUR) { + // Allow telldir-style queries. + return (off_t)dir->pos; + } + } + off_t (*real_lseek)(int, off_t, int) = + (off_t (*)(int, off_t, int))next_symbol("lseek"); + return real_lseek(fd, offset, whence); +} + +off_t lseek64(int fd, off_t offset, int whence) +{ + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + if (whence == SEEK_SET) { + dir->pos = (size_t)(offset < 0 ? 0 : offset); + return offset; + } + if (whence == SEEK_CUR) return (off_t)dir->pos; + } + off_t (*real_lseek64)(int, off_t, int) = + (off_t (*)(int, off_t, int))next_symbol("lseek64"); + if (real_lseek64 == NULL) return lseek(fd, offset, whence); + return real_lseek64(fd, offset, whence); +} + +__attribute__((constructor)) +static void preload_attach(void) +{ + g_stats = flag_enabled("FLUORINE_VFS_PRELOAD_STATS"); +} + +__attribute__((destructor)) +static void preload_detach(void) +{ + if (!g_stats) return; + if (!g_debug && g_open_calls == 0 && g_stat_calls == 0 && g_statx_calls == 0 && + g_access_calls == 0 && g_syscall_calls == 0 && g_resolve_hits == 0 && + g_resolve_misses == 0) { + return; + } + fprintf(stderr, + "[fluorine-vfs-preload] pid=%ld loaded=%d failed=%d entries=%zu " + "open_calls=%llu stat_calls=%llu statx_calls=%llu access_calls=%llu " + "syscall_calls=%llu " + "path_mismatch=%llu " + "canon_attempts=%llu canon_mount_hits=%llu canon_outside=%llu canon_failed=%llu " + "opendir_calls=%llu opendir_hits=%llu " + "readdir_calls=%llu readdir_hits=%llu " + "getdents_calls=%llu getdents_hits=%llu " + "getdents_libc_calls=%llu getdents_libc_hits=%llu " + "resolve_hit=%llu resolve_miss=%llu open_hit=%llu open_fallback=%llu " + "stat_hit=%llu access_hit=%llu mount='%s'\n", + (long)getpid(), g_loaded, g_load_failed, g_entry_count, + (unsigned long long)g_open_calls, + (unsigned long long)g_stat_calls, + (unsigned long long)g_statx_calls, + (unsigned long long)g_access_calls, + (unsigned long long)g_syscall_calls, + (unsigned long long)g_path_mismatches, + (unsigned long long)g_canon_attempts, + (unsigned long long)g_canon_mount_hits, + (unsigned long long)g_canon_outside, + (unsigned long long)g_canon_failed, + (unsigned long long)g_opendir_calls, + (unsigned long long)g_opendir_hits, + (unsigned long long)g_readdir_calls, + (unsigned long long)g_readdir_hits, + (unsigned long long)g_getdents_calls, + (unsigned long long)g_getdents_hits, + (unsigned long long)g_getdents_libc_calls, + (unsigned long long)g_getdents_libc_hits, + (unsigned long long)g_resolve_hits, + (unsigned long long)g_resolve_misses, + (unsigned long long)g_open_hits, + (unsigned long long)g_open_fallbacks, + (unsigned long long)g_stat_hits, + (unsigned long long)g_access_hits, + g_mount_point ? g_mount_point : ""); +} + +int open(const char* pathname, int flags, ...) +{ + mode_t mode = 0; + if (flags & O_CREAT) { + va_list ap; + va_start(ap, flags); + mode = (mode_t)va_arg(ap, int); + va_end(ap); + } + + int (*real_open)(const char*, int, ...) = + (int (*)(const char*, int, ...))next_symbol("open"); + __sync_fetch_and_add(&g_open_calls, 1); + if (g_inside || !read_only_flags(flags)) return real_open(pathname, flags, mode); + + Entry* e = resolve_entry(pathname); + if (g_dirs_enabled && e != NULL && e->directory) { + int newfd = synthesize_dir_fd_for(e); + if (newfd >= 0) return newfd; + } + char* real_path = resolved_real_path(e); + if (real_path == NULL) return real_open(pathname, flags, mode); + + g_inside = 1; + int fd = real_open(real_path, flags, mode); + if (fd < 0) { + __sync_fetch_and_add(&g_open_fallbacks, 1); + fd = real_open(pathname, flags, mode); + } else { + __sync_fetch_and_add(&g_open_hits, 1); + } + g_inside = 0; + free(real_path); + return fd; +} + +int open64(const char* pathname, int flags, ...) +{ + mode_t mode = 0; + if (flags & O_CREAT) { + va_list ap; + va_start(ap, flags); + mode = (mode_t)va_arg(ap, int); + va_end(ap); + } + int (*real_open64)(const char*, int, ...) = + (int (*)(const char*, int, ...))next_symbol("open64"); + if (real_open64 == NULL) real_open64 = (int (*)(const char*, int, ...))next_symbol("open"); + __sync_fetch_and_add(&g_open_calls, 1); + if (g_inside || !read_only_flags(flags)) return real_open64(pathname, flags, mode); + + Entry* e = resolve_entry(pathname); + if (g_dirs_enabled && e != NULL && e->directory) { + int newfd = synthesize_dir_fd_for(e); + if (newfd >= 0) return newfd; + } + char* real_path = resolved_real_path(e); + if (real_path == NULL) return real_open64(pathname, flags, mode); + + g_inside = 1; + int fd = real_open64(real_path, flags, mode); + if (fd < 0) { + __sync_fetch_and_add(&g_open_fallbacks, 1); + fd = real_open64(pathname, flags, mode); + } else { + __sync_fetch_and_add(&g_open_hits, 1); + } + g_inside = 0; + free(real_path); + return fd; +} + +int openat(int dirfd, const char* pathname, int flags, ...) +{ + mode_t mode = 0; + if (flags & O_CREAT) { + va_list ap; + va_start(ap, flags); + mode = (mode_t)va_arg(ap, int); + va_end(ap); + } + int (*real_openat)(int, const char*, int, ...) = + (int (*)(int, const char*, int, ...))next_symbol("openat"); + __sync_fetch_and_add(&g_open_calls, 1); + if (g_inside || pathname == NULL || !read_only_flags(flags)) { + return real_openat(dirfd, pathname, flags, mode); + } + + Entry* e = resolve_entry_at(dirfd, pathname); + if (g_dirs_enabled && e != NULL && e->directory) { + int newfd = synthesize_dir_fd_for(e); + if (newfd >= 0) return newfd; + } + char* real_path = resolved_real_path(e); + if (real_path == NULL) return real_openat(dirfd, pathname, flags, mode); + + g_inside = 1; + int fd = real_openat(AT_FDCWD, real_path, flags, mode); + if (fd < 0) { + __sync_fetch_and_add(&g_open_fallbacks, 1); + fd = real_openat(dirfd, pathname, flags, mode); + } else { + __sync_fetch_and_add(&g_open_hits, 1); + } + g_inside = 0; + free(real_path); + return fd; +} + +int openat64(int dirfd, const char* pathname, int flags, ...) +{ + mode_t mode = 0; + if (flags & O_CREAT) { + va_list ap; + va_start(ap, flags); + mode = (mode_t)va_arg(ap, int); + va_end(ap); + } + int (*real_openat64)(int, const char*, int, ...) = + (int (*)(int, const char*, int, ...))next_symbol("openat64"); + if (real_openat64 == NULL) real_openat64 = (int (*)(int, const char*, int, ...))next_symbol("openat"); + __sync_fetch_and_add(&g_open_calls, 1); + if (g_inside || pathname == NULL || !read_only_flags(flags)) { + return real_openat64(dirfd, pathname, flags, mode); + } + + Entry* e = resolve_entry_at(dirfd, pathname); + if (g_dirs_enabled && e != NULL && e->directory) { + int newfd = synthesize_dir_fd_for(e); + if (newfd >= 0) return newfd; + } + char* real_path = resolved_real_path(e); + if (real_path == NULL) return real_openat64(dirfd, pathname, flags, mode); + + g_inside = 1; + int fd = real_openat64(AT_FDCWD, real_path, flags, mode); + if (fd < 0) { + __sync_fetch_and_add(&g_open_fallbacks, 1); + fd = real_openat64(dirfd, pathname, flags, mode); + } else { + __sync_fetch_and_add(&g_open_hits, 1); + } + g_inside = 0; + free(real_path); + return fd; +} + +static int mapped_stat(const char* path, struct stat* st, + int (*real_fn)(const char*, struct stat*)) +{ + if (g_inside) return real_fn(path, st); + __sync_fetch_and_add(&g_stat_calls, 1); + Entry* e = resolve_entry(path); + if (e == NULL) return real_fn(path, st); + char* real_path = resolved_real_path(e); + + g_inside = 1; + int rc = 0; + if (e->directory || real_path == NULL) { + fill_synthetic_stat(st, e); + } else { + rc = real_fn(real_path, st); + if (rc != 0) { + fill_synthetic_stat(st, e); + rc = 0; + } + } + if (rc == 0) __sync_fetch_and_add(&g_stat_hits, 1); + g_inside = 0; + free(real_path); + return rc; +} + +int stat(const char* path, struct stat* st) +{ + return mapped_stat(path, st, (int (*)(const char*, struct stat*))next_symbol("stat")); +} + +int lstat(const char* path, struct stat* st) +{ + return mapped_stat(path, st, (int (*)(const char*, struct stat*))next_symbol("lstat")); +} + +int fstatat(int dirfd, const char* path, struct stat* st, int flags) +{ + int (*real_fstatat)(int, const char*, struct stat*, int) = + (int (*)(int, const char*, struct stat*, int))next_symbol("fstatat"); + __sync_fetch_and_add(&g_stat_calls, 1); + if (st != NULL && (flags & AT_EMPTY_PATH) && + (path == NULL || path[0] == '\0')) { + FlDir* dir = fd_map_lookup(dirfd); + if (dir != NULL) { + fill_synthetic_stat(st, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + if (g_inside || path == NULL) return real_fstatat(dirfd, path, st, flags); + Entry* e = resolve_entry_at(dirfd, path); + if (e == NULL) return real_fstatat(dirfd, path, st, flags); + char* real_path = resolved_real_path(e); + + g_inside = 1; + int rc = 0; + if (e->directory || real_path == NULL) { + fill_synthetic_stat(st, e); + } else { + rc = real_fstatat(AT_FDCWD, real_path, st, flags); + if (rc != 0) { + fill_synthetic_stat(st, e); + rc = 0; + } + } + if (rc == 0) __sync_fetch_and_add(&g_stat_hits, 1); + g_inside = 0; + free(real_path); + return rc; +} + +int access(const char* path, int mode) +{ + int (*real_access)(const char*, int) = (int (*)(const char*, int))next_symbol("access"); + __sync_fetch_and_add(&g_access_calls, 1); + if (g_inside) return real_access(path, mode); + Entry* e = resolve_entry(path); + if (e == NULL) return real_access(path, mode); + char* real_path = resolved_real_path(e); + int rc = 0; + if (mode == F_OK || e->directory || real_path == NULL) { + rc = 0; + } else { + rc = real_access(real_path, mode); + } + if (rc == 0) __sync_fetch_and_add(&g_access_hits, 1); + free(real_path); + return rc; +} + +int faccessat(int dirfd, const char* path, int mode, int flags) +{ + int (*real_faccessat)(int, const char*, int, int) = + (int (*)(int, const char*, int, int))next_symbol("faccessat"); + __sync_fetch_and_add(&g_access_calls, 1); + if (g_inside || path == NULL) return real_faccessat(dirfd, path, mode, flags); + Entry* e = resolve_entry_at(dirfd, path); + if (e == NULL) return real_faccessat(dirfd, path, mode, flags); + char* real_path = resolved_real_path(e); + int rc = 0; + if (mode == F_OK || e->directory || real_path == NULL) { + rc = 0; + } else { + rc = real_faccessat(AT_FDCWD, real_path, mode, flags); + } + if (rc == 0) __sync_fetch_and_add(&g_access_hits, 1); + free(real_path); + return rc; +} + +int statx(int dirfd, const char* path, int flags, unsigned int mask, struct statx* stx) +{ + int (*real_statx)(int, const char*, int, unsigned int, struct statx*) = + (int (*)(int, const char*, int, unsigned int, struct statx*))next_symbol("statx"); + __sync_fetch_and_add(&g_statx_calls, 1); + if (g_inside || path == NULL) { + return real_statx ? real_statx(dirfd, path, flags, mask, stx) + : (int)syscall(SYS_statx, dirfd, path, flags, mask, stx); + } + Entry* e = resolve_entry_at(dirfd, path); + if (e == NULL) { + return real_statx ? real_statx(dirfd, path, flags, mask, stx) + : (int)syscall(SYS_statx, dirfd, path, flags, mask, stx); + } + char* real_path = resolved_real_path(e); + + g_inside = 1; + int rc = 0; + if (e->directory || real_path == NULL) { + fill_synthetic_statx(stx, e); + } else { + rc = real_statx ? real_statx(AT_FDCWD, real_path, flags, mask, stx) + : (int)syscall(SYS_statx, AT_FDCWD, real_path, flags, mask, stx); + if (rc != 0) { + fill_synthetic_statx(stx, e); + rc = 0; + } + } + if (rc == 0) __sync_fetch_and_add(&g_stat_hits, 1); + g_inside = 0; + free(real_path); + return rc; +} + +// Wine 9+ / glibc 2.30+ may resolve getdents64() as a libc symbol rather than +// emitting syscall(SYS_getdents64, ...). Hook the symbol directly so synthetic +// dir fds answer NTDLL's directory reads. Mirrors the SYS_getdents64 branch in +// syscall(). +ssize_t getdents64(int fd, void* buf, size_t count) +{ + __sync_fetch_and_add(&g_getdents_libc_calls, 1); + if (!g_inside) { + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL && buf != NULL) { + long n = fl_dir_fill_getdents64(dir, buf, count); + if (n >= 0) __sync_fetch_and_add(&g_getdents_libc_hits, 1); + return (ssize_t)n; + } + } + ssize_t (*real)(int, void*, size_t) = + (ssize_t (*)(int, void*, size_t))next_symbol("getdents64"); + if (real != NULL) return real(fd, buf, count); + return (ssize_t)syscall(SYS_getdents64, fd, buf, count); +} + +long syscall(long number, ...) +{ + va_list ap; + va_start(ap, number); + long a1 = va_arg(ap, long); + long a2 = va_arg(ap, long); + long a3 = va_arg(ap, long); + long a4 = va_arg(ap, long); + long a5 = va_arg(ap, long); + long a6 = va_arg(ap, long); + va_end(ap); + + long (*real_syscall)(long, ...) = (long (*)(long, ...))next_symbol("syscall"); + __sync_fetch_and_add(&g_syscall_calls, 1); + + if (g_inside) { + return real_syscall(number, a1, a2, a3, a4, a5, a6); + } + +#ifdef SYS_openat + if (number == SYS_openat) { + int dirfd = (int)a1; + const char* path = (const char*)a2; + int flags = (int)a3; + mode_t mode = (mode_t)a4; + if (path != NULL && read_only_flags(flags)) { + Entry* e = resolve_entry_at(dirfd, path); + if (g_dirs_enabled && e != NULL && e->directory) { + int newfd = synthesize_dir_fd_for(e); + if (newfd >= 0) return newfd; + } + char* real_path = resolved_real_path(e); + if (real_path != NULL) { + g_inside = 1; + long rc = real_syscall(number, AT_FDCWD, (long)real_path, flags, mode); + if (rc < 0) { + __sync_fetch_and_add(&g_open_fallbacks, 1); + rc = real_syscall(number, a1, a2, a3, a4, a5, a6); + } else { + __sync_fetch_and_add(&g_open_hits, 1); + } + g_inside = 0; + free(real_path); + return rc; + } + } + } +#endif + +#ifdef SYS_getdents64 + if (number == SYS_getdents64) { + int fd = (int)a1; + void* buf = (void*)a2; + size_t count = (size_t)a3; + __sync_fetch_and_add(&g_getdents_calls, 1); + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL && buf != NULL) { + long n = fl_dir_fill_getdents64(dir, buf, count); + if (n >= 0) __sync_fetch_and_add(&g_getdents_hits, 1); + return n; + } + } +#endif + +#ifdef SYS_fstat + if (number == SYS_fstat) { + int fd = (int)a1; + struct stat* st = (struct stat*)a2; + if (st != NULL) { + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + fill_synthetic_stat(st, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + } +#endif + +#ifdef SYS_close + if (number == SYS_close) { + int fd = (int)a1; + FlDir* dir = fd_map_take(fd); + if (dir != NULL) free(dir); + } +#endif + +#ifdef SYS_lseek + if (number == SYS_lseek) { + int fd = (int)a1; + long offset = a2; + int whence = (int)a3; + FlDir* dir = fd_map_lookup(fd); + if (dir != NULL) { + if (whence == SEEK_SET) { + dir->pos = (size_t)(offset < 0 ? 0 : offset); + return offset; + } + if (whence == SEEK_CUR) return (long)dir->pos; + } + } +#endif + +#ifdef SYS_open + if (number == SYS_open) { + const char* path = (const char*)a1; + int flags = (int)a2; + mode_t mode = (mode_t)a3; + if (path != NULL && read_only_flags(flags)) { + Entry* e = resolve_entry(path); + if (g_dirs_enabled && e != NULL && e->directory) { + int newfd = synthesize_dir_fd_for(e); + if (newfd >= 0) return newfd; + } + char* real_path = resolved_real_path(e); + if (real_path != NULL) { + g_inside = 1; + long rc = real_syscall(number, (long)real_path, flags, mode, a4, a5, a6); + if (rc < 0) { + __sync_fetch_and_add(&g_open_fallbacks, 1); + rc = real_syscall(number, a1, a2, a3, a4, a5, a6); + } else { + __sync_fetch_and_add(&g_open_hits, 1); + } + g_inside = 0; + free(real_path); + return rc; + } + } + } +#endif + +#ifdef SYS_newfstatat + if (number == SYS_newfstatat) { + int dirfd = (int)a1; + const char* path = (const char*)a2; + struct stat* st = (struct stat*)a3; + int flags = (int)a4; + // AT_EMPTY_PATH + empty path = fstat-on-dirfd. Wine NTDLL emits this to + // verify the directory fd before calling getdents64. + if (st != NULL && (flags & AT_EMPTY_PATH) && + (path == NULL || path[0] == '\0')) { + FlDir* dir = fd_map_lookup(dirfd); + if (dir != NULL) { + fill_synthetic_stat(st, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + if (path != NULL && st != NULL) { + Entry* e = resolve_entry_at(dirfd, path); + char* real_path = resolved_real_path(e); + if (e != NULL) { + g_inside = 1; + long rc = 0; + if (e->directory || real_path == NULL) { + fill_synthetic_stat(st, e); + } else { + rc = real_syscall(number, AT_FDCWD, (long)real_path, (long)st, flags, a5, a6); + if (rc != 0) { + fill_synthetic_stat(st, e); + rc = 0; + } + } + if (rc == 0) __sync_fetch_and_add(&g_stat_hits, 1); + g_inside = 0; + free(real_path); + return rc; + } + free(real_path); + } + } +#endif + +#ifdef SYS_statx + if (number == SYS_statx) { + int dirfd = (int)a1; + const char* path = (const char*)a2; + int flags = (int)a3; + unsigned int mask = (unsigned int)a4; + struct statx* stx = (struct statx*)a5; + if (stx != NULL && (flags & AT_EMPTY_PATH) && + (path == NULL || path[0] == '\0')) { + FlDir* dir = fd_map_lookup(dirfd); + if (dir != NULL) { + fill_synthetic_statx(stx, &g_entries[dir->entry_idx]); + __sync_fetch_and_add(&g_stat_hits, 1); + return 0; + } + } + if (path != NULL && stx != NULL) { + Entry* e = resolve_entry_at(dirfd, path); + char* real_path = resolved_real_path(e); + if (e != NULL) { + g_inside = 1; + long rc = 0; + if (e->directory || real_path == NULL) { + fill_synthetic_statx(stx, e); + } else { + rc = real_syscall(number, AT_FDCWD, (long)real_path, flags, mask, (long)stx, a6); + if (rc != 0) { + fill_synthetic_statx(stx, e); + rc = 0; + } + } + if (rc == 0) __sync_fetch_and_add(&g_stat_hits, 1); + g_inside = 0; + free(real_path); + return rc; + } + free(real_path); + } + } +#endif + +#ifdef SYS_access + if (number == SYS_access) { + const char* path = (const char*)a1; + int mode = (int)a2; + if (path != NULL) { + Entry* e = resolve_entry(path); + char* real_path = resolved_real_path(e); + if (e != NULL) { + long rc = 0; + if (mode == F_OK || e->directory || real_path == NULL) { + rc = 0; + } else { + rc = real_syscall(number, (long)real_path, mode, a3, a4, a5, a6); + } + if (rc == 0) __sync_fetch_and_add(&g_access_hits, 1); + free(real_path); + return rc; + } + free(real_path); + } + } +#endif + +#ifdef SYS_faccessat + if (number == SYS_faccessat) { + int dirfd = (int)a1; + const char* path = (const char*)a2; + int mode = (int)a3; + if (path != NULL) { + Entry* e = resolve_entry_at(dirfd, path); + char* real_path = resolved_real_path(e); + if (e != NULL) { + long rc = 0; + if (mode == F_OK || e->directory || real_path == NULL) { + rc = 0; + } else { + rc = real_syscall(number, AT_FDCWD, (long)real_path, mode, a4, a5, a6); + } + if (rc == 0) __sync_fetch_and_add(&g_access_hits, 1); + free(real_path); + return rc; + } + free(real_path); + } + } +#endif + + return real_syscall(number, a1, a2, a3, a4, a5, a6); +} diff --git a/third_party/minhook/AUTHORS.txt b/third_party/minhook/AUTHORS.txt new file mode 100644 index 0000000..ebef1a6 --- /dev/null +++ b/third_party/minhook/AUTHORS.txt @@ -0,0 +1,8 @@ +Tsuda Kageyu <tsuda.kageyu@gmail.com> + Creator, maintainer + +Michael Maltsev <leahcimmar@gmail.com> + Added "Queue" functions. A lot of bug fixes. + +Andrey Unis <uniskz@gmail.com> + Rewrote the hook engine in plain C. diff --git a/third_party/minhook/LICENSE.txt b/third_party/minhook/LICENSE.txt new file mode 100644 index 0000000..74dea27 --- /dev/null +++ b/third_party/minhook/LICENSE.txt @@ -0,0 +1,81 @@ +MinHook - The Minimalistic API Hooking Library for x64/x86 +Copyright (C) 2009-2017 Tsuda Kageyu. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +================================================================================ +Portions of this software are Copyright (c) 2008-2009, Vyacheslav Patkov. +================================================================================ +Hacker Disassembler Engine 32 C +Copyright (c) 2008-2009, Vyacheslav Patkov. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- +Hacker Disassembler Engine 64 C +Copyright (c) 2008-2009, Vyacheslav Patkov. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/minhook/include/MinHook.h b/third_party/minhook/include/MinHook.h new file mode 100644 index 0000000..ae39350 --- /dev/null +++ b/third_party/minhook/include/MinHook.h @@ -0,0 +1,185 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if !(defined _M_IX86) && !(defined _M_X64) && !(defined __i386__) && !(defined __x86_64__) + #error MinHook supports only x86 and x64 systems. +#endif + +#include <windows.h> + +// MinHook Error Codes. +typedef enum MH_STATUS +{ + // Unknown error. Should not be returned. + MH_UNKNOWN = -1, + + // Successful. + MH_OK = 0, + + // MinHook is already initialized. + MH_ERROR_ALREADY_INITIALIZED, + + // MinHook is not initialized yet, or already uninitialized. + MH_ERROR_NOT_INITIALIZED, + + // The hook for the specified target function is already created. + MH_ERROR_ALREADY_CREATED, + + // The hook for the specified target function is not created yet. + MH_ERROR_NOT_CREATED, + + // The hook for the specified target function is already enabled. + MH_ERROR_ENABLED, + + // The hook for the specified target function is not enabled yet, or already + // disabled. + MH_ERROR_DISABLED, + + // The specified pointer is invalid. It points the address of non-allocated + // and/or non-executable region. + MH_ERROR_NOT_EXECUTABLE, + + // The specified target function cannot be hooked. + MH_ERROR_UNSUPPORTED_FUNCTION, + + // Failed to allocate memory. + MH_ERROR_MEMORY_ALLOC, + + // Failed to change the memory protection. + MH_ERROR_MEMORY_PROTECT, + + // The specified module is not loaded. + MH_ERROR_MODULE_NOT_FOUND, + + // The specified function is not found. + MH_ERROR_FUNCTION_NOT_FOUND +} +MH_STATUS; + +// Can be passed as a parameter to MH_EnableHook, MH_DisableHook, +// MH_QueueEnableHook or MH_QueueDisableHook. +#define MH_ALL_HOOKS NULL + +#ifdef __cplusplus +extern "C" { +#endif + + // Initialize the MinHook library. You must call this function EXACTLY ONCE + // at the beginning of your program. + MH_STATUS WINAPI MH_Initialize(VOID); + + // Uninitialize the MinHook library. You must call this function EXACTLY + // ONCE at the end of your program. + MH_STATUS WINAPI MH_Uninitialize(VOID); + + // Creates a hook for the specified target function, in disabled state. + // Parameters: + // pTarget [in] A pointer to the target function, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal); + + // Creates a hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszProcName [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal); + + // Creates a hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszProcName [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + // ppTarget [out] A pointer to the target function, which will be used + // with other functions. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal, LPVOID *ppTarget); + + // Removes an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget); + + // Enables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // enabled in one go. + MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget); + + // Disables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // disabled in one go. + MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget); + + // Queues to enable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be enabled. + MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget); + + // Queues to disable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be disabled. + MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget); + + // Applies all queued changes in one go. + MH_STATUS WINAPI MH_ApplyQueued(VOID); + + // Translates the MH_STATUS to its name as a string. + const char *WINAPI MH_StatusToString(MH_STATUS status); + +#ifdef __cplusplus +} +#endif diff --git a/third_party/minhook/src/buffer.c b/third_party/minhook/src/buffer.c new file mode 100644 index 0000000..940df65 --- /dev/null +++ b/third_party/minhook/src/buffer.c @@ -0,0 +1,312 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <windows.h> +#include "buffer.h" + +// Size of each memory block. (= page size of VirtualAlloc) +#define MEMORY_BLOCK_SIZE 0x1000 + +// Max range for seeking a memory block. (= 1024MB) +#define MAX_MEMORY_RANGE 0x40000000 + +// Memory protection flags to check the executable address. +#define PAGE_EXECUTE_FLAGS \ + (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY) + +// Memory slot. +typedef struct _MEMORY_SLOT +{ + union + { + struct _MEMORY_SLOT *pNext; + UINT8 buffer[MEMORY_SLOT_SIZE]; + }; +} MEMORY_SLOT, *PMEMORY_SLOT; + +// Memory block info. Placed at the head of each block. +typedef struct _MEMORY_BLOCK +{ + struct _MEMORY_BLOCK *pNext; + PMEMORY_SLOT pFree; // First element of the free slot list. + UINT usedCount; +} MEMORY_BLOCK, *PMEMORY_BLOCK; + +//------------------------------------------------------------------------- +// Global Variables: +//------------------------------------------------------------------------- + +// First element of the memory block list. +static PMEMORY_BLOCK g_pMemoryBlocks; + +//------------------------------------------------------------------------- +VOID InitializeBuffer(VOID) +{ + // Nothing to do for now. +} + +//------------------------------------------------------------------------- +VOID UninitializeBuffer(VOID) +{ + PMEMORY_BLOCK pBlock = g_pMemoryBlocks; + g_pMemoryBlocks = NULL; + + while (pBlock) + { + PMEMORY_BLOCK pNext = pBlock->pNext; + VirtualFree(pBlock, 0, MEM_RELEASE); + pBlock = pNext; + } +} + +//------------------------------------------------------------------------- +#if defined(_M_X64) || defined(__x86_64__) +static LPVOID FindPrevFreeRegion(LPVOID pAddress, LPVOID pMinAddr, DWORD dwAllocationGranularity) +{ + ULONG_PTR tryAddr = (ULONG_PTR)pAddress; + + // Round down to the allocation granularity. + tryAddr -= tryAddr % dwAllocationGranularity; + + // Start from the previous allocation granularity multiply. + tryAddr -= dwAllocationGranularity; + + while (tryAddr >= (ULONG_PTR)pMinAddr) + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((LPVOID)tryAddr, &mbi, sizeof(mbi)) == 0) + break; + + if (mbi.State == MEM_FREE) + return (LPVOID)tryAddr; + + if ((ULONG_PTR)mbi.AllocationBase < dwAllocationGranularity) + break; + + tryAddr = (ULONG_PTR)mbi.AllocationBase - dwAllocationGranularity; + } + + return NULL; +} +#endif + +//------------------------------------------------------------------------- +#if defined(_M_X64) || defined(__x86_64__) +static LPVOID FindNextFreeRegion(LPVOID pAddress, LPVOID pMaxAddr, DWORD dwAllocationGranularity) +{ + ULONG_PTR tryAddr = (ULONG_PTR)pAddress; + + // Round down to the allocation granularity. + tryAddr -= tryAddr % dwAllocationGranularity; + + // Start from the next allocation granularity multiply. + tryAddr += dwAllocationGranularity; + + while (tryAddr <= (ULONG_PTR)pMaxAddr) + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((LPVOID)tryAddr, &mbi, sizeof(mbi)) == 0) + break; + + if (mbi.State == MEM_FREE) + return (LPVOID)tryAddr; + + tryAddr = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize; + + // Round up to the next allocation granularity. + tryAddr += dwAllocationGranularity - 1; + tryAddr -= tryAddr % dwAllocationGranularity; + } + + return NULL; +} +#endif + +//------------------------------------------------------------------------- +static PMEMORY_BLOCK GetMemoryBlock(LPVOID pOrigin) +{ + PMEMORY_BLOCK pBlock; +#if defined(_M_X64) || defined(__x86_64__) + ULONG_PTR minAddr; + ULONG_PTR maxAddr; + + SYSTEM_INFO si; + GetSystemInfo(&si); + minAddr = (ULONG_PTR)si.lpMinimumApplicationAddress; + maxAddr = (ULONG_PTR)si.lpMaximumApplicationAddress; + + // pOrigin ± 512MB + if ((ULONG_PTR)pOrigin > MAX_MEMORY_RANGE && minAddr < (ULONG_PTR)pOrigin - MAX_MEMORY_RANGE) + minAddr = (ULONG_PTR)pOrigin - MAX_MEMORY_RANGE; + + if (maxAddr > (ULONG_PTR)pOrigin + MAX_MEMORY_RANGE) + maxAddr = (ULONG_PTR)pOrigin + MAX_MEMORY_RANGE; + + // Make room for MEMORY_BLOCK_SIZE bytes. + maxAddr -= MEMORY_BLOCK_SIZE - 1; +#endif + + // Look the registered blocks for a reachable one. + for (pBlock = g_pMemoryBlocks; pBlock != NULL; pBlock = pBlock->pNext) + { +#if defined(_M_X64) || defined(__x86_64__) + // Ignore the blocks too far. + if ((ULONG_PTR)pBlock < minAddr || (ULONG_PTR)pBlock >= maxAddr) + continue; +#endif + // The block has at least one unused slot. + if (pBlock->pFree != NULL) + return pBlock; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Alloc a new block above if not found. + { + LPVOID pAlloc = pOrigin; + while ((ULONG_PTR)pAlloc >= minAddr) + { + pAlloc = FindPrevFreeRegion(pAlloc, (LPVOID)minAddr, si.dwAllocationGranularity); + if (pAlloc == NULL) + break; + + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + pAlloc, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (pBlock != NULL) + break; + } + } + + // Alloc a new block below if not found. + if (pBlock == NULL) + { + LPVOID pAlloc = pOrigin; + while ((ULONG_PTR)pAlloc <= maxAddr) + { + pAlloc = FindNextFreeRegion(pAlloc, (LPVOID)maxAddr, si.dwAllocationGranularity); + if (pAlloc == NULL) + break; + + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + pAlloc, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (pBlock != NULL) + break; + } + } +#else + // In x86 mode, a memory block can be placed anywhere. + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + NULL, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); +#endif + + if (pBlock != NULL) + { + // Build a linked list of all the slots. + PMEMORY_SLOT pSlot = (PMEMORY_SLOT)pBlock + 1; + pBlock->pFree = NULL; + pBlock->usedCount = 0; + do + { + pSlot->pNext = pBlock->pFree; + pBlock->pFree = pSlot; + pSlot++; + } while ((ULONG_PTR)pSlot - (ULONG_PTR)pBlock <= MEMORY_BLOCK_SIZE - MEMORY_SLOT_SIZE); + + pBlock->pNext = g_pMemoryBlocks; + g_pMemoryBlocks = pBlock; + } + + return pBlock; +} + +//------------------------------------------------------------------------- +LPVOID AllocateBuffer(LPVOID pOrigin) +{ + PMEMORY_SLOT pSlot; + PMEMORY_BLOCK pBlock = GetMemoryBlock(pOrigin); + if (pBlock == NULL) + return NULL; + + // Remove an unused slot from the list. + pSlot = pBlock->pFree; + pBlock->pFree = pSlot->pNext; + pBlock->usedCount++; +#ifdef _DEBUG + // Fill the slot with INT3 for debugging. + memset(pSlot, 0xCC, sizeof(MEMORY_SLOT)); +#endif + return pSlot; +} + +//------------------------------------------------------------------------- +VOID FreeBuffer(LPVOID pBuffer) +{ + PMEMORY_BLOCK pBlock = g_pMemoryBlocks; + PMEMORY_BLOCK pPrev = NULL; + ULONG_PTR pTargetBlock = ((ULONG_PTR)pBuffer / MEMORY_BLOCK_SIZE) * MEMORY_BLOCK_SIZE; + + while (pBlock != NULL) + { + if ((ULONG_PTR)pBlock == pTargetBlock) + { + PMEMORY_SLOT pSlot = (PMEMORY_SLOT)pBuffer; +#ifdef _DEBUG + // Clear the released slot for debugging. + memset(pSlot, 0x00, sizeof(MEMORY_SLOT)); +#endif + // Restore the released slot to the list. + pSlot->pNext = pBlock->pFree; + pBlock->pFree = pSlot; + pBlock->usedCount--; + + // Free if unused. + if (pBlock->usedCount == 0) + { + if (pPrev) + pPrev->pNext = pBlock->pNext; + else + g_pMemoryBlocks = pBlock->pNext; + + VirtualFree(pBlock, 0, MEM_RELEASE); + } + + break; + } + + pPrev = pBlock; + pBlock = pBlock->pNext; + } +} + +//------------------------------------------------------------------------- +BOOL IsExecutableAddress(LPVOID pAddress) +{ + MEMORY_BASIC_INFORMATION mi; + VirtualQuery(pAddress, &mi, sizeof(mi)); + + return (mi.State == MEM_COMMIT && (mi.Protect & PAGE_EXECUTE_FLAGS)); +} diff --git a/third_party/minhook/src/buffer.h b/third_party/minhook/src/buffer.h new file mode 100644 index 0000000..204d551 --- /dev/null +++ b/third_party/minhook/src/buffer.h @@ -0,0 +1,42 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// Size of each memory slot. +#if defined(_M_X64) || defined(__x86_64__) + #define MEMORY_SLOT_SIZE 64 +#else + #define MEMORY_SLOT_SIZE 32 +#endif + +VOID InitializeBuffer(VOID); +VOID UninitializeBuffer(VOID); +LPVOID AllocateBuffer(LPVOID pOrigin); +VOID FreeBuffer(LPVOID pBuffer); +BOOL IsExecutableAddress(LPVOID pAddress); diff --git a/third_party/minhook/src/hde/hde64.c b/third_party/minhook/src/hde/hde64.c new file mode 100644 index 0000000..ce773b1 --- /dev/null +++ b/third_party/minhook/src/hde/hde64.c @@ -0,0 +1,335 @@ +/* + * Hacker Disassembler Engine 64 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#if defined(_M_X64) || defined(__x86_64__) + +#include <string.h> +#include "hde64.h" +#include "table64.h" + +unsigned int hde64_disasm(const void *code, hde64s *hs) +{ + uint8_t x, c, *p = (uint8_t *)code, cflags, opcode, pref = 0; + uint8_t *ht = hde64_table, m_mod, m_reg, m_rm, disp_size = 0; + uint8_t op64 = 0; + + memset(hs, 0, sizeof(hde64s)); + + for (x = 16; x; x--) + switch (c = *p++) { + case 0xf3: + hs->p_rep = c; + pref |= PRE_F3; + break; + case 0xf2: + hs->p_rep = c; + pref |= PRE_F2; + break; + case 0xf0: + hs->p_lock = c; + pref |= PRE_LOCK; + break; + case 0x26: case 0x2e: case 0x36: + case 0x3e: case 0x64: case 0x65: + hs->p_seg = c; + pref |= PRE_SEG; + break; + case 0x66: + hs->p_66 = c; + pref |= PRE_66; + break; + case 0x67: + hs->p_67 = c; + pref |= PRE_67; + break; + default: + goto pref_done; + } + pref_done: + + hs->flags = (uint32_t)pref << 23; + + if (!pref) + pref |= PRE_NONE; + + if ((c & 0xf0) == 0x40) { + hs->flags |= F_PREFIX_REX; + if ((hs->rex_w = (c & 0xf) >> 3) && (*p & 0xf8) == 0xb8) + op64++; + hs->rex_r = (c & 7) >> 2; + hs->rex_x = (c & 3) >> 1; + hs->rex_b = c & 1; + if (((c = *p++) & 0xf0) == 0x40) { + opcode = c; + goto error_opcode; + } + } + + if ((hs->opcode = c) == 0x0f) { + hs->opcode2 = c = *p++; + ht += DELTA_OPCODES; + } else if (c >= 0xa0 && c <= 0xa3) { + op64++; + if (pref & PRE_67) + pref |= PRE_66; + else + pref &= ~PRE_66; + } + + opcode = c; + cflags = ht[ht[opcode / 4] + (opcode % 4)]; + + if (cflags == C_ERROR) { + error_opcode: + hs->flags |= F_ERROR | F_ERROR_OPCODE; + cflags = 0; + if ((opcode & -3) == 0x24) + cflags++; + } + + x = 0; + if (cflags & C_GROUP) { + uint16_t t; + t = *(uint16_t *)(ht + (cflags & 0x7f)); + cflags = (uint8_t)t; + x = (uint8_t)(t >> 8); + } + + if (hs->opcode2) { + ht = hde64_table + DELTA_PREFIXES; + if (ht[ht[opcode / 4] + (opcode % 4)] & pref) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (cflags & C_MODRM) { + hs->flags |= F_MODRM; + hs->modrm = c = *p++; + hs->modrm_mod = m_mod = c >> 6; + hs->modrm_rm = m_rm = c & 7; + hs->modrm_reg = m_reg = (c & 0x3f) >> 3; + + if (x && ((x << m_reg) & 0x80)) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + + if (!hs->opcode2 && opcode >= 0xd9 && opcode <= 0xdf) { + uint8_t t = opcode - 0xd9; + if (m_mod == 3) { + ht = hde64_table + DELTA_FPU_MODRM + t*8; + t = ht[m_reg] << m_rm; + } else { + ht = hde64_table + DELTA_FPU_REG; + t = ht[t] << m_reg; + } + if (t & 0x80) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (pref & PRE_LOCK) { + if (m_mod == 3) { + hs->flags |= F_ERROR | F_ERROR_LOCK; + } else { + uint8_t *table_end, op = opcode; + if (hs->opcode2) { + ht = hde64_table + DELTA_OP2_LOCK_OK; + table_end = ht + DELTA_OP_ONLY_MEM - DELTA_OP2_LOCK_OK; + } else { + ht = hde64_table + DELTA_OP_LOCK_OK; + table_end = ht + DELTA_OP2_LOCK_OK - DELTA_OP_LOCK_OK; + op &= -2; + } + for (; ht != table_end; ht++) + if (*ht++ == op) { + if (!((*ht << m_reg) & 0x80)) + goto no_lock_error; + else + break; + } + hs->flags |= F_ERROR | F_ERROR_LOCK; + no_lock_error: + ; + } + } + + if (hs->opcode2) { + switch (opcode) { + case 0x20: case 0x22: + m_mod = 3; + if (m_reg > 4 || m_reg == 1) + goto error_operand; + else + goto no_error_operand; + case 0x21: case 0x23: + m_mod = 3; + if (m_reg == 4 || m_reg == 5) + goto error_operand; + else + goto no_error_operand; + } + } else { + switch (opcode) { + case 0x8c: + if (m_reg > 5) + goto error_operand; + else + goto no_error_operand; + case 0x8e: + if (m_reg == 1 || m_reg > 5) + goto error_operand; + else + goto no_error_operand; + } + } + + if (m_mod == 3) { + uint8_t *table_end; + if (hs->opcode2) { + ht = hde64_table + DELTA_OP2_ONLY_MEM; + table_end = ht + sizeof(hde64_table) - DELTA_OP2_ONLY_MEM; + } else { + ht = hde64_table + DELTA_OP_ONLY_MEM; + table_end = ht + DELTA_OP2_ONLY_MEM - DELTA_OP_ONLY_MEM; + } + for (; ht != table_end; ht += 2) + if (*ht++ == opcode) { + if ((*ht++ & pref) && !((*ht << m_reg) & 0x80)) + goto error_operand; + else + break; + } + goto no_error_operand; + } else if (hs->opcode2) { + switch (opcode) { + case 0x50: case 0xd7: case 0xf7: + if (pref & (PRE_NONE | PRE_66)) + goto error_operand; + break; + case 0xd6: + if (pref & (PRE_F2 | PRE_F3)) + goto error_operand; + break; + case 0xc5: + goto error_operand; + } + goto no_error_operand; + } else + goto no_error_operand; + + error_operand: + hs->flags |= F_ERROR | F_ERROR_OPERAND; + no_error_operand: + + c = *p++; + if (m_reg <= 1) { + if (opcode == 0xf6) + cflags |= C_IMM8; + else if (opcode == 0xf7) + cflags |= C_IMM_P66; + } + + switch (m_mod) { + case 0: + if (pref & PRE_67) { + if (m_rm == 6) + disp_size = 2; + } else + if (m_rm == 5) + disp_size = 4; + break; + case 1: + disp_size = 1; + break; + case 2: + disp_size = 2; + if (!(pref & PRE_67)) + disp_size <<= 1; + break; + } + + if (m_mod != 3 && m_rm == 4) { + hs->flags |= F_SIB; + p++; + hs->sib = c; + hs->sib_scale = c >> 6; + hs->sib_index = (c & 0x3f) >> 3; + if ((hs->sib_base = c & 7) == 5 && !(m_mod & 1)) + disp_size = 4; + } + + p--; + switch (disp_size) { + case 1: + hs->flags |= F_DISP8; + hs->disp.disp8 = *p; + break; + case 2: + hs->flags |= F_DISP16; + hs->disp.disp16 = *(uint16_t *)p; + break; + case 4: + hs->flags |= F_DISP32; + hs->disp.disp32 = *(uint32_t *)p; + break; + } + p += disp_size; + } else if (pref & PRE_LOCK) + hs->flags |= F_ERROR | F_ERROR_LOCK; + + if (cflags & C_IMM_P66) { + if (cflags & C_REL32) { + if (pref & PRE_66) { + hs->flags |= F_IMM16 | F_RELATIVE; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + goto disasm_done; + } + goto rel32_ok; + } + if (op64) { + hs->flags |= F_IMM64; + hs->imm.imm64 = *(uint64_t *)p; + p += 8; + } else if (!(pref & PRE_66)) { + hs->flags |= F_IMM32; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else + goto imm16_ok; + } + + + if (cflags & C_IMM16) { + imm16_ok: + hs->flags |= F_IMM16; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + } + if (cflags & C_IMM8) { + hs->flags |= F_IMM8; + hs->imm.imm8 = *p++; + } + + if (cflags & C_REL32) { + rel32_ok: + hs->flags |= F_IMM32 | F_RELATIVE; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else if (cflags & C_REL8) { + hs->flags |= F_IMM8 | F_RELATIVE; + hs->imm.imm8 = *p++; + } + + disasm_done: + + if ((hs->len = (uint8_t)(p-(uint8_t *)code)) > 15) { + hs->flags |= F_ERROR | F_ERROR_LENGTH; + hs->len = 15; + } + + return (unsigned int)hs->len; +} + +#endif // defined(_M_X64) || defined(__x86_64__) diff --git a/third_party/minhook/src/hde/hde64.h b/third_party/minhook/src/hde/hde64.h new file mode 100644 index 0000000..ecbf4df --- /dev/null +++ b/third_party/minhook/src/hde/hde64.h @@ -0,0 +1,112 @@ +/* + * Hacker Disassembler Engine 64 + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + * hde64.h: C/C++ header file + * + */ + +#ifndef _HDE64_H_ +#define _HDE64_H_ + +/* stdint.h - C99 standard header + * http://en.wikipedia.org/wiki/stdint.h + * + * if your compiler doesn't contain "stdint.h" header (for + * example, Microsoft Visual C++), you can download file: + * http://www.azillionmonkeys.com/qed/pstdint.h + * and change next line to: + * #include "pstdint.h" + */ +#include "pstdint.h" + +#define F_MODRM 0x00000001 +#define F_SIB 0x00000002 +#define F_IMM8 0x00000004 +#define F_IMM16 0x00000008 +#define F_IMM32 0x00000010 +#define F_IMM64 0x00000020 +#define F_DISP8 0x00000040 +#define F_DISP16 0x00000080 +#define F_DISP32 0x00000100 +#define F_RELATIVE 0x00000200 +#define F_ERROR 0x00001000 +#define F_ERROR_OPCODE 0x00002000 +#define F_ERROR_LENGTH 0x00004000 +#define F_ERROR_LOCK 0x00008000 +#define F_ERROR_OPERAND 0x00010000 +#define F_PREFIX_REPNZ 0x01000000 +#define F_PREFIX_REPX 0x02000000 +#define F_PREFIX_REP 0x03000000 +#define F_PREFIX_66 0x04000000 +#define F_PREFIX_67 0x08000000 +#define F_PREFIX_LOCK 0x10000000 +#define F_PREFIX_SEG 0x20000000 +#define F_PREFIX_REX 0x40000000 +#define F_PREFIX_ANY 0x7f000000 + +#define PREFIX_SEGMENT_CS 0x2e +#define PREFIX_SEGMENT_SS 0x36 +#define PREFIX_SEGMENT_DS 0x3e +#define PREFIX_SEGMENT_ES 0x26 +#define PREFIX_SEGMENT_FS 0x64 +#define PREFIX_SEGMENT_GS 0x65 +#define PREFIX_LOCK 0xf0 +#define PREFIX_REPNZ 0xf2 +#define PREFIX_REPX 0xf3 +#define PREFIX_OPERAND_SIZE 0x66 +#define PREFIX_ADDRESS_SIZE 0x67 + +#pragma pack(push,1) + +typedef struct { + uint8_t len; + uint8_t p_rep; + uint8_t p_lock; + uint8_t p_seg; + uint8_t p_66; + uint8_t p_67; + uint8_t rex; + uint8_t rex_w; + uint8_t rex_r; + uint8_t rex_x; + uint8_t rex_b; + uint8_t opcode; + uint8_t opcode2; + uint8_t modrm; + uint8_t modrm_mod; + uint8_t modrm_reg; + uint8_t modrm_rm; + uint8_t sib; + uint8_t sib_scale; + uint8_t sib_index; + uint8_t sib_base; + union { + uint8_t imm8; + uint16_t imm16; + uint32_t imm32; + uint64_t imm64; + } imm; + union { + uint8_t disp8; + uint16_t disp16; + uint32_t disp32; + } disp; + uint32_t flags; +} hde64s; + +#pragma pack(pop) + +#ifdef __cplusplus +extern "C" { +#endif + +/* __cdecl */ +unsigned int hde64_disasm(const void *code, hde64s *hs); + +#ifdef __cplusplus +} +#endif + +#endif /* _HDE64_H_ */ diff --git a/third_party/minhook/src/hde/pstdint.h b/third_party/minhook/src/hde/pstdint.h new file mode 100644 index 0000000..84d82a0 --- /dev/null +++ b/third_party/minhook/src/hde/pstdint.h @@ -0,0 +1,39 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <windows.h> + +// Integer types for HDE. +typedef INT8 int8_t; +typedef INT16 int16_t; +typedef INT32 int32_t; +typedef INT64 int64_t; +typedef UINT8 uint8_t; +typedef UINT16 uint16_t; +typedef UINT32 uint32_t; +typedef UINT64 uint64_t; diff --git a/third_party/minhook/src/hde/table64.h b/third_party/minhook/src/hde/table64.h new file mode 100644 index 0000000..01d4541 --- /dev/null +++ b/third_party/minhook/src/hde/table64.h @@ -0,0 +1,74 @@ +/* + * Hacker Disassembler Engine 64 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#define C_NONE 0x00 +#define C_MODRM 0x01 +#define C_IMM8 0x02 +#define C_IMM16 0x04 +#define C_IMM_P66 0x10 +#define C_REL8 0x20 +#define C_REL32 0x40 +#define C_GROUP 0x80 +#define C_ERROR 0xff + +#define PRE_ANY 0x00 +#define PRE_NONE 0x01 +#define PRE_F2 0x02 +#define PRE_F3 0x04 +#define PRE_66 0x08 +#define PRE_67 0x10 +#define PRE_LOCK 0x20 +#define PRE_SEG 0x40 +#define PRE_ALL 0xff + +#define DELTA_OPCODES 0x4a +#define DELTA_FPU_REG 0xfd +#define DELTA_FPU_MODRM 0x104 +#define DELTA_PREFIXES 0x13c +#define DELTA_OP_LOCK_OK 0x1ae +#define DELTA_OP2_LOCK_OK 0x1c6 +#define DELTA_OP_ONLY_MEM 0x1d8 +#define DELTA_OP2_ONLY_MEM 0x1e7 + +unsigned char hde64_table[] = { + 0xa5,0xaa,0xa5,0xb8,0xa5,0xaa,0xa5,0xaa,0xa5,0xb8,0xa5,0xb8,0xa5,0xb8,0xa5, + 0xb8,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xac,0xc0,0xcc,0xc0,0xa1,0xa1, + 0xa1,0xa1,0xb1,0xa5,0xa5,0xa6,0xc0,0xc0,0xd7,0xda,0xe0,0xc0,0xe4,0xc0,0xea, + 0xea,0xe0,0xe0,0x98,0xc8,0xee,0xf1,0xa5,0xd3,0xa5,0xa5,0xa1,0xea,0x9e,0xc0, + 0xc0,0xc2,0xc0,0xe6,0x03,0x7f,0x11,0x7f,0x01,0x7f,0x01,0x3f,0x01,0x01,0xab, + 0x8b,0x90,0x64,0x5b,0x5b,0x5b,0x5b,0x5b,0x92,0x5b,0x5b,0x76,0x90,0x92,0x92, + 0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x6a,0x73,0x90, + 0x5b,0x52,0x52,0x52,0x52,0x5b,0x5b,0x5b,0x5b,0x77,0x7c,0x77,0x85,0x5b,0x5b, + 0x70,0x5b,0x7a,0xaf,0x76,0x76,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b, + 0x5b,0x5b,0x86,0x01,0x03,0x01,0x04,0x03,0xd5,0x03,0xd5,0x03,0xcc,0x01,0xbc, + 0x03,0xf0,0x03,0x03,0x04,0x00,0x50,0x50,0x50,0x50,0xff,0x20,0x20,0x20,0x20, + 0x01,0x01,0x01,0x01,0xc4,0x02,0x10,0xff,0xff,0xff,0x01,0x00,0x03,0x11,0xff, + 0x03,0xc4,0xc6,0xc8,0x02,0x10,0x00,0xff,0xcc,0x01,0x01,0x01,0x00,0x00,0x00, + 0x00,0x01,0x01,0x03,0x01,0xff,0xff,0xc0,0xc2,0x10,0x11,0x02,0x03,0x01,0x01, + 0x01,0xff,0xff,0xff,0x00,0x00,0x00,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0x10, + 0x10,0x10,0x10,0x02,0x10,0x00,0x00,0xc6,0xc8,0x02,0x02,0x02,0x02,0x06,0x00, + 0x04,0x00,0x02,0xff,0x00,0xc0,0xc2,0x01,0x01,0x03,0x03,0x03,0xca,0x40,0x00, + 0x0a,0x00,0x04,0x00,0x00,0x00,0x00,0x7f,0x00,0x33,0x01,0x00,0x00,0x00,0x00, + 0x00,0x00,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0xff,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff, + 0x00,0x00,0x00,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0x00,0x00, + 0xff,0x40,0x40,0x40,0x40,0x41,0x49,0x40,0x40,0x40,0x40,0x4c,0x42,0x40,0x40, + 0x40,0x40,0x40,0x40,0x40,0x40,0x4f,0x44,0x53,0x40,0x40,0x40,0x44,0x57,0x43, + 0x5c,0x40,0x60,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40, + 0x40,0x40,0x64,0x66,0x6e,0x6b,0x40,0x40,0x6a,0x46,0x40,0x40,0x44,0x46,0x40, + 0x40,0x5b,0x44,0x40,0x40,0x00,0x00,0x00,0x00,0x06,0x06,0x06,0x06,0x01,0x06, + 0x06,0x02,0x06,0x06,0x00,0x06,0x00,0x0a,0x0a,0x00,0x00,0x00,0x02,0x07,0x07, + 0x06,0x02,0x0d,0x06,0x06,0x06,0x0e,0x05,0x05,0x02,0x02,0x00,0x00,0x04,0x04, + 0x04,0x04,0x05,0x06,0x06,0x06,0x00,0x00,0x00,0x0e,0x00,0x00,0x08,0x00,0x10, + 0x00,0x18,0x00,0x20,0x00,0x28,0x00,0x30,0x00,0x80,0x01,0x82,0x01,0x86,0x00, + 0xf6,0xcf,0xfe,0x3f,0xab,0x00,0xb0,0x00,0xb1,0x00,0xb3,0x00,0xba,0xf8,0xbb, + 0x00,0xc0,0x00,0xc1,0x00,0xc7,0xbf,0x62,0xff,0x00,0x8d,0xff,0x00,0xc4,0xff, + 0x00,0xc5,0xff,0x00,0xff,0xff,0xeb,0x01,0xff,0x0e,0x12,0x08,0x00,0x13,0x09, + 0x00,0x16,0x08,0x00,0x17,0x09,0x00,0x2b,0x09,0x00,0xae,0xff,0x07,0xb2,0xff, + 0x00,0xb4,0xff,0x00,0xb5,0xff,0x00,0xc3,0x01,0x00,0xc7,0xff,0xbf,0xe7,0x08, + 0x00,0xf0,0x02,0x00 +}; diff --git a/third_party/minhook/src/hook.c b/third_party/minhook/src/hook.c new file mode 100644 index 0000000..06f5696 --- /dev/null +++ b/third_party/minhook/src/hook.c @@ -0,0 +1,939 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <windows.h> +#include <tlhelp32.h> +#include <limits.h> + +#include "../include/MinHook.h" +#include "buffer.h" +#include "trampoline.h" + +#ifndef ARRAYSIZE + #define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0])) +#endif + +// Initial capacity of the HOOK_ENTRY buffer. +#define INITIAL_HOOK_CAPACITY 32 + +// Initial capacity of the thread IDs buffer. +#define INITIAL_THREAD_CAPACITY 128 + +// Special hook position values. +#define INVALID_HOOK_POS UINT_MAX +#define ALL_HOOKS_POS UINT_MAX + +// Freeze() action argument defines. +#define ACTION_DISABLE 0 +#define ACTION_ENABLE 1 +#define ACTION_APPLY_QUEUED 2 + +// Thread access rights for suspending/resuming threads. +#define THREAD_ACCESS \ + (THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SET_CONTEXT) + +// Hook information. +typedef struct _HOOK_ENTRY +{ + LPVOID pTarget; // Address of the target function. + LPVOID pDetour; // Address of the detour or relay function. + LPVOID pTrampoline; // Address of the trampoline function. + UINT8 backup[8]; // Original prologue of the target function. + + UINT8 patchAbove : 1; // Uses the hot patch area. + UINT8 isEnabled : 1; // Enabled. + UINT8 queueEnable : 1; // Queued for enabling/disabling when != isEnabled. + + UINT nIP : 4; // Count of the instruction boundaries. + UINT8 oldIPs[8]; // Instruction boundaries of the target function. + UINT8 newIPs[8]; // Instruction boundaries of the trampoline function. +} HOOK_ENTRY, *PHOOK_ENTRY; + +// Suspended threads for Freeze()/Unfreeze(). +typedef struct _FROZEN_THREADS +{ + LPDWORD pItems; // Data heap + UINT capacity; // Size of allocated data heap, items + UINT size; // Actual number of data items +} FROZEN_THREADS, *PFROZEN_THREADS; + +//------------------------------------------------------------------------- +// Global Variables: +//------------------------------------------------------------------------- + +// Spin lock flag for EnterSpinLock()/LeaveSpinLock(). +static volatile LONG g_isLocked = FALSE; + +// Private heap handle. If not NULL, this library is initialized. +static HANDLE g_hHeap = NULL; + +// Hook entries. +static struct +{ + PHOOK_ENTRY pItems; // Data heap + UINT capacity; // Size of allocated data heap, items + UINT size; // Actual number of data items +} g_hooks; + +//------------------------------------------------------------------------- +// Returns INVALID_HOOK_POS if not found. +static UINT FindHookEntry(LPVOID pTarget) +{ + UINT i; + for (i = 0; i < g_hooks.size; ++i) + { + if ((ULONG_PTR)pTarget == (ULONG_PTR)g_hooks.pItems[i].pTarget) + return i; + } + + return INVALID_HOOK_POS; +} + +//------------------------------------------------------------------------- +static PHOOK_ENTRY AddHookEntry() +{ + if (g_hooks.pItems == NULL) + { + g_hooks.capacity = INITIAL_HOOK_CAPACITY; + g_hooks.pItems = (PHOOK_ENTRY)HeapAlloc( + g_hHeap, 0, g_hooks.capacity * sizeof(HOOK_ENTRY)); + if (g_hooks.pItems == NULL) + return NULL; + } + else if (g_hooks.size >= g_hooks.capacity) + { + PHOOK_ENTRY p = (PHOOK_ENTRY)HeapReAlloc( + g_hHeap, 0, g_hooks.pItems, (g_hooks.capacity * 2) * sizeof(HOOK_ENTRY)); + if (p == NULL) + return NULL; + + g_hooks.capacity *= 2; + g_hooks.pItems = p; + } + + return &g_hooks.pItems[g_hooks.size++]; +} + +//------------------------------------------------------------------------- +static VOID DeleteHookEntry(UINT pos) +{ + if (pos < g_hooks.size - 1) + g_hooks.pItems[pos] = g_hooks.pItems[g_hooks.size - 1]; + + g_hooks.size--; + + if (g_hooks.capacity / 2 >= INITIAL_HOOK_CAPACITY && g_hooks.capacity / 2 >= g_hooks.size) + { + PHOOK_ENTRY p = (PHOOK_ENTRY)HeapReAlloc( + g_hHeap, 0, g_hooks.pItems, (g_hooks.capacity / 2) * sizeof(HOOK_ENTRY)); + if (p == NULL) + return; + + g_hooks.capacity /= 2; + g_hooks.pItems = p; + } +} + +//------------------------------------------------------------------------- +static DWORD_PTR FindOldIP(PHOOK_ENTRY pHook, DWORD_PTR ip) +{ + UINT i; + + if (pHook->patchAbove && ip == ((DWORD_PTR)pHook->pTarget - sizeof(JMP_REL))) + return (DWORD_PTR)pHook->pTarget; + + for (i = 0; i < pHook->nIP; ++i) + { + if (ip == ((DWORD_PTR)pHook->pTrampoline + pHook->newIPs[i])) + return (DWORD_PTR)pHook->pTarget + pHook->oldIPs[i]; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Check relay function. + if (ip == (DWORD_PTR)pHook->pDetour) + return (DWORD_PTR)pHook->pTarget; +#endif + + return 0; +} + +//------------------------------------------------------------------------- +static DWORD_PTR FindNewIP(PHOOK_ENTRY pHook, DWORD_PTR ip) +{ + UINT i; + for (i = 0; i < pHook->nIP; ++i) + { + if (ip == ((DWORD_PTR)pHook->pTarget + pHook->oldIPs[i])) + return (DWORD_PTR)pHook->pTrampoline + pHook->newIPs[i]; + } + + return 0; +} + +//------------------------------------------------------------------------- +static VOID ProcessThreadIPs(HANDLE hThread, UINT pos, UINT action) +{ + // If the thread suspended in the overwritten area, + // move IP to the proper address. + + CONTEXT c; +#if defined(_M_X64) || defined(__x86_64__) + DWORD64 *pIP = &c.Rip; +#else + DWORD *pIP = &c.Eip; +#endif + UINT count; + + c.ContextFlags = CONTEXT_CONTROL; + if (!GetThreadContext(hThread, &c)) + return; + + if (pos == ALL_HOOKS_POS) + { + pos = 0; + count = g_hooks.size; + } + else + { + count = pos + 1; + } + + for (; pos < count; ++pos) + { + PHOOK_ENTRY pHook = &g_hooks.pItems[pos]; + BOOL enable; + DWORD_PTR ip; + + switch (action) + { + case ACTION_DISABLE: + enable = FALSE; + break; + + case ACTION_ENABLE: + enable = TRUE; + break; + + default: // ACTION_APPLY_QUEUED + enable = pHook->queueEnable; + break; + } + if (pHook->isEnabled == enable) + continue; + + if (enable) + ip = FindNewIP(pHook, *pIP); + else + ip = FindOldIP(pHook, *pIP); + + if (ip != 0) + { + *pIP = ip; + SetThreadContext(hThread, &c); + } + } +} + +//------------------------------------------------------------------------- +static BOOL EnumerateThreads(PFROZEN_THREADS pThreads) +{ + BOOL succeeded = FALSE; + + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (hSnapshot != INVALID_HANDLE_VALUE) + { + THREADENTRY32 te; + te.dwSize = sizeof(THREADENTRY32); + if (Thread32First(hSnapshot, &te)) + { + succeeded = TRUE; + do + { + if (te.dwSize >= (FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(DWORD)) + && te.th32OwnerProcessID == GetCurrentProcessId() + && te.th32ThreadID != GetCurrentThreadId()) + { + if (pThreads->pItems == NULL) + { + pThreads->capacity = INITIAL_THREAD_CAPACITY; + pThreads->pItems + = (LPDWORD)HeapAlloc(g_hHeap, 0, pThreads->capacity * sizeof(DWORD)); + if (pThreads->pItems == NULL) + { + succeeded = FALSE; + break; + } + } + else if (pThreads->size >= pThreads->capacity) + { + LPDWORD p; + pThreads->capacity *= 2; + p = (LPDWORD)HeapReAlloc( + g_hHeap, 0, pThreads->pItems, pThreads->capacity * sizeof(DWORD)); + if (p == NULL) + { + succeeded = FALSE; + break; + } + + pThreads->pItems = p; + } + pThreads->pItems[pThreads->size++] = te.th32ThreadID; + } + + te.dwSize = sizeof(THREADENTRY32); + } while (Thread32Next(hSnapshot, &te)); + + if (succeeded && GetLastError() != ERROR_NO_MORE_FILES) + succeeded = FALSE; + + if (!succeeded && pThreads->pItems != NULL) + { + HeapFree(g_hHeap, 0, pThreads->pItems); + pThreads->pItems = NULL; + } + } + CloseHandle(hSnapshot); + } + + return succeeded; +} + +//------------------------------------------------------------------------- +static MH_STATUS Freeze(PFROZEN_THREADS pThreads, UINT pos, UINT action) +{ + MH_STATUS status = MH_OK; + + pThreads->pItems = NULL; + pThreads->capacity = 0; + pThreads->size = 0; + if (!EnumerateThreads(pThreads)) + { + status = MH_ERROR_MEMORY_ALLOC; + } + else if (pThreads->pItems != NULL) + { + UINT i; + for (i = 0; i < pThreads->size; ++i) + { + HANDLE hThread = OpenThread(THREAD_ACCESS, FALSE, pThreads->pItems[i]); + BOOL suspended = FALSE; + if (hThread != NULL) + { + DWORD result = SuspendThread(hThread); + if (result != 0xFFFFFFFF) + { + suspended = TRUE; + ProcessThreadIPs(hThread, pos, action); + } + CloseHandle(hThread); + } + + if (!suspended) + { + // Mark thread as not suspended, so it's not resumed later on. + pThreads->pItems[i] = 0; + } + } + } + + return status; +} + +//------------------------------------------------------------------------- +static VOID Unfreeze(PFROZEN_THREADS pThreads) +{ + if (pThreads->pItems != NULL) + { + UINT i; + for (i = 0; i < pThreads->size; ++i) + { + DWORD threadId = pThreads->pItems[i]; + if (threadId != 0) + { + HANDLE hThread = OpenThread(THREAD_ACCESS, FALSE, threadId); + if (hThread != NULL) + { + ResumeThread(hThread); + CloseHandle(hThread); + } + } + } + + HeapFree(g_hHeap, 0, pThreads->pItems); + } +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableHookLL(UINT pos, BOOL enable) +{ + PHOOK_ENTRY pHook = &g_hooks.pItems[pos]; + DWORD oldProtect; + SIZE_T patchSize = sizeof(JMP_REL); + LPBYTE pPatchTarget = (LPBYTE)pHook->pTarget; + + if (pHook->patchAbove) + { + pPatchTarget -= sizeof(JMP_REL); + patchSize += sizeof(JMP_REL_SHORT); + } + + if (!VirtualProtect(pPatchTarget, patchSize, PAGE_EXECUTE_READWRITE, &oldProtect)) + return MH_ERROR_MEMORY_PROTECT; + + if (enable) + { + PJMP_REL pJmp = (PJMP_REL)pPatchTarget; + pJmp->opcode = 0xE9; + pJmp->operand = (INT32)((LPBYTE)pHook->pDetour - (pPatchTarget + sizeof(JMP_REL))); + + if (pHook->patchAbove) + { + PJMP_REL_SHORT pShortJmp = (PJMP_REL_SHORT)pHook->pTarget; + pShortJmp->opcode = 0xEB; + pShortJmp->operand = (INT8)(0 - (sizeof(JMP_REL_SHORT) + sizeof(JMP_REL))); + } + } + else + { + if (pHook->patchAbove) + memcpy(pPatchTarget, pHook->backup, sizeof(JMP_REL) + sizeof(JMP_REL_SHORT)); + else + memcpy(pPatchTarget, pHook->backup, sizeof(JMP_REL)); + } + + VirtualProtect(pPatchTarget, patchSize, oldProtect, &oldProtect); + + // Just-in-case measure. + FlushInstructionCache(GetCurrentProcess(), pPatchTarget, patchSize); + + pHook->isEnabled = enable; + pHook->queueEnable = enable; + + return MH_OK; +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableAllHooksLL(BOOL enable) +{ + MH_STATUS status = MH_OK; + UINT i, first = INVALID_HOOK_POS; + + for (i = 0; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != enable) + { + first = i; + break; + } + } + + if (first != INVALID_HOOK_POS) + { + FROZEN_THREADS threads; + status = Freeze(&threads, ALL_HOOKS_POS, enable ? ACTION_ENABLE : ACTION_DISABLE); + if (status == MH_OK) + { + for (i = first; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != enable) + { + status = EnableHookLL(i, enable); + if (status != MH_OK) + break; + } + } + + Unfreeze(&threads); + } + } + + return status; +} + +//------------------------------------------------------------------------- +static VOID EnterSpinLock(VOID) +{ + SIZE_T spinCount = 0; + + // Wait until the flag is FALSE. + while (InterlockedCompareExchange(&g_isLocked, TRUE, FALSE) != FALSE) + { + // No need to generate a memory barrier here, since InterlockedCompareExchange() + // generates a full memory barrier itself. + + // Prevent the loop from being too busy. + if (spinCount < 32) + Sleep(0); + else + Sleep(1); + + spinCount++; + } +} + +//------------------------------------------------------------------------- +static VOID LeaveSpinLock(VOID) +{ + // No need to generate a memory barrier here, since InterlockedExchange() + // generates a full memory barrier itself. + + InterlockedExchange(&g_isLocked, FALSE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_Initialize(VOID) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap == NULL) + { + g_hHeap = HeapCreate(0, 0, 0); + if (g_hHeap != NULL) + { + // Initialize the internal function buffer. + InitializeBuffer(); + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_ALREADY_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_Uninitialize(VOID) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + status = EnableAllHooksLL(FALSE); + if (status == MH_OK) + { + // Free the internal function buffer. + + // HeapFree is actually not required, but some tools detect a false + // memory leak without HeapFree. + + UninitializeBuffer(); + + HeapFree(g_hHeap, 0, g_hooks.pItems); + HeapDestroy(g_hHeap); + + g_hHeap = NULL; + + g_hooks.pItems = NULL; + g_hooks.capacity = 0; + g_hooks.size = 0; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (IsExecutableAddress(pTarget) && IsExecutableAddress(pDetour)) + { + UINT pos = FindHookEntry(pTarget); + if (pos == INVALID_HOOK_POS) + { + LPVOID pBuffer = AllocateBuffer(pTarget); + if (pBuffer != NULL) + { + TRAMPOLINE ct; + + ct.pTarget = pTarget; + ct.pDetour = pDetour; + ct.pTrampoline = pBuffer; + if (CreateTrampolineFunction(&ct)) + { + PHOOK_ENTRY pHook = AddHookEntry(); + if (pHook != NULL) + { + pHook->pTarget = ct.pTarget; +#if defined(_M_X64) || defined(__x86_64__) + pHook->pDetour = ct.pRelay; +#else + pHook->pDetour = ct.pDetour; +#endif + pHook->pTrampoline = ct.pTrampoline; + pHook->patchAbove = ct.patchAbove; + pHook->isEnabled = FALSE; + pHook->queueEnable = FALSE; + pHook->nIP = ct.nIP; + memcpy(pHook->oldIPs, ct.oldIPs, ARRAYSIZE(ct.oldIPs)); + memcpy(pHook->newIPs, ct.newIPs, ARRAYSIZE(ct.newIPs)); + + // Back up the target function. + + if (ct.patchAbove) + { + memcpy( + pHook->backup, + (LPBYTE)pTarget - sizeof(JMP_REL), + sizeof(JMP_REL) + sizeof(JMP_REL_SHORT)); + } + else + { + memcpy(pHook->backup, pTarget, sizeof(JMP_REL)); + } + + if (ppOriginal != NULL) + *ppOriginal = pHook->pTrampoline; + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_UNSUPPORTED_FUNCTION; + } + + if (status != MH_OK) + { + FreeBuffer(pBuffer); + } + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_ALREADY_CREATED; + } + } + else + { + status = MH_ERROR_NOT_EXECUTABLE; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + if (g_hooks.pItems[pos].isEnabled) + { + FROZEN_THREADS threads; + status = Freeze(&threads, pos, ACTION_DISABLE); + if (status == MH_OK) + { + status = EnableHookLL(pos, FALSE); + + Unfreeze(&threads); + } + } + + if (status == MH_OK) + { + FreeBuffer(g_hooks.pItems[pos].pTrampoline); + DeleteHookEntry(pos); + } + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableHook(LPVOID pTarget, BOOL enable) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (pTarget == MH_ALL_HOOKS) + { + status = EnableAllHooksLL(enable); + } + else + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + if (g_hooks.pItems[pos].isEnabled != enable) + { + FROZEN_THREADS threads; + status = Freeze(&threads, pos, ACTION_ENABLE); + if (status == MH_OK) + { + status = EnableHookLL(pos, enable); + + Unfreeze(&threads); + } + } + else + { + status = enable ? MH_ERROR_ENABLED : MH_ERROR_DISABLED; + } + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget) +{ + return EnableHook(pTarget, TRUE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget) +{ + return EnableHook(pTarget, FALSE); +} + +//------------------------------------------------------------------------- +static MH_STATUS QueueHook(LPVOID pTarget, BOOL queueEnable) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (pTarget == MH_ALL_HOOKS) + { + UINT i; + for (i = 0; i < g_hooks.size; ++i) + g_hooks.pItems[i].queueEnable = queueEnable; + } + else + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + g_hooks.pItems[pos].queueEnable = queueEnable; + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget) +{ + return QueueHook(pTarget, TRUE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget) +{ + return QueueHook(pTarget, FALSE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_ApplyQueued(VOID) +{ + MH_STATUS status = MH_OK; + UINT i, first = INVALID_HOOK_POS; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + for (i = 0; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != g_hooks.pItems[i].queueEnable) + { + first = i; + break; + } + } + + if (first != INVALID_HOOK_POS) + { + FROZEN_THREADS threads; + status = Freeze(&threads, ALL_HOOKS_POS, ACTION_APPLY_QUEUED); + if (status == MH_OK) + { + for (i = first; i < g_hooks.size; ++i) + { + PHOOK_ENTRY pHook = &g_hooks.pItems[i]; + if (pHook->isEnabled != pHook->queueEnable) + { + status = EnableHookLL(i, pHook->queueEnable); + if (status != MH_OK) + break; + } + } + + Unfreeze(&threads); + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, + LPVOID *ppOriginal, LPVOID *ppTarget) +{ + HMODULE hModule; + LPVOID pTarget; + + hModule = GetModuleHandleW(pszModule); + if (hModule == NULL) + return MH_ERROR_MODULE_NOT_FOUND; + + pTarget = (LPVOID)GetProcAddress(hModule, pszProcName); + if (pTarget == NULL) + return MH_ERROR_FUNCTION_NOT_FOUND; + + if (ppTarget != NULL) + *ppTarget = pTarget; + + return MH_CreateHook(pTarget, pDetour, ppOriginal); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal) +{ + return MH_CreateHookApiEx(pszModule, pszProcName, pDetour, ppOriginal, NULL); +} + +//------------------------------------------------------------------------- +const char *WINAPI MH_StatusToString(MH_STATUS status) +{ +#define MH_ST2STR(x) \ + case x: \ + return #x; + + switch (status) { + MH_ST2STR(MH_UNKNOWN) + MH_ST2STR(MH_OK) + MH_ST2STR(MH_ERROR_ALREADY_INITIALIZED) + MH_ST2STR(MH_ERROR_NOT_INITIALIZED) + MH_ST2STR(MH_ERROR_ALREADY_CREATED) + MH_ST2STR(MH_ERROR_NOT_CREATED) + MH_ST2STR(MH_ERROR_ENABLED) + MH_ST2STR(MH_ERROR_DISABLED) + MH_ST2STR(MH_ERROR_NOT_EXECUTABLE) + MH_ST2STR(MH_ERROR_UNSUPPORTED_FUNCTION) + MH_ST2STR(MH_ERROR_MEMORY_ALLOC) + MH_ST2STR(MH_ERROR_MEMORY_PROTECT) + MH_ST2STR(MH_ERROR_MODULE_NOT_FOUND) + MH_ST2STR(MH_ERROR_FUNCTION_NOT_FOUND) + } + +#undef MH_ST2STR + + return "(unknown)"; +} diff --git a/third_party/minhook/src/trampoline.c b/third_party/minhook/src/trampoline.c new file mode 100644 index 0000000..cf8ce05 --- /dev/null +++ b/third_party/minhook/src/trampoline.c @@ -0,0 +1,320 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <windows.h> + +#if defined(_MSC_VER) && !defined(MINHOOK_DISABLE_INTRINSICS) + #define ALLOW_INTRINSICS + #include <intrin.h> +#endif + +#ifndef ARRAYSIZE + #define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0])) +#endif + +#if defined(_M_X64) || defined(__x86_64__) + #include "./hde/hde64.h" + typedef hde64s HDE; + #define HDE_DISASM(code, hs) hde64_disasm(code, hs) +#else + #include "./hde/hde32.h" + typedef hde32s HDE; + #define HDE_DISASM(code, hs) hde32_disasm(code, hs) +#endif + +#include "trampoline.h" +#include "buffer.h" + +// Maximum size of a trampoline function. +#if defined(_M_X64) || defined(__x86_64__) + #define TRAMPOLINE_MAX_SIZE (MEMORY_SLOT_SIZE - sizeof(JMP_ABS)) +#else + #define TRAMPOLINE_MAX_SIZE MEMORY_SLOT_SIZE +#endif + +//------------------------------------------------------------------------- +static BOOL IsCodePadding(LPBYTE pInst, UINT size) +{ + UINT i; + + if (pInst[0] != 0x00 && pInst[0] != 0x90 && pInst[0] != 0xCC) + return FALSE; + + for (i = 1; i < size; ++i) + { + if (pInst[i] != pInst[0]) + return FALSE; + } + return TRUE; +} + +//------------------------------------------------------------------------- +BOOL CreateTrampolineFunction(PTRAMPOLINE ct) +{ +#if defined(_M_X64) || defined(__x86_64__) + CALL_ABS call = { + 0xFF, 0x15, 0x00000002, // FF15 00000002: CALL [RIP+8] + 0xEB, 0x08, // EB 08: JMP +10 + 0x0000000000000000ULL // Absolute destination address + }; + JMP_ABS jmp = { + 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] + 0x0000000000000000ULL // Absolute destination address + }; + JCC_ABS jcc = { + 0x70, 0x0E, // 7* 0E: J** +16 + 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] + 0x0000000000000000ULL // Absolute destination address + }; +#else + CALL_REL call = { + 0xE8, // E8 xxxxxxxx: CALL +5+xxxxxxxx + 0x00000000 // Relative destination address + }; + JMP_REL jmp = { + 0xE9, // E9 xxxxxxxx: JMP +5+xxxxxxxx + 0x00000000 // Relative destination address + }; + JCC_REL jcc = { + 0x0F, 0x80, // 0F8* xxxxxxxx: J** +6+xxxxxxxx + 0x00000000 // Relative destination address + }; +#endif + + UINT8 oldPos = 0; + UINT8 newPos = 0; + ULONG_PTR jmpDest = 0; // Destination address of an internal jump. + BOOL finished = FALSE; // Is the function completed? +#if defined(_M_X64) || defined(__x86_64__) + UINT8 instBuf[16]; +#endif + + ct->patchAbove = FALSE; + ct->nIP = 0; + + do + { + HDE hs; + UINT copySize; + LPVOID pCopySrc; + ULONG_PTR pOldInst = (ULONG_PTR)ct->pTarget + oldPos; + ULONG_PTR pNewInst = (ULONG_PTR)ct->pTrampoline + newPos; + + copySize = HDE_DISASM((LPVOID)pOldInst, &hs); + if (hs.flags & F_ERROR) + return FALSE; + + pCopySrc = (LPVOID)pOldInst; + if (oldPos >= sizeof(JMP_REL)) + { + // The trampoline function is long enough. + // Complete the function with the jump to the target function. +#if defined(_M_X64) || defined(__x86_64__) + jmp.address = pOldInst; +#else + jmp.operand = (INT32)(pOldInst - (pNewInst + sizeof(jmp))); +#endif + pCopySrc = &jmp; + copySize = sizeof(jmp); + + finished = TRUE; + } +#if defined(_M_X64) || defined(__x86_64__) + else if ((hs.modrm & 0xC7) == 0x05) + { + // Instructions using RIP relative addressing. (ModR/M = 00???101B) + + // Modify the RIP relative address. + PUINT32 pRelAddr; + + // Avoid using memcpy to reduce the footprint. +#ifndef ALLOW_INTRINSICS + memcpy(instBuf, (LPBYTE)pOldInst, copySize); +#else + __movsb(instBuf, (LPBYTE)pOldInst, copySize); +#endif + pCopySrc = instBuf; + + // Relative address is stored at (instruction length - immediate value length - 4). + pRelAddr = (PUINT32)(instBuf + hs.len - ((hs.flags & 0x3C) >> 2) - 4); + *pRelAddr + = (UINT32)((pOldInst + hs.len + (INT32)hs.disp.disp32) - (pNewInst + hs.len)); + + // Complete the function if JMP (FF /4). + if (hs.opcode == 0xFF && hs.modrm_reg == 4) + finished = TRUE; + } +#endif + else if (hs.opcode == 0xE8) + { + // Direct relative CALL + ULONG_PTR dest = pOldInst + hs.len + (INT32)hs.imm.imm32; +#if defined(_M_X64) || defined(__x86_64__) + call.address = dest; +#else + call.operand = (INT32)(dest - (pNewInst + sizeof(call))); +#endif + pCopySrc = &call; + copySize = sizeof(call); + } + else if ((hs.opcode & 0xFD) == 0xE9) + { + // Direct relative JMP (EB or E9) + ULONG_PTR dest = pOldInst + hs.len; + + if (hs.opcode == 0xEB) // isShort jmp + dest += (INT8)hs.imm.imm8; + else + dest += (INT32)hs.imm.imm32; + + // Simply copy an internal jump. + if ((ULONG_PTR)ct->pTarget <= dest + && dest < ((ULONG_PTR)ct->pTarget + sizeof(JMP_REL))) + { + if (jmpDest < dest) + jmpDest = dest; + } + else + { +#if defined(_M_X64) || defined(__x86_64__) + jmp.address = dest; +#else + jmp.operand = (INT32)(dest - (pNewInst + sizeof(jmp))); +#endif + pCopySrc = &jmp; + copySize = sizeof(jmp); + + // Exit the function if it is not in the branch. + finished = (pOldInst >= jmpDest); + } + } + else if ((hs.opcode & 0xF0) == 0x70 + || (hs.opcode & 0xFC) == 0xE0 + || (hs.opcode2 & 0xF0) == 0x80) + { + // Direct relative Jcc + ULONG_PTR dest = pOldInst + hs.len; + + if ((hs.opcode & 0xF0) == 0x70 // Jcc + || (hs.opcode & 0xFC) == 0xE0) // LOOPNZ/LOOPZ/LOOP/JECXZ + dest += (INT8)hs.imm.imm8; + else + dest += (INT32)hs.imm.imm32; + + // Simply copy an internal jump. + if ((ULONG_PTR)ct->pTarget <= dest + && dest < ((ULONG_PTR)ct->pTarget + sizeof(JMP_REL))) + { + if (jmpDest < dest) + jmpDest = dest; + } + else if ((hs.opcode & 0xFC) == 0xE0) + { + // LOOPNZ/LOOPZ/LOOP/JCXZ/JECXZ to the outside are not supported. + return FALSE; + } + else + { + UINT8 cond = ((hs.opcode != 0x0F ? hs.opcode : hs.opcode2) & 0x0F); +#if defined(_M_X64) || defined(__x86_64__) + // Invert the condition in x64 mode to simplify the conditional jump logic. + jcc.opcode = 0x71 ^ cond; + jcc.address = dest; +#else + jcc.opcode1 = 0x80 | cond; + jcc.operand = (INT32)(dest - (pNewInst + sizeof(jcc))); +#endif + pCopySrc = &jcc; + copySize = sizeof(jcc); + } + } + else if ((hs.opcode & 0xFE) == 0xC2) + { + // RET (C2 or C3) + + // Complete the function if not in a branch. + finished = (pOldInst >= jmpDest); + } + + // Can't alter the instruction length in a branch. + if (pOldInst < jmpDest && copySize != hs.len) + return FALSE; + + // Trampoline function is too large. + if ((newPos + copySize) > TRAMPOLINE_MAX_SIZE) + return FALSE; + + // Trampoline function has too many instructions. + if (ct->nIP >= ARRAYSIZE(ct->oldIPs)) + return FALSE; + + ct->oldIPs[ct->nIP] = oldPos; + ct->newIPs[ct->nIP] = newPos; + ct->nIP++; + + // Avoid using memcpy to reduce the footprint. +#ifndef ALLOW_INTRINSICS + memcpy((LPBYTE)ct->pTrampoline + newPos, pCopySrc, copySize); +#else + __movsb((LPBYTE)ct->pTrampoline + newPos, (LPBYTE)pCopySrc, copySize); +#endif + newPos += copySize; + oldPos += hs.len; + } while (!finished); + + // Is there enough place for a long jump? + if (oldPos < sizeof(JMP_REL) + && !IsCodePadding((LPBYTE)ct->pTarget + oldPos, sizeof(JMP_REL) - oldPos)) + { + // Is there enough place for a short jump? + if (oldPos < sizeof(JMP_REL_SHORT) + && !IsCodePadding((LPBYTE)ct->pTarget + oldPos, sizeof(JMP_REL_SHORT) - oldPos)) + { + return FALSE; + } + + // Can we place the long jump above the function? + if (!IsExecutableAddress((LPBYTE)ct->pTarget - sizeof(JMP_REL))) + return FALSE; + + if (!IsCodePadding((LPBYTE)ct->pTarget - sizeof(JMP_REL), sizeof(JMP_REL))) + return FALSE; + + ct->patchAbove = TRUE; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Create a relay function. + jmp.address = (ULONG_PTR)ct->pDetour; + + ct->pRelay = (LPBYTE)ct->pTrampoline + newPos; + memcpy(ct->pRelay, &jmp, sizeof(jmp)); +#endif + + return TRUE; +} diff --git a/third_party/minhook/src/trampoline.h b/third_party/minhook/src/trampoline.h new file mode 100644 index 0000000..f7efb36 --- /dev/null +++ b/third_party/minhook/src/trampoline.h @@ -0,0 +1,105 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#pragma pack(push, 1) + +// Structs for writing x86/x64 instructions. + +// 8-bit relative jump. +typedef struct _JMP_REL_SHORT +{ + UINT8 opcode; // EB xx: JMP +2+xx + INT8 operand; // Relative destination address +} JMP_REL_SHORT, *PJMP_REL_SHORT; + +// 32-bit direct relative jump/call. +typedef struct _JMP_REL +{ + UINT8 opcode; // E9/E8 xxxxxxxx: JMP/CALL +5+xxxxxxxx + INT32 operand; // Relative destination address +} JMP_REL, *PJMP_REL, CALL_REL; + +// 64-bit indirect absolute jump. +typedef struct _JMP_ABS +{ + UINT8 opcode0; // FF25 00000000: JMP [+6] + UINT8 opcode1; + UINT32 dummy; + UINT64 address; // Absolute destination address +} JMP_ABS, *PJMP_ABS; + +// 64-bit indirect absolute call. +typedef struct _CALL_ABS +{ + UINT8 opcode0; // FF15 00000002: CALL [+6] + UINT8 opcode1; + UINT32 dummy0; + UINT8 dummy1; // EB 08: JMP +10 + UINT8 dummy2; + UINT64 address; // Absolute destination address +} CALL_ABS; + +// 32-bit direct relative conditional jumps. +typedef struct _JCC_REL +{ + UINT8 opcode0; // 0F8* xxxxxxxx: J** +6+xxxxxxxx + UINT8 opcode1; + INT32 operand; // Relative destination address +} JCC_REL; + +// 64bit indirect absolute conditional jumps that x64 lacks. +typedef struct _JCC_ABS +{ + UINT8 opcode; // 7* 0E: J** +16 + UINT8 dummy0; + UINT8 dummy1; // FF25 00000000: JMP [+6] + UINT8 dummy2; + UINT32 dummy3; + UINT64 address; // Absolute destination address +} JCC_ABS; + +#pragma pack(pop) + +typedef struct _TRAMPOLINE +{ + LPVOID pTarget; // [In] Address of the target function. + LPVOID pDetour; // [In] Address of the detour function. + LPVOID pTrampoline; // [In] Buffer address for the trampoline and relay function. + +#if defined(_M_X64) || defined(__x86_64__) + LPVOID pRelay; // [Out] Address of the relay function. +#endif + BOOL patchAbove; // [Out] Should use the hot patch area? + UINT nIP; // [Out] Number of the instruction boundaries. + UINT8 oldIPs[8]; // [Out] Instruction boundaries of the target function. + UINT8 newIPs[8]; // [Out] Instruction boundaries of the trampoline function. +} TRAMPOLINE, *PTRAMPOLINE; + +BOOL CreateTrampolineFunction(PTRAMPOLINE ct); |
