diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/usvfs/src | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/usvfs/src')
73 files changed, 15649 insertions, 0 deletions
diff --git a/libs/usvfs/src/shared/CMakeLists.txt b/libs/usvfs/src/shared/CMakeLists.txt new file mode 100644 index 0000000..b1a7c05 --- /dev/null +++ b/libs/usvfs/src/shared/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Boost CONFIG REQUIRED COMPONENTS algorithm interprocess filesystem) +find_package(spdlog CONFIG REQUIRED) + +file(GLOB sources "*.cpp" "*.h") +source_group("" FILES ${sources}) + +add_library(shared STATIC ${sources}) +target_link_libraries(shared + PUBLIC + Boost::algorithm Boost::interprocess comsuppw + Boost::filesystem spdlog::spdlog_header_only) +target_include_directories(shared + PUBLIC ${PROJECT_SOURCE_DIR}/include/usvfs ${CMAKE_CURRENT_SOURCE_DIR}) +target_precompile_headers(shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/pch.h) +target_compile_options(shared PUBLIC /FI"pch.h") +target_compile_definitions(shared PUBLIC + SPDLOG_USE_STD_FORMAT SPDLOG_NO_NAME SPDLOG_NO_REGISTRY_MUTEX + BOOST_INTERPROCESS_SHARED_DIR_FUNC BOOST_CONFIG_SUPPRESS_OUTDATED_MESSAGE + NOMINMAX _WINDOWS _UNICODE UNICODE + _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR +) diff --git a/libs/usvfs/src/shared/addrtools.h b/libs/usvfs/src/shared/addrtools.h new file mode 100644 index 0000000..b888ebf --- /dev/null +++ b/libs/usvfs/src/shared/addrtools.h @@ -0,0 +1,64 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "windows_sane.h" + +namespace usvfs::shared +{ + +#ifdef _M_AMD64 +typedef DWORD64 REGWORD; +#elif _M_IX86 +typedef DWORD REGWORD; +#endif + +inline LPVOID AddrAdd(LPVOID address, size_t offset) +{ + return reinterpret_cast<LPVOID>(reinterpret_cast<LPBYTE>(address) + offset); +} + +inline std::ptrdiff_t AddrDiff(LPVOID lhs, LPVOID rhs) +{ + return reinterpret_cast<LPBYTE>(lhs) - reinterpret_cast<LPBYTE>(rhs); +} + +// implicitly cast pointer to void*, from there cast to target type. +// This is supposed to be safer than directly reinterpret-casting +template <typename T> +inline T void_ptr_cast(void* ptr) +{ + return reinterpret_cast<T>(ptr); +} + +template <> +inline int64_t void_ptr_cast(void* ptr) +{ + return reinterpret_cast<int64_t>(ptr); +} + +template <> +inline uint64_t void_ptr_cast(void* ptr) +{ + return reinterpret_cast<uint64_t>(ptr); +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/directory_tree.cpp b/libs/usvfs/src/shared/directory_tree.cpp new file mode 100644 index 0000000..34cc797 --- /dev/null +++ b/libs/usvfs/src/shared/directory_tree.cpp @@ -0,0 +1,43 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "directory_tree.h" +#include "tree_container.h" + +namespace usvfs::shared +{ + +fs::path::iterator nextIter(const fs::path::iterator& iter, + const fs::path::iterator& end) +{ + fs::path::iterator next = iter; + advanceIter(next, end); + return next; +} + +void advanceIter(fs::path::iterator& iter, const fs::path::iterator& end) +{ + ++iter; + while (iter != end && (iter->wstring() == L"/" || iter->wstring() == L"\\" || + iter->wstring() == L".")) + ++iter; +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/directory_tree.h b/libs/usvfs/src/shared/directory_tree.h new file mode 100644 index 0000000..f82187f --- /dev/null +++ b/libs/usvfs/src/shared/directory_tree.h @@ -0,0 +1,629 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "exceptionex.h" +#include "logging.h" +#include "shared_memory.h" +#include "stringutils.h" +#include "wildcard.h" + +// simplify unit tests by allowing access to private members +#ifndef PRIVATE +#define PRIVATE private +#endif // PRIVATE + +namespace usvfs::shared +{ + +template <typename T, typename U> +struct SHMDataCreator +{ + static T create(const U& source, const VoidAllocatorT& allocator) + { + return T(source, allocator); + } +}; + +template <typename T, typename U> +T createData(const U& source, const VoidAllocatorT& allocator) +{ + return SHMDataCreator<T, U>::create(source, allocator); +} + +template <typename T> +T createDataEmpty(const VoidAllocatorT& allocator); + +template <typename T> +void dataAssign(T& destination, const T& source); + +// crappy little workaround for fs::path iterating over path separators +fs::path::iterator nextIter(const fs::path::iterator& iter, + const fs::path::iterator& end); + +void advanceIter(fs::path::iterator& iter, const fs::path::iterator& end); + +// decomposes a path into its components +// +class DecomposablePath +{ +public: + explicit DecomposablePath(std::string s) : m_s(std::move(s)), m_begin(0), m_end(0) + { + m_end = nextSeparator(m_begin); + } + + // move to the next component, returns false when there are no more components + // + bool next() + { + for (;;) { + if (m_end >= m_s.size()) { + // done + m_begin = m_end; + return false; + } + + // move begin to one past the last separator found + m_begin = m_end + 1; + + // find the next separator + m_end = nextSeparator(m_begin); + + // check for components that should be ignored: + // - empty, happens when the path ends with a separator + // - slashes, happens with consecutive separators + // - dot, unnecessary + const auto c = current(); + if (!c.empty() && c != "\\" && c != "/" && c != ".") { + return true; + } + } + } + + // checks if next() would return false + // + bool peekNext() const + { + auto copy = *this; + return copy.next(); + } + + // the current component, empty when next() returned false + // + std::string_view current() const { return {m_s.data() + m_begin, m_end - m_begin}; } + +private: + const std::string m_s; + std::size_t m_begin, m_end; + + // finds the next path separator + // + std::size_t nextSeparator(std::size_t from) const + { + while (from < m_s.size()) { + if (m_s[from] == '/' || m_s[from] == '\\') { + break; + } + + ++from; + } + + return from; + } +}; + +namespace bi = boost::interprocess; +namespace bmi = boost::multi_index; + +typedef uint8_t TreeFlags; + +static const TreeFlags FLAG_DIRECTORY = 0x01; +static const TreeFlags FLAG_DUMMY = 0x02; +static const TreeFlags FLAG_FIRSTUSERFLAG = 0x10; + +struct MissingThrowT +{}; +static const MissingThrowT MissingThrow = MissingThrowT(); + +template <typename NodeDataT> +class TreeContainer; + +template <typename T1, typename T2, typename Alloc> +struct mutable_pair +{ + typedef T1 first_type; + typedef T2 second_type; + + mutable_pair(Alloc alloc) : first(T1(alloc)), second(T2(alloc)) {} + + mutable_pair(const T1& f, const T2& s) : first(f), second(s) {} + + mutable_pair(const std::pair<T1, T2>& p) : first(p.first), second(p.second) {} + + T1 first; + mutable T2 second; +}; + +template <typename Key, typename T, typename Compare, typename Allocator, + typename Element = mutable_pair<Key, T, Allocator>> +using mimap = bmi::multi_index_container< + Element, + bmi::indexed_by< + bmi::ordered_unique<bmi::member<Element, Key, &Element::first>, Compare>>, + typename Allocator::template rebind<Element>::other>; + +/** + * a representation of a directory tree in memory. + * This class is designed to be stored in shared memory. + */ +template <typename NodeDataT> +class DirectoryTree +{ + template <typename T> + friend class TreeContainer; + +public: + struct CILess + { + template <typename U, typename V> + bool operator()(const U& lhs, const V& rhs) const + { + const size_t lhsLength = getLength(lhs); + const size_t rhsLength = getLength(rhs); + + const auto r = + _strnicmp(getCharPtr(lhs), getCharPtr(rhs), std::min(lhsLength, rhsLength)); + + if (r == 0) { + return lhsLength < rhsLength; + } + + return (r < 0); + } + + private: + const char* getCharPtr(const StringT& s) const { return s.c_str(); } + + const char* getCharPtr(const std::string& s) const { return s.c_str(); } + + const char* getCharPtr(const char* s) const { return s; } + + const char* getCharPtr(std::string_view s) const { return s.data(); } + + size_t getLength(const StringT& s) const { return s.size(); } + + size_t getLength(const std::string& s) const { return s.size(); } + + size_t getLength(const char* s) const { return strlen(s); } + + size_t getLength(std::string_view s) const { return s.size(); } + }; + + typedef DirectoryTree<NodeDataT> NodeT; + typedef bi::deleter<NodeT, SegmentManagerT> DeleterT; + typedef NodeDataT DataT; + + typedef bi::shared_ptr<NodeT, VoidAllocatorT, DeleterT> NodePtrT; + typedef bi::weak_ptr<NodeT, VoidAllocatorT, DeleterT> WeakPtrT; + + typedef bi::allocator<std::pair<const StringT, NodePtrT>, SegmentManagerT> + NodeEntryAllocatorT; + + typedef mimap<StringT, NodePtrT, CILess, NodeEntryAllocatorT> NodeMapT; + typedef typename NodeMapT::iterator file_iterator; + typedef typename NodeMapT::const_iterator const_file_iterator; + + typedef std::function<void(const NodePtrT&)> VisitorFunction; + + DirectoryTree() = delete; + DirectoryTree(const NodeT& reference) = delete; + DirectoryTree(NodeT&& reference) = delete; + NodeT& operator=(NodeT reference) = delete; + + /** + * @brief construct a new node to be inserted in an existing tree + **/ + DirectoryTree(std::string_view name, TreeFlags flags, const NodePtrT& parent, + const NodeDataT& data, const VoidAllocatorT& allocator) + : m_Parent(parent), m_Name(name.begin(), name.end(), allocator), m_Data(data), + m_Nodes(allocator), m_Flags(flags) + {} + + ~DirectoryTree() { m_Nodes.clear(); } + + /** + * @return parent node + */ + NodePtrT parent() const { return m_Parent.lock(); } + + /** + * @return the full path to the node + */ + fs::path path() const + { + if (m_Parent.lock().get() == nullptr) { + if (m_Name.size() == 0) { + return fs::path(); + } else { + return fs::path(m_Name.c_str()) / "\\"; + } + } else { + return m_Parent.lock()->path() / m_Name.c_str(); + } + } + + /** + * @return data connected to this node + **/ + const NodeDataT& data() const { return m_Data; } + + /** + * @return name of this node + */ + std::string name() const { return m_Name.c_str(); } + + /** + * @brief setFlag change a flag for this node + * @param enabled new state for the specified flag + */ + void setFlag(TreeFlags flag, bool enabled = true) + { + m_Flags = enabled ? m_Flags | flag : m_Flags & ~flag; + } + + /** + * @return true if the specified flag is set, false otherwise + */ + bool hasFlag(TreeFlags flag) const { return (m_Flags & flag) != 0; } + + /** + * @return true if this node is a directory, false if it's a regular file + */ + bool isDirectory() const { return hasFlag(FLAG_DIRECTORY); } + + /** + * @return the number of subnodes (directly) below this one + */ + size_t numNodes() const { return m_Nodes.size(); } + + /** + * @return number of nodes in this (sub-)tree including this one + */ + size_t numNodesRecursive() const + { + size_t result = numNodes() + 1; + + for (const auto& node : m_Nodes) { + result += node.second->numNodesRecursive(); + } + + return result; + } + + /** + * @brief find a node by its path + * @param path the path to look up + * @return a pointer to the node or a null ptr + */ + NodePtrT findNode(const fs::path& path) + { + fs::path::iterator iter = path.begin(); + return findNode(path, iter); + } + + /** + * @brief find a node by its path + * @param path the path to look up + * @return a pointer to the node or a null ptr + */ + const NodePtrT findNode(const fs::path& path) const + { + fs::path::iterator iter = path.begin(); + return findNode(path, iter); + } + + /** + * @brief visit the nodes along the specified path (in order) calling the visitor for + * each + * @param path the path to visit + * @param visitor a function called for each node + */ + void visitPath(const fs::path& path, const VisitorFunction& visitor) const + { + fs::path::iterator iter = path.begin(); + visitPath(path, iter, visitor); + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + NodePtrT node(std::string_view name, MissingThrowT) const + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + USVFS_THROW_EXCEPTION(node_missing_error()); + } + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + NodePtrT node(std::string_view name) + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + return NodePtrT(); + } + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + const NodePtrT node(std::string_view name, MissingThrowT) + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + USVFS_THROW_EXCEPTION(node_missing_error()); + } + } + + /** + * @brief retrieve a node by the specified name + * @param name name of the node + * @return the node found or an empty pointer if no such node was found + */ + const NodePtrT node(std::string_view name) const + { + auto iter = m_Nodes.find(name); + + if (iter != m_Nodes.end()) { + return iter->second; + } else { + return NodePtrT(); + } + } + + /** + * @brief test if a node by the specified name exists + * @param name name of the node + * @return true if the node exists, false otherwise + */ + bool exists(std::string_view name) const + { + return m_Nodes.find(name) != m_Nodes.end(); + } + + /** + * @brief find all matches for a pattern + * @param pattern the pattern to look for + * @return a vector of the found nodes + */ + std::vector<NodePtrT> find(const std::string& pattern) const + { + // determine if there is a prefix in the pattern that indicates a specific + // directory. + size_t fixedPart = pattern.find_first_of("*?"); + + if (fixedPart == 0) + fixedPart = std::string::npos; + if (fixedPart != std::string::npos) + fixedPart = pattern.find_last_of(R"(\/)", fixedPart); + + std::vector<NodePtrT> result; + + if (fixedPart != std::string::npos) { + // if there is a prefix, search for the node representing that path and + // search only on that + NodePtrT node = findNode(fs::path(pattern.substr(0, fixedPart))); + if (node.get() != nullptr) { + node->findLocal(result, pattern.substr(fixedPart + 1)); + } + } else { + findLocal(result, pattern); + } + + return result; + } + + /** + * @return an iterator to the first leaf + **/ + file_iterator filesBegin() { return m_Nodes.begin(); } + + /** + * @return a const iterator to the first leaf + **/ + const_file_iterator filesBegin() const { return m_Nodes.begin(); } + + /** + * @return an iterator one past the last leaf + **/ + file_iterator filesEnd() { return m_Nodes.end(); } + + /** + * @return a const iterator one past the last leaf + **/ + const_file_iterator filesEnd() const { return m_Nodes.end(); } + + /** + * @brief erase the leaf at the specified iterator + * @return an iterator to the following file + **/ + file_iterator erase(file_iterator iter) { return m_Nodes.erase(iter); } + + /** + * @brief clear all nodes + */ + void clear() { m_Nodes.clear(); } + + void removeFromTree() + { + if (auto par = parent()) { + spdlog::get("usvfs")->info("remove from tree {}", m_Name.c_str()); + auto self = par->m_Nodes.find(m_Name.c_str()); + if (self != par->m_Nodes.end()) { + par->erase(self); + } else { + // trying to remove a node that does not exist, most likely because it was + // already removed in a lower level call. this is known to happen when MoveFile + // has the MOVEFILE_COPY_ALLOWED flag and moving a mapped file. + spdlog::get("usvfs")->warn("Failed to remove inexisting node from tree: {}", + m_Name.c_str()); + } + } + } + + PRIVATE : void set(StringT key, const NodePtrT& value) + { + auto res = m_Nodes.emplace(std::move(key), value); + if (!res.second) { + res.first->second = value; + } + } + + WeakPtrT findRoot() const + { + if (m_Parent.lock().get() == nullptr) { + return m_Self; + } else { + return m_Parent.lock()->findRoot(); + } + } + + NodePtrT findNode(const fs::path& name, fs::path::iterator& iter) + { + std::string l = iter->string(); + auto subNode = m_Nodes.find(iter->string()); + advanceIter(iter, name.end()); + + if (iter == name.end()) { + // last name component, should be a local node + if (subNode != m_Nodes.end()) { + return subNode->second; + } else { + return NodePtrT(); + } + } else { + if (subNode != m_Nodes.end()) { + return subNode->second->findNode(name, iter); + } else { + return NodePtrT(); + } + } + } + + const NodePtrT findNode(const fs::path& name, fs::path::iterator& iter) const + { + auto subNode = m_Nodes.find(iter->string()); + advanceIter(iter, name.end()); + + if (iter == name.end()) { + // last name component, should be a local node + if (subNode != m_Nodes.end()) { + return subNode->second; + } else { + return NodePtrT(); + } + } else { + if (subNode != m_Nodes.end()) { + return subNode->second->findNode(name, iter); + } else { + return NodePtrT(); + } + } + } + + void visitPath(const fs::path& path, fs::path::iterator& iter, + const VisitorFunction& visitor) const + { + auto subNode = m_Nodes.find(iter->string()); + + if (subNode != m_Nodes.end()) { + visitor(subNode->second); + advanceIter(iter, path.end()); + if (iter != path.end()) { + subNode->second->visitPath(path, iter, visitor); + } + } + } + + void findLocal(std::vector<NodePtrT>& output, const std::string& pattern) const + { + for (auto iter = m_Nodes.begin(); iter != m_Nodes.end(); ++iter) { + LPCSTR remainder = nullptr; + + if (pattern.size() > 1 && (pattern[0] == '*') && + ((pattern[1] == '/') || (pattern[1] == '\\')) && + iter->second->isDirectory()) { + // the star may represent a directory (one directory level, not + // multiple!), search in subdirectory + iter->second->findLocal(output, pattern.substr(1)); + } else if ((remainder = wildcard::PartialMatch(iter->second->name().c_str(), + pattern.c_str())) != nullptr) { + if ((*remainder == '\0') || (strcmp(remainder, "*") == 0)) { + NodePtrT node = iter->second; + output.push_back(node); + } + + if (iter->second->isDirectory()) { + iter->second->findLocal(output, remainder); + } + } + } + } + + PRIVATE : TreeFlags m_Flags; + + WeakPtrT m_Parent; + WeakPtrT m_Self; + + StringT m_Name; + NodeDataT m_Data; + + NodeMapT m_Nodes; +}; + +template <typename NodeDataT> +void dumpTree(std::ostream& stream, const DirectoryTree<NodeDataT>& tree, int level = 0) +{ + stream << std::string(level, ' ') << tree.name() << " -> " << tree.data() << "\n"; + for (auto iter = tree.filesBegin(); iter != tree.filesEnd(); ++iter) { + dumpTree<NodeDataT>(stream, *iter->second, level + 1); + } +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/exceptionex.cpp b/libs/usvfs/src/shared/exceptionex.cpp new file mode 100644 index 0000000..4a814b5 --- /dev/null +++ b/libs/usvfs/src/shared/exceptionex.cpp @@ -0,0 +1,82 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "exceptionex.h" +#include "winapi.h" +#include <spdlog/spdlog.h> + +namespace usvfs::shared +{ + +std::string windows_error::constructMessage(const std::string& input, int inErrorCode) +{ + std::ostringstream finalMessage; + finalMessage << input; + + LPSTR buffer = nullptr; + + DWORD errorCode = inErrorCode != -1 ? inErrorCode : GetLastError(); + + // TODO: the message is not english? + if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&buffer, 0, nullptr) == 0) { + finalMessage << " (errorcode " << errorCode << ")"; + } else { + LPSTR lastChar = buffer + strlen(buffer) - 2; + *lastChar = '\0'; + finalMessage << " (" << buffer << " [" << errorCode << "])"; + LocalFree(buffer); // allocated by FormatMessage + } + + SetLastError( + errorCode); // restore error code because FormatMessage might have modified it + return finalMessage.str(); +} + +} // namespace usvfs::shared + +void logExtInfo(const std::exception& e, LogLevel logLevel) +{ + std::string content; + + if (const std::string* msg = boost::get_error_info<ex_msg>(e)) { + content = *msg; + } + + if (const DWORD* errorCode = boost::get_error_info<ex_win_errcode>(e)) { + content = std::string("error: ") + winapi::ex::ansi::errorString(*errorCode); + } + + switch (logLevel) { + case LogLevel::Debug: + spdlog::get("usvfs")->debug(content); + break; + case LogLevel::Info: + spdlog::get("usvfs")->info(content); + break; + case LogLevel::Warning: + spdlog::get("usvfs")->warn(content); + break; + case LogLevel::Error: + spdlog::get("usvfs")->error(content); + break; + } +} diff --git a/libs/usvfs/src/shared/exceptionex.h b/libs/usvfs/src/shared/exceptionex.h new file mode 100644 index 0000000..4c298a5 --- /dev/null +++ b/libs/usvfs/src/shared/exceptionex.h @@ -0,0 +1,99 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "logging.h" +#include <boost/exception/all.hpp> +#include <stdexcept> + +typedef boost::error_info<struct tag_message, DWORD> ex_win_errcode; +typedef boost::error_info<struct tag_message, std::string> ex_msg; + +struct std_boost_exception : virtual boost::exception, virtual std::exception +{ + const char* what() const noexcept override + { + return boost::diagnostic_information_what(*this); + } +}; + +struct incompatibility_error : std_boost_exception +{}; +struct usage_error : std_boost_exception +{}; +struct data_error : std_boost_exception +{}; +struct file_not_found_error : std_boost_exception +{}; +struct timeout_error : std_boost_exception +{}; +struct unknown_error : std_boost_exception +{}; +struct node_missing_error : std_boost_exception +{}; + +#define USVFS_THROW_EXCEPTION(x) BOOST_THROW_EXCEPTION(x) + +void logExtInfo(const std::exception& e, LogLevel logLevel = LogLevel::Warning); + +namespace usvfs::shared +{ + +class windows_error : public std::runtime_error +{ +public: + windows_error(const std::string& message, int errorcode = GetLastError()) + : runtime_error(constructMessage(message, errorcode)), m_ErrorCode(errorcode) + {} + + int getErrorCode() const { return m_ErrorCode; } + +private: + int m_ErrorCode; + + std::string constructMessage(const std::string& input, int errorcode); +}; + +class guard +{ +public: + explicit guard(std::function<void()> f) : m_f(std::move(f)) {} + + ~guard() + { + if (m_f) { + m_f(); + } + } + + guard(const guard&); + guard& operator=(const guard&); + +private: + std::function<void()> m_f; +}; + +} // namespace usvfs::shared + +#define CONCATENATE_DIRECT(s1, s2) s1##s2 +#define CONCATENATE(s1, s2) CONCATENATE_DIRECT(s1, s2) +#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__) +#define ON_BLOCK_EXIT(f) usvfs::shared::guard ANONYMOUS_VARIABLE(guard)(f) diff --git a/libs/usvfs/src/shared/formatters.h b/libs/usvfs/src/shared/formatters.h new file mode 100644 index 0000000..3d0e47e --- /dev/null +++ b/libs/usvfs/src/shared/formatters.h @@ -0,0 +1,158 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2024. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <format> +#include <type_traits> + +#include "ntdll_declarations.h" + +// formatters for standard types + +namespace usvfs::log +{ +std::string to_string(LPCWSTR value); +std::string to_string(PCUNICODE_STRING value); +} // namespace usvfs::log + +template <class Enum, class CharT> + requires std::is_enum_v<Enum> +struct std::formatter<Enum, CharT> : std::formatter<std::underlying_type_t<Enum>, CharT> +{ + template <class FmtContext> + FmtContext::iterator format(Enum v, FmtContext& ctx) const + { + return std::formatter<std::underlying_type_t<Enum>, CharT>::format( + static_cast<std::underlying_type_t<Enum>>(v), ctx); + } +}; + +template <> +struct std::formatter<LPCWSTR, char> : std::formatter<std::string, char> +{ + template <class FmtContext> + FmtContext::iterator format(LPCWSTR v, FmtContext& ctx) const + { + return std::formatter<std::string, char>::format(usvfs::log::to_string(v), ctx); + } +}; + +template <> +struct std::formatter<std::wstring, char> : std::formatter<LPCWSTR, char> +{ + template <class FmtContext> + FmtContext::iterator format(const std::wstring& v, FmtContext& ctx) const + { + return std::formatter<LPCWSTR, char>::format(v.c_str(), ctx); + } +}; + +template <> +struct std::formatter<PCUNICODE_STRING, char> : std::formatter<std::string, char> +{ + template <class FmtContext> + FmtContext::iterator format(PCUNICODE_STRING v, FmtContext& ctx) const + { + return std::formatter<std::string, char>::format(usvfs::log::to_string(v), ctx); + } +}; + +template <> +struct std::formatter<UNICODE_STRING, char> : std::formatter<std::string, char> +{ + template <class FmtContext> + FmtContext::iterator format(UNICODE_STRING v, FmtContext& ctx) const + { + return std::formatter<std::string, char>::format(usvfs::log::to_string(&v), ctx); + } +}; + +template <class Pointer> + requires(std::is_pointer_v<Pointer> && !std::is_same_v<Pointer, const char*> && + !std::is_same_v<Pointer, char*> && !std::is_same_v<Pointer, const void*> && + !std::is_same_v<Pointer, void*>) +struct std::formatter<Pointer, char> : std::formatter<const void*, char> +{ + template <class FmtContext> + FmtContext::iterator format(Pointer v, FmtContext& ctx) const + { + return std::formatter<const void*, char>::format(v, ctx); + } +}; + +namespace usvfs::log +{ + +/** + * a small helper class to wrap any object. The whole point is to give us a way + * to ensure our own operator<< is used in addParam calls + */ +template <typename T> +class Wrap +{ +public: + explicit Wrap(const T& data) : m_Data(data) {} + Wrap(Wrap<T>&& reference) : m_Data(std::move(reference.m_Data)) {} + + Wrap(const Wrap<T>& reference) = delete; + Wrap<T>& operator=(const Wrap<T>& reference) = delete; + +private: + friend struct ::std::formatter<Wrap<T>, char>; + const T& m_Data; +}; + +template <typename T> +Wrap<T> wrap(const T& data) +{ + return Wrap<T>(data); +} + +} // namespace usvfs::log + +template <> +struct std::formatter<usvfs::log::Wrap<DWORD>, char> : std::formatter<DWORD, char> +{ + template <class FmtContext> + FmtContext::iterator format(const usvfs::log::Wrap<DWORD>& v, FmtContext& ctx) const + { + return std::format_to(ctx.out(), "{:x}", v.m_Data); + } +}; + +template <> +struct std::formatter<usvfs::log::Wrap<NTSTATUS>, char> : std::formatter<NTSTATUS, char> +{ + template <class FmtContext> + FmtContext::iterator format(const usvfs::log::Wrap<NTSTATUS>& v, + FmtContext& ctx) const + { + switch (v.m_Data) { + case 0x00000000: + return std::format_to(ctx.out(), "ok"); + case 0xC0000022: + return std::format_to(ctx.out(), "access denied"); + case 0xC0000035: + return std::format_to(ctx.out(), "exists already"); + } + return std::format_to(ctx.out(), "err {:x}", v.m_Data); + } +}; diff --git a/libs/usvfs/src/shared/loghelpers.cpp b/libs/usvfs/src/shared/loghelpers.cpp new file mode 100644 index 0000000..bfcabf1 --- /dev/null +++ b/libs/usvfs/src/shared/loghelpers.cpp @@ -0,0 +1,96 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include <format> + +#include "formatters.h" +#include "loghelpers.h" +#include "stringcast.h" +#include "stringutils.h" + +namespace ush = usvfs::shared; + +namespace usvfs::log +{ + +// this is declared in formatters but is defined here to avoid a whole .cpp +// file for two small functions +// +// comments from old code, taken as is: +// +// TODO this does not correctly support surrogate pairs since the size used here +// is the number of 16-bit characters in the buffer whereas toNarrow expects the +// actual number of characters. It will always underestimate though, so worst +// case scenario we truncate the string +// +std::string to_string(LPCWSTR value) +{ + if (value == nullptr) { + return "<null>"; + } + try { + return ush::string_cast<std::string>(value, ush::CodePage::UTF8); + } catch (const std::exception& e) { + return std::format("<err: {}>", e.what()); + } +} +std::string to_string(PCUNICODE_STRING value) +{ + try { + return ush::string_cast<std::string>(value->Buffer, ush::CodePage::UTF8, + value->Length / sizeof(WCHAR)); + } catch (const std::exception& e) { + return std::format("<err: {}>", e.what()); + } +} + +spdlog::level::level_enum ConvertLogLevel(LogLevel level) +{ + switch (level) { + case LogLevel::Debug: + return spdlog::level::debug; + case LogLevel::Info: + return spdlog::level::info; + case LogLevel::Warning: + return spdlog::level::warn; + case LogLevel::Error: + return spdlog::level::err; + default: + return spdlog::level::debug; + } +} + +LogLevel ConvertLogLevel(spdlog::level::level_enum level) +{ + switch (level) { + case spdlog::level::debug: + return LogLevel::Debug; + case spdlog::level::info: + return LogLevel::Info; + case spdlog::level::warn: + return LogLevel::Warning; + case spdlog::level::err: + return LogLevel::Error; + default: + return LogLevel::Debug; + } +} + +} // namespace usvfs::log diff --git a/libs/usvfs/src/shared/loghelpers.h b/libs/usvfs/src/shared/loghelpers.h new file mode 100644 index 0000000..071ad9d --- /dev/null +++ b/libs/usvfs/src/shared/loghelpers.h @@ -0,0 +1,123 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "dllimport.h" +#include "formatters.h" +#include "ntdll_declarations.h" +#include "shmlogger.h" +#include "stringutils.h" + +namespace usvfs::log +{ + +enum class DisplayStyle : uint8_t +{ + Hex = 0x01 +}; + +class CallLoggerDummy +{ +public: + template <typename T> + CallLoggerDummy& addParam(const char*, const T&, uint8_t style = 0) + { + return *this; + } +}; + +class CallLogger +{ +public: + explicit CallLogger(const char* function) + { + const char* namespaceend = strrchr(function, ':'); + + if (namespaceend != nullptr) { + function = namespaceend + 1; + } + + m_Message = function; + } + + ~CallLogger() + { + try { + static std::shared_ptr<spdlog::logger> log = spdlog::get("hooks"); + log->debug("{}", m_Message); + } catch (...) { + // suppress all exceptions in destructor + } + } + + template <typename T> + CallLogger& addParam(const char* name, const T& value, uint8_t style = 0); + +private: + std::string m_Message; +}; + +template <typename T> +CallLogger& CallLogger::addParam(const char* name, const T& value, uint8_t style) +{ + static bool enabled = spdlog::get("hooks")->should_log(spdlog::level::debug); + typedef std::underlying_type<DisplayStyle>::type DSType; + + if (enabled) { + if constexpr (std::is_pointer_v<T>) { + if (value == nullptr) { + std::format_to(std::back_inserter(m_Message), "[{}=<null>]", name); + } else { + std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value); + } + } else if constexpr (std::is_integral_v<T>) { + if (style & static_cast<DSType>(DisplayStyle::Hex)) { + std::format_to(std::back_inserter(m_Message), "[{}={:x}]", name, value); + } else { + std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value); + } + } else { + std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value); + } + } + + return *this; +} + +spdlog::level::level_enum ConvertLogLevel(LogLevel level); +LogLevel ConvertLogLevel(spdlog::level::level_enum level); + +} // namespace usvfs::log + +// prefer the short variant of the function name, without signature. +// Fall back to the portable boost macro +#ifdef __FUNCTION__ +#define __MYFUNC__ __FUNCTION__ +#else +#define __MYFUNC__ BOOST_CURRENT_FUNCTION +#endif + +#define LOG_CALL() usvfs::log::CallLogger(__MYFUNC__) + +#define PARAM(val) addParam(#val, val) +#define PARAMHEX(val) \ + addParam(#val, val, static_cast<uint8_t>(usvfs::log::DisplayStyle::Hex)) +#define PARAMWRAP(val) addParam(#val, usvfs::log::wrap(val)) diff --git a/libs/usvfs/src/shared/ntdll_declarations.cpp b/libs/usvfs/src/shared/ntdll_declarations.cpp new file mode 100644 index 0000000..b4bf0f5 --- /dev/null +++ b/libs/usvfs/src/shared/ntdll_declarations.cpp @@ -0,0 +1,75 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "ntdll_declarations.h" +#include <cassert> + +#define LOAD_EXT(mod, name) \ + name = reinterpret_cast<name##_type>(::GetProcAddress(mod, #name)); \ + assert(name != nullptr) + +NtQueryDirectoryFile_type NtQueryDirectoryFile; +NtQueryDirectoryFileEx_type NtQueryDirectoryFileEx; +NtQueryFullAttributesFile_type NtQueryFullAttributesFile; +NtQueryAttributesFile_type NtQueryAttributesFile; +NtQueryObject_type NtQueryObject; +NtQueryInformationFile_type NtQueryInformationFile; +NtQueryInformationByName_type NtQueryInformationByName; +NtOpenFile_type NtOpenFile; +NtCreateFile_type NtCreateFile; +NtClose_type NtClose; +RtlDoesFileExists_U_type RtlDoesFileExists_U; +RtlDosPathNameToRelativeNtPathName_U_WithStatus_type + RtlDosPathNameToRelativeNtPathName_U_WithStatus; +RtlReleaseRelativeName_type RtlReleaseRelativeName; +RtlGetVersion_type RtlGetVersion; +NtTerminateProcess_type NtTerminateProcess; + +static bool ntdll_initialized; + +void ntdll_declarations_init() +{ + if (!ntdll_initialized) { + HMODULE ntDLLMod = GetModuleHandleW(L"ntdll.dll"); + + LOAD_EXT(ntDLLMod, NtQueryDirectoryFile); + LOAD_EXT(ntDLLMod, NtQueryDirectoryFileEx); + LOAD_EXT(ntDLLMod, NtQueryFullAttributesFile); + LOAD_EXT(ntDLLMod, NtQueryAttributesFile); + LOAD_EXT(ntDLLMod, NtQueryObject); + LOAD_EXT(ntDLLMod, NtQueryInformationFile); + LOAD_EXT(ntDLLMod, NtQueryInformationByName); + LOAD_EXT(ntDLLMod, NtCreateFile); + LOAD_EXT(ntDLLMod, NtOpenFile); + LOAD_EXT(ntDLLMod, NtClose); + LOAD_EXT(ntDLLMod, RtlDoesFileExists_U); + LOAD_EXT(ntDLLMod, RtlDosPathNameToRelativeNtPathName_U_WithStatus); + LOAD_EXT(ntDLLMod, RtlReleaseRelativeName); + LOAD_EXT(ntDLLMod, RtlGetVersion); + LOAD_EXT(ntDLLMod, NtTerminateProcess); + + ntdll_initialized = true; + } +} + +static struct __Initializer +{ + __Initializer() { ntdll_declarations_init(); } +} __initializer; diff --git a/libs/usvfs/src/shared/ntdll_declarations.h b/libs/usvfs/src/shared/ntdll_declarations.h new file mode 100644 index 0000000..813ad8c --- /dev/null +++ b/libs/usvfs/src/shared/ntdll_declarations.h @@ -0,0 +1,612 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "windows_sane.h" + +#pragma warning(push) +#pragma warning(disable : 4201) + +typedef LONG NTSTATUS; + +typedef struct _IO_STATUS_BLOCK +{ + union + { + NTSTATUS Status; + PVOID Pointer; + } DUMMYUNIONNAME; + + ULONG_PTR Information; +} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; + +typedef struct _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]; +} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION; + +typedef struct _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]; +} FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION; + +typedef struct _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]; +} FILE_ID_FULL_DIR_INFORMATION, *PFILE_ID_FULL_DIR_INFORMATION; + +typedef struct _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]; +} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION; + +typedef struct _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]; +} FILE_ID_BOTH_DIR_INFORMATION, *PFILE_ID_BOTH_DIR_INFORMATION; + +typedef struct _FILE_BASIC_INFORMATION +{ + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + ULONG FileAttributes; +} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION; + +typedef struct _FILE_RENAME_INFORMATION +{ +#if (_WIN32_WINNT >= _WIN32_WINNT_WIN10_RS1) + union + { + BOOLEAN ReplaceIfExists; // FileRenameInformation + ULONG Flags; // FileRenameInformationEx + } DUMMYUNIONNAME; +#else + BOOLEAN ReplaceIfExists; +#endif + HANDLE RootDirectory; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION; + +typedef struct _FILE_STANDARD_INFORMATION +{ + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG NumberOfLinks; + BOOLEAN DeletePending; + BOOLEAN Directory; +} FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION; + +typedef struct _FILE_NAMES_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION; + +typedef struct _FILE_INTERNAL_INFORMATION +{ + LARGE_INTEGER IndexNumber; +} FILE_INTERNAL_INFORMATION, *PFILE_INTERNAL_INFORMATION; + +typedef struct _FILE_EA_INFORMATION +{ + ULONG EaSize; +} FILE_EA_INFORMATION, *PFILE_EA_INFORMATION; + +typedef struct _FILE_ACCESS_INFORMATION +{ + ACCESS_MASK AccessFlags; +} FILE_ACCESS_INFORMATION, *PFILE_ACCESS_INFORMATION; + +typedef struct _FILE_POSITION_INFORMATION +{ + LARGE_INTEGER CurrentByteOffset; +} FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION; + +typedef struct _FILE_MODE_INFORMATION +{ + ULONG Mode; +} FILE_MODE_INFORMATION, *PFILE_MODE_INFORMATION; + +typedef struct _FILE_ALIGNMENT_INFORMATION +{ + ULONG AlignmentRequirement; +} FILE_ALIGNMENT_INFORMATION, *PFILE_ALIGNMENT_INFORMATION; + +typedef struct _FILE_NAME_INFORMATION +{ + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION; + +typedef struct _FILE_ID_EXTD_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; + ULONG ReparsePointTag; + FILE_ID_128 FileId; + WCHAR FileName[1]; +} FILE_ID_EXTD_DIR_INFORMATION, *PFILE_ID_EXTD_DIR_INFORMATION; + +typedef struct _FILE_ID_EXTD_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; + ULONG ReparsePointTag; + FILE_ID_128 FileId; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_ID_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_EXTD_BOTH_DIR_INFORMATION; + +typedef struct _FILE_ID_64_EXTD_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; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + WCHAR FileName[1]; +} FILE_ID_64_EXTD_DIR_INFORMATION, *PFILE_ID_64_EXTD_DIR_INFORMATION; + +typedef struct _FILE_ID_64_EXTD_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; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_ID_64_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_64_EXTD_BOTH_DIR_INFORMATION; + +typedef struct _FILE_ID_ALL_EXTD_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; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + FILE_ID_128 FileId128; + WCHAR FileName[1]; +} FILE_ID_ALL_EXTD_DIR_INFORMATION, *PFILE_ID_ALL_EXTD_DIR_INFORMATION; + +typedef struct _FILE_ID_ALL_EXTD_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; + ULONG ReparsePointTag; + LARGE_INTEGER FileId; + FILE_ID_128 FileId128; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION; + +typedef struct _FILE_ALL_INFORMATION +{ + FILE_BASIC_INFORMATION BasicInformation; + FILE_STANDARD_INFORMATION StandardInformation; + FILE_INTERNAL_INFORMATION InternalInformation; + FILE_EA_INFORMATION EaInformation; + FILE_ACCESS_INFORMATION AccessInformation; + FILE_POSITION_INFORMATION PositionInformation; + FILE_MODE_INFORMATION ModeInformation; + FILE_ALIGNMENT_INFORMATION AlignmentInformation; + FILE_NAME_INFORMATION NameInformation; +} FILE_ALL_INFORMATION, *PFILE_ALL_INFORMATION; + +typedef struct _FILE_OBJECTID_INFORMATION +{ + LONGLONG FileReference; + UCHAR ObjectId[16]; + union + { + struct + { + UCHAR BirthVolumeId[16]; + UCHAR BirthObjectId[16]; + UCHAR DomainId[16]; + }; + UCHAR ExtendedInfo[48]; + }; +} FILE_OBJECTID_INFORMATION, *PFILE_OBJECTID_INFORMATION; + +typedef struct _FILE_REPARSE_POINT_INFORMATION +{ + LONGLONG FileReference; + ULONG Tag; +} FILE_REPARSE_POINT_INFORMATION, *PFILE_REPARSE_POINT_INFORMATION; + +// copied from ntstatus.h +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L) +#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L) +#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) +#define STATUS_NO_SUCH_FILE ((NTSTATUS)0xC000000FL) + +#define SL_RESTART_SCAN 0x01 +#define SL_RETURN_SINGLE_ENTRY 0x02 +#define SL_INDEX_SPECIFIED 0x04 +#define SL_RETURN_ON_DISK_ENTRIES_ONLY 0x08 + +#define SL_QUERY_DIRECTORY_MASK 0x0b + +typedef enum _FILE_INFORMATION_CLASS +{ + FileDirectoryInformation = 1, + FileFullDirectoryInformation = 2, + FileBothDirectoryInformation = 3, + FileStandardInformation = 5, + FileNameInformation = 9, + FileRenameInformation = 10, + FileNamesInformation = 12, + FileAllInformation = 18, + FileObjectIdInformation = 29, + FileReparsePointInformation = 33, + FileIdBothDirectoryInformation = 37, + FileIdFullDirectoryInformation = 38, + FileNormalizedNameInformation = 48, + FileIdExtdDirectoryInformation = 60, + FileIdExtdBothDirectoryInformation = 63, + FileId64ExtdDirectoryInformation = 78, + FileId64ExtdBothDirectoryInformation = 79, + FileIdAllExtdDirectoryInformation = 80, + FileIdAllExtdBothDirectoryInformation = 81 +} FILE_INFORMATION_CLASS, + *PFILE_INFORMATION_CLASS; + +typedef enum _MODE +{ + KernelMode, + UserMode, + MaximumMode +} MODE; + +typedef struct _IO_STATUS_BLOCK IO_STATUS_BLOCK; + +typedef struct _IO_STATUS_BLOCK* PIO_STATUS_BLOCK; +// typedef VOID (NTAPI *PIO_APC_ROUTINE )(__in PVOID ApcContext, __in +// PIO_STATUS_BLOCK IoStatusBlock, __in ULONG Reserved); +typedef VOID(NTAPI* PIO_APC_ROUTINE)(PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, + ULONG Reserved); +typedef enum _FILE_INFORMATION_CLASS FILE_INFORMATION_CLASS; + +typedef struct _UNICODE_STRING +{ + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} UNICODE_STRING, *PUNICODE_STRING; +typedef const UNICODE_STRING* PCUNICODE_STRING; + +typedef struct _OBJECT_ATTRIBUTES +{ + ULONG Length; + HANDLE RootDirectory; + PUNICODE_STRING ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; + PVOID SecurityQualityOfService; +} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES; + +typedef enum _POOL_TYPE +{ + NonPagedPool, + NonPagedPoolExecute = NonPagedPool, + PagedPool, + NonPagedPoolMustSucceed = NonPagedPool + 2, + DontUseThisType, + NonPagedPoolCacheAligned = NonPagedPool + 4, + PagedPoolCacheAligned, + NonPagedPoolCacheAlignedMustS = NonPagedPool + 6, + MaxPoolType, + NonPagedPoolBase = 0, + NonPagedPoolBaseMustSucceed = NonPagedPoolBase + 2, + NonPagedPoolBaseCacheAligned = NonPagedPoolBase + 4, + NonPagedPoolBaseCacheAlignedMustS = NonPagedPoolBase + 6, + NonPagedPoolSession = 32, + PagedPoolSession = NonPagedPoolSession + 1, + NonPagedPoolMustSucceedSession = PagedPoolSession + 1, + DontUseThisTypeSession = NonPagedPoolMustSucceedSession + 1, + NonPagedPoolCacheAlignedSession = DontUseThisTypeSession + 1, + PagedPoolCacheAlignedSession = NonPagedPoolCacheAlignedSession + 1, + NonPagedPoolCacheAlignedMustSSession = PagedPoolCacheAlignedSession + 1, + NonPagedPoolNx = 512, + NonPagedPoolNxCacheAligned = NonPagedPoolNx + 4, + NonPagedPoolSessionNx = NonPagedPoolNx + 32 +} POOL_TYPE; + +typedef struct _OBJECT_TYPE_INITIALIZER +{ + WORD Length; + UCHAR ObjectTypeFlags; + ULONG CaseInsensitive : 1; + ULONG UnnamedObjectsOnly : 1; + ULONG UseDefaultObject : 1; + ULONG SecurityRequired : 1; + ULONG MaintainHandleCount : 1; + ULONG MaintainTypeList : 1; + ULONG ObjectTypeCode; + ULONG InvalidAttributes; + GENERIC_MAPPING GenericMapping; + ULONG ValidAccessMask; + POOL_TYPE PoolType; + ULONG DefaultPagedPoolCharge; + ULONG DefaultNonPagedPoolCharge; + PVOID DumpProcedure; + LONG* OpenProcedure; + PVOID CloseProcedure; + PVOID DeleteProcedure; + LONG* ParseProcedure; + LONG* SecurityProcedure; + LONG* QueryNameProcedure; + UCHAR* OkayToCloseProcedure; +} OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER; + +typedef CCHAR KPROCESSOR_MODE; + +typedef struct _OBJECT_HANDLE_INFORMATION +{ + ULONG HandleAttributes; + ACCESS_MASK GrantedAccess; +} OBJECT_HANDLE_INFORMATION, *POBJECT_HANDLE_INFORMATION; + +typedef struct _OBJECT_NAME_INFORMATION +{ + UNICODE_STRING Name; +} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION; + +typedef enum _OBJECT_INFORMATION_CLASS +{ + ObjectBasicInformation = 0, + ObjectNameInformation = 1, + ObjectTypeInformation = 2 +} OBJECT_INFORMATION_CLASS; + +typedef struct _RTL_RELATIVE_NAME +{ + UNICODE_STRING RelativeName; + HANDLE ContainingDirectory; + void* CurDirRef; +} RTL_RELATIVE_NAME, *PRTL_RELATIVE_NAME; + +typedef struct _FILE_NETWORK_OPEN_INFORMATION +{ + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG FileAttributes; +} FILE_NETWORK_OPEN_INFORMATION, *PFILE_NETWORK_OPEN_INFORMATION; + +#define FILE_DIRECTORY_FILE 0x00000001 +#define FILE_WRITE_THROUGH 0x00000002 +#define FILE_SEQUENTIAL_ONLY 0x00000004 +#define FILE_NO_INTERMEDIATE_BUFFERING 0x00000008 +#define FILE_SYNCHRONOUS_IO_ALERT 0x00000010 +#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 +#define FILE_NON_DIRECTORY_FILE 0x00000040 +#define FILE_CREATE_TREE_CONNECTION 0x00000080 +#define FILE_COMPLETE_IF_OPLOCKED 0x00000100 +#define FILE_NO_EA_KNOWLEDGE 0x00000200 +#define FILE_OPEN_REMOTE_INSTANCE 0x00000400 +#define FILE_RANDOM_ACCESS 0x00000800 +#define FILE_DELETE_ON_CLOSE 0x00001000 +#define FILE_OPEN_BY_FILE_ID 0x00002000 +#define FILE_OPEN_FOR_BACKUP_INTENT 0x00004000 +#define FILE_NO_COMPRESSION 0x00008000 +#define FILE_OPEN_REQUIRING_OPLOCK 0x00010000 +#define FILE_DISALLOW_EXCLUSIVE 0x00020000 +#define FILE_SESSION_AWARE 0x00040000 +#define FILE_RESERVE_OPFILTER 0x00100000 +#define FILE_OPEN_REPARSE_POINT 0x00200000 +#define FILE_OPEN_NO_RECALL 0x00400000 +#define FILE_OPEN_FOR_FREE_SPACE_QUERY 0x00800000 +#define FILE_CONTAINS_EXTENDED_CREATE_INFORMATION 0x10000000 + +// Nt + +using NtQueryDirectoryFile_type = NTSTATUS(WINAPI*)(HANDLE, HANDLE, PIO_APC_ROUTINE, + PVOID, PIO_STATUS_BLOCK, PVOID, + ULONG, FILE_INFORMATION_CLASS, + BOOLEAN, PUNICODE_STRING, BOOLEAN); +using NtQueryDirectoryFileEx_type = NTSTATUS(WINAPI*)(HANDLE, HANDLE, PIO_APC_ROUTINE, + PVOID, PIO_STATUS_BLOCK, PVOID, + ULONG, FILE_INFORMATION_CLASS, + ULONG, PUNICODE_STRING); + +using NtQueryFullAttributesFile_type = + NTSTATUS(WINAPI*)(POBJECT_ATTRIBUTES, PFILE_NETWORK_OPEN_INFORMATION); +using NtQueryAttributesFile_type = NTSTATUS(WINAPI*)(POBJECT_ATTRIBUTES, + PFILE_BASIC_INFORMATION); + +using NtQueryObject_type = NTSTATUS(WINAPI*)( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength); +using NtQueryInformationFile_type = NTSTATUS(WINAPI*)( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +using NtQueryInformationByName_type = NTSTATUS(WINAPI*)( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +using NtOpenFile_type = NTSTATUS(WINAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, + PIO_STATUS_BLOCK, ULONG, ULONG); +using NtCreateFile_type = NTSTATUS(WINAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, + PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG, + ULONG, ULONG, ULONG, PVOID, ULONG); + +using NtClose_type = NTSTATUS(WINAPI*)(HANDLE); + +using NtTerminateProcess_type = NTSTATUS(WINAPI*)(HANDLE ProcessHandle, + NTSTATUS ExitStatus); + +// Rtl + +using RtlDoesFileExists_U_type = NTSYSAPI BOOLEAN(NTAPI*)(PCWSTR); +using RtlDosPathNameToRelativeNtPathName_U_WithStatus_type = + NTSTATUS(NTAPI*)(PCWSTR DosFileName, PUNICODE_STRING NtFileName, PWSTR* FilePath, + PRTL_RELATIVE_NAME RelativeName); +using RtlReleaseRelativeName_type = void(NTAPI*)(PRTL_RELATIVE_NAME RelativeName); +using RtlGetVersion_type = NTSTATUS(NTAPI*)(PRTL_OSVERSIONINFOW); + +extern NtQueryDirectoryFile_type NtQueryDirectoryFile; +extern NtQueryDirectoryFileEx_type NtQueryDirectoryFileEx; +extern NtQueryFullAttributesFile_type NtQueryFullAttributesFile; +extern NtQueryAttributesFile_type NtQueryAttributesFile; +extern NtQueryObject_type NtQueryObject; +extern NtQueryInformationFile_type NtQueryInformationFile; +extern NtQueryInformationByName_type NtQueryInformationByName; +extern NtOpenFile_type NtOpenFile; +extern NtCreateFile_type NtCreateFile; +extern NtClose_type NtClose; +extern NtTerminateProcess_type NtTerminateProcess; +extern RtlDoesFileExists_U_type RtlDoesFileExists_U; +extern RtlDosPathNameToRelativeNtPathName_U_WithStatus_type + RtlDosPathNameToRelativeNtPathName_U_WithStatus; +extern RtlReleaseRelativeName_type RtlReleaseRelativeName; +extern RtlGetVersion_type RtlGetVersion; + +// ensures ntdll functions have been initialized (only needed during static objects +// initialization) +void ntdll_declarations_init(); + +#pragma warning(pop) diff --git a/libs/usvfs/src/shared/pch.cpp b/libs/usvfs/src/shared/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/usvfs/src/shared/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/usvfs/src/shared/pch.h b/libs/usvfs/src/shared/pch.h new file mode 100644 index 0000000..e787f68 --- /dev/null +++ b/libs/usvfs/src/shared/pch.h @@ -0,0 +1,87 @@ +#pragma once + +#include <algorithm> +#include <atomic> +#include <bitset> +#include <cassert> +#include <codecvt> +#include <cstddef> +#include <cstdint> +#include <cstring> +#include <ctime> +#include <exception> +#include <format> +#include <functional> +#include <future> +#include <iomanip> +#include <ios> +#include <limits> +#include <map> +#include <memory> +#include <mutex> +#include <regex> +#include <shared_mutex> +#include <sstream> +#include <stdexcept> +#include <string> +#include <thread> +#include <type_traits> +#include <utility> +#include <vector> + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +// clang-format off +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> +#include <ShlObj.h> +#include <comutil.h> +#include <Psapi.h> +#include <shellapi.h> +#include <shlwapi.h> +#include <fileapi.h> +#include <DbgHelp.h> +// clang-format on + +#define BOOST_INTERPROCESS_SEGMENT_MANAGER_ABI 1 +#include <boost/algorithm/string.hpp> +#include <boost/algorithm/string/case_conv.hpp> +#include <boost/any.hpp> +#include <boost/container/flat_set.hpp> +#include <boost/container/map.hpp> +#include <boost/container/scoped_allocator.hpp> +#include <boost/container/slist.hpp> +#include <boost/container/string.hpp> +#include <boost/container/vector.hpp> +#include <boost/current_function.hpp> +#include <boost/dll/runtime_symbol_info.hpp> +#include <boost/filesystem.hpp> +#include <boost/filesystem/detail/utf8_codecvt_facet.hpp> +#include <boost/filesystem/operations.hpp> +#include <boost/filesystem/path.hpp> +#include <boost/format.hpp> +#include <boost/interprocess/ipc/message_queue.hpp> +#include <boost/interprocess/managed_windows_shared_memory.hpp> +#include <boost/interprocess/offset_ptr.hpp> +#include <boost/interprocess/smart_ptr/deleter.hpp> +#include <boost/interprocess/smart_ptr/shared_ptr.hpp> +#include <boost/interprocess/smart_ptr/weak_ptr.hpp> +#include <boost/interprocess/sync/named_mutex.hpp> +#include <boost/lexical_cast.hpp> +#include <boost/locale.hpp> +#include <boost/multi_index/member.hpp> +#include <boost/multi_index/ordered_index.hpp> +#include <boost/multi_index_container.hpp> +#include <boost/predef.h> +#include <boost/static_assert.hpp> +#include <boost/thread.hpp> +#include <boost/thread/shared_lock_guard.hpp> +#include <boost/thread/shared_mutex.hpp> +#include <boost/tokenizer.hpp> +#include <boost/type_traits.hpp> + +#include <spdlog/spdlog.h> + +namespace fs = boost::filesystem; diff --git a/libs/usvfs/src/shared/shared_memory.h b/libs/usvfs/src/shared/shared_memory.h new file mode 100644 index 0000000..28da34b --- /dev/null +++ b/libs/usvfs/src/shared/shared_memory.h @@ -0,0 +1,53 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +namespace bi = boost::interprocess; +namespace bc = boost::container; + +namespace usvfs::shared +{ + +template <typename T> +using OffsetPtrT = bi::offset_ptr<T, std::int32_t, std::uint64_t>; + +using VoidPointerT = OffsetPtrT<void>; + +// important: the windows shared memory mechanism, unlike other implementations, +// automatically removes the SHM object when there are no more "subscribers". +// MO currently depends on that feature! + +// managed_windows_shared_memory apparently doesn't support sharing between +// 64bit and 32bit processes +using managed_windows_shared_memory = bi::basic_managed_windows_shared_memory< + char, bi::rbtree_best_fit<bi::mutex_family, VoidPointerT, 8>, bi::iset_index>; + +using SharedMemoryT = managed_windows_shared_memory; +using SegmentManagerT = SharedMemoryT::segment_manager; + +using VoidAllocatorT = boost::container::scoped_allocator_adaptor< + boost::interprocess::allocator<void, SegmentManagerT>>; +using CharAllocatorT = VoidAllocatorT::rebind<char>::other; + +using StringT = bc::basic_string<char, std::char_traits<char>, CharAllocatorT>; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/shmlogger.cpp b/libs/usvfs/src/shared/shmlogger.cpp new file mode 100644 index 0000000..66512de --- /dev/null +++ b/libs/usvfs/src/shared/shmlogger.cpp @@ -0,0 +1,205 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma warning(disable : 4714) +#pragma warning(disable : 4503) +#pragma warning(push, 3) +#include "shmlogger.h" +#pragma warning(pop) + +#pragma warning(disable : 4996) + +using namespace boost::interprocess; +using namespace boost::posix_time; + +SHMLogger* SHMLogger::s_Instance = nullptr; + +SHMLogger::owner_t SHMLogger::owner; +SHMLogger::client_t SHMLogger::client; + +SHMLogger::SHMLogger(owner_t, const std::string& queueName) + : m_QueueName(queueName), + m_LogQueue(create_only, queueName.c_str(), MESSAGE_COUNT, MESSAGE_SIZE), + m_DroppedMessages(0) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + s_Instance = this; + } +} + +SHMLogger::SHMLogger(client_t, const std::string& queueName) + : m_QueueName(queueName), m_LogQueue(open_only, queueName.c_str()), + m_DroppedMessages(0) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + s_Instance = this; + } +} + +SHMLogger::~SHMLogger() +{ + s_Instance = nullptr; + message_queue_interop::remove(m_QueueName.c_str()); +} + +SHMLogger& SHMLogger::create(const char* instanceName) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + std::string queueName = std::string("__shm_sink_") + instanceName; + message_queue::remove(queueName.c_str()); + new SHMLogger(owner, queueName); + atexit([]() { + delete s_Instance; + }); + } + return *s_Instance; +} + +SHMLogger& SHMLogger::open(const char* instanceName) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("duplicate shm logger instantiation"); + } else { + new SHMLogger(client, std::string("__shm_sink_") + instanceName); + } + return *s_Instance; +} + +void SHMLogger::free() +{ + if (s_Instance != nullptr) { + SHMLogger* temp = s_Instance; + s_Instance = nullptr; + delete temp; + } +} + +bool SHMLogger::tryGet(char* buffer, size_t bufferSize) +{ + message_queue_interop::size_type receivedSize; + unsigned int prio; + bool res = m_LogQueue.try_receive(buffer, static_cast<unsigned int>(bufferSize), + receivedSize, prio); + if (res) { + buffer[std::min(bufferSize - 1, static_cast<size_t>(receivedSize))] = '\0'; + } + return res; +} + +void SHMLogger::get(char* buffer, size_t bufferSize) +{ + message_queue_interop::size_type receivedSize; + unsigned int prio; + m_LogQueue.receive(buffer, static_cast<unsigned int>(bufferSize), receivedSize, prio); + buffer[std::min(bufferSize - 1, static_cast<size_t>(receivedSize))] = '\0'; +} + +usvfs::sinks::shm_sink::shm_sink(const char* queueName) + : m_LogQueue(open_only, (std::string("__shm_sink_") + queueName).c_str()), + m_DroppedMessages(0) +{} + +void usvfs::sinks::shm_sink::flush_() {} + +void usvfs::sinks::shm_sink::sink_it_(const spdlog::details::log_msg& msg) +{ + int droppedMessages = m_DroppedMessages.load(std::memory_order_relaxed); + if (droppedMessages > 0) { + auto dropMessage = std::format("{} debug messages dropped", droppedMessages); + if (m_LogQueue.try_send(dropMessage.c_str(), + static_cast<unsigned int>(dropMessage.size()), 0)) { + m_DroppedMessages.store(0, std::memory_order_relaxed); + } + } + + std::string message{msg.payload}; + + // blacklist %USERNAME% because PII + static const std::string username = std::string(getenv("USERNAME")); + boost::algorithm::ireplace_all(message, std::string("\\") + username, "\\USERNAME"); + boost::algorithm::ireplace_all(message, std::string("/") + username, "/USERNAME"); + + if (message.length() > SHMLogger::MESSAGE_SIZE) { + std::vector<std::string> splitVec; + boost::split(splitVec, message, boost::is_any_of("\r\n"), boost::token_compress_on); + for (const std::string& line : splitVec) { + output(msg.level, line); + } + } else { + output(msg.level, message); + } +} + +void usvfs::sinks::shm_sink::output(spdlog::level::level_enum lev, + const std::string& message) +{ + bool sent = true; + + // spdlog auto-append line breaks which we don't need. Probably would be + // better to not write the breaks to begin with? + size_t count = std::min(message.find_last_not_of("\r\n") + 1, + static_cast<size_t>(m_LogQueue.get_max_msg_size())); + + // depending on the log level, drop less important messages if the receiver + // can't keep up + switch (lev) { + case spdlog::level::trace: + case spdlog::level::debug: + case spdlog::level::info: { + // m_LogQueue.send(message.c_str(), count, 0); + sent = m_LogQueue.try_send(message.c_str(), static_cast<unsigned int>(count), 0); + } break; + case spdlog::level::err: + case spdlog::level::critical: { + m_LogQueue.send(message.c_str(), static_cast<unsigned int>(count), 0); + } break; + default: { + boost::posix_time::ptime time = + microsec_clock::universal_time() + boost::posix_time::milliseconds(200); + sent = m_LogQueue.timed_send(message.c_str(), static_cast<unsigned int>(count), 0, + time); + } break; + } + + if (!sent) { + m_DroppedMessages.fetch_add(1, std::memory_order_relaxed); + } +} + +void __cdecl boost::interprocess::ipcdetail::get_shared_dir(std::string& shared_dir) +{ + PWSTR path; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &path))) { + _bstr_t bPath(path); + shared_dir = (char*)bPath; + shared_dir += "\\USVFS"; + } else { + shared_dir = "C:\\ProgramData\\USVFS"; + } + boost::filesystem::path boostPath(shared_dir); + if (!boost::filesystem::exists(boostPath)) + boost::filesystem::create_directories(boostPath); +} diff --git a/libs/usvfs/src/shared/shmlogger.h b/libs/usvfs/src/shared/shmlogger.h new file mode 100644 index 0000000..158f941 --- /dev/null +++ b/libs/usvfs/src/shared/shmlogger.h @@ -0,0 +1,105 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <spdlog/details/null_mutex.h> +#include <spdlog/sinks/base_sink.h> + +#include "logging.h" +#include "shared_memory.h" +#include "windows_sane.h" + +typedef boost::interprocess::message_queue_t<usvfs::shared::VoidPointerT> + message_queue_interop; + +namespace usvfs::sinks +{ +class shm_sink : public spdlog::sinks::base_sink<spdlog::details::null_mutex> +{ +public: + shm_sink(const char* queueName); + +protected: + void sink_it_(const spdlog::details::log_msg& msg) override; + void output(spdlog::level::level_enum lev, const std::string& message); + void flush_() override; + +private: + message_queue_interop m_LogQueue; + std::atomic<int> m_DroppedMessages; +}; + +} // namespace usvfs::sinks + +class SHMLogger +{ +public: + static const size_t MESSAGE_COUNT = 1024; + static const size_t MESSAGE_SIZE = 512; + + static SHMLogger& create(const char* instanceName); + static SHMLogger& open(const char* instanceName); + static void free(); + + static bool isInstantiated() { return s_Instance != nullptr; } + + static inline SHMLogger& instance() + { + if (s_Instance == nullptr) { + throw std::runtime_error("shm logger not instantiated"); + } + + return *s_Instance; + } + + void log(LogLevel logLevel, const std::string& message); + bool tryGet(char* buffer, size_t bufferSize); + void get(char* buffer, size_t bufferSize); + +private: + struct owner_t + {}; + static owner_t owner; + + struct client_t + {}; + static client_t client; + +private: + SHMLogger(owner_t, const std::string& instanceName); + SHMLogger(client_t, const std::string& instanceName); + + SHMLogger(const SHMLogger&) = delete; + SHMLogger& operator=(const SHMLogger&) = delete; + + ~SHMLogger(); + +private: + static SHMLogger* s_Instance; + + message_queue_interop m_LogQueue; + + std::string m_SHMName; + std::string m_LockName; + std::string m_QueueName; + + std::atomic<int> m_DroppedMessages; +}; diff --git a/libs/usvfs/src/shared/stringcast.cpp b/libs/usvfs/src/shared/stringcast.cpp new file mode 100644 index 0000000..47822d5 --- /dev/null +++ b/libs/usvfs/src/shared/stringcast.cpp @@ -0,0 +1,40 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "stringcast.h" + +namespace usvfs::shared +{ + +UINT windowsCP(CodePage codePage) +{ + switch (codePage) { + case CodePage::LOCAL: + return CP_ACP; + case CodePage::UTF8: + return CP_UTF8; + case CodePage::LATIN1: + return 850; + } + // this should not be possible in practice + throw std::runtime_error("unsupported codePage"); +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/stringcast.h b/libs/usvfs/src/shared/stringcast.h new file mode 100644 index 0000000..0ce56d1 --- /dev/null +++ b/libs/usvfs/src/shared/stringcast.h @@ -0,0 +1,170 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "exceptionex.h" + +namespace usvfs::shared +{ + +enum class CodePage +{ + LOCAL, + LATIN1, + UTF8 +}; + +template <typename ToT, typename FromT> +class string_cast_impl +{ +public: + static ToT cast(const FromT& source, CodePage codePage, size_t sourceLength); +}; + +template <typename ToT, typename FromT> +ToT string_cast(FromT source, CodePage codePage = CodePage::LOCAL, + size_t sourceLength = std::numeric_limits<size_t>::max()) +{ + return string_cast_impl<ToT, FromT>::cast(source, codePage, sourceLength); +} + +template <typename ToT, typename CharT> +class string_cast_impl<ToT, std::basic_string<CharT>> +{ +public: + static ToT cast(const std::basic_string<CharT>& source, CodePage codePage, + size_t sourceLength) + { + return string_cast_impl<ToT, const CharT*>::cast(source.c_str(), codePage, + sourceLength); + } +}; + +template <typename ToT, typename CharT> +class string_cast_impl<ToT, CharT*> +{ + BOOST_STATIC_ASSERT(!boost::is_base_and_derived<ToT, CharT>::value); + +public: + static ToT cast(CharT* source, CodePage codePage, size_t sourceLength) + { + return string_cast_impl<ToT, const CharT*>::cast(source, codePage, sourceLength); + } +}; + +template <typename ToT, typename CharT, int N> +class string_cast_impl<ToT, CharT[N]> +{ +public: + static ToT cast(CharT (&source)[N], CodePage codePage, size_t sourceLength) + { + return string_cast_impl<ToT, const CharT*>::cast(source, codePage, sourceLength); + } +}; + +UINT windowsCP(CodePage codePage); + +template <> +class string_cast_impl<std::string, const wchar_t*> +{ +public: + static std::string cast(const wchar_t* const& source, CodePage codePage, + size_t sourceLength) + { + std::string result; + + if (sourceLength == std::numeric_limits<size_t>::max()) { + sourceLength = wcslen(source); + } + + if (sourceLength > 0) { + // use utf8 or local 8-bit encoding depending on user choice + UINT cp = windowsCP(codePage); + // preflight to find out the required buffer size + int outLength = WideCharToMultiByte(cp, 0, source, static_cast<int>(sourceLength), + nullptr, 0, nullptr, nullptr); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + result.resize(outLength); + outLength = WideCharToMultiByte(cp, 0, source, static_cast<int>(sourceLength), + &result[0], outLength, nullptr, nullptr); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + // fix output string length (i.e. in case of unconvertible characters + while (result[outLength - 1] == L'\0') { + result.resize(--outLength); + } + } + + return result; + } +}; + +template <> +class string_cast_impl<std::wstring, const char*> +{ +public: + static std::wstring cast(const char* const& source, CodePage codePage, + size_t sourceLength) + { + std::wstring result; + + if (sourceLength == std::numeric_limits<size_t>::max()) { + sourceLength = strlen(source); + } + + if (sourceLength > 0) { + // use utf8 or local 8-bit encoding depending on user choice + UINT cp = windowsCP(codePage); + // preflight to find out the required source size + int outLength = MultiByteToWideChar(cp, 0, source, static_cast<int>(sourceLength), + &result[0], 0); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + result.resize(outLength); + outLength = MultiByteToWideChar(cp, 0, source, static_cast<int>(sourceLength), + &result[0], outLength); + if (outLength == 0) { + throw windows_error("string conversion failed"); + } + while (result[outLength - 1] == L'\0') { + result.resize(--outLength); + } + } + + return result; + } +}; + +template <> +class string_cast_impl<std::wstring, const wchar_t*> +{ +public: + static std::wstring cast(const wchar_t* const& source, CodePage, size_t) + { + return std::wstring(source); + } +}; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/stringutils.cpp b/libs/usvfs/src/shared/stringutils.cpp new file mode 100644 index 0000000..2ddbf64 --- /dev/null +++ b/libs/usvfs/src/shared/stringutils.cpp @@ -0,0 +1,146 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "stringutils.h" +#include "windows_sane.h" + +namespace usvfs::shared +{ + +void strncpy_sz(char* dest, const char* src, size_t destSize) +{ + if (destSize > 0) { + strncpy(dest, src, destSize - 1); + dest[destSize - 1] = '\0'; + } +} + +void wcsncpy_sz(wchar_t* dest, const wchar_t* src, size_t destSize) +{ + if ((destSize > 0) && (dest != nullptr)) { + wcsncpy(dest, src, destSize - 1); + dest[destSize - 1] = L'\0'; + } +} + +bool startswith(const wchar_t* string, const wchar_t* subString) +{ + while ((*string != '\0') && (*subString != '\0')) { + if (towlower(*string) != towlower(*subString)) { + return false; + } + ++string; + ++subString; + } + + return *subString == '\0'; +} + +static fs::path normalize(const fs::path& path) +{ + fs::path result; + + boost::locale::generator gen; + auto loc = gen("en_US.UTF-8"); + for (fs::path::iterator iter = path.begin(); iter != path.end(); ++iter) { + if (*iter == "..") { + result = result.parent_path(); + } else if (*iter != ".") { + result /= boost::to_lower_copy(iter->string(), loc); + } // single dot is ignored + } + return result; +} + +fs::path make_relative(const fs::path& fromIn, const fs::path& toIn) +{ + // converting path to lower case to make iterator comparison work correctly + // on case-insenstive filesystems + fs::path from(fs::absolute(fromIn)); + fs::path to(fs::absolute(toIn)); + + // find common base + fs::path::const_iterator fromIter(from.begin()); + fs::path::const_iterator toIter(to.begin()); + + // TODO the following equivalent test is probably quite expensive as new + // paths are created for each iteration but the case sensitivity depends on + // the fs + while ((fromIter != from.end()) && (toIter != to.end()) && + (boost::iequals(fromIter->string(), toIter->string()))) { + ++fromIter; + ++toIter; + } + + // Navigate backwards in directory to reach previously found base + fs::path result; + for (; fromIter != from.end(); ++fromIter) { + if (*fromIter != ".") { + result /= ".."; + } + } + + // Now navigate down the directory branch + for (; toIter != to.end(); ++toIter) { + result /= *toIter; + } + return result; +} + +std::string to_hex(void* bufferIn, size_t bufferSize) +{ + unsigned char* buffer = static_cast<unsigned char*>(bufferIn); + std::ostringstream temp; + temp << std::hex; + for (size_t i = 0; i < bufferSize; ++i) { + temp << std::setfill('0') << std::setw(2) << (unsigned int)buffer[i]; + if ((i % 16) == 15) { + temp << "\n"; + } else { + temp << " "; + } + } + return temp.str(); +} + +std::wstring to_upper(const std::wstring& input) +{ + std::wstring result; + result.resize(input.size()); + ::LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, input.c_str(), + static_cast<int>(input.size()), &result[0], + static_cast<int>(result.size())); + return result; +} + +std::string byte_string(std::size_t n) +{ + auto s = std::to_string(n); + auto p = s.size(); + + while (p > 3) { + p -= 3; + s.insert(p, 1, ','); + } + + return s + " B"; +} + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/stringutils.h b/libs/usvfs/src/shared/stringutils.h new file mode 100644 index 0000000..64d0af7 --- /dev/null +++ b/libs/usvfs/src/shared/stringutils.h @@ -0,0 +1,57 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +namespace usvfs::shared +{ + +void strncpy_sz(char* dest, const char* src, size_t destSize); +void wcsncpy_sz(wchar_t* dest, const wchar_t* src, size_t destSize); + +bool startswith(const wchar_t* string, const wchar_t* subString); + +// Return path when appended to a_From will resolve to same as a_To +fs::path make_relative(const fs::path& from, const fs::path& to); + +std::string to_hex(void* bufferIn, size_t bufferSize); + +// convert unicode string to upper-case (locale invariant) +std::wstring to_upper(const std::wstring& input); + +// formats a number with thousand separators and B at the end +// +std::string byte_string(std::size_t n); + +class FormatGuard +{ + std::ostream& m_Stream; + std::ios::fmtflags m_Flags; + +public: + FormatGuard(std::ostream& stream) : m_Stream(stream), m_Flags(stream.flags()) {} + + ~FormatGuard() { m_Stream.flags(m_Flags); } + + FormatGuard(const FormatGuard&) = delete; + FormatGuard& operator=(FormatGuard&) = delete; +}; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/tree_container.h b/libs/usvfs/src/shared/tree_container.h new file mode 100644 index 0000000..a2f61fa --- /dev/null +++ b/libs/usvfs/src/shared/tree_container.h @@ -0,0 +1,630 @@ +#pragma once + +#include "directory_tree.h" +#include "shared_memory.h" + +namespace usvfs::shared +{ + +// smart pointer to DirectoryTrees (only intended for top-level nodes). This +// will transparently switch to new shared memory regions in case they get +// reallocated +// +// +// reassign() is called from a variety of places when the current chunk of +// shared memory is full or is marked as being outdated; its job is to either +// find another chunk that may have been created by another process or to create +// a brand new one +// +// +// if there is only a single process hooked, it will slowly fill up the shared +// memory when adding files, eventually throw a bi::bad_alloc and end up in +// reassign(); a new block will be allocated, the data copied over, and the old +// block will be deallocated +// +// +// when multiple processes are involved, things are more complicated +// +// two processes A and B will start by using the same shared memory, but they +// have their own pointer to it that's local to the process (the `m_TreeMeta` +// member variable) +// +// so when process A fills up the shared memory and reallocates it, process B +// is still pointing to the old shared memory; only when process B does some +// operation that accesses the file tree will the pointer be checked and +// adjusted to point to the new shared memory +// +// in this example, when process A ran out of memory, it set `outdated` to +// `true` in the shared memory block, allocated a new one and copied the data +// over, but it did not deallocate the block because process B is still +// pointing to it +// +// when process B tries to access the block, it checks `outdated` (see get()); +// if it's true, it means that it's pointing to an outdated shared memory block +// and must find the new one that process A created +// +// the new block is not necessarily the very next name, it is possible for +// process A to burn through a series of blocks quickly when adding a bunch of +// new files, and these blocks will all have been deallocated by the time +// process B tries to find the newest one +// +// so when a process sees its block as outdated, it will try to open a bunch of +// names until one exists that is not outdated; if it can't find the block +// (shouldn't happen), it will just create a new one +// +template <typename TreeT> +class TreeContainer +{ +public: + /** + * @brief Constructor + * @param SHMName name of the shared memory holding the tree. This should contain the + * running number + * @param size initial size in bytes of the container. since the tree is resized by + * doubling this should be a power of two. 64k is supposed to be the page size on + * windows so smaller allocations make little sense + * @note size can't be too small. If initial allocations fail automatic growing won't + * work + */ + TreeContainer(const std::string& SHMName, size_t size = 64 * 1024) + : m_TreeMeta(nullptr), m_SHMName(SHMName) + { + std::locale global_loc = std::locale(); + std::locale loc(global_loc, new fs::detail::utf8_codecvt_facet); + fs::path::imbue(loc); + + // append _1 to the name if it doesn't end with _N already + std::regex pattern(R"exp((.*_)(\d+))exp"); + std::smatch match; + std::string shmName(m_SHMName.c_str()); + regex_match(shmName, match, pattern); + + if (match.size() != 3) { + m_SHMName += "_1"; + } + + // creates a new memory block if this is the first process to run or attach + // to an already existing one + createOrOpen(m_SHMName, size); + + spdlog::get("usvfs")->info("attached to {0} with {1} nodes, size {2}", m_SHMName, + m_TreeMeta->tree->numNodesRecursive(), + byte_string(m_SHM->get_size())); + } + + TreeContainer(const TreeContainer&) = delete; + TreeContainer& operator=(const TreeContainer&) = delete; + + ~TreeContainer() + { + if (unassign(m_SHM, m_TreeMeta)) { + bi::shared_memory_object::remove(m_SHMName.c_str()); + } + } + + /** + * @return retrieve an allocator that can be used to create objects in this tree + */ + VoidAllocatorT allocator() { return VoidAllocatorT(m_SHM->get_segment_manager()); } + + template <typename... Arguments> + typename TreeT::DataT create(Arguments&&... args) + { + return TreeT::DataT(std::forward<Arguments>(args)..., allocator()); + } + + TreeT* operator->() { return get(); } + + /** + * @return raw pointer to the managed tree + */ + TreeT* get() + { + if (m_TreeMeta->outdated) { + reassign(); + } + + return m_TreeMeta->tree.get(); + } + + /** + * @return raw const pointer to the managed tree + */ + const TreeT* get() const + { + if (m_TreeMeta->outdated) { + // safe const_cast, TreeContainer are never created const + const_cast<TreeContainer<TreeT>*>(this)->reassign(); + } + + return m_TreeMeta->tree.get(); + } + + const TreeT* operator->() const { return get(); } + + /** + * @return current name of the managed shared memory + */ + std::string shmName() const { return m_SHMName; } + + void clear() { m_TreeMeta->tree->clear(); } + + /** + * @brief add a new file to the tree + * + * @param name name of the file, expected to be relative to this directory + * @param data the file data to attach + * @param flags flags for this files + * @param overwrite if true, the new leaf will overwrite an existing one that compares + *as "equal" + * @return pointer to the new node or a null ptr + **/ + template <typename T> + typename TreeT::NodePtrT addFile(const fs::path& name, const T& data, + TreeFlags flags = 0, bool overwrite = true) + { + for (;;) { + DecomposablePath dp(name.string()); + + try { + return addNode(m_TreeMeta->tree.get(), dp, data, overwrite, flags, allocator()); + } catch (const bi::bad_alloc&) { + } + + reassign(); + } + } + + /** + * @brief add a new directory to the tree + * + * @param name name of the file, expected to be relative to this directory + * @param data the file data to attach + * @param flags flags for this files + * @param overwrite if true, the new leaf will overwrite an existing one that compares + *as "equal" + * @return pointer to the new node or a null ptr + **/ + template <typename T> + typename TreeT::NodePtrT addDirectory(const fs::path& name, const T& data, + TreeFlags flags = 0, bool overwrite = true) + { + for (;;) { + DecomposablePath dp(name.string()); + + try { + return addNode(m_TreeMeta->tree.get(), dp, data, overwrite, + flags | FLAG_DIRECTORY, allocator()); + } catch (const bi::bad_alloc&) { + } + + reassign(); + } + } + + void getBuffer(void*& buffer, size_t& bufferSize) const + { + buffer = m_SHM->get_address(); + bufferSize = m_SHM->get_size(); + } + +private: + struct TreeMeta + { + TreeMeta(const typename TreeT::DataT& data, SegmentManagerT* segmentManager) + : tree(segmentManager->construct<TreeT>(bi::anonymous_instance)( + "", true, TreeT::NodePtrT(), data, VoidAllocatorT(segmentManager))), + referenceCount(0), // reference count only set on top level node + outdated(false) + {} + + OffsetPtrT<TreeT> tree; + long referenceCount; + bool outdated; + bi::interprocess_mutex mutex; + }; + + std::string m_SHMName; + std::shared_ptr<SharedMemoryT> m_SHM; + TreeMeta* m_TreeMeta; + + typename TreeT::DataT createEmpty() + { + return createDataEmpty<typename TreeT::DataT>(allocator()); + } + + template <typename T> + TreeT* createSubNode(const VoidAllocatorT& allocator, std::string_view name, + unsigned long flags, const T& data) + { + auto* manager = allocator.get_segment_manager(); + + return manager->construct<TreeT>(bi::anonymous_instance)( + name, flags, TreeT::NodePtrT(), + createData<typename TreeT::DataT, T>(data, allocator), manager); + } + + typename TreeT::NodePtrT createSubPtr(TreeT* subNode) + { + SharedMemoryT::segment_manager* manager = m_SHM->get_segment_manager(); + return TreeT::NodePtrT(subNode, allocator(), TreeT::DeleterT(manager)); + } + + template <typename T> + typename TreeT::NodePtrT addNode(TreeT* base, DecomposablePath& path, const T& data, + bool overwrite, unsigned int flags, + const VoidAllocatorT& allocator) + { + if (!path.peekNext()) { + typename TreeT::NodePtrT newNode = base->node(path.current()); + + if (!newNode) { + // last name component, should be the filename + TreeT* node = createSubNode(allocator, path.current(), flags, data); + newNode = createSubPtr(node); + newNode->m_Self = TreeT::WeakPtrT(newNode); + newNode->m_Parent = base->m_Self; + base->set(StringT(path.current(), allocator), newNode); + return newNode; + } else if (overwrite) { + newNode->m_Data = createData<typename TreeT::DataT, T>(data, allocator); + newNode->m_Flags = static_cast<usvfs::shared::TreeFlags>(flags); + return newNode; + } else { + // the node is already in the tree, overwrite is false, nothing to do + return {}; + } + } else { + // not last component, continue search in child node + auto subNode = base->m_Nodes.find(path.current()); + + if (subNode == base->m_Nodes.end()) { + typename TreeT::NodePtrT newNode = createSubPtr(createSubNode( + allocator, path.current(), FLAG_DIRECTORY | FLAG_DUMMY, createEmpty())); + + subNode = + base->m_Nodes.emplace(StringT(path.current(), allocator), newNode).first; + subNode->second->m_Self = TreeT::WeakPtrT(subNode->second); + subNode->second->m_Parent = base->m_Self; + } + + path.next(); + + return addNode(subNode->second.get().get(), path, data, overwrite, flags, + allocator); + } + } + + /** + * @brief copy content of one tree to a different tree (in a different shared memory + * segment + * @param destination + * @param reference + * @note at the time this is called, destination needs to refer to the shm of + * "destination" so that objects can be allocated in the new tree + */ + void copyTree(TreeT* destination, const TreeT* reference) + { + VoidAllocatorT allocator = VoidAllocatorT(m_SHM->get_segment_manager()); + destination->m_Flags = reference->m_Flags; + dataAssign(destination->m_Data, reference->m_Data); + destination->m_Name.assign(reference->m_Name.c_str()); + + for (const auto& kv : reference->m_Nodes) { + TreeT* newNode = createSubNode(allocator, "", true, createEmpty()); + typename TreeT::NodePtrT newNodePtr = createSubPtr(newNode); + + // need to set self BEFORE recursively copying the subtree, otherwise + // how would we assign parent pointers? + newNode->m_Self = newNodePtr; + + TreeT* source = reinterpret_cast<TreeT*>(kv.second.get().get()); + copyTree(newNode, source); + destination->set(newNode->m_Name, newNodePtr); + newNode->m_Parent = destination->m_Self; + } + } + + int increaseRefCount(TreeMeta* treeMeta) + { + bi::scoped_lock<bi::interprocess_mutex> lock(treeMeta->mutex); + return ++treeMeta->referenceCount; + } + + int decreaseRefCount(TreeMeta* treeMeta) + { + bi::scoped_lock<bi::interprocess_mutex> lock(treeMeta->mutex); + return --treeMeta->referenceCount; + } + + // see activateSHM() for return value + // + std::optional<std::string> createOrOpen(const std::string& SHMName, size_t size) + { + SharedMemoryT* newSHM = openSHM(SHMName); + + if (newSHM) { + spdlog::get("usvfs")->info("{} opened in process {}", SHMName, + ::GetCurrentProcessId()); + } else { + newSHM = createSHM(SHMName, size); + + if (newSHM) { + spdlog::get("usvfs")->info("{} created in process {}", SHMName, + ::GetCurrentProcessId()); + } + } + + if (!newSHM) { + spdlog::get("usvfs")->error("failed to create or open {} in process {}", SHMName, + ::GetCurrentProcessId()); + + throw std::exception("no shm instance"); + } + + return activateSHM(newSHM, SHMName); + } + + // see activateSHM() for return value + // + SharedMemoryT* createSHM(const std::string& SHMName, size_t size) + { + try { + return new SharedMemoryT(bi::create_only, SHMName.c_str(), + static_cast<unsigned int>(size)); + } catch (const bi::interprocess_exception&) { + } + + return nullptr; + } + + // see activateSHM() for return value + // + SharedMemoryT* openSHM(const std::string& SHMName) + { + try { + return new SharedMemoryT(bi::open_only, SHMName.c_str()); + } catch (const bi::interprocess_exception&) { + } + + return nullptr; + } + + // makes the given shm current, returns the name of the previous shm block + // if it is now unused and must be destroyed; if the block is still used by + // another process, returns empty + // + // see reassign() + // + std::optional<std::string> activateSHM(SharedMemoryT* shm, const std::string& SHMName) + { + std::shared_ptr<SharedMemoryT> oldSHM = m_SHM; + + m_SHM.reset(shm); + std::pair<TreeMeta*, SharedMemoryT::size_type> res = m_SHM->find<TreeMeta>("Meta"); + + if (res.first == nullptr) { + res.first = m_SHM->construct<TreeMeta>("Meta")(createEmpty(), + m_SHM->get_segment_manager()); + if (res.first == nullptr) { + USVFS_THROW_EXCEPTION(bi::bad_alloc()); + } + if (m_TreeMeta != nullptr) { + copyTree(res.first->tree.get(), m_TreeMeta->tree.get()); + } + } + + increaseRefCount(res.first); + + std::optional<std::string> deadSHMName; + + if (oldSHM.get() != nullptr) { + const bool lastUser = unassign(oldSHM, m_TreeMeta); + if (lastUser) { + deadSHMName = m_SHMName; + } + } + + m_TreeMeta = res.first; + m_SHMName = SHMName; + + return deadSHMName; + } + + static std::string followupName(const std::string& currentName) + { + std::regex pattern(R"exp((.*_)(\d+))exp"); + std::smatch match; + regex_match(currentName, match, pattern); + + if (match.size() != 3) { + USVFS_THROW_EXCEPTION(usage_error() << ex_msg("shared memory name invalid")); + } + + const int count = boost::lexical_cast<int>(match[2]); + return match[1].str() + std::to_string(count + 1); + } + + bool unassign(const std::shared_ptr<SharedMemoryT>& shm, TreeMeta* tree) + { + if (tree == nullptr) { + return true; + } + + if (decreaseRefCount(tree) == 0) { + shm->get_segment_manager()->destroy_ptr(tree); + return true; + } else { + return false; + } + } + + // re-entrancy: every time a process switches to a new shared memory block, it + // will know whether it was the last process to have a handle to it; when that + // happens, the block will be destroyed to avoid leaking it + // + // destroying these blocks is somewhat dangerous: it ends up in boost, which + // will try to access the filesystem to see if the name of the shared memory + // corresponds to a file on the drive, which can call hooked functions and end + // up right back here + // + // (note that in usvfs, only the shared memory for the log file uses a real + // file on the filesystem, see shmlogger.cpp; all the tree stuff uses + // anonymous, memory mapped files that live in the Windows pagefile) + // + // so the old blocks can be destroyed, but only after all the shenanigans with + // finding the correct shared memory block are over and `m_TreeMeta` points to + // a valid block, so all the names of the dead shared memory blocks are kept + // in a vector and deallocated at the very end + // + void reassign() + { + // list of all the shared memory blocks that are now unused and can be + // destroyed + std::vector<std::string> deadSHMNames; + + if (m_TreeMeta->outdated) { + // this block was marked as outdated, which should only happen when + // another process has ran out of memory and started allocating blocks + // with higher numbers + + spdlog::get("usvfs")->info("tree {0} is outdated, looking for another one", + m_SHMName); + + if (findNewerBlock(deadSHMNames)) { + // the new block was found and activated + return; + } + } else { + // this block isn't outdated, so reassign() was called because a bad_alloc + // exception was thrown + spdlog::get("usvfs")->info( + "ran out of memory in tree {0}, will create another one", m_SHMName); + } + + // either the block is full or it's outdated, but no higher block was found; + // just create a new one + + createNewBlock(deadSHMNames); + + // remove the old shared memory blocks; this can be recursive and call + // reassign() again, but it's safe at this point + for (const std::string& name : deadSHMNames) { + spdlog::get("usvfs")->info("destroying {0}", name); + bi::shared_memory_object::remove(name.c_str()); + } + } + + // tries a series of shm names above the current one in the hope of finding + // the most recent one + // + // if the new block was found and activated correctly, returns true + // + // when findNewerBlock() returns false, this container will either still be + // pointing to the same, outdated block as it started with, or it might have + // moved up to another outdated block, but it will always be pointing to + // a valid block in memory + // + bool findNewerBlock(std::vector<std::string>& deadSHMNames) + { + // how many blocks are checked above the current one; it's unlikely there + // will ever be more than 15 to 20 blocks allocated, but this is high + // because it's really the only way to keep processes connected to the same + // shm block, so processes must not fail to find the next one + constexpr int Tries = 100; + + std::string nextName = m_SHMName; + + for (int i = 0; i < Tries; ++i) { + // the shm name is something like "mod_organizer_3", which becomes + // "mod_organizer_4" + nextName = followupName(nextName); + spdlog::get("usvfs")->info("opening {0}", nextName); + + // open the shm, see if it exists + SharedMemoryT* shm = openSHM(nextName); + + if (!shm) { + // this is not necessarily an error, another process might have created + // a bunch of blocks and destroyed them before this process had a chance + // to see them, so just keep going + spdlog::get("usvfs")->info("{0} doesn't exist", nextName); + continue; + } + + spdlog::get("usvfs")->info("{0} exists, activating", nextName); + const auto deadSHMName = activateSHM(shm, nextName); + + // if this process was the last user of the previous block, it must be + // deallocated, but only after this whole thing is finished, because it + // can end up calling reassign() again + if (deadSHMName) { + spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName); + deadSHMNames.push_back(*deadSHMName); + } + + // another process might have already created this block, run out of + // memory and created more, so make sure to only stop when finding a block + // that's not outdated + if (m_TreeMeta->outdated) { + spdlog::get("usvfs")->info("{0} is also outdated", nextName); + } else { + // shm opened correctly, activated and not outdated, done + spdlog::get("usvfs")->info("{0} not outdated, taking it, size now {1}", + nextName, byte_string(m_SHM->get_size())); + + return true; + } + } + + // this block is outdated, but no other valid block was found above; this + // shouldn't happen, but just create a new block to make sure programs can + // still run + + spdlog::get("usvfs")->error( + "found no existing tree above {0}, will create a new one", m_SHMName); + + return false; + } + + // creates a new block and activates it, throws on failure + // + void createNewBlock(std::vector<std::string>& deadSHMNames) + { + // the current block is now considered stale, so make sure other processes + // are aware of it and try to find the new block + // + // todo: there really should be some synchronization here, another process + // can pick up this flag before the next block has been created + m_TreeMeta->outdated = true; + + // the shm name is something like "mod_organizer_3", which becomes + // "mod_organizer_4" + const std::string nextName = followupName(m_SHMName); + spdlog::get("usvfs")->info("creating {0}", nextName); + + SharedMemoryT* shm = createSHM(nextName, m_SHM->get_size() * 2); + + if (!shm) { + // this shouldn't happen + spdlog::get("usvfs")->error("failed to create {0}", nextName); + throw std::exception("cannot create block"); + } + + spdlog::get("usvfs")->info("{0} created, activating", nextName); + const auto deadSHMName = activateSHM(shm, nextName); + + // if this process was the last user of the previous block, it must be + // deallocated, but only after this whole thing is finished, because it + // can end up calling reassign() again + if (deadSHMName) { + spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName); + deadSHMNames.push_back(*deadSHMName); + } + + spdlog::get("usvfs")->info("tree {0} size now {1}", m_SHMName, + byte_string(m_SHM->get_size())); + } +}; + +} // namespace usvfs::shared diff --git a/libs/usvfs/src/shared/unicodestring.cpp b/libs/usvfs/src/shared/unicodestring.cpp new file mode 100644 index 0000000..f997e7f --- /dev/null +++ b/libs/usvfs/src/shared/unicodestring.cpp @@ -0,0 +1,77 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "unicodestring.h" +#include "logging.h" +#include "stringcast.h" +#include "stringutils.h" + +namespace usvfs +{ + +UnicodeString::UnicodeString(LPCWSTR string, size_t length) +{ + if (length == std::string::npos) { + length = wcslen(string); + } + m_Buffer.resize(length + 1); + memcpy(m_Buffer.data(), string, length * sizeof(wchar_t)); + update(); +} + +UnicodeString::UnicodeString(const std::wstring& string) +{ + m_Buffer.resize(string.length() + 1); + memcpy(m_Buffer.data(), string.data(), string.length() * sizeof(wchar_t)); + update(); +} + +UnicodeString& UnicodeString::operator=(const std::wstring& string) +{ + m_Buffer.resize(string.length() + 1); + memcpy(m_Buffer.data(), string.data(), string.length() * sizeof(wchar_t)); + update(); + return *this; +} + +UnicodeString& UnicodeString::appendPath(PUNICODE_STRING path) +{ + if (path != nullptr && path->Buffer && path->Length) { + auto appendAt = size(); + if (appendAt) { + m_Buffer.resize(m_Buffer.size() + path->Length / sizeof(WCHAR) + 1); + m_Buffer[appendAt++] = L'\\'; + } else + m_Buffer.resize(path->Length / sizeof(WCHAR) + 1); + memcpy(&m_Buffer[appendAt], path->Buffer, path->Length); + update(); + } + + return *this; +} + +void UnicodeString::update() +{ + m_Data.Length = static_cast<USHORT>(size() * sizeof(WCHAR)); + m_Data.MaximumLength = static_cast<USHORT>((m_Buffer.capacity() - 1) * sizeof(WCHAR)); + m_Data.Buffer = m_Buffer.data(); +} + +} // namespace usvfs diff --git a/libs/usvfs/src/shared/unicodestring.h b/libs/usvfs/src/shared/unicodestring.h new file mode 100644 index 0000000..77071ad --- /dev/null +++ b/libs/usvfs/src/shared/unicodestring.h @@ -0,0 +1,110 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "ntdll_declarations.h" +#include "windows_sane.h" + +#include "formatters.h" + +namespace usvfs +{ + +/** + * @brief C++ wrapper for the windows UNICODE_STRING structure + */ +class UnicodeString +{ +public: + UnicodeString() : m_Buffer(1) { update(); } + + UnicodeString(const UnicodeString& other) : m_Buffer(other.m_Buffer) { update(); } + + UnicodeString(UnicodeString&& other) : m_Buffer(std::move(other.m_Buffer)) + { + update(); + } + + UnicodeString(const std::wstring& string); + UnicodeString(LPCWSTR string, size_t length = std::string::npos); + + UnicodeString& operator=(const std::wstring& string); + + UnicodeString& operator=(const UnicodeString& other) + { + m_Buffer = other.m_Buffer; + update(); + return *this; + } + + UnicodeString& operator=(UnicodeString&& other) + { + m_Buffer = std::move(other.m_Buffer); + update(); + return *this; + } + + /** + * @brief convert to a WinNt Api-style unicode string. This is only valid as long + * as the string isn't modified + */ + explicit operator PUNICODE_STRING() { return &m_Data; } + + /** + * @brief convert to a Win32 Api-style unicode string. This is only valid as long + * as the string isn't modified + */ + explicit operator LPCWSTR() const { return m_Data.Buffer; } + + /** + * @return length of the string in 16-bit words (not including zero termination) + */ + size_t size() const { return m_Buffer.size() - 1; } + + wchar_t operator[](size_t pos) const { return m_Buffer[pos]; } + + UnicodeString& appendPath(PUNICODE_STRING path); + +private: + friend struct ::std::formatter<UnicodeString, char>; + + void update(); + + UNICODE_STRING m_Data; + std::vector<wchar_t> m_Buffer; +}; + +} // namespace usvfs + +template <> +struct std::formatter<usvfs::UnicodeString, char> + : std::formatter<PCUNICODE_STRING, char> +{ + template <class FmtContext> + FmtContext::iterator format(const usvfs::UnicodeString& v, FmtContext& ctx) const + { + if (v.size() == 0) { + return std::format_to(ctx.out(), "<empty string>"); + } else { + return std::formatter<PCUNICODE_STRING, char>::format(&v.m_Data, ctx); + } + } +}; diff --git a/libs/usvfs/src/shared/wildcard.cpp b/libs/usvfs/src/shared/wildcard.cpp new file mode 100644 index 0000000..a2efe6d --- /dev/null +++ b/libs/usvfs/src/shared/wildcard.cpp @@ -0,0 +1,194 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "wildcard.h" +#include "logging.h" +#include "windows_sane.h" + +static bool IsInnerMatch(LPCWSTR pszString, LPCWSTR pszMatch) +{ + while (*pszMatch != L'\0') { + if ((*pszMatch == L'?') || (*pszMatch == L'>')) { + if (!*pszString) { + // ? must match exactly one character + return false; + } + + ++pszString; + ++pszMatch; + } else if ((*pszMatch == L'*') || (*pszMatch == L'<')) { + if (IsInnerMatch(pszString, pszMatch + 1)) { + // * may match empty string or we may have matched something already + return true; + } + + return *pszString && IsInnerMatch(pszString + 1, pszMatch); + + // the rest of the string can't be matched + } else { + if (CharUpperW(MAKEINTRESOURCEW(MAKELONG(*pszString++, 0))) != + CharUpperW(MAKEINTRESOURCEW(MAKELONG(*pszMatch++, 0)))) { + // regular chars compare + return false; + } + } + } + + return !*pszString && !*pszMatch; +} + +static LPCSTR InnerMatch(LPCSTR pszString, LPCSTR pszMatch) +{ + // We have a special case where string is empty ("") and the mask is "*". + // We need to handle this too. So we can't test on !*pszString here. + // The loop breaks when the match string is exhausted. + while (*pszString != '\0') { + // Single wildcard character + if ((*pszMatch == '?') || (*pszMatch == '>')) { + // can't match directory separator + if ((*pszString == '\\') || (*pszString == '/')) + return nullptr; + + // Matches any character except empty string + if (*pszString == '\0') { + // string consumed, part of the pattern is left + return pszMatch; + } + + // OK next + ++pszString; + ++pszMatch; + } else if ((*pszMatch == '*') || (*pszMatch == '<')) { + // * doesn't match directory separators + if ((*pszString == '\\') || (*pszString == '/')) { + ++pszMatch; + continue; + } + + // Need to do some tricks. + + // 1. The wildcard * is ignored. + // So just an empty string matches. This is done by recursion. + // Because we eat one character from the match string, the + // recursion will stop. + { + LPCSTR remainder = InnerMatch(pszString, pszMatch + 1); + if (remainder != nullptr) { + // we have a match and the * replaces no other character + return remainder; + } + } + + // 2. Chance we eat the next character and try it again, with a + // wildcard * match. This is done by recursion. Because we eat + // one character from the string, the recursion will stop. + if (*pszString != '\0') { + LPCSTR remainder = InnerMatch(pszString + 1, pszMatch); + if (remainder != nullptr) { + return remainder; + } + } + + // Nothing worked with this wildcard. + return nullptr; + } else { + // Standard compare of 2 chars. Note that *pszSring might be 0 + // here, but then we never get a match on *pszMask that has always + // a value while inside this loop. + if (CharUpperA(MAKEINTRESOURCEA(MAKELONG(*pszString++, 0))) != + CharUpperA(MAKEINTRESOURCEA(MAKELONG(*pszMatch++, 0)))) + return nullptr; + } + } + + // successful match if the input string was completely consumed + if (*pszString == '\0') { + while ((*pszMatch == '*') || (*pszMatch == '<')) { + ++pszMatch; + } + return pszMatch; + } else { + return nullptr; + } +} + +namespace usvfs::shared::wildcard +{ + +bool Match(LPCWSTR pszString, LPCWSTR pszMatch) +{ + if (*pszString == L'.') { + // cmd.exe seems to ignore + return Match(pszString + 1, pszMatch); + } else { + size_t len = wcslen(pszMatch); + if ((len > 2) && (wcscmp(pszMatch + len - 2, L".*") == 0)) { + // cmd.exe seems to completely ignore .* at the end. + std::wstring temp(pszMatch, pszMatch + len - 2); + return IsInnerMatch(pszString, temp.c_str()); + } + return IsInnerMatch(pszString, pszMatch); + } +} + +bool Match(LPCSTR pszString, LPCSTR pszMatch) +{ + if (*pszString == '.') { + // cmd.exe seems to ignore + return Match(pszString + 1, pszMatch); + } else { + size_t len = strlen(pszMatch); + LPCSTR res = nullptr; + if ((len > 2) && (strcmp(pszMatch + len - 2, ".*") == 0)) { + // cmd.exe seems to completely ignore .* at the end. + std::string temp(pszMatch, pszMatch + len - 2); + res = InnerMatch(pszString, temp.c_str()); + } else { + res = InnerMatch(pszString, pszMatch); + } + return ((res != nullptr) && (*res == '\0')); + } +} + +LPCSTR PartialMatch(LPCSTR pszString, LPCSTR pszMatch) +{ + if (*pszString == '.') { + // cmd.exe seems to ignore dots at the start + return PartialMatch(pszString + 1, pszMatch); + } else { + size_t len = strlen(pszMatch); + if ((len > 2) && (strcmp(pszMatch + len - 2, ".*") == 0)) { + // in cmd.exe there seems to be no difference between <something>* and + // <something>*.* + std::string temp(pszMatch, pszMatch + len - 2); + LPCSTR pos = InnerMatch(pszString, temp.c_str()); + if (pos != nullptr) { + if (*pos == '\0') { + return pszMatch + strlen(pszMatch); + } else { + return pszMatch + (pos - temp.c_str()); + } + } + } + return InnerMatch(pszString, pszMatch); + } +} + +} // namespace usvfs::shared::wildcard diff --git a/libs/usvfs/src/shared/wildcard.h b/libs/usvfs/src/shared/wildcard.h new file mode 100644 index 0000000..bf5b4a3 --- /dev/null +++ b/libs/usvfs/src/shared/wildcard.h @@ -0,0 +1,67 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +/// Wildcard matching code +/// by Martin Richter +/// licensed under The Code Project Open License (CPOL) + +#pragma once + +#include "windows_sane.h" + +namespace usvfs::shared::wildcard +{ + +/** + * @brief match string to wildcard windows-style + * @param pszString Input string to match + * @param pszMatch Match mask that may contain wildcards like ? and * + * @note A ? sign matches any character, except an empty string. + * @note A * sign matches any string inclusive an empty string. + * @note Characters are compared caseless. + * @return true if the string matches the pattern + */ +bool Match(LPCWSTR pszString, LPCWSTR pszMatch); + +/** + * @brief match string to wildcard windows-style + * @param pszString Input string to match + * @param pszMatch Match mask that may contain wildcards like ? and * + * @note A ? sign matches any character, except an empty string. + * @note A * sign matches any string inclusive an empty string. + * @note Characters are compared caseless. + * @return true if the string matches the pattern + */ +bool Match(LPCSTR pszString, LPCSTR pszMatch); + +/** + * @brief match string to wildcard windows-style + * @param pszString Input string to match + * @param pszMatch Match mask that may contain wildcards like ? and * + * @note A ? sign matches any character, except an empty string. + * @note A * sign matches any string inclusive an empty string. + * @note Characters are compared caseless. + * @return the "not-consumed" remainder of the pattern. If this points to a + * zero terminator, this was a full match. + * Returns nullptr if no match is possible + */ +LPCSTR PartialMatch(LPCSTR pszString, LPCSTR pszMatch); + +} // namespace usvfs::shared::wildcard diff --git a/libs/usvfs/src/shared/winapi.cpp b/libs/usvfs/src/shared/winapi.cpp new file mode 100644 index 0000000..e28a13c --- /dev/null +++ b/libs/usvfs/src/shared/winapi.cpp @@ -0,0 +1,490 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "winapi.h" +#include "exceptionex.h" +#include "logging.h" +#include "ntdll_declarations.h" +#include "stringcast.h" +#include "stringutils.h" +#include "unicodestring.h" + +namespace winapi::ansi +{ + +std::string getModuleFileName(HMODULE module, HANDLE process) +{ + std::wstring result = wide::getModuleFileName(module, process); + return usvfs::shared::string_cast<std::string>(result); +} + +std::string getCurrentDirectory() +{ + std::string result; + DWORD required = GetCurrentDirectoryA(0, nullptr); + if (required == 0UL) { + throw usvfs::shared::windows_error("failed to determine current working directory"); + } + result.resize(required); + GetCurrentDirectoryA(required, &result[0]); + result.resize(required - 1); + return result; +} + +std::pair<std::string, std::string> getFullPathName(LPCSTR fileName) +{ + static const int INIT_SIZE = 128; + std::string result; + result.resize(INIT_SIZE); + LPSTR filePart = nullptr; + DWORD requiredSize = GetFullPathNameA(fileName, INIT_SIZE, &result[0], &filePart); + if (requiredSize >= INIT_SIZE) { + result.resize(requiredSize); + GetFullPathNameA(fileName, requiredSize, &result[0], &filePart); + } + if (requiredSize != 0UL) { + return std::make_pair(result, std::string(filePart != nullptr ? filePart : "")); + } else { + return make_pair(result, std::string()); + } +} + +} // namespace winapi::ansi + +namespace winapi::wide +{ + +std::wstring getModuleFileName(HMODULE module, HANDLE process) +{ + std::wstring result; + result.resize(64); + DWORD rc = 0UL; + + while ((rc = (process == INVALID_HANDLE_VALUE) + ? ::GetModuleFileNameW(module, &result[0], + static_cast<DWORD>(result.size())) + : ::GetModuleFileNameExW(process, module, &result[0], + static_cast<DWORD>(result.size()))) == + result.size()) { + result.resize(result.size() * 2); + } + + if (rc == 0UL) { + if (::GetLastError() == ERROR_PARTIAL_COPY) { +#if BOOST_ARCH_X86_64 + return L"unknown (32-bit process)"; +#else + return L"unknown (64-bit process)"; +#endif + } else { + throw usvfs::shared::windows_error("failed to retrieve module file name"); + } + } + + result.resize(rc); + + return result; +} + +std::pair<std::wstring, std::wstring> getFullPathName(LPCWSTR fileName) +{ + wchar_t buf1[MAX_PATH]; + std::vector<wchar_t> buf2; + wchar_t* result = buf1; + LPWSTR filePart = nullptr; + DWORD requiredSize = GetFullPathNameW(fileName, MAX_PATH, result, &filePart); + if (requiredSize >= MAX_PATH) { + buf2.resize(requiredSize); + result = &buf2[0]; + requiredSize = GetFullPathNameW(fileName, requiredSize, result, &filePart); + } + return make_pair(std::wstring(result, requiredSize), + std::wstring((requiredSize && filePart) ? filePart : L"")); +} + +std::wstring getCurrentDirectory() +{ + // really great api this (::GetCurrentDirectoryW) + // - if it succeeds, returns size in characters WITHOUT zero termination + // - if it fails due to buffer too small, returns size in characters WITH zero + // termination + // - if it fails for other reasons, returns 0 + std::wstring result; + DWORD required = GetCurrentDirectoryW(0, nullptr); + if (required == 0UL) { + throw usvfs::shared::windows_error("failed to determine current working directory"); + } + result.resize(required); + GetCurrentDirectoryW(required, &result[0]); + result.resize(required - 1); + return result; +} + +std::wstring getKnownFolderPath(REFKNOWNFOLDERID folderID) +{ + PWSTR writablePath; + + ::SHGetKnownFolderPath(folderID, 0, nullptr, &writablePath); + + ON_BLOCK_EXIT([writablePath]() { + ::CoTaskMemFree(writablePath); + }); + + return std::wstring(writablePath); +} + +} // namespace winapi::wide + +namespace winapi::ex +{ + +std::pair<uintptr_t, uintptr_t> getSectionRange(HANDLE moduleHandle) +{ + std::pair<uintptr_t, uintptr_t> result; + bool found = false; + uintptr_t exeModule = reinterpret_cast<uintptr_t>(moduleHandle); + if (exeModule == 0) { + throw std::runtime_error("failed to determine address range of executable"); + } + + std::pair<uintptr_t, uintptr_t> totalRange{UINT_MAX, 0}; + + PIMAGE_DOS_HEADER dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(exeModule); + PIMAGE_NT_HEADERS ntHeader = + reinterpret_cast<PIMAGE_NT_HEADERS>(exeModule + dosHeader->e_lfanew); + PIMAGE_SECTION_HEADER sectionHeader = + reinterpret_cast<PIMAGE_SECTION_HEADER>(ntHeader + 1); + for (int i = 0; i < ntHeader->FileHeader.NumberOfSections && !found; ++i) { + if (memcmp(sectionHeader->Name, ".text", 5) == 0) { + result.first = exeModule + sectionHeader->VirtualAddress; + result.second = result.first + sectionHeader->Misc.VirtualSize; + found = true; + } else { + uintptr_t start = exeModule + sectionHeader->VirtualAddress; + totalRange.first = std::min(totalRange.first, start); + totalRange.second = std::max<uintptr_t>(totalRange.second, + start + sectionHeader->Misc.VirtualSize); + } + ++sectionHeader; + } + + if (!found) { + return totalRange; + } + + return result; +} + +OSVersion getOSVersion() +{ + RTL_OSVERSIONINFOEXW versionInfo; + ZeroMemory(&versionInfo, sizeof(RTL_OSVERSIONINFOEXW)); + versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW); + RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo); + + OSVersion result; + result.major = versionInfo.dwMajorVersion; + result.minor = versionInfo.dwMinorVersion; + result.build = versionInfo.dwBuildNumber; + result.platformid = versionInfo.dwPlatformId; + result.servicpack = + versionInfo.wServicePackMajor << 16 | versionInfo.wServicePackMinor; + return result; +} + +} // namespace winapi::ex + +namespace winapi::ex::ansi +{ + +std::string errorString(DWORD errorCode) +{ + std::ostringstream finalMessage; + + LPSTR buffer = nullptr; + + DWORD currentErrorCode = GetLastError(); + + errorCode = + errorCode != std::numeric_limits<DWORD>::max() ? errorCode : currentErrorCode; + + // TODO: the message is not english? + if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, errorCode, + 0 //, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) + , + (LPSTR)&buffer, 0, nullptr) == 0) { + finalMessage << "(unknown error [" << errorCode << "])"; + } else { + if (buffer != nullptr) { + size_t end = strlen(buffer) - 1; + while ((buffer[end] == '\n') || (buffer[end] == '\r')) { + buffer[end--] = '\0'; + } + finalMessage << "(" << buffer << " [" << errorCode << "])"; + LocalFree(buffer); // allocated by FormatMessage + } + } + + SetLastError(currentErrorCode); // restore error code because FormatMessage might + // have modified it + return finalMessage.str(); +} + +std::string toString(const FILETIME& time) +{ + SYSTEMTIME temp; + FileTimeToSystemTime(&time, &temp); + std::ostringstream stream; + stream << temp.wYear << "-" << temp.wMonth << "-" << temp.wDay << temp.wHour << ":" + << temp.wMinute << ":" << temp.wSecond; + return stream.str(); +} + +LPCSTR GetBaseName(LPCSTR string) +{ + LPCSTR result = string + strlen(string) - 1; + while (result > string) { + if ((*result == '\\') || (*result == '/')) { + ++result; + break; + } else { + --result; + } + } + return result; +} + +} // namespace winapi::ex::ansi + +namespace winapi::ex::wide +{ + +bool fileExists(LPCWSTR fileName, bool* isDirectory) +{ + DWORD attrib = GetFileAttributesW(fileName); + + if (attrib == INVALID_FILE_ATTRIBUTES) { + return false; + } else { + if (isDirectory != nullptr) { + *isDirectory = (attrib & FILE_ATTRIBUTE_DIRECTORY) != 0; + } + return true; + } +} + +std::wstring errorString(DWORD errorCode) +{ + std::wostringstream finalMessage; + + LPWSTR buffer = nullptr; + + DWORD currentErrorCode = GetLastError(); + + errorCode = + errorCode != std::numeric_limits<DWORD>::max() ? errorCode : currentErrorCode; + + // TODO: the message is not english? + if (FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, errorCode, + 0 //, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) + , + (LPWSTR)&buffer, 0, nullptr) == 0) { + finalMessage << L"(unknown error [" << errorCode << "])"; + } else { + if (buffer != nullptr) { + size_t end = wcslen(buffer) - 1; + while ((buffer[end] == L'\n') || (buffer[end] == L'\r')) { + buffer[end--] = L'\0'; + } + finalMessage << L"(" << buffer << L" [" << errorCode << L"])"; + LocalFree(buffer); // allocated by FormatMessage + } + } + + SetLastError(currentErrorCode); // restore error code because FormatMessage might + // have modified it + return finalMessage.str(); +} + +std::wstring toString(const FILETIME& time) +{ + SYSTEMTIME temp; + FileTimeToSystemTime(&time, &temp); + std::wostringstream stream; + stream << temp.wYear << "-" << temp.wMonth << "-" << temp.wDay << temp.wHour << ":" + << temp.wMinute << ":" << temp.wSecond; + return stream.str(); +} + +LPCWSTR GetBaseName(LPCWSTR string) +{ + LPCWSTR result; + if ((string == nullptr) || (string[0] == L'\0')) { + result = string; + } else { + result = string + wcslen(string) - 1; + } + + while (result > string) { + if ((*result == L'\\') || (*result == L'/')) { + ++result; + break; + } else { + --result; + } + } + return result; +} + +LPWSTR GetBaseName(LPWSTR path) +{ + LPCWSTR result = GetBaseName(static_cast<LPCWSTR>(path)); + return const_cast<LPWSTR>(result); +} + +std::wstring getSectionName(PVOID addressIn, HANDLE process) +{ + if (process == nullptr) { + process = GetCurrentProcess(); + } + HMODULE modules[1024]; + intptr_t address = reinterpret_cast<intptr_t>(addressIn); + DWORD required; + if (::EnumProcessModules(process, modules, sizeof(modules), &required)) { + for (DWORD i = 0; i < (std::min<DWORD>(1024UL, required) / sizeof(HMODULE)); ++i) { + std::pair<intptr_t, intptr_t> range = getSectionRange(modules[i]); + if ((address > range.first) && (address < range.second)) { + try { + return winapi::wide::getModuleFileName(modules[i], process); + } catch (const std::exception&) { + return std::wstring(L"unknown"); + } + } + } + } + return std::wstring(L"unknown"); +} + +std::vector<FileResult> quickFindFiles(LPCWSTR directoryName, LPCWSTR pattern) +{ + std::vector<FileResult> result; + + static const unsigned int BUFFER_SIZE = 1024; + + HANDLE hdl = CreateFileW(directoryName, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + + ON_BLOCK_EXIT([hdl]() { + CloseHandle(hdl); + }); + + uint8_t buffer[BUFFER_SIZE]; + + NTSTATUS res = STATUS_SUCCESS; // status success + while (res == STATUS_SUCCESS) { + IO_STATUS_BLOCK status; + + res = NtQueryDirectoryFile( + hdl, nullptr, nullptr, nullptr, &status, buffer, BUFFER_SIZE, + FileFullDirectoryInformation, FALSE, + static_cast<PUNICODE_STRING>(usvfs::UnicodeString(pattern)), FALSE); + if (res == STATUS_SUCCESS) { + FILE_FULL_DIR_INFORMATION* info = + reinterpret_cast<FILE_FULL_DIR_INFORMATION*>(buffer); + void* endPos = buffer + status.Information; + while (info < endPos) { + FileResult file; + file.fileName = + std::wstring(info->FileName, info->FileNameLength / sizeof(wchar_t)); + file.attributes = info->FileAttributes; + + result.push_back(file); + if (info->NextEntryOffset == 0) { + break; + } else { + info = reinterpret_cast<FILE_FULL_DIR_INFORMATION*>( + reinterpret_cast<uint8_t*>(info) + info->NextEntryOffset); + } + } + } + } + + return result; +} + +bool createPath(boost::filesystem::path path, LPSECURITY_ATTRIBUTES securityAttributes) +{ + // sanity and guaranteed recursion end: + if (!path.has_relative_path()) + throw usvfs::shared::windows_error( + "createPath() refusing to create non-existing top level path: " + + path.string()); + + DWORD attr = GetFileAttributesW(path.c_str()); + DWORD err = GetLastError(); + if (attr != INVALID_FILE_ATTRIBUTES) { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return false; // if directory already exists all is good + else + throw usvfs::shared::windows_error("createPath() called on a file: " + + path.string()); + } + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw usvfs::shared::windows_error( + "createPath() GetFileAttributesW failed on: " + path.string(), err); + + if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory + // already exists + createPath(path.parent_path(), + securityAttributes); // otherwise create parent directory (recursively) + + BOOL res = CreateDirectoryW(path.c_str(), securityAttributes); + if (!res) { + err = GetLastError(); + throw usvfs::shared::windows_error( + "createPath() CreateDirectoryW failed on: " + path.string(), err); + } + return true; +} + +std::wstring getWindowsBuildLab(bool ex) +{ + HKEY hKey = nullptr; + auto res = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + LR"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", 0, + KEY_READ, &hKey); + if (res != ERROR_SUCCESS || !hKey) + return L"Opening HKLM Windows NT\\CurrentVersion failed?!"; + WCHAR buf[200]; + DWORD size = static_cast<DWORD>(sizeof(buf)); + res = RegQueryValueExW(hKey, ex ? L"BuildLabEx" : L"BuildLab", NULL, NULL, + reinterpret_cast<LPBYTE>(buf), &size); + if (res != ERROR_SUCCESS || size > sizeof(buf)) + return ex ? L"BuildLabEx reg value not found?!" : L"BuildLab reg value not found?!"; + size /= sizeof(buf[0]); + if (size && !buf[size - 1]) + --size; + return std::wstring(buf, size); +} + +} // namespace winapi::ex::wide diff --git a/libs/usvfs/src/shared/winapi.h b/libs/usvfs/src/shared/winapi.h new file mode 100644 index 0000000..f94eff6 --- /dev/null +++ b/libs/usvfs/src/shared/winapi.h @@ -0,0 +1,610 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "logging.h" +#include "stringcast.h" +#include "windows_sane.h" +#include <ShlObj.h> + +#define ALIAS(alias, original) \ + template <typename... Args> \ + auto alias(Args&&... args) -> decltype(original(std::forward<Args>(args)...)) \ + { \ + return original(std::forward<Args>(args)...); \ + } + +#define ALIAST(alias, original) \ + template <typename T, typename... Args> \ + auto alias<T>(Args && ... args) \ + -> decltype(original<T>(std::forward<Args>(args)...)) \ + { \ + return original<T>(std::forward<Args>(args)...); \ + } + +namespace winapi +{ + +struct parameter_error : public std::runtime_error +{ + parameter_error(const std::string& msg) : runtime_error(msg) {} +}; + +} // namespace winapi + +namespace winapi::process +{ + +/** + * @brief result of process creation + */ +struct Result +{ + Result() + { + ::ZeroMemory(&processInfo, sizeof(PROCESS_INFORMATION)); + ::ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); + startupInfo.cb = sizeof(STARTUPINFO); + } + + Result(Result&& reference) + : valid(reference.valid), startupInfo(reference.startupInfo), + processInfo(reference.processInfo), errorCode(reference.errorCode) + { + reference.valid = false; + } + + ~Result() + { + if (valid) { + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + } + + if (stdoutPipe != INVALID_HANDLE_VALUE) { + CloseHandle(stdoutPipe); + } + } + + Result(const Result&) = delete; + + size_t readStdout(std::vector<uint8_t>& buffer, bool& eof) + { + if (stdoutPipe != INVALID_HANDLE_VALUE) { + DWORD read; + BOOL res = ReadFile(stdoutPipe, &buffer[0], static_cast<DWORD>(buffer.size()), + &read, nullptr); + eof = (res == TRUE) && (read == 0); + return static_cast<size_t>(read); + } else { + eof = true; + return 0; + } + } + + bool valid{false}; + STARTUPINFO startupInfo; + PROCESS_INFORMATION processInfo; + DWORD errorCode{0UL}; + + HANDLE stdoutPipe{INVALID_HANDLE_VALUE}; +}; + +/** + * @brief internal class to handle process creation with named parameters. + */ +template <typename CharT> +class _Create +{ +public: + _Create(const std::basic_string<CharT>& binaryName); + _Create(const _Create<CharT>& reference) = delete; + _Create<CharT>& operator=(const _Create<CharT>& reference) = delete; + + _Create(_Create<CharT>&& reference) + : m_CurrentDirectory(std::move(reference.m_CurrentDirectory)), + m_ProcessAttributes(reference.m_ProcessAttributes), + m_ThreadAttributes(reference.m_ThreadAttributes), + m_InheritHandles(reference.m_InheritHandles), + m_CreationFlags(std::move(reference.m_CreationFlags)), + m_Executed(reference.m_Executed) + { + // stringstream should be moveable but it seems it isn't on mingw + m_CommandLine << reference.m_CommandLine.rdbuf(); + } + + /// named parameter "argument". May be called repeatedly. This is + /// directly appended to the command line with a separating space. No + /// quoting happens + template <typename ArgT> + _Create& argument(const ArgT& argin) + { + m_CommandLine << " " << argin; + return *this; + } + + template <typename ArgT> + _Create& arg(const ArgT& argin) + { + return this->argument(argin); + } + + template <typename IterT> + _Create& arguments(IterT begin, IterT end) + { + for (; begin != end; ++begin) { + m_CommandLine << " " << *begin; + } + + return *this; + } + + /// @brief set the working directory for the process + _Create& workingDirectory(const std::basic_string<CharT>& path); + + /// @brief set process attributes + _Create& processAttributes(SECURITY_ATTRIBUTES* attributes); + + /// @brief set thread attributes + _Create& threadAttributes(SECURITY_ATTRIBUTES* attributes); + + /// @brief activate inheriting handles + _Create& inheritHandles(); + + /// @brief have the process start suspended + _Create& suspended(); + + /// @brief set the process up to output stout to a pipe which can be + /// retrieved through the result object + _Create& stdoutPipe(); + + /// @brief end the named parameter cascade and create the process + Result operator()() + { + m_CommandLine.seekp(0, std::ios::end); + unsigned int length = static_cast<unsigned int>(m_CommandLine.tellp()); + std::unique_ptr<CharT[]> clBuffer(new CharT[length + 1]); + memset(clBuffer.get(), 0, (length + 1) * sizeof(CharT)); + memcpy(clBuffer.get(), m_CommandLine.str().c_str(), length * sizeof(CharT)); + Result result; + + if (m_StdoutPipe) { + result.stdoutPipe = setupPipe(result.startupInfo.hStdOutput); + result.startupInfo.dwFlags |= STARTF_USESTDHANDLES; + } + + result.valid = + createProcessInt(nullptr, clBuffer.get(), m_ProcessAttributes, + m_ThreadAttributes, m_InheritHandles, m_CreationFlags, nullptr, + m_CurrentDirectory.length() > 0 ? m_CurrentDirectory.c_str() + : nullptr, + &result.startupInfo, &result.processInfo) == TRUE; + + if (m_Stdout != INVALID_HANDLE_VALUE) { + // got to close the write end of pipes + CloseHandle(result.startupInfo.hStdOutput); + } + + if (result.valid) { + result.errorCode = NOERROR; + } else { + result.errorCode = GetLastError(); + } + return result; + } + +private: + static BOOL createProcessInt(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + SECURITY_ATTRIBUTES* lpProcessAttributes, + SECURITY_ATTRIBUTES* lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, + LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation) + { + return ::CreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, + lpThreadAttributes, bInheritHandles, dwCreationFlags, + lpEnvironment, lpCurrentDirectory, lpStartupInfo, + lpProcessInformation); + } + + static BOOL createProcessInt(LPCSTR lpApplicationName, LPSTR lpCommandLine, + SECURITY_ATTRIBUTES* lpProcessAttributes, + SECURITY_ATTRIBUTES* lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, + LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation) + { + std::wstring executable; + if (lpApplicationName != nullptr) { + executable = usvfs::shared::string_cast<std::wstring>(lpApplicationName); + } + + std::wstring cmdline; + if (lpCommandLine != nullptr) { + cmdline = usvfs::shared::string_cast<std::wstring>(lpCommandLine); + } + + std::wstring cwd; + if (lpCurrentDirectory != nullptr) { + cwd = usvfs::shared::string_cast<std::wstring>(lpCurrentDirectory); + } + + return ::CreateProcessW(lpApplicationName != nullptr ? executable.c_str() : nullptr, + lpCommandLine != nullptr ? &cmdline[0] : nullptr, + lpProcessAttributes, lpThreadAttributes, bInheritHandles, + dwCreationFlags, lpEnvironment, + lpCurrentDirectory != nullptr ? cwd.c_str() : nullptr, + lpStartupInfo, lpProcessInformation); + } + + HANDLE setupPipe(HANDLE& childHandle) + { + SECURITY_ATTRIBUTES attr; + attr.nLength = sizeof(SECURITY_ATTRIBUTES); + attr.bInheritHandle = TRUE; + attr.lpSecurityDescriptor = nullptr; + + HANDLE pipe[2]; + + CreatePipe(&pipe[0], &pipe[1], &attr, 0); + SetHandleInformation(pipe[0], HANDLE_FLAG_INHERIT, 0); + + childHandle = pipe[1]; + + return pipe[0]; + } + +private: + std::basic_stringstream<CharT> m_CommandLine; + std::basic_string<CharT> m_CurrentDirectory{}; + SECURITY_ATTRIBUTES* m_ProcessAttributes{nullptr}; + SECURITY_ATTRIBUTES* m_ThreadAttributes{nullptr}; + BOOL m_InheritHandles{false}; + DWORD m_CreationFlags{0UL}; + bool m_Executed{false}; + bool m_StdoutPipe{false}; + + HANDLE m_Stdout{INVALID_HANDLE_VALUE}; +}; + +} // namespace winapi::process + +namespace winapi::file +{ +/** + * @brief internal class to handle file creation (opening) with named + * parameters. + */ +template <typename CharT, DWORD DefaultDisposition> +class _Create +{ +public: + _Create(const std::basic_string<CharT>& fileName) : m_FileName(fileName) {} + + _Create& access(DWORD desiredAccess) + { + m_DesiredAccess = desiredAccess; + return *this; + } + + _Create& share(DWORD shareMode) + { + m_ShareMode = shareMode; + return *this; + } + + _Create& createAlways() + { + m_CreationDisposition = CREATE_ALWAYS; + return *this; + } + + _Create& openAlways() + { + m_CreationDisposition = OPEN_ALWAYS; + return *this; + } + + _Create& security(SECURITY_ATTRIBUTES* attributes) + { + m_SecurityAttributes = attributes; + return *this; + } + + _Create& templateFile(HANDLE templateFile) + { + m_Template = templateFile; + return *this; + } + + /// @brief end the named parameter cascade and open the file + HANDLE operator()() + { + return callDelegate( + std::integral_constant<bool, sizeof(CharT) == sizeof(wchar_t)>()); + } + +private: + HANDLE callDelegate(std::true_type) + { + return ::CreateFileW(m_FileName.c_str(), m_DesiredAccess, m_ShareMode, + m_SecurityAttributes, m_CreationDisposition, m_Flags, + m_Template); + } + + HANDLE callDelegate(std::false_type) + { + return ::CreateFileA(m_FileName.c_str(), m_DesiredAccess, m_ShareMode, + m_SecurityAttributes, m_CreationDisposition, m_Flags, + m_Template); + } + +private: + std::basic_string<CharT> m_FileName; + DWORD m_DesiredAccess{GENERIC_ALL}; + DWORD m_ShareMode{0UL}; + DWORD m_CreationDisposition{DefaultDisposition}; + DWORD m_Flags{FILE_ATTRIBUTE_NORMAL}; + HANDLE m_Template{nullptr}; + SECURITY_ATTRIBUTES* m_SecurityAttributes{nullptr}; +}; + +} // namespace winapi::file + +namespace winapi::ansi +{ + +std::string getModuleFileName(HMODULE module, HANDLE process = INVALID_HANDLE_VALUE); +std::pair<std::string, std::string> getFullPathName(LPCSTR fileName); +std::string getCurrentDirectory(); +typedef process::_Create<char> createProcess; +typedef file::_Create<char, CREATE_NEW> createFile; +typedef file::_Create<char, OPEN_EXISTING> openFile; + +} // namespace winapi::ansi + +namespace winapi::wide +{ + +std::wstring getModuleFileName(HMODULE module, HANDLE process = INVALID_HANDLE_VALUE); +std::pair<std::wstring, std::wstring> getFullPathName(LPCWSTR fileName); +std::wstring getCurrentDirectory(); +std::wstring getKnownFolderPath(REFKNOWNFOLDERID folderID); + +typedef process::_Create<wchar_t> createProcess; +typedef file::_Create<wchar_t, CREATE_NEW> createFile; +typedef file::_Create<wchar_t, OPEN_EXISTING> openFile; + +} // namespace winapi::wide + +/** + * useful convenience functions close to the api + */ +namespace winapi::ex +{ + +/** + * @brief retrieve the address range covering the code section of a module + * @param moduleHandle handle to the module + * @return start and end address of the code section + * @note the code section can only be identified if it has the standardized section name + * ".text" Otherwise the whole address range of all sections in the module is returned. + * This happens for compressed exectuables for example + */ +std::pair<uintptr_t, uintptr_t> getSectionRange(HANDLE moduleHandle); + +struct OSVersion +{ + DWORD major; + DWORD minor; + DWORD build; + DWORD platformid; + DWORD servicpack; +}; + +OSVersion getOSVersion(); + +} // namespace winapi::ex + +namespace winapi::ex::ansi +{ + +/** + * @brief retrieve an error string for a windows error message + * @param errorCode the error code to look up. If this is left at the default, + * ::GetLastError is used + * @return string representation of the error. Currently this is localized + */ +std::string errorString(DWORD errorCode = std::numeric_limits<DWORD>::max()); + +/** + * @brief convert filetime to string + * @param time time to convert + * @return a string representation (currently only supports utc and iso format with + * second precision) + */ +std::string toString(const FILETIME& time); + +/** + * @brief find file name in a windows file path + * @param path the path to search in + * @return the file name of the path or an empty string if the path ends on + * a slash + * @note this function doesn't access the file system so it doesn't depend + * on whether the file actually exists. This also means it can't + * determine if a path that doesn't end on a slash refers to a file or + * directory + * @note the return value is a pointer into the same buffer, no copy is + * created + */ +LPCSTR GetBaseName(LPCSTR string); + +} // namespace winapi::ex::ansi + +namespace winapi::ex::wide +{ +/** + * retrieve the name of the binary section containing the specified address + * @param address the address to test + * @param process the process for which to retrieve the section. If this is + * nullptr, the current process is analized. + * @return name of the section or "unknown" if no matching section was found + */ +std::wstring getSectionName(PVOID address, HANDLE process = nullptr); + +/** + * @brief test if a file exists + * @param path path to check + * @param isDirectory (optional) if this isn't null, it will be set to true if the path + * specifies a directory, false otherwise + * @return true if the file (or directory) exists. + */ +bool fileExists(LPCWSTR fileName, bool* isDirectory = nullptr); + +/** + * @brief retrieve an error string for a windows error message + * @param errorCode the error code to look up. If this is left at the default, + * ::GetLastError is used + * @return string representation of the error. Currently this is localized + */ +std::wstring errorString(DWORD errorCode = std::numeric_limits<DWORD>::max()); + +/** + * @brief convert filetime to string + * @param time time to convert + * @return a string representation (currently only supports utc and iso format with + * second precision) + */ +std::wstring toString(const FILETIME& time); + +/** + * @brief find file name in a windows file path + * @param path the path to search in + * @return the file name of the path or an empty string if the path ends on + * a slash + * @note this function doesn't access the file system so it doesn't depend + * on whether the file actually exists. This also means it can't + * determine if a path that doesn't end on a slash refers to a file or + * directory + * @note the return value is a pointer into the same buffer, no copy is + * created + */ +LPCWSTR GetBaseName(LPCWSTR path); + +/** + * @see const-variant of this function + */ +LPWSTR GetBaseName(LPWSTR path); + +struct FileResult +{ + std::wstring fileName; + ULONG attributes; +}; + +/** + * @brief a quick function to find all files in a directory or files following a + * pattern. This uses NtQueryDirectoryFile api internally so it should be faster than + * the usual FindFirstFile/FindNextFile pattern + * @param directoryName name of the directory to search in + * @param pattern name pattern that needs to match + * @return the list of files found + */ +std::vector<FileResult> quickFindFiles(LPCWSTR directoryName, LPCWSTR pattern); + +/** + * @brief create the specified directory including all intermediate + * directories + * @param path the path to create + * @param securityAttributes the security attributes to use for all created + * directories. if this is null (default), the standard attributes + * are used + * @return true if the directory (and possibly parent directories) were actually created + * and false if the directory already existed. Throws exceptions on failure. + */ +bool createPath(boost::filesystem::path path, + LPSECURITY_ATTRIBUTES securityAttributes = nullptr); +inline bool createPath(LPCWSTR path, LPSECURITY_ATTRIBUTES securityAttributes = nullptr) +{ + return createPath(boost::filesystem::path(path), securityAttributes); +} + +std::wstring getWindowsBuildLab(bool ex = false); + +} // namespace winapi::ex::wide + +namespace winapi::process +{ + +template <typename CharT> +_Create<CharT>::_Create(const std::basic_string<CharT>& binaryName) +{ + if (binaryName.length() > MAX_PATH) { + throw parameter_error("executable filename can't be longer than 260 characters"); + } + m_CommandLine << "\"" << binaryName << "\""; +} + +template <typename CharT> +_Create<CharT>& _Create<CharT>::workingDirectory(const std::basic_string<CharT>& path) +{ + m_CurrentDirectory = path; + return *this; +} + +template <typename CharT> +_Create<CharT>& _Create<CharT>::processAttributes(SECURITY_ATTRIBUTES* attributes) +{ + m_ProcessAttributes = attributes; + return *this; +} + +template <typename CharT> +_Create<CharT>& _Create<CharT>::threadAttributes(SECURITY_ATTRIBUTES* attributes) +{ + m_ThreadAttributes = attributes; + return *this; +} + +template <typename CharT> +_Create<CharT>& _Create<CharT>::inheritHandles() +{ + m_InheritHandles = true; + return *this; +} + +template <typename CharT> +_Create<CharT>& _Create<CharT>::suspended() +{ + m_CreationFlags |= CREATE_SUSPENDED; + return *this; +} + +template <typename CharT> +_Create<CharT>& _Create<CharT>::stdoutPipe() +{ + m_StdoutPipe = true; + return *this; +} + +} // namespace winapi::process diff --git a/libs/usvfs/src/shared/windows_sane.h b/libs/usvfs/src/shared/windows_sane.h new file mode 100644 index 0000000..7bb3726 --- /dev/null +++ b/libs/usvfs/src/shared/windows_sane.h @@ -0,0 +1,28 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> diff --git a/libs/usvfs/src/thooklib/CMakeLists.txt b/libs/usvfs/src/thooklib/CMakeLists.txt new file mode 100644 index 0000000..48426db --- /dev/null +++ b/libs/usvfs/src/thooklib/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(asmjit CONFIG REQUIRED) +find_library(LIBUDIS86_LIBRARY libudis86) +find_package(spdlog CONFIG REQUIRED) + +file(GLOB sources CONFIGURE_DEPENDS "*.cpp" "*.h") +source_group("" FILES ${sources}) + +add_library(thooklib STATIC ${sources}) +target_link_libraries(thooklib PRIVATE shared asmjit::asmjit ${LIBUDIS86_LIBRARY} spdlog::spdlog_header_only) +target_include_directories(thooklib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(thooklib PROPERTIES FOLDER injection) +target_precompile_headers(shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../shared/pch.h) diff --git a/libs/usvfs/src/thooklib/asmjit_sane.h b/libs/usvfs/src/thooklib/asmjit_sane.h new file mode 100644 index 0000000..365622a --- /dev/null +++ b/libs/usvfs/src/thooklib/asmjit_sane.h @@ -0,0 +1,29 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#pragma warning(push) +#pragma warning(disable : 4201) +#pragma warning(disable : 4244) +#pragma warning(disable : 4245) +#include <asmjit/asmjit.h> +#include <asmjit/x86.h> +#pragma warning(pop) diff --git a/libs/usvfs/src/thooklib/hooklib.cpp b/libs/usvfs/src/thooklib/hooklib.cpp new file mode 100644 index 0000000..9dbbafa --- /dev/null +++ b/libs/usvfs/src/thooklib/hooklib.cpp @@ -0,0 +1,740 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "udis86wrapper.h" +#include <boost/format.hpp> +#include <boost/predef.h> +#include <map> +#pragma warning(push, 3) +#include <asmjit/asmjit.h> +#pragma warning(pop) +#include "hooklib.h" +#include "ttrampolinepool.h" +#include "utility.h" +#include <addrtools.h> +#include <formatters.h> +#include <shmlogger.h> +#include <winapi.h> + +#if BOOST_ARCH_X86_64 +#pragma message("64bit build") +#define JUMP_SIZE 13 +#elif BOOST_ARCH_X86_32 +#define JUMP_SIZE 5 +#else +#error "unsupported architecture" +#endif + +using namespace asmjit; +// from here on out I'll only test for 64 or "other" + +using namespace HookLib; +using namespace asmjit; + +using namespace usvfs; + +struct THookInfo +{ + LPVOID originalFunction; + LPVOID replacementFunction; + LPVOID detour; // detour to call the original function after hook was installed. + LPVOID trampoline; // code fragment that decides whether the replacement function or + // detour is executed (preventing endless loops) + std::vector<uint8_t> + preamble; // part of the detour that needs to be re-inserted into the original + // function to return it to vanilla state + bool stub; // if this is true, the trampoline calls the "replacement"-function that + // before the original function, not instead of it + enum + { + TYPE_HOTPATCH, // official hot-patch variant as used on 32-bit windows + TYPE_WIN64PATCH, // custom patch variant used on 64-bit windows + TYPE_CHAINPATCH, // the hook is part of the hook chain (and not the first) + TYPE_OVERWRITE, // full jump overwrite used if none of the above work + TYPE_RIPINDIRECT // the function already started on a rip-relative jump so we only + // modified that variable + } type; +}; + +UDis86Wrapper& disasm() +{ + static UDis86Wrapper instance; + return instance; +} + +void PauseOtherThreads() +{ + // TODO: implement me! +} + +void ResumePausedThreads() +{ + // TODO: implement me! should resume only the threads paused by PauseOtherThreads +} + +// not using the disassembler because this is simpler +LPBYTE ShortJumpTarget(LPBYTE address) +{ + int8_t off = *(address + 1); + return address + 2 + off; +} + +size_t GetJumpSize(LPBYTE, LPVOID) +{ + // TODO: it would be neater to use asmjit to generate this jump and ask asmjit for + // the size of this jump but with asmjit I can only generate absolute jumps, which + // take too much space. + + // Since trampoline buffers is always allocated within 32-bit + // address range of jump, we can say with confidence that a 5-byte jump is possible + return 5; +} + +void WriteLongJump(LPBYTE jumpAddr, LPVOID destination) +{ + // TODO: not using asmjit here because I couldn't figure out how to generate + // a working, space-optimized, relative jump to outside the generated code and + // we do want to optimize this jump +#if BOOST_ARCH_X86_64 + intptr_t dist = reinterpret_cast<intptr_t>(destination) - + (reinterpret_cast<intptr_t>(jumpAddr) + 5); + int32_t distShort = static_cast<int32_t>(dist); +#else + int32_t distShort = reinterpret_cast<intptr_t>(destination) - + (reinterpret_cast<intptr_t>(jumpAddr) + 5); +#endif + *jumpAddr = 0xE9; + *reinterpret_cast<int32_t*>(jumpAddr + 1) = distShort; +} + +void WriteSingleJump(THookInfo& hookInfo, HookError* error) +{ + DWORD oldprotect, ignore; + // Set the target function to copy on write, so we don't modify code for other + // processes + if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, PAGE_EXECUTE_WRITECOPY, + &oldprotect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + WriteLongJump(reinterpret_cast<LPBYTE>(hookInfo.originalFunction), + hookInfo.trampoline); + + // restore old memory protection + if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, oldprotect, &ignore)) { + throw std::runtime_error("failed to change virtual protection"); + } + + if (error != nullptr) { + *error = ERR_NONE; + } +} + +void WriteShortJump(LPBYTE jumpAddr, const signed char offset) +{ + *jumpAddr = 0xEB; + *(jumpAddr + 1) = offset; +} + +void WriteIndirectJump(THookInfo& hookInfo, size_t jumpSize, HookError* error) +{ + DWORD oldProtect = 0; + LPBYTE jumpAddr = reinterpret_cast<LPBYTE>(hookInfo.originalFunction) - jumpSize; + // allow write to jump address + the short jump inside the function + if (!VirtualProtect(jumpAddr, jumpSize + 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + // insert the long jump first, then the short jump to the long jump, thus + // activating the reroute + WriteLongJump(jumpAddr, hookInfo.trampoline); + + PauseOtherThreads(); + WriteShortJump(jumpAddr + jumpSize, -(static_cast<int8_t>(jumpSize) + 2)); + ResumePausedThreads(); + + // restore access protection + if (!VirtualProtect(jumpAddr, jumpSize + 2, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + if (error != nullptr) { + *error = ERR_NONE; + } +} + +/// implements function hooking using the mechanism intended for hot patching +/// Explanation: the visual studio compiler offers an option to prepare functions +/// for hot patching. In this case the compiler leaves room for one far jump before +/// the actual function and a 2-byte nop inside the function. +/// to hook such a function we write a jump to our replacement function to the +/// space before the function and short jump to that jump to where the 2-byte nop +/// was. +/// On 32-bit windows, MS seems to have compiled all relevant functions in the +/// windows API for hot patching starting with Windows XP SP3 +/// On 64-bit windows a lot of functions have the space for a jump before the function +/// but they don't have the 2-byte nop so this function doesn't work +/// \param hookInfo info about the hook to be installed +/// \return true on success, false on error +BOOL HookHotPatch(THookInfo& hookInfo, HookError* error) +{ + LPVOID original = reinterpret_cast<LPVOID>(hookInfo.originalFunction); + + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + shared::AddrAdd(original, 2)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + shared::AddrAdd(original, 2)); + } + + WriteIndirectJump(hookInfo, JUMP_SIZE, error); + hookInfo.type = THookInfo::TYPE_HOTPATCH; + // in this case we don't need a separate detour, we simply jump past the 2-byte nop + hookInfo.detour = reinterpret_cast<LPBYTE>(hookInfo.originalFunction) + 2; + + return TRUE; +} + +uintptr_t followJumps(THookInfo& hookInfo) +{ + LPBYTE original = reinterpret_cast<LPBYTE>(hookInfo.originalFunction); + LPBYTE shortTarget = ShortJumpTarget(original); + + // disassemble the long jump + disasm().setInputBuffer(shortTarget, JUMP_SIZE); + + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + uint64_t res = ud_insn_off(disasm()) + ud_insn_len(disasm()); + res += disasm().jumpOffset(); + + return static_cast<uintptr_t>(res); +} + +/// +/// \brief hook a call that is implemented as a short-jump to a long jump using a +/// rip-relative address variable +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, the referred-to +/// variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookRIPIndirection(THookInfo& hookInfo, HookError* error) +{ + uintptr_t res = followJumps(hookInfo); + + const ud_operand_t* op = ud_insn_opr(disasm(), 0); + + if (op->base != UD_R_RIP) { + throw std::runtime_error("expected rip-relative addressing"); + } + + auto chainNext = disasm().jumpTarget(); + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainNext)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainNext)); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast<LPVOID>(res), 2, PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + *reinterpret_cast<uintptr_t*>(res) = reinterpret_cast<uintptr_t>(hookInfo.trampoline); + + if (!VirtualProtect(reinterpret_cast<LPVOID>(res), 2, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + hookInfo.type = THookInfo::TYPE_RIPINDIRECT; + hookInfo.detour = reinterpret_cast<LPVOID>(chainNext); + + if (error != nullptr) { + *error = ERR_NONE; + } + + return TRUE; +} + +BOOL HookChainHook(THookInfo& hookInfo, LPBYTE jumpPos, HookError* error) +{ + // disassemble the long jump + disasm().setInputBuffer(jumpPos, JUMP_SIZE); + + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + auto chainTarget = disasm().jumpTarget(); + + size_t size = ud_insn_len(disasm()); + + // save the original code for the preamble so we can restore it later + hookInfo.preamble.resize(size); + memcpy(&hookInfo.preamble[0], jumpPos, size); + + spdlog::get("usvfs")->info("existing hook to {0:x} in {1}", chainTarget, + shared::string_cast<std::string>( + winapi::ex::wide::getSectionName((void*)chainTarget))); + + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainTarget)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainTarget)); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(jumpPos, size, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + WriteLongJump(jumpPos, hookInfo.trampoline); + + if (!VirtualProtect(jumpPos, size, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + hookInfo.type = THookInfo::TYPE_CHAINPATCH; + hookInfo.detour = reinterpret_cast<LPVOID>(chainTarget); + + if (error != nullptr) { + *error = ERR_NONE; + } + + return TRUE; +} + +/// +/// implements hooking by chaining to an existing hook +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, +/// the referred-to variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookChainHook(THookInfo& hookInfo, HookError* error) +{ + return HookChainHook(hookInfo, reinterpret_cast<LPBYTE>(hookInfo.originalFunction), + error); +} + +/// +/// implements hooking by chaining to an existing hot patch +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, +/// the referred-to variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookChainPatch(THookInfo& hookInfo, HookError* error) +{ + LPBYTE original = reinterpret_cast<LPBYTE>(hookInfo.originalFunction); + LPBYTE shortTarget = ShortJumpTarget(original); + + return HookChainHook(hookInfo, shortTarget, error); +} + +/// implements function hooking by overwriting the first n bytes of the function +/// with a jump to the replacement function. Since this is destructive to the original +/// function code the first n bytes of the function need to be copied somewhere else +/// and that code needs to be called via a detour. This is a lot more complex than +/// the hotpatch mechanism. +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, the referred-to +/// variable is set to an error code +/// \return true on success, false on error +BOOL HookDisasm(THookInfo& hookInfo, HookError* error) +{ + LPBYTE address = reinterpret_cast<LPBYTE>(hookInfo.originalFunction); + ud_set_input_buffer(disasm(), address, 40); + + size_t jumpSize = GetJumpSize( + static_cast<LPBYTE>(hookInfo.originalFunction), + TrampolinePool::instance().currentBufferAddress(hookInfo.originalFunction)); + + // test if we have room for a jump before the function + bool jumpspace = true; + for (size_t i = 0; i < jumpSize; ++i) { + if (*(address - i - 1) != 0x90) { + jumpspace = false; + break; + } + } + + size_t minSize = jumpspace ? 2 : jumpSize; + + // iterate over all instructions that overlap with the jump instructions + // we want to write. + // TODO right now, this does not test if the function is smaller than the jump. + size_t size = 0; + while (size < minSize) { + if (ud_disassemble(disasm()) == 0) { + throw std::runtime_error("premature end of file in disassembly"); + } + + if ((size == 0) && (ud_insn_mnemonic(disasm()) == UD_Ijmp)) { + if (error != nullptr) { + *error = ERR_JUMP; + } + return FALSE; + } + + size += ud_insn_len(disasm()); + + // ret instruction or int3 smells like function end + if ((ud_insn_mnemonic(disasm()) == UD_Iret) || + (ud_insn_mnemonic(disasm()) == UD_Iint3)) { + if (error != nullptr) { + *error = ERR_FUNCEND; + } + return FALSE; + } + + // check the operands for relative addressing (not supported) + for (int i = 0; i < 3; ++i) { + const ud_operand* op = ud_insn_opr(disasm(), i); + if (op) { + if ( + // rip-relative are not handled, usually relative jumps + op->base == UD_R_RIP + + // rsp-relative call are not handled (there are valid rsp-relative + // instructions, e.g. sub) + || (ud_insn_mnemonic(disasm()) == UD_Icall && op->base == UD_R_RSP)) { + if (error != nullptr) { + *error = ERR_RIP; + } + return FALSE; + } + } + } + } + + // save the original code for the preamble so we can restore it later + hookInfo.preamble.resize(size); + memcpy(&hookInfo.preamble[0], hookInfo.originalFunction, size); + + size_t rerouteOffset = 0; + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset); + } + + if (jumpspace) { + WriteIndirectJump(hookInfo, jumpSize, error); + hookInfo.type = THookInfo::TYPE_WIN64PATCH; + } else { + WriteSingleJump(hookInfo, error); + hookInfo.type = THookInfo::TYPE_OVERWRITE; + } + hookInfo.detour = reinterpret_cast<LPBYTE>(hookInfo.trampoline) + rerouteOffset; + + return TRUE; +} + +enum EPreamble +{ + PRE_PATCHFREE, + PRE_PATCHUSED, + PRE_RIPINDIRECT, + PRE_FOREIGNHOOK, + PRE_UNKNOWN +}; + +EPreamble DeterminePreamble(LPBYTE address) +{ + ud_set_input_buffer(disasm(), address, JUMP_SIZE); + ud_disassemble(disasm()); + + if ((ud_insn_mnemonic(disasm()) == UD_Imov) && + (ud_insn_opr(disasm(), 0) == ud_insn_opr(disasm(), 1)) && + (ud_insn_opr(disasm(), 0)->type == UD_OP_REG)) { + // mov edi, edi + return PRE_PATCHFREE; + } else if ((ud_insn_mnemonic(disasm()) == UD_Ijmp) && (ud_insn_len(disasm()) == 2)) { + // determine target of the short jump + LPBYTE shortTarget = ShortJumpTarget(address); + + // test if that short jump leads to a long jump + ud_set_input_buffer(disasm(), shortTarget, JUMP_SIZE); + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) == UD_Ijmp) { + const ud_operand* op = ud_insn_opr(disasm(), 0); + if (op->base == UD_R_RIP) { + return PRE_RIPINDIRECT; + } else { + return PRE_PATCHUSED; + } + } else { + return PRE_UNKNOWN; + } + } else if (ud_insn_mnemonic(disasm()) == UD_Ijmp) { + return PRE_FOREIGNHOOK; + } else { + return PRE_UNKNOWN; + } +} + +static std::map<HOOKHANDLE, THookInfo> s_Hooks; + +static HOOKHANDLE GenerateHandle() +{ + static ULONG NextHandle = 1; + return NextHandle++; +} + +HOOKHANDLE applyHook(THookInfo info, HookError* error) +{ + // apply the correct hook function depending on how the function start looks + EPreamble preamble = DeterminePreamble((LPBYTE)info.originalFunction); + + BOOL success = FALSE; + switch (preamble) { + case PRE_PATCHUSED: { + success = HookChainPatch(info, error); + } break; + case PRE_PATCHFREE: { + success = HookHotPatch(info, error); + } break; + case PRE_RIPINDIRECT: { + success = HookRIPIndirection(info, error); + } break; + case PRE_FOREIGNHOOK: { + success = HookChainHook(info, error); + } break; + default: { + success = HookDisasm(info, error); + } break; + } + + if (success == TRUE) { + HOOKHANDLE handle = GenerateHandle(); + s_Hooks[handle] = info; + return handle; + } else { + return INVALID_HOOK; + } +} + +HOOKHANDLE HookLib::InstallStub(LPVOID functionAddress, LPVOID stubAddress, + HookError* error) +{ + if (functionAddress == nullptr) { + if (error != nullptr) + *error = ERR_INVALIDPARAMETERS; + return INVALID_HOOK; + } + + THookInfo info; + info.originalFunction = functionAddress; + info.replacementFunction = stubAddress; + info.stub = true; + info.detour = nullptr; + info.trampoline = nullptr; + info.type = THookInfo::TYPE_OVERWRITE; + + return applyHook(info, error); +} + +HOOKHANDLE HookLib::InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress, + HookError* error) +{ + LPVOID funcAddr = MyGetProcAddress(module, functionName); + return InstallStub(funcAddr, stubAddress, error); +} + +HOOKHANDLE HookLib::InstallHook(LPVOID functionAddress, LPVOID hookAddress, + HookError* error) +{ + if (functionAddress == nullptr) { + if (error != nullptr) + *error = ERR_INVALIDPARAMETERS; + return INVALID_HOOK; + } + THookInfo info; + info.originalFunction = functionAddress; + info.replacementFunction = hookAddress; + info.stub = false; + info.detour = nullptr; + info.trampoline = nullptr; + info.type = THookInfo::TYPE_OVERWRITE; + + return applyHook(info, error); +} + +HOOKHANDLE HookLib::InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress, + HookError* error) +{ + LPVOID funcAddr = MyGetProcAddress(module, functionName); + return InstallHook(funcAddr, hookAddress, error); +} + +void HookLib::RemoveHook(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + PauseOtherThreads(); + LPBYTE address = reinterpret_cast<LPBYTE>(info.originalFunction); + if (info.type == THookInfo::TYPE_HOTPATCH) { + // return the short jump to 2-byte nop + // TODO: This doesn't take into account if we chain-loaded another hook + + DWORD oldProtect = 0; + if (!VirtualProtect(address, 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + *address = 0x8b; + *(address + 1) = 0xff; + VirtualProtect(address, 2, oldProtect, &oldProtect); + } else if ((info.type == THookInfo::TYPE_OVERWRITE) || + (info.type == THookInfo::TYPE_WIN64PATCH)) { + DWORD oldProtect = 0; + if (!VirtualProtect(address, info.preamble.size(), PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + // TODO: remove hook by restoring the original function. This only works if we + // have the exact code available somewhere + memcpy(address, &info.preamble[0], info.preamble.size()); + VirtualProtect(address, info.preamble.size(), oldProtect, &oldProtect); + } else if (info.type == THookInfo::TYPE_CHAINPATCH) { + // we could attempt to restore the original function preamble but I'm not + // sure we can reliably write the jump with same (or lower) size. + // Instead overwrite our own trampoline + disasm().setInputBuffer(static_cast<uint8_t*>(info.originalFunction), JUMP_SIZE); + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + LPBYTE jumpPos = static_cast<LPBYTE>(info.originalFunction); + if (ud_insn_len(disasm()) == 2) { + jumpPos = reinterpret_cast<LPBYTE>(disasm().jumpTarget()); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(jumpPos, info.preamble.size(), PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + memcpy(jumpPos, info.preamble.data(), info.preamble.size()); + VirtualProtect(jumpPos, info.preamble.size(), oldProtect, &oldProtect); + } else if (info.type == THookInfo::TYPE_RIPINDIRECT) { + uintptr_t res = followJumps(info); + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast<LPVOID>(res), JUMP_SIZE, + PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + *reinterpret_cast<uintptr_t*>(res) = + reinterpret_cast<uintptr_t>(info.originalFunction); + VirtualProtect(reinterpret_cast<LPVOID>(res), JUMP_SIZE, oldProtect, &oldProtect); + } else { + spdlog::get("usvfs")->critical("can't remove hook, unknown hook type!"); + } + + s_Hooks.erase(iter); + ResumePausedThreads(); + } else { + spdlog::get("usvfs")->info("handle unknown: {0:x}", handle); + } +} + +const char* HookLib::GetErrorString(HookError err) +{ + switch (err) { + case ERR_NONE: + return "No Error"; + case ERR_INVALIDPARAMETERS: + return "Invalid parameters"; + case ERR_FUNCEND: + return "Function too short"; + case ERR_JUMP: + return "Function starts on a jump"; + case ERR_RIP: + return "RIP-relative addressing can't be relocated."; + case ERR_RELJUMP: + return "Relative Jump can't be relocated."; + default: + return "Unkown error code"; + } +} + +const char* HookLib::GetHookType(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + switch (info.type) { + case THookInfo::TYPE_HOTPATCH: + return "hot patch"; + case THookInfo::TYPE_WIN64PATCH: + return "64-bit hot patch"; + case THookInfo::TYPE_CHAINPATCH: + return "chained patch"; + case THookInfo::TYPE_OVERWRITE: + return "overwrite"; + case THookInfo::TYPE_RIPINDIRECT: + return "rip indirection modified"; + default: { + spdlog::get("usvfs")->error("invalid hook type {0}", info.type); + return "invalid hook type"; + } + } + } + return "invalid handle"; +} + +LPVOID HookLib::GetDetour(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + return info.detour; + } + return nullptr; +} diff --git a/libs/usvfs/src/thooklib/hooklib.h b/libs/usvfs/src/thooklib/hooklib.h new file mode 100644 index 0000000..a549c03 --- /dev/null +++ b/libs/usvfs/src/thooklib/hooklib.h @@ -0,0 +1,125 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <map> +#include <windows_sane.h> + +namespace HookLib +{ + +enum HookError +{ + ERR_NONE, + ERR_INVALIDPARAMETERS, // parameters are invalid + ERR_FUNCEND, // function is too short to be hooked + ERR_JUMP, // function consists only of an unconditional jump. Maybe it has already + // been hooked? + ERR_RIP, // segment of the function to be overwritten contains a instruction-relative + // operation + ERR_RELJUMP // segment of the function to be overwritten contains a relative jump we + // can't relocated +}; + +typedef ULONG HOOKHANDLE; +static const HOOKHANDLE INVALID_HOOK = (HOOKHANDLE)-1; + +/// +/// \brief install a stub (function to be called before the target function) +/// \param functionAddress address of the function to stub +/// \param stubAddress address of the stub function. This function has to have the +/// signature of void foobar(LPVOID address). +/// address receives the address of the function. +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallStub(LPVOID functionAddress, LPVOID stubAddress, + HookError* error = nullptr); + +/// +/// \brief install a stub (function to be called before the target function) +/// \param module the module containing the function to hook +/// \param functionName name of the function to stub (as exported by the library) +/// \param stubAddress address of the stub function. This function has to have the +/// signature of void foobar(LPVOID address). +/// address receives the address of the function. +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress, + HookError* error = nullptr); + +/// +/// \brief install a hook (function replacing the existing functionality of the +/// function) +/// \param functionAddress address of the function to hook +/// \param hookAddress address of the replacement function. This function has to have +/// the exact same signature as the replaced function +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallHook(LPVOID functionAddress, LPVOID hookAddress, + HookError* error = nullptr); + +/// +/// \brief install a hook (function replacing the existing functionality of the +/// function) +/// \param functionName name of the function to hook (as exported by the library) +/// \param hookAddress address of the replacement function. This function has to have +/// the exact same signature as the replaced function +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress, + HookError* error = nullptr); + +/// +/// \brief remove a hook +/// \param handle handle returned in InstallStub or InstallHook +/// +void RemoveHook(HOOKHANDLE handle); + +/// +/// \brief determine the type of a hook +/// \param handle the handle to look up +/// \return a string describing the used hooking mechanism +/// +const char* GetHookType(HOOKHANDLE handle); + +/// +/// \brief retrieve the address that can be used to directly call a detour +/// \param handle handle for the hook +/// \return function address +/// +LPVOID GetDetour(HOOKHANDLE handle); + +/// +/// \brief resolve an error code to a descriptive string +/// \param err the error code to resolve +/// \return the error string +/// +const char* GetErrorString(HookError err); + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/pch.cpp b/libs/usvfs/src/thooklib/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/usvfs/src/thooklib/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.cpp b/libs/usvfs/src/thooklib/ttrampolinepool.cpp new file mode 100644 index 0000000..3b6a51f --- /dev/null +++ b/libs/usvfs/src/thooklib/ttrampolinepool.cpp @@ -0,0 +1,601 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "ttrampolinepool.h" +#include <addrtools.h> +#include <shmlogger.h> +// #include <boost/thread/lock_guard.hpp> +#include "udis86wrapper.h" + +using namespace asmjit; +#if BOOST_ARCH_X86_64 +using namespace x86; +#elif BOOST_ARCH_X86_32 +using namespace asmjit::x86; +#endif + +using namespace usvfs::shared; + +namespace HookLib +{ + +TrampolinePool* TrampolinePool::s_Instance = nullptr; + +TrampolinePool::TrampolinePool() : m_MaxTrampolineSize(sizeof(LPVOID)) +{ + m_BarrierAddr = &TrampolinePool::barrier; + m_ReleaseAddr = &TrampolinePool::release; + + SYSTEM_INFO sysInfo; + ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO)); + GetSystemInfo(&sysInfo); + m_BufferSize = sysInfo.dwPageSize; + + // if search range = ffffff then addressmask = ffffffffff000000 + // => all jumps between xxxxxxxxxx000000 and xxxxxxxxxxffffff will use the same buffer + // for trampolines which is guaranteed to be in that range + // TODO it should be valid to use 2 ^ 32 as the search range to increase our chances + // of finding a memory block we can reserve but then there is a problem with + // converting negative jump distances to 32 bit I didn't understand. Everything + // up to 2 ^ 31 seems to be fine though + m_SearchRange = static_cast<size_t>(pow(2, 30)) - 1; + m_AddressMask = std::numeric_limits<uint64_t>::max() - m_SearchRange; +} + +// static +void TrampolinePool::initialize() +{ + if (!s_Instance) + s_Instance = new TrampolinePool(); +} + +void TrampolinePool::setBlock(bool block) +{ + m_FullBlock = block; + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } +} + +#if BOOST_ARCH_X86_64 +// push all registers (except rax) and flags to the stack +static void pushAll(X86Assembler& assembler) +{ + assembler.pushf(); + assembler.push(rcx); + assembler.push(rdx); + assembler.push(rbx); + assembler.push(rbp); + assembler.push(rsi); + assembler.push(rdi); + assembler.push(r8); + assembler.push(r9); + assembler.push(r10); + assembler.push(r11); + assembler.push(r12); + assembler.push(r13); + assembler.push(r14); + assembler.push(r15); +} + +// pop all registers (except rax) and flags from stack +static void popAll(X86Assembler& assembler) +{ + assembler.pop(r15); + assembler.pop(r14); + assembler.pop(r13); + assembler.pop(r12); + assembler.pop(r11); + assembler.pop(r10); + assembler.pop(r9); + assembler.pop(r8); + assembler.pop(rdi); + assembler.pop(rsi); + assembler.pop(rbp); + assembler.pop(rbx); + assembler.pop(rdx); + assembler.pop(rcx); + assembler.popf(); +} +#endif // BOOST_ARCH_X86_64 + +void TrampolinePool::addBarrier(LPVOID rerouteAddr, LPVOID original, + X86Assembler& assembler) +{ + Label skipLabel = assembler.newLabel(); + +#if BOOST_ARCH_X86_64 + pushAll(assembler); + assembler.mov(rcx, + imm(reinterpret_cast<int64_t>( + original))); // set call parameter for call to barrier function + assembler.mov(rax, imm((intptr_t)(void*)barrier)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); + // test barrier + assembler.cmp(rax, 0); // test if the barrier is locked + assembler.jz(skipLabel); // skip if barrier was locked + + // call replacement function + // for this call no registers are saved. the called function is a compiled function so + // it should correctly save non-volatile registers, and the caller can't expect the + // volatile ones to remain valid + assembler.pop(r10); + assembler.mov(dword_ptr(rax), r10); // store that return address to the variable + // supplied by the barrier function + assembler.mov(rax, imm((intptr_t)(LPVOID)rerouteAddr)); + assembler.call(rax); + assembler.push(rax); // save away result + + // open the barrier again + pushAll(assembler); + assembler.mov(rcx, imm(reinterpret_cast<int64_t>(original))); + assembler.mov(rax, imm((intptr_t)(void*)release)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); + assembler.pop(r10); // get the result from the replacement function to a register + assembler.push(rax); // push the original return address back on the stack + assembler.mov(rax, r10); // move result of actual call to rax + assembler.ret(); // return, using the original return address +#else // BOOST_ARCH_X86_64 + assembler.push(imm(void_ptr_cast<int32_t>( + original))); // push original function, as parameter to barrier + assembler.mov(ecx, (Ptr) static_cast<void*>(TrampolinePool::barrier)); + assembler.call(ecx); // call barrier function + assembler.cmp(eax, 0); + assembler.jz(skipLabel); // if barrier is locked, jump to end of function + + // case a: we got through the barrier + assembler.pop(ecx); // pop the return address into ecx + assembler.mov(dword_ptr(eax), ecx); // store that return address in the variable + // supplied by the barrier function + + assembler.mov(eax, (Ptr) static_cast<void*>(rerouteAddr)); + assembler.call(eax); // call replacement function (pointer was stored in front of the + // trampoline) (this function gets the parameters that were on + // the stack already and cleans + // them up itself (stdcall convention)) + assembler.push(eax); // save away result + assembler.push(imm(void_ptr_cast<int32_t>(original))); // open the barrier again + assembler.mov(eax, (Ptr) static_cast<void*>(TrampolinePool::release)); + assembler.call(eax); + assembler.pop(ecx); // pop the result from the actual call to ecx + assembler.push(eax); // push the original return address (returned by + // TTrampolinePool::release) back on the stack + assembler.mov(eax, ecx); // move result of actual call to eax + assembler.ret(); // return, using the original return address +#endif // BOOST_ARCH_X86_64 + + assembler.bind(skipLabel); +} + +LPVOID TrampolinePool::roundAddress(LPVOID address) const +{ + return reinterpret_cast<LPVOID>(reinterpret_cast<intptr_t>(address) & m_AddressMask); +} + +TrampolinePool::BufferList& TrampolinePool::getBufferList(LPVOID address) +{ + LPVOID rounded = roundAddress(address); + auto iter = m_Buffers.find(rounded); + if (iter == m_Buffers.end()) { + BufferList newBufList = {0, std::vector<LPVOID>()}; + m_Buffers[rounded] = newBufList; + iter = allocateBuffer(address); + } + return iter->second; +} + +LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + // ??? write address of reroute to trampoline and move past the address + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; +#if BOOST_ARCH_X86_64 + X86Assembler assembler(&runtime); +#else + X86Assembler assembler(&runtime); +#endif + addCallToStub(assembler, original, reroute); + addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(returnAddress)); + + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeStub(reroute, original, returnAddress); + } + + // adjust relative jumps for move to buffer + codeSize = assembler.relocCode(spot); + + uint8_t* code = assembler.getBuffer(); + memcpy(spot, code, codeSize); + + bufferList.offset += codeSize; + + return spot; +} + +LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original, + LPVOID returnAddress) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addBarrier(reroute, original, assembler); +#if BOOST_ARCH_X86_64 + assembler.mov(rax, imm((intptr_t)(void*)(returnAddress))); + assembler.jmp(rax); +#else + assembler.mov(eax, imm((intptr_t)(void*)(returnAddress))); + assembler.jmp(eax); +#endif + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeTrampoline(reroute, original, returnAddress); + } + + // adjust relative jumps for move to buffer + codeSize = assembler.relocCode(spot); + + // copy code to buffer + uint8_t* code = assembler.getBuffer(); + memcpy(spot, code, codeSize); + + bufferList.offset += codeSize; + return spot; +} + +#if BOOST_ARCH_X86_64 +void TrampolinePool::copyCode(X86Assembler& assembler, LPVOID source, size_t numBytes) +{ + static UDis86Wrapper disasm; + + disasm.setInputBuffer(static_cast<const uint8_t*>(source), numBytes); + + size_t offset = 0; + + while (ud_disassemble(disasm) != 0) { + // rewrite relative jumps, blind copy everything else + + offset += ud_insn_len(disasm); + + // WARNING: doesn't support conditional jumps + if ((ud_insn_mnemonic(disasm) == UD_Ijmp) && + (ud_insn_opr(disasm, 0)->type == UD_OP_JIMM)) { + uintptr_t dest = disasm.jumpTarget(); + assembler.mov(rax, imm(static_cast<uint64_t>(dest))); + assembler.jmp(rax); + } else { + assembler.embed(ud_insn_ptr(&disasm.obj()), ud_insn_len(&disasm.obj())); + // assembler.data(); + } + } +} +#endif + +void TrampolinePool::addCallToStub(X86Assembler& assembler, LPVOID original, + LPVOID reroute) +{ +#if BOOST_ARCH_X86_64 + pushAll(assembler); + assembler.mov(rcx, imm(reinterpret_cast<int64_t>(original))); + assembler.mov(rax, imm((intptr_t)(LPVOID)reroute)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); +#else // BOOST_ARCH_X86_64 + assembler.push(reinterpret_cast<int64_t>(original)); + assembler.mov(ecx, imm((intptr_t)(LPVOID)reroute)); + assembler.call(ecx); + assembler.pop(ecx); // remove argument from stack +#endif // BOOST_ARCH_X86_64 +} + +void TrampolinePool::addAbsoluteJump(X86Assembler& assembler, uint64_t destination) +{ +#if BOOST_ARCH_X86_64 + assembler.push(rax); + assembler.push(rax); + assembler.mov(rax, imm(destination)); + assembler.mov(ptr(rsp, 8), rax); + assembler.pop(rax); + assembler.ret(); +#else // BOOST_ARCH_X86_64 + assembler.push(imm(destination)); + assembler.ret(); +#endif // BOOST_ARCH_X86_64 +} + +LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addCallToStub(assembler, original, reroute); +#if BOOST_ARCH_X86_64 + // insert backup code + *rerouteOffset = assembler.getCodeSize(); + copyCode(assembler, original, preambleSize); +#else // BOOST_ARCH_X86_64 + assembler.embed(original, preambleSize); +#endif // BOOST_ARCH_X86_64 + addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(original) + preambleSize); + + // adjust relative jumps for move to buffer + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeStub(reroute, original, preambleSize, rerouteOffset); + } + + // copy code to buffer + codeSize = assembler.relocCode(spot); + + bufferList.offset += preambleSize + codeSize; + return spot; +} + +LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original, + size_t preambleSize, size_t* rerouteOffset) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addBarrier(reroute, original, assembler); + // insert backup code + *rerouteOffset = assembler.getCodeSize(); + assembler.embed(original, static_cast<uint32_t>(preambleSize)); + addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(original) + preambleSize); + + // adjust relative jumps for move to buffer + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // TODO this does not take into account that the code size may technically change + // after relocation in which case the following test may determine the code fits into + // the buffer when it really doesnt't. asmjit doesn't seem to provide a way to adjust + // jumps without actually moving the code though + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeTrampoline(reroute, original, preambleSize, rerouteOffset); + } + + // copy code to buffer + codeSize = static_cast<size_t>(assembler.relocCode(spot)); + + bufferList.offset += preambleSize + codeSize; + + return spot; +} + +LPVOID TrampolinePool::currentBufferAddress(LPVOID addressNear) +{ + LPVOID rounded = roundAddress(addressNear); + auto lookupAddress = m_Buffers.find(rounded); + + if (lookupAddress == m_Buffers.end()) { + lookupAddress = m_Buffers.insert(std::make_pair(rounded, BufferList())).first; + } + if (lookupAddress->second.buffers.size() == 0) { + allocateBuffer(addressNear); + } + + LPVOID res = *(lookupAddress->second.buffers.rbegin()); + return res; +} + +void TrampolinePool::forceUnlockBarrier() +{ + if (m_ThreadGuards.get() != nullptr) { + for (auto funcId : *m_ThreadGuards) { + (*m_ThreadGuards)[funcId.first] = nullptr; + } + } // else no barriers to unlock +} + +TrampolinePool::BufferMap::iterator TrampolinePool::allocateBuffer(LPVOID addressNear) +{ + // allocate a buffer that we can write to and that is executable + SYSTEM_INFO sysInfo; + ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO)); + GetSystemInfo(&sysInfo); + + LPVOID rounded = roundAddress(addressNear); + auto iter = m_Buffers.find(rounded); + uintptr_t lowerEnd = reinterpret_cast<uintptr_t>(rounded); + if (iter->second.buffers.size() > 0) { + // start searching were we last found a buffer + lowerEnd = reinterpret_cast<uintptr_t>(*iter->second.buffers.rbegin()) + + sysInfo.dwPageSize; + } + + uintptr_t start = + std::max(std::max(lowerEnd, MIN_ALLOC_ADDR), + reinterpret_cast<uintptr_t>(sysInfo.lpMinimumApplicationAddress)); + uintptr_t upperEnd = reinterpret_cast<uintptr_t>(rounded) + m_SearchRange; + uintptr_t end = std::min( + upperEnd, reinterpret_cast<uintptr_t>(sysInfo.lpMaximumApplicationAddress)); + + LPVOID buffer = nullptr; + for (uintptr_t cur = start; cur < end; cur += sysInfo.dwPageSize) { + buffer = VirtualAlloc(reinterpret_cast<LPVOID>(cur), m_BufferSize, + MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (buffer != nullptr) { + break; + } + } + if (buffer == nullptr) { + throw std::runtime_error("failed to allocate buffer in range"); + } + + // the caller must have looked up the bufferlist in order to determine that a + // buffer has to be allocated + assert(iter != m_Buffers.end()); + + iter->second.offset = 0; + iter->second.buffers.push_back(buffer); + spdlog::get("usvfs")->debug( + "allocated trampoline buffer for jumps between {0:p} and {1:x} at {2:p}" + "(size {3})", + rounded, (reinterpret_cast<uintptr_t>(rounded) + m_SearchRange), buffer, + m_BufferSize); + return iter; +} + +LPVOID TrampolinePool::barrier(LPVOID function) +{ + DWORD err = GetLastError(); + LPVOID res = instance().barrierInt(function); + SetLastError(err); + return res; +} + +LPVOID TrampolinePool::release(LPVOID function) +{ + DWORD err = GetLastError(); + LPVOID res = instance().releaseInt(function); + SetLastError(err); + return res; +} + +LPVOID TrampolinePool::barrierInt(LPVOID func) +{ + if (m_FullBlock) { + return nullptr; + } + + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } + + auto iter = m_ThreadGuards->find(func); + if ((iter == m_ThreadGuards->end()) || (iter->second == nullptr)) { + (*m_ThreadGuards)[func] = reinterpret_cast<LPVOID>(1); + return &(*m_ThreadGuards)[func]; + } else { + return nullptr; + } +} + +LPVOID TrampolinePool::releaseInt(LPVOID func) +{ + DWORD lastError = GetLastError(); + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } + + auto iter = m_ThreadGuards->find(func); + if (iter == m_ThreadGuards->end()) { + spdlog::get("hooks")->error("failed to release barrier for func {}", func); + ::SetLastError(lastError); + return nullptr; + } + + LPVOID res = (*m_ThreadGuards)[func]; + (*m_ThreadGuards)[func] = nullptr; + + ::SetLastError(lastError); + return res; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.h b/libs/usvfs/src/thooklib/ttrampolinepool.h new file mode 100644 index 0000000..5b44c51 --- /dev/null +++ b/libs/usvfs/src/thooklib/ttrampolinepool.h @@ -0,0 +1,222 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "asmjit_sane.h" +#include <logging.h> +#include <map> +#include <vector> +#include <windows_sane.h> + +#ifdef _MSC_VER +#pragma warning(disable : 4714) +#include <boost/config/compiler/visualc.hpp> +#else +#include <boost/config/compiler/gcc.hpp> +#endif +#include <boost/predef.h> +#include <boost/thread.hpp> +#include <mutex> + +// #include <boost/thread/mutex.hpp> + +namespace HookLib +{ + +/// +/// trampolines are runtime-generated mini-functions that are used to call the +/// original code of a function +/// +class TrampolinePool +{ +public: + /// Call initialize before you use the TrampolinePool + static void initialize(); + + static TrampolinePool& instance() + { + // This is a very very sensitive place so we want to keep this function as simple as + // possible and having it inlined probably cann't hurt + return *s_Instance; + } + + void setBlock(bool block); + + /// + /// store a stub without moving code from the original function. This is used in cases + /// where the hook can be placed without overwriting logic (i.e. hot-patchable + /// functions and when chaining hooks) + /// \param reroute the stub function to call before the regular function (on x86 this + /// needs to be cdecl calling convention!) + /// \param original the original function + /// \param returnAddress address under which the original functionality can be + /// reached. + /// for the first hook this should be (original + 2), otherwise + /// the address of the next hook in the chain + /// \return address of the created trampoline function + /// + LPVOID storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress); + + /// + /// store a stub, moving part of the original function to the trampoline + /// \param reroute the stub function to call before the regular function (on x86 this + /// needs to be cdecl calling convention!) + /// \param original the original function + /// \param preambleSize number of bytes from the original function to backup. Needs to + /// correspond to complete instructions + /// \param rerouteOffset offset in bytes from the created trampoline to the preamble + /// that leads back to the original code + /// \return address of the created trampoline function + /// + LPVOID storeStub(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset); + + /// + /// store a trampoline for hot-patchable functions, where the original function + /// is unharmed. + /// \param reroute the reroute function + /// \param original original function + /// \param returnAddress address under which the original functionality can be + /// reached. + /// \return address of the trampoline function + /// + LPVOID storeTrampoline(LPVOID reroute, LPVOID original, LPVOID returnAddress); + + /// + /// store a trampoline, copying a part of the original function to the trampoline. + /// This is used for the case where the hooking mechanism needs to overwrite part of + /// the function + /// \param reroute the reroute function + /// \param original original function + /// \param preambleSize number of bytes from the original function to backup. Needs to + /// correspond to complete instructions + /// \param rerouteOffset offset in bytes from the created trampoline to the preamble + /// that leads us back to the original code + /// \return address of the trampoline function + /// + LPVOID storeTrampoline(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset); + + /// + /// \param addressNear used to find a trampoline buffer near the jump instruction + /// \return retrieve address of current trampoline buffer + /// + LPVOID currentBufferAddress(LPVOID addressNear); + + /// + /// \brief forces the barrier(s) for the current thread to be released + /// + void forceUnlockBarrier(); + +private: + struct BufferList + { + size_t offset; + std::vector<LPVOID> buffers; + }; + + typedef std::map<LPVOID, BufferList> BufferMap; + static const intptr_t ADDRESS_MASK = + 0xFFFFFFFFFF000000LL; // mask to "round" addresses to consolidate near + // trampolines + +private: + TrampolinePool(); + + TrampolinePool& operator=(const TrampolinePool& reference); // not implemented + + /** + * @brief allocates a buffer with read, write and execute rights near the + * specified adress. The purpose is that we want to be able to jump from + * adressNear to generated code with a 5-byte jump, even on x64 systems. + * @param addressNear the reference adress + * @note the resulting buffer is stored in the m_Buffers map + */ + BufferMap::iterator allocateBuffer(LPVOID addressNear); + + void addBarrier(LPVOID rerouteAddr, LPVOID original, asmjit::X86Assembler& assembler); + +#if BOOST_ARCH_X86_64 + void copyCode(asmjit::X86Assembler& assembler, LPVOID source, size_t numBytes); +#endif // BOOST_ARCH_X86_64 + + BufferList& getBufferList(LPVOID address); + + LPVOID roundAddress(LPVOID address) const; + +public: + static LPVOID __stdcall barrier(LPVOID function); + static LPVOID __stdcall release(LPVOID function); + + LPVOID barrierInt(LPVOID function); + LPVOID releaseInt(LPVOID function); + + void addCallToStub(asmjit::X86Assembler& assembler, LPVOID original, LPVOID reroute); + +private: + /// + /// \brief add a jump to an address outside the custom generated asm code (without + /// modifying registers) + /// \param assembler the assembler generator to write to + /// \param destination destination address + /// \note this currently generates a lot code on x64, may be overly complicated + /// + void addAbsoluteJump(asmjit::X86Assembler& assembler, uint64_t destination); + + DWORD determinePageSize(); + +private: +#if BOOST_ARCH_X86_64 + static const int SIZE_OF_JUMP = 13; +#elif BOOST_ARCH_X86_32 + static const int SIZE_OF_JUMP = 5; +#endif + + // the hook lib tries to allocate buffer close to the hooked functions, + // for 32-bits application, this often falls on 0x40000000, which is the + // address at which DLLs can be loaded on Windows + // + // some old-style DRM (e.g. Dragon Age 2) check that address and prevent + // the game from running if it's not 0x40000000, so we give them a bit of + // spaces before allocating our buffers + // + static constexpr uintptr_t MIN_ALLOC_ADDR = 0x40000000 + 0x200000; + + static TrampolinePool* s_Instance; + + bool m_FullBlock{false}; + + BufferMap m_Buffers; + + typedef std::map<void*, void*> TThreadMap; + boost::thread_specific_ptr<TThreadMap> m_ThreadGuards; + + LPVOID m_BarrierAddr; + LPVOID m_ReleaseAddr; + + DWORD m_BufferSize = {1024}; + size_t m_SearchRange; + uint64_t m_AddressMask; + + int m_MaxTrampolineSize; +}; + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/udis86wrapper.cpp b/libs/usvfs/src/thooklib/udis86wrapper.cpp new file mode 100644 index 0000000..1ac7332 --- /dev/null +++ b/libs/usvfs/src/thooklib/udis86wrapper.cpp @@ -0,0 +1,102 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "udis86wrapper.h" +#include "pch.h" +#include <boost/predef.h> +#include <shmlogger.h> +#include <stdexcept> + +namespace HookLib +{ + +UDis86Wrapper::UDis86Wrapper() +{ + ud_init(&m_Obj); + ud_set_syntax(&m_Obj, UD_SYN_INTEL); +#if BOOST_ARCH_X86_64 + ud_set_mode(&m_Obj, 64); +#else + ud_set_mode(&m_Obj, 32); +#endif +} + +void UDis86Wrapper::setInputBuffer(const uint8_t* buffer, size_t size) +{ + m_Buffer = buffer; + ud_set_input_buffer(&m_Obj, buffer, size); + ud_set_pc(&m_Obj, reinterpret_cast<uint64_t>(m_Buffer)); +} + +ud_t& UDis86Wrapper::obj() +{ + return m_Obj; +} + +bool UDis86Wrapper::isRelativeJump() +{ + ud_mnemonic_code code = ud_insn_mnemonic(&m_Obj); + // all conditional jumps and loops are relative, as are unconditional jumps with an + // offset operand + return (code == UD_Ijo) || (code == UD_Ijno) || (code == UD_Ijb) || + (code == UD_Ijae) || (code == UD_Ijz) || (code == UD_Ijnz) || + (code == UD_Ijbe) || (code == UD_Ija) || (code == UD_Ijs) || + (code == UD_Ijns) || (code == UD_Ijp) || (code == UD_Ijnp) || + (code == UD_Ijl) || (code == UD_Ijge) || (code == UD_Ijle) || + (code == UD_Ijg) || (code == UD_Ijcxz) || (code == UD_Ijecxz) || + (code == UD_Ijrcxz) || (code == UD_Iloop) || (code == UD_Iloope) || + (code == UD_Iloopne) || + ((code == UD_Icall) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM)) || + ((code == UD_Ijmp) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM)); +} + +intptr_t UDis86Wrapper::jumpOffset() +{ + const ud_operand_t* op = ud_insn_opr(&m_Obj, 0); + switch (op->size) { + case 8: + return static_cast<intptr_t>(op->lval.sbyte); + case 16: + return static_cast<intptr_t>(op->lval.sword); + case 32: + return static_cast<intptr_t>(op->lval.sdword); + case 64: + return static_cast<intptr_t>(op->lval.sqword); + default: + throw std::runtime_error("unsupported jump size"); + } +} + +uint64_t UDis86Wrapper::jumpTarget() +{ + // TODO: assert we're actually on a jump + + uint64_t res = ud_insn_off(&m_Obj) + ud_insn_len(&m_Obj); + + res += jumpOffset(); + + if (ud_insn_opr(&m_Obj, 0)->base == UD_R_RIP) { + res = *reinterpret_cast<uintptr_t*>(res); + } + + return res; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/udis86wrapper.h b/libs/usvfs/src/thooklib/udis86wrapper.h new file mode 100644 index 0000000..48d8a42 --- /dev/null +++ b/libs/usvfs/src/thooklib/udis86wrapper.h @@ -0,0 +1,62 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <udis86.h> +#undef inline // libudis86/types.h defines inline to __inline which is no longer legal + // since vs2012 + +namespace HookLib +{ + +class UDis86Wrapper +{ + +public: + UDis86Wrapper(); + + void setInputBuffer(const uint8_t* buffer, size_t size); + + ud_t& obj(); + + operator ud_t*() { return &m_Obj; } + + bool isRelativeJump(); + + intptr_t jumpOffset(); + + /// + /// determines the absolute jump target at the current instruction, taking into + /// account relative instructions of all sizes and RIP-relative addressing. + /// \return absolute address of the jump at the current disassembler instruction + /// \note this works correctly ONLY if the input buffer has been set with + /// setInputBuffer or + /// if ud_set_pc has been called + /// + uint64_t jumpTarget(); + +private: +private: + ud_t m_Obj; + const uint8_t* m_Buffer{nullptr}; +}; + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/utility.cpp b/libs/usvfs/src/thooklib/utility.cpp new file mode 100644 index 0000000..124141f --- /dev/null +++ b/libs/usvfs/src/thooklib/utility.cpp @@ -0,0 +1,86 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "utility.h" +#include "exceptionex.h" +#include <cstdlib> + +namespace HookLib +{ + +FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName) +{ + // determine position of the exports of the module + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)module; + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) { + return nullptr; + } + + PIMAGE_NT_HEADERS ntHeaders = + (PIMAGE_NT_HEADERS)(((LPBYTE)dosHeader) + dosHeader->e_lfanew); + if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) { + return nullptr; + } + + PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader; + if (optionalHeader->NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT) { + return nullptr; + } + PIMAGE_DATA_DIRECTORY dataDirectory = + &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + PIMAGE_EXPORT_DIRECTORY exportDirectory = + (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)dosHeader + dataDirectory->VirtualAddress); + + ULONG* addressOfNames = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfNames); + ULONG* funcAddr = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfFunctions); + + // search exports for the specified name + for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { + char* curFunctionName = (char*)((LPBYTE)module + addressOfNames[i]); + USHORT* nameOrdinals = + (USHORT*)((LPBYTE)module + exportDirectory->AddressOfNameOrdinals); + if (strcmp(functionName, curFunctionName) == 0) { + if (funcAddr[nameOrdinals[i]] >= dataDirectory->VirtualAddress && + funcAddr[nameOrdinals[i]] < + dataDirectory->VirtualAddress + dataDirectory->Size) { + char* forwardLibName = _strdup((LPSTR)module + funcAddr[nameOrdinals[i]]); + ON_BLOCK_EXIT([forwardLibName]() { + free(forwardLibName); + }); + char* forwardFunctionName = strchr(forwardLibName, '.'); + *forwardFunctionName = 0; + ++forwardFunctionName; + + HMODULE forwardLib = LoadLibraryA(forwardLibName); + FARPROC forward = nullptr; + if (forwardLib != nullptr) { + forward = MyGetProcAddress(forwardLib, forwardFunctionName); + FreeLibrary(forwardLib); + } + + return forward; + } + return (FARPROC)((LPBYTE)module + funcAddr[nameOrdinals[i]]); + } + } + return nullptr; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/utility.h b/libs/usvfs/src/thooklib/utility.h new file mode 100644 index 0000000..e0be9f7 --- /dev/null +++ b/libs/usvfs/src/thooklib/utility.h @@ -0,0 +1,35 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <windows_sane.h> + +namespace HookLib +{ + +/// \brief reimplementation of GetProcAddress to circumvent foreign hooks of +/// GetProcAddress like AcLayer +/// \param module handle to the module that contains the function or variable +/// \param functionName function to retrieve the address of +/// \return address of the exported function +FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName); + +} // namespace HookLib diff --git a/libs/usvfs/src/tinjectlib/CMakeLists.txt b/libs/usvfs/src/tinjectlib/CMakeLists.txt new file mode 100644 index 0000000..096608e --- /dev/null +++ b/libs/usvfs/src/tinjectlib/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(asmjit CONFIG REQUIRED) + +add_library(tinjectlib STATIC asmjit_sane.h injectlib.cpp injectlib.h) +target_link_libraries(tinjectlib PRIVATE shared asmjit::asmjit) +target_include_directories(tinjectlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(tinjectlib PROPERTIES FOLDER injection) diff --git a/libs/usvfs/src/tinjectlib/asmjit_sane.h b/libs/usvfs/src/tinjectlib/asmjit_sane.h new file mode 100644 index 0000000..70ee117 --- /dev/null +++ b/libs/usvfs/src/tinjectlib/asmjit_sane.h @@ -0,0 +1,32 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#pragma warning(push, 3) +#pragma warning(disable : 4201) +#pragma warning(disable : 4244) +#pragma warning(disable : 4245) +// #ifndef ASMJIT_API +// #define ASMJIT_API +// #endif // ASMJIT_API +#include <asmjit/asmjit.h> +#include <asmjit/x86.h> +#pragma warning(pop) diff --git a/libs/usvfs/src/tinjectlib/injectlib.cpp b/libs/usvfs/src/tinjectlib/injectlib.cpp new file mode 100644 index 0000000..2c7362a --- /dev/null +++ b/libs/usvfs/src/tinjectlib/injectlib.cpp @@ -0,0 +1,482 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include <cstdio> +#include <stdexcept> + +#include <boost/filesystem.hpp> +#include <boost/predef.h> +#include <boost/static_assert.hpp> +#include <boost/type_traits.hpp> +namespace fs = boost::filesystem; + +#include "injectlib.h" +#include <addrtools.h> +#include <exceptionex.h> +#include <stringcast.h> +#include <stringutils.h> +// local version of asmjit with warning suppression +#include "asmjit_sane.h" +#include <TlHelp32.h> +#include <spdlog/spdlog.h> + +using namespace asmjit; +using namespace usvfs::shared; + +#if BOOST_ARCH_X86_64 +#pragma message("64bit build") +using namespace x86; +#elif BOOST_ARCH_X86_32 +#pragma message("32bit build") +using namespace asmjit::x86; +#else +#error "unsupported architecture" +#endif + +typedef HMODULE(WINAPI* TLoadLibraryType)(LPCWSTR); +typedef FARPROC(WINAPI* TGetProcAddressType)(HMODULE, LPCSTR); +typedef DWORD(WINAPI* TGetLastErrorType)(); + +typedef BOOL(WINAPI* TSetXStateFeaturesMaskType)(PCONTEXT, DWORD64); + +static const size_t MAX_FUNCTIONAME = 20; + +struct TDataRemote +{ + TLoadLibraryType loadLibrary; + TGetProcAddressType getProcAddress; + TGetLastErrorType getLastError; + REGWORD returnAddress; + + char initFunction[MAX_FUNCTIONAME + 1]; + WCHAR dllName[MAX_PATH]; +}; + +#if BOOST_ARCH_X86_64 +void pushAll(X86Assembler& assembler) +{ + assembler.pushf(); + assembler.push(rax); + assembler.push(rcx); + assembler.push(rdx); + assembler.push(rbx); + assembler.push(rbp); + assembler.push(rsi); + assembler.push(rdi); + assembler.push(r8); + assembler.push(r9); + assembler.push(r10); + assembler.push(r11); + assembler.push(r12); + assembler.push(r13); + assembler.push(r14); + assembler.push(r15); +} + +void popAll(X86Assembler& assembler) +{ + assembler.pop(r15); + assembler.pop(r14); + assembler.pop(r13); + assembler.pop(r12); + assembler.pop(r11); + assembler.pop(r10); + assembler.pop(r9); + assembler.pop(r8); + assembler.pop(rdi); + assembler.pop(rsi); + assembler.pop(rbp); + assembler.pop(rbx); + assembler.pop(rdx); + assembler.pop(rcx); + assembler.pop(rax); + assembler.popf(); +} +#endif // BOOST_ARCH_X86_64 + +void addStub(size_t userDataSize, X86Assembler& assembler, bool skipInit, + TDataRemote* localData, TDataRemote* remoteData, LPCSTR initFunction) +{ + Label Label_DLLLoaded = assembler.newLabel(); + +#if BOOST_ARCH_X86_64 + pushAll(assembler); + // call load library for the actual injection + assembler.mov(rcx, imm(reinterpret_cast<int64_t>(&remoteData->dllName))); + assembler.mov(rax, imm((intptr_t)(void*)localData->loadLibrary)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + + // cancel out of here if we failed to load the dll + // TODO: would be great to report this error. But how? + assembler.test(rax, rax); + assembler.jnz(Label_DLLLoaded); + /* this commented out code may seem pointless but it is a simple way to get at the + error code when debugging. assembler.mov(rax, + imm((intptr_t)(void*)localData->getLastError)); assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + assembler.int3();*/ + popAll(assembler); + assembler.ret(); + assembler.bind(Label_DLLLoaded); + + // determine address of the init function + if (initFunction != nullptr) { + Label Label_SkipInit = assembler.newLabel(); + assembler.mov(rcx, rax); // handle of the dll + assembler.mov(rdx, imm(reinterpret_cast<int64_t>( + remoteData->initFunction))); // name of init function + assembler.mov(rax, imm((intptr_t)(void*)localData->getProcAddress)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + + if (skipInit) { + assembler.test(rax, rax); + assembler.jz(Label_SkipInit); + } + + // call the init function with user data + assembler.mov(rcx, + imm(reinterpret_cast<int64_t>(remoteData) + sizeof(TDataRemote))); + assembler.mov(rdx, imm(static_cast<int64_t>(userDataSize))); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + assembler.bind(Label_SkipInit); + } + + // restore registers + popAll(assembler); +#else + + // save registers + assembler.push(eax); + assembler.pushf(); + + // call load library for the actual injection + assembler.push(imm(void_ptr_cast<int64_t>(remoteData->dllName))); + // assembler.call(ptr_abs(static_cast<void*>(remoteData->loadLibrary))); + assembler.mov(eax, imm(void_ptr_cast<int64_t>(localData->loadLibrary))); + assembler.call(eax); + + assembler.test(eax, eax); + assembler.jnz(Label_DLLLoaded); + /* this commented out code may seem pointless but it is a simple way to get at the + error code when debugging. assembler.mov(eax, + imm((intptr_t)(void*)localData->getLastError)); assembler.call(eax); + assembler.int3();*/ + assembler.popf(); + assembler.pop(eax); + assembler.ret(); + assembler.bind(Label_DLLLoaded); + + // determine address of the init function + if (initFunction != nullptr) { + Label Label_SkipInit = assembler.newLabel(); + assembler.push(imm( + void_ptr_cast<int64_t>(remoteData->initFunction))); // name of init function + assembler.push(eax); // handle of the dll + assembler.mov(eax, imm(void_ptr_cast<int64_t>(localData->getProcAddress))); + assembler.call(eax); + if (skipInit) { + assembler.cmp(eax, 0); + assembler.jz(Label_SkipInit); + } else { + assembler.cmp(eax, 0); + assembler.jnz(Label_SkipInit); + // heading for a crash! give an attached debugger a chance to analyse the error + assembler.mov(eax, imm(void_ptr_cast<int64_t>(localData->getLastError))); + assembler.call(eax); + assembler.int3(); + assembler.bind(Label_SkipInit); + } + + // call the init function with user data + assembler.push(userDataSize); + assembler.push(imm(void_ptr_cast<int64_t>(remoteData) + sizeof(TDataRemote))); + assembler.call(eax); + // init function is declared __cdecl so we have to remove parameters from the stack + assembler.pop(eax); + assembler.pop(eax); + if (skipInit) { + assembler.bind(Label_SkipInit); + } + } + + // restore registers + assembler.popf(); + assembler.pop(eax); + +#endif +} + +REGWORD WriteInjectionStub(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction, + LPCVOID userData, size_t userDataSize, bool skipInit, + REGWORD returnAddress) +{ + HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll")); + TDataRemote data = {0}; + + if (k32mod != nullptr) { + data.loadLibrary = + reinterpret_cast<TLoadLibraryType>(GetProcAddress(k32mod, "LoadLibraryW")); + data.getProcAddress = + reinterpret_cast<TGetProcAddressType>(GetProcAddress(k32mod, "GetProcAddress")); + data.getLastError = + reinterpret_cast<TGetLastErrorType>(GetProcAddress(k32mod, "GetLastError")); + if ((data.loadLibrary == nullptr) || (data.getProcAddress == nullptr) || + (data.getLastError == nullptr)) { + throw windows_error("failed to determine address for required functions"); + } + } else { + throw windows_error("kernel32.dll not loaded?"); + } + + data.returnAddress = returnAddress; + + if (initFunction != nullptr) { + strncpy_s(data.initFunction, MAX_FUNCTIONAME, initFunction, MAX_FUNCTIONAME); + data.initFunction[MAX_FUNCTIONAME] = '\0'; + } else { + data.initFunction[0] = '\0'; + } + + wcsncpy_s(data.dllName, MAX_PATH, dllName, MAX_PATH - 1); + data.dllName[MAX_PATH - 1] = L'\0'; + + size_t totalSize = sizeof(TDataRemote) + userDataSize; + + // allocate memory in the target process and write the data-block there + LPVOID remoteMem = VirtualAllocEx(processHandle, nullptr, totalSize, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + + if (remoteMem == nullptr) { + throw windows_error("failed to allocate memory in target process"); + } + + SIZE_T written; + if (!WriteProcessMemory(processHandle, remoteMem, &data, sizeof(TDataRemote), + &written)) { + throw windows_error("failed to write control data to target process"); + } + if (written != sizeof(TDataRemote)) { + throw windows_error("failed to write whole control data to target process"); + } + + // write user data to remote memory if necessary + if (userData != nullptr) { + if (!WriteProcessMemory(processHandle, AddrAdd(remoteMem, sizeof(TDataRemote)), + userData, userDataSize, &written)) { + throw windows_error("failed to write user data to target process"); + } + if (written != userDataSize) { + throw windows_error("failed to write whole user data to target process"); + } + } + + TDataRemote* remoteData = reinterpret_cast<TDataRemote*>(remoteMem); + + // now for the interesting part: write a stub into the target process that is run + // before any code of the original binary. + + JitRuntime runtime; +#if BOOST_ARCH_X86_64 + X86Assembler assembler(&runtime); + if (returnAddress != 0) { + // put return address on the stack + // (this damages rax which hopefully doesn't matter) + assembler.mov(rax, imm((intptr_t)(void*)data.returnAddress)); + assembler.push(rax); + } // otherwise no return address was specified here. It better be on the stack + // already +#else + X86Assembler assembler(&runtime); + if (returnAddress != 0) { + assembler.push(imm((intptr_t)(void*)data.returnAddress)); + } +#endif + + addStub(userDataSize, assembler, skipInit, &data, remoteData, initFunction); + assembler.ret(0); + + size_t stubSize = assembler.getCodeSize(); + + // reserve memory for the stub + PBYTE stubRemote = reinterpret_cast<PBYTE>( + VirtualAllocEx(processHandle, nullptr, stubSize, MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE)); + if (stubRemote == nullptr) { + throw windows_error("failed to allocate memory for stub"); + } + + // almost there. copy stub to target process + if (!WriteProcessMemory(processHandle, stubRemote, assembler.getBuffer(), stubSize, + &written) || + (written != stubSize)) { + throw windows_error("failed to write stub to target process"); + } + + return reinterpret_cast<REGWORD>(stubRemote); +} + +void InjectDLLEIP(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName, + LPCSTR initFunction, LPCVOID userData, size_t userDataSize, + bool skipInit) +{ + threadHandle = + OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), + FALSE, GetThreadId(threadHandle)); + + if (threadHandle == nullptr) { + throw windows_error("failed to open thread"); + } + + CONTEXT threadContext; + threadContext.ContextFlags = CONTEXT_CONTROL; + + // documentation says starting with Win7 SP1 you HAVE to call SetXStateFeaturesMask + HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll")); + if (k32mod == nullptr) { + throw windows_error("failed to load kernel32.dll"); + } + TSetXStateFeaturesMaskType sxsfm = reinterpret_cast<TSetXStateFeaturesMaskType>( + GetProcAddress(k32mod, "SetXStateFeaturesMask")); + if (sxsfm != nullptr) { + sxsfm(&threadContext, 0); + } + ::FreeLibrary(k32mod); + + if (GetThreadContext(threadHandle, &threadContext) == 0) { + throw windows_error("failed to access thread context."); + } + +#if BOOST_ARCH_X86_64 + REGWORD returnAddress = threadContext.Rip; +#else + REGWORD returnAddress = threadContext.Eip; +#endif + + REGWORD stubAddress = + WriteInjectionStub(processHandle, dllName, initFunction, userData, userDataSize, + skipInit, returnAddress); + + // make the stub the new next thing for the thread to execute +#if BOOST_ARCH_X86_64 + threadContext.Rip = stubAddress; +#else + threadContext.Eip = stubAddress; +#endif + + if (SetThreadContext(threadHandle, &threadContext) == 0) { + throw windows_error("failed to overwrite thread context"); + } +} + +void InjectDLLRemoteThread(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction, + LPCVOID userData, size_t userDataSize, bool skipInit) +{ + REGWORD stubAddress = WriteInjectionStub(processHandle, dllName, initFunction, + userData, userDataSize, skipInit, 0); + + DWORD threadId = 0UL; + HANDLE threadHandle = CreateRemoteThread( + processHandle, nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(stubAddress), + nullptr, 0, &threadId); + if (threadHandle == nullptr) { + throw windows_error("failed to start remote thread"); + } + ResumeThread(threadHandle); + spdlog::get("usvfs")->info("waiting for {0:x} to complete", + GetThreadId(threadHandle)); + ::WaitForSingleObject(threadHandle, 100); + ::CloseHandle(threadHandle); +} + +void InjectLib::InjectDLL(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName, + LPCSTR initFunction, LPCVOID userData, size_t userDataSize, + bool skipInit) +{ + namespace bfs = boost::filesystem; + if (!exists(bfs::path(dllName))) { + USVFS_THROW_EXCEPTION(file_not_found_error() + << ex_msg(string_cast<std::string>(dllName))); + } + if (threadHandle == INVALID_HANDLE_VALUE) { +#pragma message( \ + "doesn't seem to work as usvfs causes an exception in the first static initialization or pretty much on any function call. Because process is in different session? CRT related?") + /* + InjectDLLRemoteThread(processHandle, dllName, + initFunction, userData, userDataSize, skipInit); + */ + + DWORD pid = GetProcessId(processHandle); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + THREADENTRY32 threadInfo; + threadInfo.dwSize = sizeof(THREADENTRY32); + BOOL moreThreads = Thread32First(snapshot, &threadInfo); + std::vector<HANDLE> threadHandles; + HANDLE injectThread = INVALID_HANDLE_VALUE; + FILETIME injectThreadTime; + spdlog::get("usvfs")->info("inject dll to process {0}", pid); + while (moreThreads) { + if (threadInfo.th32OwnerProcessID == pid) { + HANDLE thread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, threadInfo.th32ThreadID); + + if (thread != nullptr) { + DWORD suspCount = SuspendThread(thread); + if (suspCount == 0) { + FILETIME creationTime, exitTime, kernelTime, userTime; + ::GetThreadTimes(thread, &creationTime, &exitTime, &kernelTime, &userTime); + + if ((injectThread == INVALID_HANDLE_VALUE) || + (CompareFileTime(&creationTime, &injectThreadTime) < 0)) { + spdlog::get("usvfs")->info("candidate for oldest thread: {0}", + threadInfo.th32ThreadID); + injectThread = thread; + injectThreadTime = creationTime; + } + } + threadHandles.push_back(thread); + } + } + moreThreads = Thread32Next(snapshot, &threadInfo); + } + if (injectThread != INVALID_HANDLE_VALUE) { + spdlog::get("usvfs")->debug("going to inject dll"); + InjectDLLEIP(processHandle, injectThread, dllName, initFunction, userData, + userDataSize, skipInit); + } else { + spdlog::get("usvfs")->critical("found no thread to use for injecting"); + } + + for (HANDLE hdl : threadHandles) { + spdlog::get("usvfs")->info("resuming thread {0}", ::GetThreadId(hdl)); + ResumeThread(hdl); + CloseHandle(hdl); + } + CloseHandle(snapshot); + } else { + InjectDLLEIP(processHandle, threadHandle, dllName, initFunction, userData, + userDataSize, skipInit); + } +} diff --git a/libs/usvfs/src/tinjectlib/injectlib.h b/libs/usvfs/src/tinjectlib/injectlib.h new file mode 100644 index 0000000..f165667 --- /dev/null +++ b/libs/usvfs/src/tinjectlib/injectlib.h @@ -0,0 +1,50 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <windows_sane.h> + +namespace InjectLib +{ + +/** + * @brief inject a dll into the target process + * @param processHandle handle of the process to inject to + * @param threadHandle handle of the main thread in the process + * @param dllname name/path of the dll to inject. The path can't be longer than MAX_PATH + * characters + * @param initFunction name of the initialization function. Can't be longer than 20 + * characters. If this is null, no function is called. Important: the init function has + * to exist, be exported, take the two userdata parameters and must be __cdecl calling + * convention on 32bit windows! Failing any of these the target process will crash or + * the dll isn't loaded Why __cdecl? Because otherwise we would need .def files to + * export the init function with a GetProcAddress-able function name. + * @param userData data passed to the init function + * @param userDataSize size of the data to be passed + * @param skipInit skip the call to the init function if the named function wasn't found + * in the dll. If false, the target process will crash if the function isn't exported in + * the dll + */ +void InjectDLL(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName, + LPCSTR initFunction = nullptr, LPCVOID userData = nullptr, + size_t userDataSize = 0, bool skipInit = false); + +} // namespace InjectLib diff --git a/libs/usvfs/src/usvfs_dll/CMakeLists.txt b/libs/usvfs/src/usvfs_dll/CMakeLists.txt new file mode 100644 index 0000000..2520b50 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.16) + +include(GNUInstallDirs) + +find_package(Boost CONFIG REQUIRED COMPONENTS thread) +find_package(spdlog CONFIG REQUIRED) + +file(GLOB sources "*.cpp" "*.h") +source_group("" FILES ${sources}) + +file(GLOB hooks "hooks/*.cpp" "hooks/*.h") +source_group("dlls" FILES ${hooks}) + +add_library(usvfs_dll SHARED) +target_sources(usvfs_dll + PRIVATE + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfsparametersprivate.h + ${sources} + ${hooks} + version.rc + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${PROJECT_SOURCE_DIR}/include + FILES + ${PROJECT_SOURCE_DIR}/include/usvfs/dllimport.h + ${PROJECT_SOURCE_DIR}/include/usvfs/logging.h + ${PROJECT_SOURCE_DIR}/include/usvfs/sharedparameters.h + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfs_version.h + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfs.h + ${PROJECT_SOURCE_DIR}/include/usvfs/usvfsparameters.h +) +target_link_libraries(usvfs_dll + PRIVATE shared thooklib usvfs_helper + Boost::thread spdlog::spdlog_header_only + Shlwapi) +target_compile_definitions(usvfs_dll PRIVATE BUILDING_USVFS_DLL) +set_target_properties(usvfs_dll + PROPERTIES + ARCHIVE_OUTPUT_NAME usvfs${ARCH_POSTFIX} + ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR} + LIBRARY_OUTPUT_NAME usvfs${ARCH_POSTFIX} + LIBRARY_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + LIBRARY_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR} + PDB_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + PDB_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR} + RUNTIME_OUTPUT_NAME usvfs${ARCH_POSTFIX} + RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR} + RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR}) + +install(TARGETS usvfs_dll EXPORT usvfs${ARCH_POSTFIX}Targets FILE_SET HEADERS) +install(FILES $<TARGET_PDB_FILE:usvfs_dll> DESTINATION pdb OPTIONAL) +install(EXPORT usvfs${ARCH_POSTFIX}Targets + FILE usvfs${ARCH_POSTFIX}Targets.cmake + NAMESPACE usvfs${ARCH_POSTFIX}:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/usvfs +) diff --git a/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp b/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp new file mode 100644 index 0000000..32510c9 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp @@ -0,0 +1,112 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "hookcallcontext.h" +#include "hookcontext.h" +#include <logging.h> + +namespace usvfs +{ + +class HookStack +{ +public: + static HookStack& instance() + { + PreserveGetLastError boostChangesGetLastError; + if (s_Instance.get() == nullptr) { + s_Instance.reset(new HookStack()); + } + return *s_Instance.get(); + } + + bool setGroup(MutExHookGroup group) + { + if (m_ActiveGroups.test(static_cast<size_t>(group)) || + m_ActiveGroups.test(static_cast<size_t>(MutExHookGroup::ALL_GROUPS))) { + return false; + } else { + m_ActiveGroups.set(static_cast<size_t>(group), true); + return true; + } + } + + void unsetGroup(MutExHookGroup group) + { + m_ActiveGroups.set(static_cast<size_t>(group), false); + } + +private: + HookStack() {} + +private: + static boost::thread_specific_ptr<HookStack> s_Instance; + std::bitset<static_cast<size_t>(MutExHookGroup::LAST)> m_ActiveGroups; +}; + +boost::thread_specific_ptr<HookStack> HookStack::s_Instance; + +HookCallContext::HookCallContext() : m_Active(true), m_Group(MutExHookGroup::NO_GROUP) +{ + updateLastError(); +} + +HookCallContext::HookCallContext(MutExHookGroup group) + : m_Active(HookStack::instance().setGroup(group)), m_Group(group) +{ + updateLastError(); +} + +HookCallContext::~HookCallContext() +{ + if (m_Active && (m_Group != MutExHookGroup::NO_GROUP)) { + HookStack::instance().unsetGroup(m_Group); + } + SetLastError(m_LastError); +} + +void HookCallContext::restoreLastError() +{ + SetLastError(m_LastError); +} + +void HookCallContext::updateLastError(DWORD lastError) +{ + m_LastError = lastError; +} + +bool HookCallContext::active() const +{ + return m_Active; +} + +FunctionGroupLock::FunctionGroupLock(MutExHookGroup group) : m_Group(group) +{ + m_Active = HookStack::instance().setGroup(m_Group); +} + +FunctionGroupLock::~FunctionGroupLock() +{ + if (m_Active) { + HookStack::instance().unsetGroup(m_Group); + } +} + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hookcallcontext.h b/libs/usvfs/src/usvfs_dll/hookcallcontext.h new file mode 100644 index 0000000..ebb3bf9 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcallcontext.h @@ -0,0 +1,87 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +namespace usvfs +{ + +/** + * @brief groups of hooks which may be used to implement each other, so only the first + * call should be to one should be manipulated + */ +enum class MutExHookGroup : int +{ + ALL_GROUPS = 0, // An ALL_GROUPS-hook prevents all other hooks from becoming active + // BUT hooks from other groups don't prevent the ALL_GROUPS-hook from + // becoming activated + OPEN_FILE = 1, + CREATE_PROCESS = 2, + FILE_ATTRIBUTES = 3, + FIND_FILES = 4, + LOAD_LIBRARY = 5, + FULL_PATHNAME = 6, + SHELL_FILEOP = 7, + DELETE_FILE = 8, + GET_FILE_VERSION = 9, + GET_MODULE_HANDLE = 10, + SEARCH_FILES = 11, + + NO_GROUP = 12, + LAST = NO_GROUP, +}; + +class HookCallContext +{ + +public: + HookCallContext(); + HookCallContext(MutExHookGroup group); + ~HookCallContext(); + + HookCallContext(const HookCallContext& reference) = delete; + HookCallContext& operator=(const HookCallContext& reference) = delete; + + void restoreLastError(); + + void updateLastError(DWORD lastError = GetLastError()); + + DWORD lastError() const { return m_LastError; } + + bool active() const; + +private: + DWORD m_LastError; + bool m_Active; + MutExHookGroup m_Group; +}; + +class FunctionGroupLock +{ +public: + FunctionGroupLock(MutExHookGroup group); + ~FunctionGroupLock(); + +private: + MutExHookGroup m_Group; + bool m_Active; +}; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hookcontext.cpp b/libs/usvfs/src/usvfs_dll/hookcontext.cpp new file mode 100644 index 0000000..f47e42d --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcontext.cpp @@ -0,0 +1,330 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "hookcontext.h" +#include "exceptionex.h" +#include "hookcallcontext.h" +#include "loghelpers.h" +#include "usvfs.h" +#include <shared_memory.h> +#include <sharedparameters.h> +#include <usvfsparameters.h> +#include <winapi.h> + +namespace bi = boost::interprocess; +using usvfs::shared::SharedMemoryT; +using usvfs::shared::VoidAllocatorT; + +using namespace usvfs; +namespace ush = usvfs::shared; + +HookContext* HookContext::s_Instance = nullptr; + +void printBuffer(const char* buffer, size_t size) +{ + static const int bufferSize = 16 * 3; + char temp[bufferSize + 1]; + temp[bufferSize] = '\0'; + + for (size_t i = 0; i < size; ++i) { + size_t offset = i % 16; + _snprintf(&temp[offset * 3], 3, "%02x ", (unsigned char)buffer[i]); + if (offset == 15) { + spdlog::get("hooks")->info("{0:x} - {1}", i - offset, temp); + } + } + + spdlog::get("hooks")->info(temp); +} + +HookContext::HookContext(const usvfsParameters& params, HMODULE module) + : m_ConfigurationSHM(bi::open_or_create, params.instanceName, 64 * 1024), + m_Parameters(retrieveParameters(params)), + m_Tree(m_Parameters->currentSHMName(), + 4 * 1024 * 1024) // 4 MiB empirically covers most small setups without + // need to resize + , + m_InverseTree( + m_Parameters->currentInverseSHMName(), + 128 * 1024) // 128 KiB should cover reverse tree for even larger setups + , + m_DLLModule(module) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("singleton duplicate instantiation (HookContext)"); + } + + const auto userCount = m_Parameters->userConnected(); + + spdlog::get("usvfs")->debug("context current shm: {0} (now {1} connections)", + m_Parameters->currentSHMName(), userCount); + + s_Instance = this; + + if (m_Tree.get() == nullptr) { + USVFS_THROW_EXCEPTION(usage_error() + << ex_msg("shm not found") << ex_msg(params.instanceName)); + } +} + +void HookContext::remove(const char* instanceName) +{ + bi::shared_memory_object::remove(instanceName); +} + +HookContext::~HookContext() +{ + spdlog::get("usvfs")->info("releasing hook context"); + + s_Instance = nullptr; + const auto userCount = m_Parameters->userDisconnected(); + + if (userCount == 0) { + spdlog::get("usvfs")->info("removing tree {}", m_Parameters->instanceName()); + bi::shared_memory_object::remove(m_Parameters->instanceName().c_str()); + } else { + spdlog::get("usvfs")->info("{} users left", userCount); + } +} + +SharedParameters* HookContext::retrieveParameters(const usvfsParameters& params) +{ + std::pair<SharedParameters*, SharedMemoryT::size_type> res = + m_ConfigurationSHM.find<SharedParameters>("parameters"); + + if (res.first == nullptr) { + // not configured yet + spdlog::get("usvfs")->info("create config in {}", ::GetCurrentProcessId()); + + res.first = m_ConfigurationSHM.construct<SharedParameters>("parameters")( + params, VoidAllocatorT(m_ConfigurationSHM.get_segment_manager())); + + if (res.first == nullptr) { + USVFS_THROW_EXCEPTION(bi::bad_alloc()); + } + } else { + spdlog::get("usvfs")->info("access existing config in {}", ::GetCurrentProcessId()); + } + + spdlog::get("usvfs")->info("{} processes", res.first->registeredProcessCount()); + + return res.first; +} + +HookContext::ConstPtr HookContext::readAccess(const char*) +{ + BOOST_ASSERT(s_Instance != nullptr); + + // TODO: this should be a shared mutex! + s_Instance->m_Mutex.wait(200); + return ConstPtr(s_Instance, unlockShared); +} + +HookContext::Ptr HookContext::writeAccess(const char*) +{ + BOOST_ASSERT(s_Instance != nullptr); + + s_Instance->m_Mutex.wait(200); + return Ptr(s_Instance, unlock); +} + +void HookContext::setDebugParameters(LogLevel level, CrashDumpsType dumpType, + const std::string& dumpPath, + std::chrono::milliseconds delayProcess) +{ + m_Parameters->setDebugParameters(level, dumpType, dumpPath, delayProcess); +} + +void HookContext::updateParameters() const +{ + m_Parameters->setSHMNames(m_Tree.shmName(), m_InverseTree.shmName()); +} + +usvfsParameters HookContext::callParameters() const +{ + updateParameters(); + return m_Parameters->makeLocal(); +} + +std::wstring HookContext::dllPath() const +{ + std::wstring path = winapi::wide::getModuleFileName(m_DLLModule); + return boost::filesystem::path(path).parent_path().make_preferred().wstring(); +} + +void HookContext::registerProcess(DWORD pid) +{ + m_Parameters->registerProcess(pid); +} + +void HookContext::unregisterCurrentProcess() +{ + m_Parameters->unregisterProcess(::GetCurrentProcessId()); +} + +std::vector<DWORD> HookContext::registeredProcesses() const +{ + return m_Parameters->registeredProcesses(); +} + +void HookContext::blacklistExecutable(const std::wstring& wexe) +{ + const auto exe = shared::string_cast<std::string>(wexe, shared::CodePage::UTF8); + + spdlog::get("usvfs")->debug("blacklisting '{}'", exe); + m_Parameters->blacklistExecutable(exe); +} + +void HookContext::clearExecutableBlacklist() +{ + spdlog::get("usvfs")->debug("clearing blacklist"); + m_Parameters->clearExecutableBlacklist(); +} + +BOOL HookContext::executableBlacklisted(LPCWSTR wapp, LPCWSTR wcmd) const +{ + std::string app; + if (wapp) { + app = ush::string_cast<std::string>(wapp, ush::CodePage::UTF8); + } + + std::string cmd; + if (wcmd) { + cmd = ush::string_cast<std::string>(wcmd, ush::CodePage::UTF8); + } + + return m_Parameters->executableBlacklisted(app, cmd); +} + +void usvfs::HookContext::addSkipFileSuffix(const std::wstring& fileSuffix) +{ + const auto fsuffix = + shared::string_cast<std::string>(fileSuffix, shared::CodePage::UTF8); + + if (fsuffix.empty()) { + return; + } + + spdlog::get("usvfs")->debug("added skip file suffix '{}'", fsuffix); + m_Parameters->addSkipFileSuffix(fsuffix); +} + +void usvfs::HookContext::clearSkipFileSuffixes() +{ + spdlog::get("usvfs")->debug("clearing skip file suffixes"); + m_Parameters->clearSkipFileSuffixes(); +} + +std::vector<std::string> usvfs::HookContext::skipFileSuffixes() const +{ + return m_Parameters->skipFileSuffixes(); +} + +void usvfs::HookContext::addSkipDirectory(const std::wstring& directory) +{ + const auto dir = shared::string_cast<std::string>(directory, shared::CodePage::UTF8); + + if (dir.empty()) { + return; + } + + spdlog::get("usvfs")->debug("added skip directory '{}'", dir); + m_Parameters->addSkipDirectory(dir); +} + +void usvfs::HookContext::clearSkipDirectories() +{ + spdlog::get("usvfs")->debug("clearing skip directories"); + m_Parameters->clearSkipDirectories(); +} + +std::vector<std::string> usvfs::HookContext::skipDirectories() const +{ + return m_Parameters->skipDirectories(); +} + +void HookContext::forceLoadLibrary(const std::wstring& wprocess, + const std::wstring& wpath) +{ + const auto process = + shared::string_cast<std::string>(wprocess, shared::CodePage::UTF8); + + const auto path = shared::string_cast<std::string>(wpath, shared::CodePage::UTF8); + + spdlog::get("usvfs")->debug("adding forced library '{}' for process '{}'", path, + process); + + m_Parameters->addForcedLibrary(process, path); +} + +void HookContext::clearLibraryForceLoads() +{ + spdlog::get("usvfs")->debug("clearing forced libraries"); + m_Parameters->clearForcedLibraries(); +} + +std::vector<std::wstring> +HookContext::librariesToForceLoad(const std::wstring& processName) +{ + const auto v = m_Parameters->forcedLibraries( + shared::string_cast<std::string>(processName, shared::CodePage::UTF8)); + + std::vector<std::wstring> wv; + for (const auto& s : v) { + wv.push_back(shared::string_cast<std::wstring>(s, shared::CodePage::UTF8)); + } + + return wv; +} + +void HookContext::registerDelayed(std::future<int> delayed) +{ + m_Futures.push_back(std::move(delayed)); +} + +std::vector<std::future<int>>& HookContext::delayed() +{ + return m_Futures; +} + +void HookContext::unlock(HookContext* instance) +{ + instance->m_Mutex.signal(); +} + +void HookContext::unlockShared(const HookContext* instance) +{ + instance->m_Mutex.signal(); +} + +// deprecated +// +extern "C" DLLEXPORT HookContext* __cdecl CreateHookContext( + const USVFSParameters& oldParams, HMODULE module) +{ + const usvfsParameters p(oldParams); + return usvfsCreateHookContext(p, module); +} + +extern "C" DLLEXPORT usvfs::HookContext* WINAPI +usvfsCreateHookContext(const usvfsParameters& params, HMODULE module) +{ + return new HookContext(params, module); +} diff --git a/libs/usvfs/src/usvfs_dll/hookcontext.h b/libs/usvfs/src/usvfs_dll/hookcontext.h new file mode 100644 index 0000000..c9f6eb1 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookcontext.h @@ -0,0 +1,224 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "dllimport.h" +#include "redirectiontree.h" +#include "semaphore.h" +#include "tree_container.h" +#include <directory_tree.h> +#include <exceptionex.h> +#include <usvfsparameters.h> +#include <usvfsparametersprivate.h> +#include <winapi.h> + +namespace usvfs +{ + +class DLLEXPORT SharedParameters; + +/** + * @brief context available to hooks. This is protected by a many-reader + * single-writer mutex + */ +class HookContext +{ + +public: + typedef std::unique_ptr<const HookContext, void (*)(const HookContext*)> ConstPtr; + typedef std::unique_ptr<HookContext, void (*)(HookContext*)> Ptr; + typedef unsigned int DataIDT; + +public: + HookContext(const usvfsParameters& params, HMODULE module); + + HookContext(const HookContext& reference) = delete; + + DLLEXPORT ~HookContext(); + + HookContext& operator=(const HookContext& reference) = delete; + + static void remove(const char* instance); + + /** + * @brief get read access to the context. + * @return smart ptr to the context. mutex will automatically be released when + * this leaves scope + */ + static ConstPtr readAccess(const char* source); + + /** + * @brief get write access to the context. + * @return smart ptr to the context. mutex will automatically be released when + * this leaves scope + */ + static Ptr writeAccess(const char* source); + + /** + * @return table containing file redirection information + */ + RedirectionTreeContainer& redirectionTable() { return m_Tree; } + + /** + * @return table containing file redirection information + */ + const RedirectionTreeContainer& redirectionTable() const { return m_Tree; } + + RedirectionTreeContainer& inverseTable() { return m_InverseTree; } + + const RedirectionTreeContainer& inverseTable() const { return m_InverseTree; } + + /** + * @return the parameters passed in on dll initialisation + */ + usvfsParameters callParameters() const; + + /** + * @return path to the calling library itself + */ + std::wstring dllPath() const; + + /** + * @brief get access to custom data + * @note the caller gains write access to the data, independent on the lock on + * the context + * as a whole. The caller himself has to ensure thread safety + */ + template <typename T> + T& customData(DataIDT id) const + { + auto iter = m_CustomData.find(id); + if (iter == m_CustomData.end()) { + iter = m_CustomData.insert(std::make_pair(id, T())).first; + } + // std::map is supposed to not invalidate any iterators when elements are + // added + // so it should be safe to return a pointer here + T* res = boost::any_cast<T>(&iter->second); + return *res; + } + + void registerProcess(DWORD pid); + void unregisterCurrentProcess(); + std::vector<DWORD> registeredProcesses() const; + + void blacklistExecutable(const std::wstring& executableName); + void clearExecutableBlacklist(); + BOOL executableBlacklisted(LPCWSTR lpApplicationName, LPCWSTR lpCommandLine) const; + + void addSkipFileSuffix(const std::wstring& fileSuffix); + void clearSkipFileSuffixes(); + std::vector<std::string> skipFileSuffixes() const; + + void addSkipDirectory(const std::wstring& directory); + void clearSkipDirectories(); + std::vector<std::string> skipDirectories() const; + + void forceLoadLibrary(const std::wstring& processName, + const std::wstring& libraryPath); + void clearLibraryForceLoads(); + std::vector<std::wstring> librariesToForceLoad(const std::wstring& processName); + + void setDebugParameters(LogLevel level, CrashDumpsType dumpType, + const std::string& dumpPath, + std::chrono::milliseconds delayProcess); + + void updateParameters() const; + + void registerDelayed(std::future<int> delayed); + + std::vector<std::future<int>>& delayed(); + +private: + static void unlock(HookContext* instance); + static void unlockShared(const HookContext* instance); + + SharedParameters* retrieveParameters(const usvfsParameters& params); + +private: + static HookContext* s_Instance; + + shared::SharedMemoryT m_ConfigurationSHM; + SharedParameters* m_Parameters{nullptr}; + RedirectionTreeContainer m_Tree; + RedirectionTreeContainer m_InverseTree; + + std::vector<std::future<int>> m_Futures; + + mutable std::map<DataIDT, boost::any> m_CustomData; + + HMODULE m_DLLModule; + + // mutable std::recursive_mutex m_Mutex; + mutable RecursiveBenaphore m_Mutex; +}; + +} // namespace usvfs + +extern "C" DLLEXPORT usvfs::HookContext* WINAPI +usvfsCreateHookContext(const usvfsParameters& params, HMODULE module); + +class PreserveGetLastError +{ +public: + PreserveGetLastError() : m_err(GetLastError()) {} + ~PreserveGetLastError() { SetLastError(m_err); } + +private: + DWORD m_err; +}; + +// declare an identifier that is guaranteed to be unique across the application +#define DATA_ID(name) static const usvfs::HookContext::DataIDT name = __COUNTER__ + +// set of macros. These ensure a call context is created but most of all these +// ensure exceptions are caught. + +#define READ_CONTEXT() HookContext::readAccess(__MYFUNC__) +#define WRITE_CONTEXT() HookContext::writeAccess(__MYFUNC__) + +#define HOOK_START_GROUP(group) \ + try { \ + HookCallContext callContext(group); + +#define HOOK_START \ + try { \ + HookCallContext callContext; + +#define HOOK_END \ + } \ + catch (const std::exception& e) \ + { \ + spdlog::get("usvfs")->error("exception in {0}: {1}", __MYFUNC__, e.what()); \ + logExtInfo(e); \ + } + +#define HOOK_ENDP(param) \ + } \ + catch (const std::exception& e) \ + { \ + spdlog::get("usvfs")->error("exception in {0} ({1}): {2}", __MYFUNC__, param, \ + e.what()); \ + logExtInfo(e); \ + } + +#define PRE_REALCALL callContext.restoreLastError(); +#define POST_REALCALL callContext.updateLastError(); diff --git a/libs/usvfs/src/usvfs_dll/hookmanager.cpp b/libs/usvfs/src/usvfs_dll/hookmanager.cpp new file mode 100644 index 0000000..731b856 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookmanager.cpp @@ -0,0 +1,333 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "hookmanager.h" +#include "../thooklib/ttrampolinepool.h" +#include "../thooklib/utility.h" +#include "exceptionex.h" +#include "hooks/kernel32.h" +#include "hooks/ntdll.h" +#include "usvfs.h" +#include <VersionHelpers.h> +#include <directory_tree.h> +#include <logging.h> +#include <shmlogger.h> +#include <usvfsparameters.h> +#include <winapi.h> + +using namespace HookLib; +namespace bf = boost::filesystem; + +namespace usvfs +{ + +HookManager* HookManager::s_Instance = nullptr; + +HookManager::HookManager(const usvfsParameters& params, HMODULE module) + : m_Context(params, module) +{ + if (s_Instance != nullptr) { + throw std::runtime_error("singleton duplicate instantiation (HookManager)"); + } + + s_Instance = this; + + m_Context.registerProcess(::GetCurrentProcessId()); + spdlog::get("usvfs")->info("Process registered in shared process list : {}", + ::GetCurrentProcessId()); + + winapi::ex::OSVersion version = winapi::ex::getOSVersion(); + spdlog::get("usvfs")->info( + "Windows version {}.{}.{} sp {} platform {} ({})", version.major, version.minor, + version.build, version.servicpack, version.platformid, + shared::string_cast<std::string>(winapi::ex::wide::getWindowsBuildLab(true)) + .c_str()); + + initHooks(); + + if (params.debugMode) { + while (!::IsDebuggerPresent()) { + // wait for debugger to attach + ::Sleep(100); + } + } +} + +HookManager::~HookManager() +{ + spdlog::get("hooks")->debug("end hook of process {}", GetCurrentProcessId()); + removeHooks(); + m_Context.unregisterCurrentProcess(); +} + +HookManager& HookManager::instance() +{ + if (s_Instance == nullptr) { + throw std::runtime_error("singleton not instantiated"); + } + + return *s_Instance; +} + +LPVOID HookManager::detour(const char* functionName) +{ + auto iter = m_Hooks.find(functionName); + if (iter != m_Hooks.end()) { + return GetDetour(iter->second); + } else { + return nullptr; + } +} + +void HookManager::removeHook(const std::string& functionName) +{ + auto iter = m_Hooks.find(functionName); + if (iter != m_Hooks.end()) { + try { + RemoveHook(iter->second); + m_Hooks.erase(iter); + spdlog::get("usvfs")->info("removed hook for {}", functionName); + } catch (const std::exception& e) { + spdlog::get("usvfs")->critical("failed to remove hook of {}: {}", functionName, + e.what()); + } + } else { + spdlog::get("usvfs")->info("{} wasn't hooked", functionName); + } +} + +void HookManager::logStubInt(LPVOID address) +{ + if (m_Stubs.find(address) != m_Stubs.end()) { + spdlog::get("hooks")->warn("{0} called", m_Stubs[address]); + } else { + spdlog::get("hooks")->warn("unknown function at {0} called", address); + } +} + +void HookManager::logStub(LPVOID address) +{ + try { + instance().logStubInt(address); + } catch (const std::exception& e) { + spdlog::get("hooks")->debug("function at {0} called after shutdown: {1}", address, + e.what()); + } +} + +void HookManager::installHook(HMODULE module1, HMODULE module2, + const std::string& functionName, LPVOID hook, + LPVOID* fillFuncAddr = nullptr) +{ + BOOST_ASSERT(hook != nullptr); + HOOKHANDLE handle = INVALID_HOOK; + HookError err = ERR_NONE; + LPVOID funcAddr = nullptr; + HMODULE usedModule = nullptr; + // both module1 and module2 are allowed to be null + if (module1 != nullptr) { + funcAddr = MyGetProcAddress(module1, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallHook(funcAddr, hook, &err); + } + if (handle != INVALID_HOOK) + usedModule = module1; + } + + if ((handle == INVALID_HOOK) && (module2 != nullptr)) { + funcAddr = MyGetProcAddress(module2, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallHook(funcAddr, hook, &err); + } + if (handle != INVALID_HOOK) + usedModule = module2; + } + + if (fillFuncAddr) + *fillFuncAddr = funcAddr; + + if (handle == INVALID_HOOK) { + spdlog::get("usvfs")->error("failed to hook {0}: {1}", functionName, + GetErrorString(err)); + } else { + m_Stubs.insert(make_pair(funcAddr, functionName)); + m_Hooks.insert(make_pair(std::string(functionName), handle)); + spdlog::get("usvfs")->info("hooked {0} ({1}) in {2} type {3}", functionName, + funcAddr, winapi::ansi::getModuleFileName(usedModule), + GetHookType(handle)); + } +} + +void HookManager::installStub(HMODULE module1, HMODULE module2, + const std::string& functionName) +{ + HOOKHANDLE handle = INVALID_HOOK; + HookError err = ERR_NONE; + LPVOID funcAddr = nullptr; + HMODULE usedModule = nullptr; + // both module1 and module2 are allowed to be null + if (module1 != nullptr) { + funcAddr = MyGetProcAddress(module1, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallStub(funcAddr, logStub, &err); + } else { + spdlog::get("usvfs")->debug("{} doesn't contain {}", + winapi::ansi::getModuleFileName(module1), + functionName); + } + if (handle != INVALID_HOOK) + usedModule = module1; + } + + if ((handle == INVALID_HOOK) && (module2 != nullptr)) { + funcAddr = MyGetProcAddress(module2, functionName.c_str()); + if (funcAddr != nullptr) { + handle = InstallStub(funcAddr, logStub, &err); + } else { + spdlog::get("usvfs")->debug("{} doesn't contain {}", + winapi::ansi::getModuleFileName(module2), + functionName); + } + if (handle != INVALID_HOOK) + usedModule = module2; + } + + if (handle == INVALID_HOOK) { + spdlog::get("usvfs")->error("failed to stub {0}: {1}", functionName, + GetErrorString(err)); + } else { + m_Stubs.insert(make_pair(funcAddr, functionName)); + m_Hooks.insert(make_pair(std::string(functionName), handle)); + spdlog::get("usvfs")->info("stubbed {0} ({1}) in {2} type {3}", functionName, + funcAddr, winapi::ansi::getModuleFileName(usedModule), + GetHookType(handle)); + } +} + +void HookManager::initHooks() +{ + TrampolinePool::initialize(); + + HookLib::TrampolinePool::instance().setBlock(true); + + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + spdlog::get("usvfs")->debug("kernel32.dll at {0:x}", + reinterpret_cast<uintptr_t>(k32Mod)); + // kernelbase.dll contains the actual implementation for functions formerly in + // kernel32.dll and advapi32.dll, starting with Windows 7 + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd371752(v=vs.85).aspx + HMODULE kbaseMod = GetModuleHandleA("kernelbase.dll"); + spdlog::get("usvfs")->debug("kernelbase.dll at {0:x}", + reinterpret_cast<uintptr_t>(kbaseMod)); + + installHook(kbaseMod, k32Mod, "GetFileAttributesExA", hook_GetFileAttributesExA); + installHook(kbaseMod, k32Mod, "GetFileAttributesA", hook_GetFileAttributesA); + installHook(kbaseMod, k32Mod, "GetFileAttributesExW", hook_GetFileAttributesExW); + installHook(kbaseMod, k32Mod, "GetFileAttributesW", hook_GetFileAttributesW); + installHook(kbaseMod, k32Mod, "SetFileAttributesW", hook_SetFileAttributesW); + + installHook(kbaseMod, k32Mod, "CreateDirectoryW", hook_CreateDirectoryW); + installHook(kbaseMod, k32Mod, "RemoveDirectoryW", hook_RemoveDirectoryW); + installHook(kbaseMod, k32Mod, "DeleteFileW", hook_DeleteFileW); + installHook(kbaseMod, k32Mod, "GetCurrentDirectoryA", hook_GetCurrentDirectoryA); + installHook(kbaseMod, k32Mod, "GetCurrentDirectoryW", hook_GetCurrentDirectoryW); + installHook(kbaseMod, k32Mod, "SetCurrentDirectoryA", hook_SetCurrentDirectoryA); + installHook(kbaseMod, k32Mod, "SetCurrentDirectoryW", hook_SetCurrentDirectoryW); + + installHook(kbaseMod, k32Mod, "ExitProcess", hook_ExitProcess); + + installHook(kbaseMod, k32Mod, "CreateProcessInternalW", hook_CreateProcessInternalW, + reinterpret_cast<LPVOID*>(&CreateProcessInternalW)); + + installHook(kbaseMod, k32Mod, "MoveFileA", hook_MoveFileA); + installHook(kbaseMod, k32Mod, "MoveFileW", hook_MoveFileW); + installHook(kbaseMod, k32Mod, "MoveFileExA", hook_MoveFileExA); + installHook(kbaseMod, k32Mod, "MoveFileExW", hook_MoveFileExW); + installHook(kbaseMod, k32Mod, "MoveFileWithProgressA", hook_MoveFileWithProgressA); + installHook(kbaseMod, k32Mod, "MoveFileWithProgressW", hook_MoveFileWithProgressW); + + installHook(kbaseMod, k32Mod, "CopyFileExW", hook_CopyFileExW); + if (IsWindows8OrGreater()) + installHook(kbaseMod, k32Mod, "CopyFile2", hook_CopyFile2, + reinterpret_cast<LPVOID*>(&CopyFile2)); + + installHook(kbaseMod, k32Mod, "GetPrivateProfileStringA", + hook_GetPrivateProfileStringA); + installHook(kbaseMod, k32Mod, "GetPrivateProfileStringW", + hook_GetPrivateProfileStringW); + installHook(kbaseMod, k32Mod, "GetPrivateProfileSectionA", + hook_GetPrivateProfileSectionA); + installHook(kbaseMod, k32Mod, "GetPrivateProfileSectionW", + hook_GetPrivateProfileSectionW); + installHook(kbaseMod, k32Mod, "WritePrivateProfileStringA", + hook_WritePrivateProfileStringA); + installHook(kbaseMod, k32Mod, "WritePrivateProfileStringW", + hook_WritePrivateProfileStringW); + + installHook(kbaseMod, k32Mod, "GetFullPathNameA", hook_GetFullPathNameA); + installHook(kbaseMod, k32Mod, "GetFullPathNameW", hook_GetFullPathNameW); + + installHook(kbaseMod, k32Mod, "FindFirstFileExW", hook_FindFirstFileExW); + + HMODULE ntdllMod = GetModuleHandleA("ntdll.dll"); + spdlog::get("usvfs")->debug("ntdll.dll at {0:x}", + reinterpret_cast<uintptr_t>(ntdllMod)); + installHook(ntdllMod, nullptr, "NtQueryFullAttributesFile", + hook_NtQueryFullAttributesFile); + installHook(ntdllMod, nullptr, "NtQueryAttributesFile", hook_NtQueryAttributesFile); + installHook(ntdllMod, nullptr, "NtQueryDirectoryFile", hook_NtQueryDirectoryFile); + installHook(ntdllMod, nullptr, "NtQueryDirectoryFileEx", hook_NtQueryDirectoryFileEx); + installHook(ntdllMod, nullptr, "NtQueryObject", hook_NtQueryObject); + installHook(ntdllMod, nullptr, "NtQueryInformationFile", hook_NtQueryInformationFile); + installHook(ntdllMod, nullptr, "NtQueryInformationByName", + hook_NtQueryInformationByName); + installHook(ntdllMod, nullptr, "NtOpenFile", hook_NtOpenFile); + installHook(ntdllMod, nullptr, "NtCreateFile", hook_NtCreateFile); + installHook(ntdllMod, nullptr, "NtClose", hook_NtClose); + installHook(ntdllMod, nullptr, "NtTerminateProcess", hook_NtTerminateProcess); + + installHook(kbaseMod, k32Mod, "LoadLibraryExA", hook_LoadLibraryExA); + installHook(kbaseMod, k32Mod, "LoadLibraryExW", hook_LoadLibraryExW); + + // install this hook late as usvfs is calling it itself for debugging purposes + installHook(kbaseMod, k32Mod, "GetModuleFileNameA", hook_GetModuleFileNameA); + installHook(kbaseMod, k32Mod, "GetModuleFileNameW", hook_GetModuleFileNameW); + + spdlog::get("usvfs")->debug("hooks installed"); + HookLib::TrampolinePool::instance().setBlock(false); +} + +void HookManager::removeHooks() +{ + while (m_Hooks.size() > 0) { + auto iter = m_Hooks.begin(); + try { + RemoveHook(iter->second); + spdlog::get("usvfs")->debug("removed hook {}", iter->first); + } catch (const std::exception& e) { + spdlog::get("usvfs")->critical("failed to remove hook: {}", e.what()); + } + + // remove either way, otherwise this is an endless loop + m_Hooks.erase(iter); + } +} + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hookmanager.h b/libs/usvfs/src/usvfs_dll/hookmanager.h new file mode 100644 index 0000000..636a29c --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hookmanager.h @@ -0,0 +1,81 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "hookcontext.h" +#include <hooklib.h> +#include <usvfsparameters.h> + +namespace usvfs +{ + +class HookManager +{ +public: + HookManager(const usvfsParameters& params, HMODULE module); + ~HookManager(); + + HookManager(const HookManager& reference) = delete; + + HookManager& operator=(const HookManager& reference) = delete; + + static HookManager& instance(); + + HookContext* context() { return &m_Context; } + + /// + /// \brief retrieve address of the detour of a function + /// \param functionName name of the function to look up + /// \return function address that can be used to directly execute the original code + /// + LPVOID detour(const char* functionName); + + /// + /// \brief remove the hook on the specified function. + /// \param functionName name of the function to unhook + /// \note This function is only exposed to allow a workaround for ExitProcess and may + /// be + /// removed if a better solution is found there. If you have another legit use + /// case, please let me know! + /// + void removeHook(const std::string& functionName); + +private: + void logStubInt(LPVOID address); + static void logStub(LPVOID address); + + void installHook(HMODULE module1, HMODULE module2, const std::string& functionName, + LPVOID hook, LPVOID* fillFuncAddr); + void installStub(HMODULE module1, HMODULE module2, const std::string& functionName); + void initHooks(); + void removeHooks(); + +private: + static HookManager* s_Instance; + + std::map<std::string, HookLib::HOOKHANDLE> m_Hooks; + + std::map<LPVOID, std::string> m_Stubs; + + HookContext m_Context; +}; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h new file mode 100644 index 0000000..dbd3597 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h @@ -0,0 +1,263 @@ +#pragma once + +/** + * This header defines function to deal with the various FileInformationClass + * enumeration. + */ + +#include <type_traits> + +#include <ntdll_declarations.h> + +#include <windows.h> + +namespace usvfs::details +{ + +#define DECLARE_HAS_FIELD(Field) \ + template <typename T> \ + constexpr auto HasFieldImpl##Field(int)->decltype(std::declval<T>().Field, void(), \ + std::true_type()) \ + { \ + return {}; \ + } \ + template <typename T> \ + constexpr auto HasFieldImpl##Field(...) \ + { \ + return std::false_type{}; \ + } \ + template <typename T> \ + constexpr auto HasField##Field() \ + { \ + return HasFieldImpl##Field<T>(0); \ + } + +DECLARE_HAS_FIELD(FileName) +DECLARE_HAS_FIELD(NextEntryOffset) +DECLARE_HAS_FIELD(ShortName) + +#undef DECLARE_HAS_FIELD + +template <FILE_INFORMATION_CLASS fileInformationClass, class FileInformationClass> +struct FileInformationClassUtilsImpl +{ + static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName) + { + const FileInformationClass* info = + reinterpret_cast<const FileInformationClass*>(address); + + // default value + offset = sizeof(FileInformationClass); + + if constexpr (HasFieldFileName<FileInformationClass>()) { + if constexpr (HasFieldNextEntryOffset<FileInformationClass>()) { + offset = info->NextEntryOffset; + } + fileName = std::wstring(info->FileName, info->FileNameLength / sizeof(WCHAR)); + } + } + + static void set_offset(LPVOID address, ULONG offset) + { + FileInformationClass* info = reinterpret_cast<FileInformationClass*>(address); + + if constexpr (HasFieldNextEntryOffset<FileInformationClass>()) { + info->NextEntryOffset = offset; + } + } + + static void set_filename(LPVOID address, const std::wstring& fileName) + { + FileInformationClass* info = reinterpret_cast<FileInformationClass*>(address); + + if constexpr (HasFieldFileName<FileInformationClass>()) { + memset(info->FileName, L'\0', info->FileNameLength); + info->FileNameLength = static_cast<ULONG>(fileName.length() * sizeof(WCHAR)); + + // doesn't need to be 0-terminated + memcpy(info->FileName, fileName.c_str(), info->FileNameLength); + + if constexpr (HasFieldShortName<FileInformationClass>()) { + if (info->ShortNameLength > 0) { + info->ShortNameLength = static_cast<CCHAR>( + GetShortPathNameW(fileName.c_str(), info->ShortName, 8)); + } + } + } + } +}; + +template <FILE_INFORMATION_CLASS fileInformationClass> +struct FileInformationClassUtils; + +template <> +struct FileInformationClassUtils<FileDirectoryInformation> + : FileInformationClassUtilsImpl<FileDirectoryInformation, + FILE_DIRECTORY_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileFullDirectoryInformation> + : FileInformationClassUtilsImpl<FileFullDirectoryInformation, + FILE_FULL_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileBothDirectoryInformation, + FILE_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileStandardInformation> + : FileInformationClassUtilsImpl<FileStandardInformation, FILE_STANDARD_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileNameInformation> + : FileInformationClassUtilsImpl<FileNameInformation, FILE_NAME_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileRenameInformation> + : FileInformationClassUtilsImpl<FileRenameInformation, FILE_RENAME_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileNamesInformation> + : FileInformationClassUtilsImpl<FileNamesInformation, FILE_NAMES_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileObjectIdInformation> + : FileInformationClassUtilsImpl<FileObjectIdInformation, FILE_OBJECTID_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileReparsePointInformation> + : FileInformationClassUtilsImpl<FileReparsePointInformation, + FILE_REPARSE_POINT_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdBothDirectoryInformation, + FILE_ID_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdFullDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdFullDirectoryInformation, + FILE_ID_FULL_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileNormalizedNameInformation> + : FileInformationClassUtilsImpl<FileNormalizedNameInformation, + FILE_NAME_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdExtdDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdExtdDirectoryInformation, + FILE_ID_EXTD_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdExtdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdExtdBothDirectoryInformation, + FILE_ID_EXTD_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileId64ExtdDirectoryInformation> + : FileInformationClassUtilsImpl<FileId64ExtdDirectoryInformation, + FILE_ID_64_EXTD_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileId64ExtdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileId64ExtdBothDirectoryInformation, + FILE_ID_64_EXTD_BOTH_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdAllExtdDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdAllExtdDirectoryInformation, + FILE_ID_ALL_EXTD_DIR_INFORMATION> +{}; +template <> +struct FileInformationClassUtils<FileIdAllExtdBothDirectoryInformation> + : FileInformationClassUtilsImpl<FileIdAllExtdBothDirectoryInformation, + FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION> +{}; + +// FILE_ALL_INFORMATION needs to be handled differently because it has a field that +// is itself a structure +template <> +struct FileInformationClassUtils<FileAllInformation> +{ + static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName) + { + FileInformationClassUtils<FileNameInformation>::get_data( + &reinterpret_cast<const FILE_ALL_INFORMATION*>(address)->NameInformation, + offset, fileName); + } + + static void set_offset(LPVOID address, ULONG offset) + { + // this is a no-op but it's consistent to do that everywhere + FileInformationClassUtils<FileNameInformation>::set_offset( + &reinterpret_cast<FILE_ALL_INFORMATION*>(address)->NameInformation, offset); + } + + static void set_filename(LPVOID address, const std::wstring& fileName) + { + FileInformationClassUtils<FileNameInformation>::set_filename( + &reinterpret_cast<FILE_ALL_INFORMATION*>(address)->NameInformation, fileName); + } +}; + +} // namespace usvfs::details + +#define _APP_FINFO_CASE(clazz, fn, ...) \ + case clazz: \ + return usvfs::details::FileInformationClassUtils<clazz>::fn(__VA_ARGS__); + +#define _APPLY_FILEINFO_FN(fn, ...) \ + _APP_FINFO_CASE(FileAllInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileFullDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileStandardInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileRenameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNamesInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileObjectIdInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileReparsePointInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdFullDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileNormalizedNameInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdExtdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileId64ExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileId64ExtdBothDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdAllExtdDirectoryInformation, fn, __VA_ARGS__) \ + _APP_FINFO_CASE(FileIdAllExtdBothDirectoryInformation, fn, __VA_ARGS__) + +void GetFileInformationData(FILE_INFORMATION_CLASS fileInformationClass, + LPCVOID address, ULONG& offset, std::wstring& fileName) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(get_data, address, offset, fileName); + default: + offset = ULONG_MAX; + } +} + +void SetFileInformationOffset(FILE_INFORMATION_CLASS fileInformationClass, + LPVOID address, ULONG offset) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(set_offset, address, offset); + default: + break; + } +} + +void SetFileInformationFileName(FILE_INFORMATION_CLASS fileInformationClass, + LPVOID address, const std::wstring& fileName) +{ + switch (fileInformationClass) { + _APPLY_FILEINFO_FN(set_filename, address, fileName); + default: + break; + } +} + +#undef _APP_FINFO_CASE +#undef _APPLY_FILEINFO_FN diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp new file mode 100644 index 0000000..6599eb0 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp @@ -0,0 +1,1860 @@ +#include "kernel32.h" +#include "sharedids.h" + +#include "../hookcallcontext.h" +#include "../hookcontext.h" +#include "../hookmanager.h" +#include "../maptracker.h" +#include <formatters.h> +#include <inject.h> +#include <loghelpers.h> +#include <stringcast.h> +#include <stringutils.h> +#include <usvfs.h> +#include <winapi.h> +#include <winbase.h> + +namespace ush = usvfs::shared; +using ush::CodePage; +using ush::string_cast; + +namespace usvfs +{ +MapTracker k32DeleteTracker; +MapTracker k32FakeDirTracker; +} // namespace usvfs + +class CurrentDirectoryTracker +{ +public: + using wstring = std::wstring; + + bool get(wstring& currentDir, const wchar_t* forRelativePath = nullptr) + { + int index = m_currentDrive; + if (forRelativePath && *forRelativePath && forRelativePath[1] == ':') + if (!getDriveIndex(forRelativePath, index)) + spdlog::get("usvfs")->warn( + "CurrentDirectoryTracker::get() invalid drive letter: {}, will use current " + "drive {}", + string_cast<std::string>(forRelativePath), + static_cast<char>('A' + index)); // prints '@' for m_currentDrive == -1 + if (index < 0) + return false; + + std::shared_lock<std::shared_mutex> lock(m_mutex); + if (m_perDrive[index].empty()) + return false; + else { + currentDir = m_perDrive[index]; + return true; + } + } + + bool set(const wstring& currentDir) + { + int index = -1; + bool good = !currentDir.empty() && getDriveIndex(currentDir.c_str(), index) && + currentDir[1] == ':'; + std::unique_lock<std::shared_mutex> lock(m_mutex); + m_currentDrive = good ? index : -1; + if (good) + m_perDrive[index] = currentDir; + return good; + } + +private: + static bool getDriveIndex(const wchar_t* path, int& index) + { + if (*path >= 'a' && *path <= 'z') + index = *path - 'a'; + else if (*path >= 'A' && *path <= 'Z') + index = *path - 'A'; + else + return false; + return true; + } + + mutable std::shared_mutex m_mutex; + wstring m_perDrive['z' - 'a' + 1]; + int m_currentDrive{-1}; +}; + +CurrentDirectoryTracker k32CurrentDirectoryTracker; + +// attempts to copy source to destination and return the error code +static inline DWORD copyFileDirect(LPCWSTR source, LPCWSTR destination, bool overwrite) +{ + usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::SHELL_FILEOP); + return CopyFileExW(source, destination, NULL, NULL, NULL, + overwrite ? 0 : COPY_FILE_FAIL_IF_EXISTS) + ? ERROR_SUCCESS + : GetLastError(); +} + +static inline WCHAR pathNameDriveLetter(LPCWSTR path) +{ + if (!path || !path[0]) + return 0; + if (path[1] == ':') + return path[0]; + // if path is not ?: or \* then we need to get absolute path: + std::wstring buf; + if (path[0] != '\\') { + buf = winapi::wide::getFullPathName(path).first; + path = buf.c_str(); + if (!path[0] || path[1] == ':') + return path[0]; + } + // check for \??\C: + if (path[1] && path[2] && path[3] && path[4] && path[0] == '\\' && path[3] == '\\' && + path[5] == ':') + return path[4]; + // give up + return 0; +} + +// returns false also in case we fail to determine the drive letter of the path +static inline bool pathsOnDifferentDrives(LPCWSTR path1, LPCWSTR path2) +{ + WCHAR drive1 = pathNameDriveLetter(path1); + WCHAR drive2 = pathNameDriveLetter(path2); + return drive1 && drive2 && towupper(drive1) != towupper(drive2); +} + +HMODULE WINAPI usvfs::hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile, + DWORD dwFlags) +{ + HMODULE res = nullptr; + + HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY) + const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName); + + PRE_REALCALL + res = LoadLibraryExW(fileName.c_str(), hFile, dwFlags); + POST_REALCALL + + HOOK_END + return res; +} + +HMODULE WINAPI usvfs::hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile, + DWORD dwFlags) +{ + HMODULE res = nullptr; + + HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + PRE_REALCALL + res = ::LoadLibraryExW(reroute.fileName(), hFile, dwFlags); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +/// determine name of the binary to run based on parameters for createprocess +std::wstring getBinaryName(LPCWSTR applicationName, LPCWSTR lpCommandLine) +{ + if (applicationName != nullptr) { + std::pair<std::wstring, std::wstring> fullPath = + winapi::wide::getFullPathName(applicationName); + return fullPath.second; + } else { + if (lpCommandLine[0] == '"') { + const wchar_t* endQuote = wcschr(lpCommandLine, '"'); + if (endQuote != nullptr) { + return std::wstring(lpCommandLine + 1, endQuote - 1); + } + } + + // according to the documentation, if the commandline is unquoted and has + // spaces, it will be interpreted in multiple ways, i.e. + // c:\program.exe files\sub dir\program name + // c:\program files\sub.exe dir\program name + // c:\program files\sub dir\program.exe name + // c:\program files\sub dir\program name.exe + LPCWSTR space = wcschr(lpCommandLine, L' '); + while (space != nullptr) { + std::wstring subString(lpCommandLine, space); + bool isDirectory = true; + if (winapi::ex::wide::fileExists(subString.c_str(), &isDirectory) && + !isDirectory) { + return subString; + } else { + space = wcschr(space + 1, L' '); + } + } + return std::wstring(lpCommandLine); + } +} + +BOOL(WINAPI* usvfs::CreateProcessInternalW)( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); + +BOOL WINAPI usvfs::hook_CreateProcessInternalW( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::CREATE_PROCESS) + if (!callContext.active()) { + res = CreateProcessInternalW( + token, lpApplicationName, lpCommandLine, lpProcessAttributes, + lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, + lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken); + callContext.updateLastError(); + return res; + } + + // remember if the caller wanted the process to be suspended. If so, we + // don't resume when we're done + BOOL susp = dwCreationFlags & CREATE_SUSPENDED; + dwCreationFlags |= CREATE_SUSPENDED; + + RerouteW applicationReroute; + RerouteW cmdReroute; + LPWSTR cend = nullptr; + + std::wstring dllPath; + usvfsParameters callParameters; + + { // scope for context lock + auto context = READ_CONTEXT(); + + if (RerouteW::interestingPath(lpCommandLine)) { + // First "argument" in the commandline is the command, we need to identify it and + // reroute it: + if (*lpCommandLine == '"') { + // If the first argument is quoted we trust its is quoted correctly + for (cend = lpCommandLine; *cend && *cend != ' '; ++cend) + if (*cend == '"') { + int escaped = 0; + for (++cend; *cend && (*cend != '"' || escaped % 2 != 0); ++cend) + escaped = *cend == '\\' ? escaped + 1 : 0; + } + + if (*(cend - 1) == '"') + --cend; + auto old_cend = *cend; + *cend = 0; + cmdReroute = RerouteW::create(context, callContext, lpCommandLine + 1); + *cend = old_cend; + if (old_cend == '"') + ++cend; + } else { + // If the first argument we have no choice but to test all the options to quote + // the command as the real CreateProcess will do this: + cend = lpCommandLine; + while (true) { + while (*cend && *cend != ' ') + ++cend; + + auto old_cend = *cend; + *cend = 0; + cmdReroute = RerouteW::create(context, callContext, lpCommandLine); + *cend = old_cend; + if (cmdReroute.wasRerouted() || pathIsFile(cmdReroute.fileName())) + break; + + while (*cend == ' ') + ++cend; + + if (!*cend) { + // if we reached the end of the string we'll just use the whole commandline + // as is: + cend = nullptr; + break; + } + } + } + } + + applicationReroute = RerouteW::create(context, callContext, lpApplicationName); + + dllPath = context->dllPath(); + callParameters = context->callParameters(); + } + + std::wstring cmdline; + if (cend && cmdReroute.fileName()) { + auto fileName = cmdReroute.fileName(); + cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2); + if (*fileName != '"') + cmdline += L"\""; + cmdline += fileName; + if (*fileName != '"') + cmdline += L"\""; + cmdline += cend; + } + + PRE_REALCALL + res = CreateProcessInternalW(token, applicationReroute.fileName(), + cmdline.empty() ? lpCommandLine : &cmdline[0], + lpProcessAttributes, lpThreadAttributes, bInheritHandles, + dwCreationFlags, lpEnvironment, lpCurrentDirectory, + lpStartupInfo, lpProcessInformation, newToken); + POST_REALCALL + + BOOL blacklisted = FALSE; + { // limit scope of context + auto context = READ_CONTEXT(); + blacklisted = context->executableBlacklisted(applicationReroute.fileName(), + cmdReroute.fileName()); + } + + if (res) { + if (!blacklisted) { + try { + injectProcess(dllPath, callParameters, *lpProcessInformation); + } catch (const std::exception& e) { + spdlog::get("hooks")->error("failed to inject into {0}: {1}", + lpApplicationName != nullptr + ? applicationReroute.fileName() + : static_cast<LPCWSTR>(lpCommandLine), + e.what()); + } + } + + // resume unless process is supposed to start suspended + if (!susp && (ResumeThread(lpProcessInformation->hThread) == (DWORD)-1)) { + spdlog::get("hooks")->error("failed to inject into spawned process"); + res = FALSE; + } + } + + LOG_CALL() + .PARAM(lpApplicationName) + .PARAM(applicationReroute.fileName()) + .PARAM(cmdReroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()) + .PARAM(cmdline); + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_GetFileAttributesExA(LPCSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesExA(lpFileName, fInfoLevelId, lpFileInformation); + callContext.updateLastError(); + return res; + } + HOOK_END + + HOOK_START + const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName); + + PRE_REALCALL + res = GetFileAttributesExW(fileName.c_str(), fInfoLevelId, lpFileInformation); + POST_REALCALL + + HOOK_END + return res; +} + +BOOL WINAPI usvfs::hook_GetFileAttributesExW(LPCWSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesExW(lpFileName, fInfoLevelId, lpFileInformation); + callContext.updateLastError(); + return res; + } + + fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str()); + + PRE_REALCALL + res = ::GetFileAttributesExW(reroute.fileName(), fInfoLevelId, lpFileInformation); + POST_REALCALL + + DWORD originalError = callContext.lastError(); + DWORD fixedError = originalError; + // In case the target does not exist the error value varies to differentiate if the + // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND). + // If the original target's parent folder doesn't actually exist it may exist in the + // the virtualized sense, or if we rerouted the query the parent of the original path + // might exist while the parent of the rerouted path might not: + if (!res && fixedError == ERROR_PATH_NOT_FOUND) { + // first query original file parent (if we rerouted it): + fs::path originalParent = canonicalFile.parent_path(); + WIN32_FILE_ATTRIBUTE_DATA parentAttr; + if (reroute.wasRerouted() && + ::GetFileAttributesExW(originalParent.c_str(), GetFileExInfoStandard, + &parentAttr) && + (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + else { + // now query the rerouted path for parent (which can be different from the parent + // of the rerouted path) + RerouteW rerouteParent = + RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str()); + if (rerouteParent.wasRerouted() && + ::GetFileAttributesExW(rerouteParent.fileName(), GetFileExInfoStandard, + &parentAttr) && + (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + } + } + if (fixedError != originalError) + callContext.updateLastError(fixedError); + + if (reroute.wasRerouted() || fixedError != originalError) { + DWORD resAttrib; + if (res && fInfoLevelId == GetFileExInfoStandard && lpFileInformation) + resAttrib = reinterpret_cast<WIN32_FILE_ATTRIBUTE_DATA*>(lpFileInformation) + ->dwFileAttributes; + else + resAttrib = (DWORD)-1; + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(fInfoLevelId) + .PARAMHEX(res) + .PARAMHEX(resAttrib) + .PARAM(originalError) + .PARAM(fixedError); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFileAttributesA(LPCSTR lpFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesA(lpFileName); + callContext.updateLastError(); + return res; + } + HOOK_END + + HOOK_START + const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName); + + PRE_REALCALL + res = GetFileAttributesW(fileName.c_str()); + POST_REALCALL + + HOOK_END + return res; +} + +DWORD WINAPI usvfs::hook_GetFileAttributesW(LPCWSTR lpFileName) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = GetFileAttributesW(lpFileName); + callContext.updateLastError(); + return res; + } + + fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str()); + + if (reroute.wasRerouted()) + PRE_REALCALL + res = ::GetFileAttributesW(reroute.fileName()); + POST_REALCALL + + DWORD originalError = callContext.lastError(); + DWORD fixedError = originalError; + // In case the target does not exist the error value varies to differentiate if the + // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND). + // If the original target's parent folder doesn't actually exist it may exist in the + // the virtualized sense, or if we rerouted the query the parent of the original path + // might exist while the parent of the rerouted path might not: + if (res == INVALID_FILE_ATTRIBUTES && fixedError == ERROR_PATH_NOT_FOUND) { + // first query original file parent (if we rerouted it): + fs::path originalParent = canonicalFile.parent_path(); + DWORD attr; + if (reroute.wasRerouted() && + (attr = ::GetFileAttributesW(originalParent.c_str())) != + INVALID_FILE_ATTRIBUTES && + (attr & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + else { + // now query the rerouted path for parent (which can be different from the parent + // of the rerouted path) + RerouteW rerouteParent = + RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str()); + if (rerouteParent.wasRerouted() && + (attr = ::GetFileAttributesW(rerouteParent.fileName())) != + INVALID_FILE_ATTRIBUTES && + (attr & FILE_ATTRIBUTE_DIRECTORY)) + fixedError = ERROR_FILE_NOT_FOUND; + } + } + if (fixedError != originalError) + callContext.updateLastError(fixedError); + + if (reroute.wasRerouted() || fixedError != originalError) { + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(originalError) + .PARAM(fixedError); + } + + HOOK_ENDP(lpFileName); + + return res; +} + +DWORD WINAPI usvfs::hook_SetFileAttributesW(LPCWSTR lpFileName, DWORD dwFileAttributes) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + PRE_REALCALL + res = ::SetFileAttributesW(reroute.fileName(), dwFileAttributes); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL().PARAM(reroute.fileName()).PARAM(res).PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::DELETE_FILE) + // Why is the usual if (!callContext.active()... check missing? + + const std::wstring path = + RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)).wstring(); + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, path.c_str()); + + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::DeleteFileW(reroute.fileName()); + } else { + res = ::DeleteFileW(path.c_str()); + } + POST_REALCALL + + if (res) { + reroute.removeMapping(READ_CONTEXT()); + } + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +BOOL rewriteChangedDrives(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + const usvfs::RerouteW& readReroute, + const usvfs::CreateRerouter& writeReroute) +{ + return ((readReroute.wasRerouted() || writeReroute.wasRerouted()) && + pathsOnDifferentDrives(readReroute.fileName(), writeReroute.fileName()) && + !pathsOnDifferentDrives(lpExistingFileName, lpNewFileName)); +} + +BOOL WINAPI usvfs::hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileA(lpExistingFileName, lpNewFileName); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const auto& existingFileName = ush::string_cast<std::wstring>(lpExistingFileName); + const auto& newFileName = ush::string_cast<std::wstring>(lpNewFileName); + + PRE_REALCALL + res = MoveFileW(existingFileName.c_str(), newFileName.c_str()); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileW(lpExistingFileName, lpNewFileName); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = 0; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, false, + "hook_MoveFileW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + if (movedDrives) + newFlags |= MOVEFILE_COPY_ALLOWED; + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else if (newFlags) + res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags); + else + res = ::MoveFileW(readReroute.fileName(), writeReroute.fileName()); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + readReroute.removeMapping( + READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted + // file entries should make this okay + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, + DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileExA(lpExistingFileName, lpNewFileName, dwFlags); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const std::wstring existingFileName = + ush::string_cast<std::wstring>(lpExistingFileName); + + // careful: lpNewFileName can be null if dwFlags is + // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure + // the null pointer is forwarded correctly + std::wstring newFileNameWstring; + const wchar_t* newFileName = nullptr; + + if (lpNewFileName) { + newFileNameWstring = ush::string_cast<std::wstring>(lpNewFileName); + newFileName = newFileNameWstring.c_str(); + } + + PRE_REALCALL + res = MoveFileExW(existingFileName.c_str(), newFileName, dwFlags); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileExW(lpExistingFileName, lpNewFileName, dwFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = dwFlags; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, + newFlags & MOVEFILE_REPLACE_EXISTING, + "hook_MoveFileExW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else + res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + readReroute.removeMapping( + READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted + // file entries should make this okay + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(dwFlags) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileWithProgressA(LPCSTR lpExistingFileName, + LPCSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + + if (!callContext.active()) { + res = MoveFileWithProgressA(lpExistingFileName, lpNewFileName, lpProgressRoutine, + lpData, dwFlags); + callContext.updateLastError(); + return res; + } + + HOOK_END + HOOK_START + + const auto& existingFileName = ush::string_cast<std::wstring>(lpExistingFileName); + + // careful: lpNewFileName can be null if dwFlags is + // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure + // the null pointer is forwarded correctly + std::wstring newFileNameWstring; + const wchar_t* newFileName = nullptr; + + if (lpNewFileName) { + newFileNameWstring = ush::string_cast<std::wstring>(lpNewFileName); + newFileName = newFileNameWstring.c_str(); + } + + PRE_REALCALL + res = MoveFileWithProgressW(existingFileName.c_str(), newFileName, lpProgressRoutine, + lpData, dwFlags); + POST_REALCALL + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags) +{ + + // TODO: Remove all redundant hooks to moveFile alternatives. + // it would appear that all other moveFile functions end up calling this one with no + // additional code. + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = MoveFileWithProgressW(lpExistingFileName, lpNewFileName, lpProgressRoutine, + lpData, dwFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + usvfs::CreateRerouter writeReroute; + bool callOriginal = true; + DWORD newFlags = dwFlags; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, + newFlags & MOVEFILE_REPLACE_EXISTING, + "hook_MoveFileWithProgressW"); + } + + if (callOriginal) { + bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName, + readReroute, writeReroute); + if (movedDrives) + newFlags |= MOVEFILE_COPY_ALLOWED; + + bool isDirectory = pathIsDirectory(readReroute.fileName()); + + PRE_REALCALL + if (isDirectory && movedDrives) { + SHFILEOPSTRUCTW sf = {0}; + sf.wFunc = FO_MOVE; + sf.hwnd = 0; + sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI; + sf.pFrom = readReroute.fileName(); + sf.pTo = writeReroute.fileName(); + int shRes = ::SHFileOperationW(&sf); + switch (shRes) { + case 0x78: + callContext.updateLastError(ERROR_ACCESS_DENIED); + break; + case 0x7C: + callContext.updateLastError(ERROR_FILE_NOT_FOUND); + break; + case 0x7E: + case 0x80: + callContext.updateLastError(ERROR_FILE_EXISTS); + break; + default: + callContext.updateLastError(shRes); + } + res = shRes == 0; + } else + res = ::MoveFileWithProgressW(readReroute.fileName(), writeReroute.fileName(), + lpProgressRoutine, lpData, newFlags); + POST_REALCALL + + if (res) + SetLastError(ERROR_SUCCESS); + + writeReroute.updateResult(callContext, res); + + if (res) { + // TODO: this call causes the node to be removed twice in case of + // MOVEFILE_COPY_ALLOWED as the deleteFile hook lower level already takes care of + // it, but deleteFile can't be disabled since we are relying on it in case of + // MOVEFILE_REPLACE_EXISTING for the destination file. + readReroute.removeMapping( + READ_CONTEXT(), + isDirectory); // Updating the rerouteCreate to check deleted file entries + // should make this okay (not related to comments above) + + if (writeReroute.newReroute()) { + if (isDirectory) + RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName), + fs::path(writeReroute.fileName())); + else + writeReroute.insertMapping(WRITE_CONTEXT()); + } + } + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAMWRAP(dwFlags) + .PARAMWRAP(newFlags) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_CopyFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, LPVOID lpData, + LPBOOL pbCancel, DWORD dwCopyFlags) +{ + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = CopyFileExW(lpExistingFileName, lpNewFileName, lpProgressRoutine, lpData, + pbCancel, dwCopyFlags); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + usvfs::CreateRerouter writeReroute; + bool callOriginal = true; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, lpExistingFileName); + callOriginal = writeReroute.rerouteNew( + context, callContext, lpNewFileName, + (dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0, "hook_CopyFileExW"); + } + + if (callOriginal) { + PRE_REALCALL + res = ::CopyFileExW(readReroute.fileName(), writeReroute.fileName(), + lpProgressRoutine, lpData, pbCancel, dwCopyFlags); + POST_REALCALL + writeReroute.updateResult(callContext, res); + + if (res && writeReroute.newReroute()) + writeReroute.insertMapping(WRITE_CONTEXT()); + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer) +{ + DWORD res = 0; + + HOOK_START + + std::wstring buffer; + buffer.resize(nBufferLength); + + PRE_REALCALL + res = GetCurrentDirectoryW(nBufferLength, &buffer[0]); + POST_REALCALL + + if (res > 0) { + res = WideCharToMultiByte(CP_ACP, 0, buffer.c_str(), res + 1, lpBuffer, + nBufferLength, nullptr, nullptr); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer) +{ + DWORD res = FALSE; + + HOOK_START + + std::wstring actualCWD; + + if (!k32CurrentDirectoryTracker.get(actualCWD)) { + PRE_REALCALL + res = ::GetCurrentDirectoryW(nBufferLength, lpBuffer); + POST_REALCALL + } else { + ush::wcsncpy_sz(lpBuffer, &actualCWD[0], + std::min(static_cast<size_t>(nBufferLength), actualCWD.size() + 1)); + + // yupp, that's how GetCurrentDirectory actually works... + if (actualCWD.size() < nBufferLength) { + res = static_cast<DWORD>(actualCWD.size()); + } else { + res = static_cast<DWORD>(actualCWD.size() + 1); + } + } + + if (nBufferLength) + LOG_CALL() + .PARAM(std::wstring(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(actualCWD.size()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_SetCurrentDirectoryA(LPCSTR lpPathName) +{ + return SetCurrentDirectoryW(ush::string_cast<std::wstring>(lpPathName).c_str()); +} + +BOOL WINAPI usvfs::hook_SetCurrentDirectoryW(LPCWSTR lpPathName) +{ + BOOL res = FALSE; + + HOOK_START + + const fs::path& realPath = RerouteW::canonizePath(RerouteW::absolutePath(lpPathName)); + const std::wstring& realPathStr = realPath.wstring(); + std::wstring finalRoute; + BOOL found = FALSE; + + if (fs::exists(realPath)) + finalRoute = realPathStr; + else { + WCHAR processDir[MAX_PATH]; + if (::GetModuleFileNameW(NULL, processDir, MAX_PATH) != 0 && + ::PathRemoveFileSpecW(processDir)) { + WCHAR processName[MAX_PATH]; + ::GetModuleFileNameW(NULL, processName, MAX_PATH); + fs::path routedName = realPath / processName; + RerouteW rerouteTest = + RerouteW::create(READ_CONTEXT(), callContext, routedName.wstring().c_str()); + if (rerouteTest.wasRerouted()) { + std::wstring reroutedPath = rerouteTest.fileName(); + if (routedName.wstring().find(processDir) != std::string::npos) { + fs::path finalPath(reroutedPath); + finalRoute = finalPath.parent_path().wstring(); + found = TRUE; + } + } + } + + if (!found) { + RerouteW reroute = + RerouteW::create(READ_CONTEXT(), callContext, realPathStr.c_str()); + finalRoute = reroute.fileName(); + } + } + + PRE_REALCALL + res = ::SetCurrentDirectoryW(finalRoute.c_str()); + POST_REALCALL + + if (res) + if (!k32CurrentDirectoryTracker.set(realPathStr)) + spdlog::get("usvfs")->warn("Updating actual current directory failed: {} ?!", + string_cast<std::string>(realPathStr)); + + LOG_CALL() + .PARAM(lpPathName) + .PARAM(realPathStr) + .PARAM(finalRoute) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DLLEXPORT BOOL WINAPI usvfs::hook_CreateDirectoryW( + LPCWSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes) +{ + BOOL res = FALSE; + HOOK_START + + RerouteW reroute = RerouteW::createOrNew(READ_CONTEXT(), callContext, lpPathName); + + PRE_REALCALL + res = ::CreateDirectoryW(reroute.fileName(), lpSecurityAttributes); + POST_REALCALL + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT(), true); + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpPathName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DLLEXPORT BOOL WINAPI usvfs::hook_RemoveDirectoryW(LPCWSTR lpPathName) +{ + + BOOL res = FALSE; + + HOOK_START_GROUP(MutExHookGroup::DELETE_FILE) + // Why is the usual if (!callContext.active()... check missing? + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpPathName); + + PRE_REALCALL + if (reroute.wasRerouted()) { + res = ::RemoveDirectoryW(reroute.fileName()); + } else { + res = ::RemoveDirectoryW(lpPathName); + } + POST_REALCALL + + if (res) { + reroute.removeMapping(READ_CONTEXT(), true); + } + + if (reroute.wasRerouted()) + LOG_CALL() + .PARAM(lpPathName) + .PARAM(reroute.fileName()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength, + LPSTR lpBuffer, LPSTR* lpFilePart) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME) + if (!callContext.active()) { + res = GetFullPathNameA(lpFileName, nBufferLength, lpBuffer, lpFilePart); + callContext.updateLastError(); + return res; + } + + std::string resolvedWithCMD; + + std::wstring actualCWD; + fs::path filePath = ush::string_cast<std::wstring>(lpFileName, CodePage::UTF8); + if (k32CurrentDirectoryTracker.get(actualCWD, filePath.wstring().c_str())) { + if (!filePath.is_absolute()) + resolvedWithCMD = ush::string_cast<std::string>( + (actualCWD / filePath.relative_path()).wstring()); + } + + PRE_REALCALL + res = + ::GetFullPathNameA(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(), + nBufferLength, lpBuffer, lpFilePart); + POST_REALCALL + + if (false && nBufferLength) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(resolvedWithCMD) + .PARAM(std::string(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength, + LPWSTR lpBuffer, LPWSTR* lpFilePart) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME) + if (!callContext.active()) { + res = GetFullPathNameW(lpFileName, nBufferLength, lpBuffer, lpFilePart); + callContext.updateLastError(); + return res; + } + + std::wstring resolvedWithCMD; + + std::wstring actualCWD; + if (k32CurrentDirectoryTracker.get(actualCWD, lpFileName)) { + fs::path filePath = lpFileName; + if (!filePath.is_absolute()) + resolvedWithCMD = (actualCWD / filePath.relative_path()).wstring(); + } + + PRE_REALCALL + res = + ::GetFullPathNameW(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(), + nBufferLength, lpBuffer, lpFilePart); + POST_REALCALL + + if (false && nBufferLength) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(resolvedWithCMD) + .PARAM(std::wstring(lpBuffer, res)) + .PARAM(nBufferLength) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename, + DWORD nSize) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + + PRE_REALCALL + res = ::GetModuleFileNameA(hModule, lpFilename, nSize); + POST_REALCALL + if ((res != 0) && callContext.active()) { + std::vector<char> buf; + // If GetModuleFileNameA failed because the buffer is not large enough this + // complicates matters because we are dealing with incomplete information + // (consider for example the case that we have a long real path which will + // be routed to a short virtual so the call should actually succeed in such + // a case). To solve this we simply use our own buffer to find the complete + // module path: + DWORD full_res = res; + size_t buf_size = nSize; + while (full_res == buf_size) { + buf_size = std::max(static_cast<size_t>(MAX_PATH), buf_size * 2); + buf.resize(buf_size); + full_res = ::GetModuleFileNameA(hModule, buf.data(), buf_size); + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, + ush::string_cast<std::wstring>(buf.empty() ? lpFilename : buf.data()).c_str(), + true); + if (reroute.wasRerouted()) { + DWORD reroutedSize = static_cast<DWORD>(wcslen(reroute.fileName())); + if (reroutedSize >= nSize) { + reroutedSize = nSize - 1; + callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER); + res = nSize; + } else + res = reroutedSize; + memcpy(lpFilename, ush::string_cast<std::string>(reroute.fileName()).c_str(), + reroutedSize * sizeof(lpFilename[0])); + lpFilename[reroutedSize] = 0; + + LOG_CALL() + .PARAM(hModule) + .addParam("lpFilename", (res != 0UL) ? lpFilename : "<not set>") + .PARAM(nSize) + .PARAM(res) + .PARAM(callContext.lastError()); + } + } + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename, + DWORD nSize) +{ + DWORD res = 0UL; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + + PRE_REALCALL + res = ::GetModuleFileNameW(hModule, lpFilename, nSize); + POST_REALCALL + if ((res != 0) && callContext.active()) { + std::vector<WCHAR> buf; + // If GetModuleFileNameW failed because the buffer is not large enough this + // complicates matters because we are dealing with incomplete information (consider + // for example the case that we have a long real path which will be routed to a + // short virtual so the call should actually succeed in such a case). To solve this + // we simply use our own buffer to find the complete module path: + DWORD full_res = res; + size_t buf_size = nSize; + while (full_res == buf_size) { + buf_size = std::max(static_cast<size_t>(MAX_PATH), buf_size * 2); + buf.resize(buf_size); + full_res = ::GetModuleFileNameW(hModule, buf.data(), buf_size); + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, + buf.empty() ? lpFilename : buf.data(), true); + if (reroute.wasRerouted()) { + DWORD reroutedSize = static_cast<DWORD>(wcslen(reroute.fileName())); + if (reroutedSize >= nSize) { + reroutedSize = nSize - 1; + callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER); + res = nSize; + } else + res = reroutedSize; + memcpy(lpFilename, reroute.fileName(), reroutedSize * sizeof(lpFilename[0])); + lpFilename[reroutedSize] = 0; + + LOG_CALL() + .PARAM(hModule) + .addParam("lpFilename", (res != 0UL) ? lpFilename : L"<not set>") + .PARAM(nSize) + .PARAM(res) + .PARAM(callContext.lastError()); + } + } + HOOK_END + + return res; +} + +HANDLE WINAPI usvfs::hook_FindFirstFileExW( + LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData, + FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags) +{ + HANDLE res = INVALID_HANDLE_VALUE; + + HOOK_START_GROUP(MutExHookGroup::SEARCH_FILES) + if (!callContext.active()) { + res = FindFirstFileExW(lpFileName, fInfoLevelId, lpFindFileData, fSearchOp, + lpSearchFilter, dwAdditionalFlags); + callContext.updateLastError(); + return res; + } + + // FindFirstFileEx() must fail early if the path ends with a slash + if (lpFileName) { + const auto len = wcslen(lpFileName); + if (len > 0) { + if (lpFileName[len - 1] == L'\\' || lpFileName[len - 1] == L'/') { + spdlog::get("usvfs")->warn( + "hook_FindFirstFileExW(): path '{}' ends with slash, always fails", + fs::path(lpFileName).string()); + return INVALID_HANDLE_VALUE; + } + } + } + + fs::path finalPath; + RerouteW reroute; + fs::path originalPath; + + bool usedRewrite = false; + + // We need to do some trickery here, since we only want to use the hooked + // NtQueryDirectoryFile for rerouted locations we need to check if the Directory path + // has been routed instead of the full path. + originalPath = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)); + PRE_REALCALL + res = ::FindFirstFileExW(originalPath.c_str(), fInfoLevelId, lpFindFileData, + fSearchOp, lpSearchFilter, dwAdditionalFlags); + POST_REALCALL + + if (res == INVALID_HANDLE_VALUE) { + fs::path searchPath = originalPath.filename(); + fs::path parentPath = originalPath.parent_path(); + std::wstring findPath = parentPath.wstring(); + while (findPath.find(L"*?<>\"", 0, 1) != std::wstring::npos) { + searchPath = parentPath.filename() / searchPath; + parentPath = parentPath.parent_path(); + findPath = parentPath.wstring(); + } + reroute = RerouteW::create(READ_CONTEXT(), callContext, parentPath.c_str()); + if (reroute.wasRerouted()) { + finalPath = reroute.fileName(); + finalPath /= searchPath.wstring(); + } + if (!finalPath.empty()) { + PRE_REALCALL + usedRewrite = true; + res = ::FindFirstFileExW(finalPath.c_str(), fInfoLevelId, lpFindFileData, + fSearchOp, lpSearchFilter, dwAdditionalFlags); + POST_REALCALL + } + } + + if (res != INVALID_HANDLE_VALUE) { + // store the original search path for use during iteration + WRITE_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[res] = lpFileName; + } + + LOG_CALL() + .PARAM(lpFileName) + .PARAM(originalPath.c_str()) + .PARAM(res) + .PARAM(callContext.lastError()); + if (usedRewrite) + LOG_CALL() + .PARAM(lpFileName) + .PARAM(finalPath.c_str()) + .PARAM(res) + .PARAM(callContext.lastError()); + + HOOK_END + + return res; +} + +HRESULT(WINAPI* usvfs::CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); + +HRESULT WINAPI usvfs::hook_CopyFile2(PCWSTR pwszExistingFileName, + PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters) +{ + HRESULT res = E_FAIL; + + typedef HRESULT(WINAPI * CopyFile2_t)(PCWSTR, PCWSTR, COPYFILE2_EXTENDED_PARAMETERS*); + + HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP) + if (!callContext.active()) { + res = CopyFile2(pwszExistingFileName, pwszNewFileName, pExtendedParameters); + callContext.updateLastError(); + return res; + } + + RerouteW readReroute; + CreateRerouter writeReroute; + bool callOriginal = true; + + { + auto context = READ_CONTEXT(); + readReroute = RerouteW::create(context, callContext, pwszExistingFileName); + callOriginal = writeReroute.rerouteNew( + context, callContext, pwszNewFileName, + pExtendedParameters && + (pExtendedParameters->dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0, + "hook_CopyFile2"); + } + + if (callOriginal) { + PRE_REALCALL + res = + CopyFile2(readReroute.fileName(), writeReroute.fileName(), pExtendedParameters); + POST_REALCALL + writeReroute.updateResult(callContext, SUCCEEDED(res)); + + if (SUCCEEDED(res) && writeReroute.newReroute()) + writeReroute.insertMapping(WRITE_CONTEXT()); + + if (readReroute.wasRerouted() || writeReroute.wasRerouted() || + writeReroute.changedError()) + LOG_CALL() + .PARAM(readReroute.fileName()) + .PARAM(writeReroute.fileName()) + .PARAM(res) + .PARAM(writeReroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpDefault, + LPSTR lpReturnedString, DWORD nSize, + LPCSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileStringA(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str()); + + PRE_REALCALL + res = ::GetPrivateProfileStringA( + lpAppName, lpKeyName, lpDefault, lpReturnedString, nSize, + ush::string_cast<std::string>(reroute.fileName()).c_str()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName, + LPCWSTR lpDefault, + LPWSTR lpReturnedString, DWORD nSize, + LPCWSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + + PRE_REALCALL + res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString, + nSize, reroute.fileName()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileSectionA(LPCSTR lpAppName, + LPSTR lpReturnedString, DWORD nSize, + LPCSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileSectionA(lpAppName, lpReturnedString, nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create( + READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str()); + + PRE_REALCALL + res = ::GetPrivateProfileSectionA( + lpAppName, lpReturnedString, nSize, + ush::string_cast<std::string>(reroute.fileName()).c_str()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +DWORD WINAPI usvfs::hook_GetPrivateProfileSectionW(LPCWSTR lpAppName, + LPWSTR lpReturnedString, DWORD nSize, + LPCWSTR lpFileName) +{ + DWORD res = 0; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize, lpFileName); + callContext.updateLastError(); + return res; + } + + RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName); + + PRE_REALCALL + res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize, + reroute.fileName()); + POST_REALCALL + + if (reroute.wasRerouted()) { + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_WritePrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpString, LPCSTR lpFileName) +{ + BOOL res = false; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::WritePrivateProfileStringA(lpAppName, lpKeyName, lpString, lpFileName); + callContext.updateLastError(); + return res; + } + + CreateRerouter reroute; + bool callOriginal = reroute.rerouteNew( + READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str(), + true, "hook_WritePrivateProfileStringA"); + + if (callOriginal) { + PRE_REALCALL + res = ::WritePrivateProfileStringA( + lpAppName, lpKeyName, lpString, + ush::string_cast<std::string>(reroute.fileName()).c_str()); + POST_REALCALL + reroute.updateResult(callContext, res); + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT()); + + if (reroute.wasRerouted() || reroute.changedError()) + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(reroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +BOOL WINAPI usvfs::hook_WritePrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName, + LPCWSTR lpString, LPCWSTR lpFileName) +{ + BOOL res = false; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + + if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) { + res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString, lpFileName); + callContext.updateLastError(); + return res; + } + + CreateRerouter reroute; + bool callOriginal = reroute.rerouteNew(READ_CONTEXT(), callContext, lpFileName, true, + "hook_WritePrivateProfileStringW"); + + if (callOriginal) { + PRE_REALCALL + res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString, + reroute.fileName()); + POST_REALCALL + reroute.updateResult(callContext, res); + + if (res && reroute.newReroute()) + reroute.insertMapping(WRITE_CONTEXT()); + + if (reroute.wasRerouted() || reroute.changedError()) + LOG_CALL() + .PARAM(lpAppName) + .PARAM(lpKeyName) + .PARAM(lpFileName) + .PARAM(reroute.fileName()) + .PARAMHEX(res) + .PARAM(reroute.originalError()) + .PARAM(callContext.lastError()); + } + + HOOK_END + + return res; +} + +VOID WINAPI usvfs::hook_ExitProcess(UINT exitCode) +{ + HOOK_START + + { + HookContext::Ptr context = WRITE_CONTEXT(); + + std::vector<std::future<int>>& delayed = context->delayed(); + + if (!delayed.empty()) { + // ensure all delayed tasks are completed before we exit the process + for (std::future<int>& delayedOp : delayed) { + delayedOp.get(); + } + delayed.clear(); + } + } + + // exitprocess doesn't return so logging the call after the real call doesn't + // make much sense. + // nor does any pre/post call macro + LOG_CALL().PARAM(exitCode); + + usvfsDisconnectVFS(); + + // HookManager::instance().removeHook("ExitProcess"); + // PRE_REALCALL + ::ExitProcess(exitCode); + // POST_REALCALL + + HOOK_END +} diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.h b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h new file mode 100644 index 0000000..7087b2e --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h @@ -0,0 +1,116 @@ +#pragma once + +#include "ntdll_declarations.h" +#include <dllimport.h> + +namespace usvfs +{ + +DLLEXPORT BOOL WINAPI hook_GetFileAttributesExA(LPCSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation); +DLLEXPORT BOOL WINAPI hook_GetFileAttributesExW(LPCWSTR lpFileName, + GET_FILEEX_INFO_LEVELS fInfoLevelId, + LPVOID lpFileInformation); +DLLEXPORT DWORD WINAPI hook_GetFileAttributesA(LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetFileAttributesW(LPCWSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_SetFileAttributesW(LPCWSTR lpFileName, + DWORD dwFileAttributes); + +DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer); +DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer); +DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryA(LPCSTR lpPathName); +DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryW(LPCWSTR lpPathName); + +DLLEXPORT DWORD WINAPI hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength, + LPSTR lpBuffer, LPSTR* lpFilePart); +DLLEXPORT DWORD WINAPI hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength, + LPWSTR lpBuffer, LPWSTR* lpFilePart); + +DLLEXPORT BOOL WINAPI hook_CreateDirectoryW(LPCWSTR lpPathName, + LPSECURITY_ATTRIBUTES lpSecurityAttributes); +DLLEXPORT BOOL WINAPI hook_RemoveDirectoryW(LPCWSTR lpPathName); + +DLLEXPORT BOOL WINAPI hook_DeleteFileW(LPCWSTR lpFileName); + +DLLEXPORT BOOL WINAPI hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName); +DLLEXPORT BOOL WINAPI hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName); +DLLEXPORT BOOL WINAPI hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, + DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileExW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressA(LPCSTR lpExistingFileName, + LPCSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags); +DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, DWORD dwFlags); +; + +DLLEXPORT BOOL WINAPI hook_CopyFileExW(LPCWSTR lpExistingFileName, + LPCWSTR lpNewFileName, + LPPROGRESS_ROUTINE lpProgressRoutine, + LPVOID lpData, LPBOOL pbCancel, + DWORD dwCopyFlags); + +extern HRESULT(WINAPI* CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); +DLLEXPORT HRESULT WINAPI +hook_CopyFile2(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName, + COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters); + +DLLEXPORT HMODULE WINAPI hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile, + DWORD dwFlags); +DLLEXPORT HMODULE WINAPI hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile, + DWORD dwFlags); + +extern BOOL(WINAPI* CreateProcessInternalW)( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); +DLLEXPORT BOOL WINAPI hook_CreateProcessInternalW( + LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken); + +DLLEXPORT DWORD WINAPI hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename, + DWORD nSize); +DLLEXPORT DWORD WINAPI hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename, + DWORD nSize); + +DLLEXPORT HANDLE WINAPI hook_FindFirstFileExW( + LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData, + FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags); + +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName, + LPCSTR lpDefault, + LPSTR lpReturnedString, + DWORD nSize, LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringW(LPCWSTR lpAppName, + LPCWSTR lpKeyName, + LPCWSTR lpDefault, + LPWSTR lpReturnedString, + DWORD nSize, LPCWSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionA(LPCSTR lpAppName, + LPSTR lpReturnedString, + DWORD nSize, LPCSTR lpFileName); +DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionW(LPCWSTR lpAppName, + LPWSTR lpReturnedString, + DWORD nSize, LPCWSTR lpFileName); +DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringA(LPCSTR lpAppName, + LPCSTR lpKeyName, LPCSTR lpString, + LPCSTR lpFileName); +DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringW(LPCWSTR lpAppName, + LPCWSTR lpKeyName, + LPCWSTR lpString, + LPCWSTR lpFileName); + +DLLEXPORT VOID WINAPI hook_ExitProcess(UINT exitCode); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp new file mode 100644 index 0000000..ad53418 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp @@ -0,0 +1,1520 @@ +#include "ntdll.h" + +#include <mutex> +#include <queue> +#include <set> + +#include <boost/filesystem.hpp> + +#include <addrtools.h> +#include <loghelpers.h> +#include <stringcast.h> +#include <stringutils.h> +#include <unicodestring.h> +#include <usvfs.h> + +#include "../hookcallcontext.h" +#include "../hookcontext.h" +#include "../maptracker.h" +#include "../stringcast_boost.h" + +#include "file_information_utils.h" +#include "sharedids.h" + +namespace ulog = usvfs::log; +namespace ush = usvfs::shared; +namespace bfs = boost::filesystem; + +using usvfs::UnicodeString; + +// flag definitions below are copied from winternl.h +#define FILE_SUPERSEDE 0x00000000 +#define FILE_OPEN 0x00000001 +#define FILE_CREATE 0x00000002 +#define FILE_OPEN_IF 0x00000003 +#define FILE_OVERWRITE 0x00000004 +#define FILE_OVERWRITE_IF 0x00000005 +#define FILE_MAXIMUM_DISPOSITION 0x00000005 + +template <typename T> +using unique_ptr_deleter = std::unique_ptr<T, void (*)(T*)>; + +class RedirectionInfo +{ +public: + UnicodeString path; + bool redirected; + + RedirectionInfo() {} + RedirectionInfo(UnicodeString path, bool redirected) + : path(path), redirected(redirected) + {} +}; + +class HandleTracker +{ +public: + using handle_type = HANDLE; + using info_type = UnicodeString; + + HandleTracker() { insert_current_directory(); } + + info_type lookup(handle_type handle) const + { + if (valid_handle(handle)) { + std::shared_lock<std::shared_mutex> lock(m_mutex); + auto find = m_map.find(handle); + if (find != m_map.end()) + return find->second; + } + return info_type(); + } + + void insert(handle_type handle, const info_type& info) + { + if (!valid_handle(handle)) + return; + std::unique_lock<std::shared_mutex> lock(m_mutex); + m_map[handle] = info; + } + + void erase(handle_type handle) + { + if (!valid_handle(handle)) + return; + std::unique_lock<std::shared_mutex> lock(m_mutex); + m_map.erase(handle); + } + +private: + static bool valid_handle(handle_type handle) + { + return handle && handle != INVALID_HANDLE_VALUE; + } + + void insert_current_directory() + { + ntdll_declarations_init(); + + UNICODE_STRING p{0}; + RTL_RELATIVE_NAME r{0}; + NTSTATUS status = + RtlDosPathNameToRelativeNtPathName_U_WithStatus(L"x", &p, nullptr, &r); + if (status >= 0) { + if (r.ContainingDirectory && p.Buffer) { + size_t len = p.Length / sizeof(WCHAR); + size_t trim = strlen("\\x") + (p.Buffer[len - 1] ? 0 : 1); + if (len > trim) + insert(r.ContainingDirectory, info_type(p.Buffer, len - trim)); + } + RtlReleaseRelativeName(&r); + if (p.Buffer) + HeapFree(GetProcessHeap(), 0, p.Buffer); + } + } + + mutable std::shared_mutex m_mutex; + std::unordered_map<handle_type, info_type> m_map; +}; + +HandleTracker ntdllHandleTracker; + +UnicodeString CreateUnicodeString(const OBJECT_ATTRIBUTES* objectAttributes) +{ + UnicodeString result = ntdllHandleTracker.lookup(objectAttributes->RootDirectory); + if (objectAttributes->ObjectName != nullptr) { + result.appendPath(objectAttributes->ObjectName); + } + return result; +} + +std::ostream& operator<<(std::ostream& os, const _UNICODE_STRING& str) +{ + try { + // TODO this does not correctly support surrogate pairs + // since the size used here is the number of 16-bit characters in the buffer + // whereas + // toNarrow expects the actual number of characters. + // It will always underestimate though, so worst case scenario we truncate + // the string + os << ush::string_cast<std::string>(str.Buffer, ush::CodePage::UTF8, + str.Length / sizeof(WCHAR)); + } catch (const std::exception& e) { + os << e.what(); + } + + return os; +} + +std::ostream& operator<<(std::ostream& os, POBJECT_ATTRIBUTES attr) +{ + return operator<<(os, *attr->ObjectName); +} + +RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context, + const usvfs::HookCallContext& callContext, + const UnicodeString& inPath) +{ + RedirectionInfo result; + result.path = inPath; + result.redirected = false; + + if (callContext.active()) { + // see if the file exists in the redirection tree + std::string lookupPath = ush::string_cast<std::string>( + static_cast<LPCWSTR>(result.path) + 4, ush::CodePage::UTF8); + auto node = context->redirectionTable()->findNode(lookupPath.c_str()); + // if so, replace the file name with the path to the mapped file + if ((node.get() != nullptr) && + (!node->data().linkTarget.empty() || node->isDirectory())) { + std::wstring reroutePath; + + if (node->data().linkTarget.length() > 0) { + reroutePath = ush::string_cast<std::wstring>(node->data().linkTarget.c_str(), + ush::CodePage::UTF8); + } else { + reroutePath = + ush::string_cast<std::wstring>(node->path().c_str(), ush::CodePage::UTF8); + } + if ((*reroutePath.rbegin() == L'\\') && (*lookupPath.rbegin() != '\\')) { + reroutePath.resize(reroutePath.size() - 1); + } + std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\'); + if (reroutePath[1] == L'\\') + reroutePath[1] = L'?'; + result.path = LR"(\??\)" + reroutePath; + result.redirected = true; + } + } + return result; +} + +RedirectionInfo applyReroute(const usvfs::CreateRerouter& rerouter) +{ + RedirectionInfo result; + result.path = rerouter.fileName(); + result.redirected = rerouter.wasRerouted(); + + std::wstring reroutePath(static_cast<LPCWSTR>(result.path)); + std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\'); + if (reroutePath[1] == L'\\') + reroutePath[1] = L'?'; + if (!((reroutePath[0] == L'\\') && (reroutePath[1] == L'?') && + (reroutePath[2] == L'?') && (reroutePath[3] == L'\\'))) { + result.path = LR"(\??\)" + reroutePath; + } + + return result; +} + +struct FindCreateTarget +{ + usvfs::RedirectionTree::NodePtrT target; + void operator()(usvfs::RedirectionTree::NodePtrT node) + { + if (node->hasFlag(usvfs::shared::FLAG_CREATETARGET)) { + target = node; + } + } +}; + +std::pair<UnicodeString, UnicodeString> +findCreateTarget(const usvfs::HookContext::ConstPtr& context, + const UnicodeString& inPath) +{ + std::pair<UnicodeString, UnicodeString> result; + result.first = inPath; + result.second = UnicodeString(); + + std::string lookupPath = ush::string_cast<std::string>( + static_cast<LPCWSTR>(result.first) + 4, ush::CodePage::UTF8); + FindCreateTarget visitor; + usvfs::RedirectionTree::VisitorFunction visitorWrapper = + [&](const usvfs::RedirectionTree::NodePtrT& node) { + visitor(node); + }; + context->redirectionTable()->visitPath(lookupPath, visitorWrapper); + if (visitor.target.get() != nullptr) { + bfs::path relativePath = + ush::make_relative(visitor.target->path(), bfs::path(lookupPath)); + + bfs::path target(visitor.target->data().linkTarget.c_str()); + target /= relativePath; + + result.second = UnicodeString(target.wstring().c_str()); + winapi::ex::wide::createPath(target.parent_path()); + } + return result; +} + +RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context, + const usvfs::HookCallContext& callContext, + POBJECT_ATTRIBUTES inAttributes) +{ + return applyReroute(context, callContext, CreateUnicodeString(inAttributes)); +} + +int NextDividableBy(int number, int divider) +{ + return static_cast<int>( + ceilf(static_cast<float>(number) / static_cast<float>(divider)) * divider); +} + +// Something is trying to create a variety of files starting with "\Device\", +// such as "\Device\DeviceApi\Dev\Query", "\Device\MMCSS\MmThread", +// "\Device\DeviceApi\CMNotify", etc. +// +// This used to create a bunch of spurious "Device" and "MMCSS" folders when +// using Explorer++. Some of these names seem to part of PnP, others from the +// "Multimedia Class Scheduler Service". +// +// There's basically zero information on this stuff online. +// +// Regardless, these files ended up being rerouted, so they became something +// like C:\game\Data\Somewhere\Device\MMCSS\MmThread, which ended up being +// rerouted to overwrite and created as a fake path +// "overwrite\Somewhere\Device\MMCSS" +// +// These paths are weird, they feel like pipes or something. It's not clear how +// they should be reliably recognized, so this is a hardcoded check for any +// path that starts with "\Device\". These paths will be forwarded directly +// to NtCreateFile/NtOpenFile +// +bool isDeviceFile(std::wstring_view name) +{ + static const std::wstring_view DevicePrefix(L"\\Device\\"); + + // starts with + if (name.size() < DevicePrefix.size()) { + return false; + } else { + return (_wcsnicmp(name.data(), DevicePrefix.data(), DevicePrefix.size()) == 0); + } +} + +NTSTATUS addNtSearchData(HANDLE hdl, PUNICODE_STRING FileName, + const std::wstring& fakeName, + FILE_INFORMATION_CLASS FileInformationClass, PVOID& buffer, + ULONG& bufferSize, std::set<std::wstring>& foundFiles, + HANDLE event, PIO_APC_ROUTINE apcRoutine, PVOID apcContext, + BOOLEAN returnSingleEntry) +{ + NTSTATUS res = STATUS_NO_SUCH_FILE; + if (hdl != INVALID_HANDLE_VALUE) { + PVOID lastValidRecord = nullptr; + PVOID bufferInit = buffer; + IO_STATUS_BLOCK status; + res = NtQueryDirectoryFile(hdl, event, apcRoutine, apcContext, &status, buffer, + bufferSize, FileInformationClass, returnSingleEntry, + FileName, FALSE); + + if ((res != STATUS_SUCCESS) || (status.Information <= 0)) { + bufferSize = 0UL; + } else { + ULONG totalOffset = 0; + PVOID lastSkipPos = nullptr; + + while (totalOffset < status.Information) { + ULONG offset; + std::wstring fileName; + GetFileInformationData(FileInformationClass, buffer, offset, fileName); + // in case this is a single-file search result and the specified + // filename differs from the file name found, replace it in the + // information structure + if ((totalOffset == 0) && (offset == 0) && (fakeName.length() > 0)) { + // if the fake name is larger than what is in the buffer and there is + // not enough room, that's a buffer overflow + if ((fakeName.length() > fileName.length()) && + ((fakeName.length() - fileName.length()) > + (bufferSize - status.Information))) { + res = STATUS_BUFFER_OVERFLOW; + break; + } + // WARNING for the case where the fake name is longer this needs to + // move back all further results and update the offset first + SetFileInformationFileName(FileInformationClass, buffer, fakeName); + fileName = fakeName; + } + bool add = true; + if (fileName.length() > 0) { + auto insertRes = foundFiles.insert(ush::to_upper(fileName)); + add = insertRes.second; // add only if we didn't find this file before + } + if (!add) { + if (lastSkipPos == nullptr) { + lastSkipPos = buffer; + } + } else { + if (lastSkipPos != nullptr) { + memmove(lastSkipPos, buffer, status.Information - totalOffset); + ULONG delta = static_cast<ULONG>(ush::AddrDiff(buffer, lastSkipPos)); + totalOffset -= delta; + + buffer = lastSkipPos; + lastSkipPos = nullptr; + } + lastValidRecord = buffer; + } + + if (offset == 0) { + offset = static_cast<ULONG>(status.Information) - totalOffset; + } + buffer = ush::AddrAdd(buffer, offset); + totalOffset += offset; + } + + if (lastSkipPos != nullptr) { + buffer = lastSkipPos; + bufferSize = static_cast<ULONG>(ush::AddrDiff(buffer, bufferInit)); + // null out the unused rest if there is some + memset(lastSkipPos, 0, status.Information - bufferSize); + } else { + bufferSize = static_cast<ULONG>(ush::AddrDiff(buffer, bufferInit)); + } + } + if (lastValidRecord != nullptr) { + SetFileInformationOffset(FileInformationClass, lastValidRecord, 0); + } + } + return res; +} + +DATA_ID(SearchInfo); + +struct Searches +{ + struct Info + { + struct VirtualMatch + { + // full path to where the file/directory actually is + std::wstring realPath; + // virtual filename (only filename since it has to be within the searched + // directory) + // this is left empty when a folder with all its content is mapped to the + // search directory + std::wstring virtualName; + }; + + Info() : currentSearchHandle(INVALID_HANDLE_VALUE) {} + std::set<std::wstring> foundFiles; + HANDLE currentSearchHandle; + std::queue<VirtualMatch> virtualMatches; + UnicodeString searchPattern; + bool regularComplete{false}; + }; + + Searches() = default; + + // must provide a special copy constructor because boost::mutex is + // non-copyable + Searches(const Searches& reference) : info(reference.info) {} + + Searches& operator=(const Searches&) = delete; + + std::recursive_mutex queryMutex; + + std::map<HANDLE, Info> info; +}; + +void gatherVirtualEntries(const UnicodeString& dirName, + const usvfs::RedirectionTreeContainer& redir, + PUNICODE_STRING FileName, Searches::Info& info) +{ + LPCWSTR dirNameW = static_cast<LPCWSTR>(dirName); + // fix directory name. I'd love to know why microsoft sometimes uses "\??\" vs + // "\\?\" + if ((wcsncmp(dirNameW, LR"(\\?\)", 4) == 0) || + (wcsncmp(dirNameW, LR"(\??\)", 4) == 0)) { + dirNameW += 4; + } + auto node = redir->findNode(boost::filesystem::path(dirNameW)); + if (node.get() != nullptr) { + std::string searchPattern = + FileName != nullptr + ? ush::string_cast<std::string>(FileName->Buffer, ush::CodePage::UTF8) + : "*.*"; + + boost::replace_all(searchPattern, "\"", "."); + + for (const auto& subNode : node->find(searchPattern)) { + if (((subNode->data().linkTarget.length() > 0) || subNode->isDirectory()) && + !subNode->hasFlag(usvfs::shared::FLAG_DUMMY)) { + std::wstring vName = + ush::string_cast<std::wstring>(subNode->name(), ush::CodePage::UTF8); + + Searches::Info::VirtualMatch m; + if (subNode->data().linkTarget.length() > 0) { + m = {ush::string_cast<std::wstring>(subNode->data().linkTarget.c_str(), + ush::CodePage::UTF8), + vName}; + } else { + m = {ush::string_cast<std::wstring>(subNode->path().c_str(), + ush::CodePage::UTF8), + vName}; + } + + info.virtualMatches.push(m); + info.foundFiles.insert(ush::to_upper(vName)); + } + } + } +} + +/** + * @brief insert a virtual entry into the search result + * @param FileInformation + * @param FileInformationClass + * @param info + * @param realPath path were the actual file resides + * @param virtualName virtual file name (without path). will often be the same + * as the name component of realpath + * @param ReturnSingleEntry + * @param dataRead + * @return true if a virtual result was added, false if the search handle in the + * info object yields no more results + */ +bool addVirtualSearchResult(PVOID& FileInformation, + FILE_INFORMATION_CLASS FileInformationClass, + Searches::Info& info, const std::wstring& realPath, + const std::wstring& virtualName, BOOLEAN ReturnSingleEntry, + ULONG& dataRead) +{ + // this opens a search in the real location, then copies the information about + // files we care about (the ones being mapped) to the result we intend to + // return + bfs::path fullPath(realPath); + if (fullPath.filename().wstring() == L".") { + fullPath = fullPath.parent_path(); + } + if (info.currentSearchHandle == INVALID_HANDLE_VALUE) { + std::wstring dirName = fullPath.parent_path().wstring(); + if (dirName.length() >= MAX_PATH && !ush::startswith(dirName.c_str(), LR"(\\?\)")) + dirName = LR"(\\?\)" + dirName; + info.currentSearchHandle = + CreateFileW(dirName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } + std::wstring fileName = + ush::string_cast<std::wstring>(fullPath.filename().string(), ush::CodePage::UTF8); + NTSTATUS subRes = addNtSearchData( + info.currentSearchHandle, + (fileName != L".") ? static_cast<PUNICODE_STRING>(UnicodeString(fileName.c_str())) + : nullptr, + virtualName, FileInformationClass, FileInformation, dataRead, info.foundFiles, + nullptr, nullptr, nullptr, ReturnSingleEntry); + if (subRes == STATUS_SUCCESS) { + return true; + } else { + // STATUS_NO_MORE_FILES merely means the search ended, everything else is an + // error message. Either way, the search here is finished and we should + // resume in the next mapped directory + if (subRes != STATUS_NO_MORE_FILES) { + spdlog::get("hooks")->warn("error reported listing files in {0}: {1:x}", + fullPath.string(), static_cast<uint32_t>(subRes)); + } + return false; + } +} + +NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFile( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, BOOLEAN RestartScan) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + // this is quite messy... + // first, this will gather the virtual locations mapping to the iterated one + // then we return results from the real location, skipping those that exist + // in the virtual locations, as those take precedence + // finally the virtual results are returned, adding each result to a skip + // list, so they don't get added twice + // + // if we don't add the regular files first, "." and ".." wouldn't be in the + // first search result of wildcard searches which may confuse the caller + NTSTATUS res = STATUS_NO_MORE_FILES; + HOOK_START_GROUP(MutExHookGroup::FIND_FILES) + if (!callContext.active()) { + return ::NtQueryDirectoryFile( + FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, FileInformation, + Length, FileInformationClass, ReturnSingleEntry, FileName, RestartScan); + } + + // std::unique_lock<std::recursive_mutex> queryLock; + std::map<HANDLE, Searches::Info>::iterator infoIter; + bool firstSearch = false; + + { // scope to limit context lifetime + HookContext::ConstPtr context = READ_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // queryLock = std::unique_lock<std::recursive_mutex>(activeSearches.queryMutex); + + if (RestartScan) { + auto iter = activeSearches.info.find(FileHandle); + if (iter != activeSearches.info.end()) { + activeSearches.info.erase(iter); + } + } + + // see if we already have a running search + infoIter = activeSearches.info.find(FileHandle); + firstSearch = (infoIter == activeSearches.info.end()); + } + + if (firstSearch) { + HookContext::Ptr context = WRITE_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // tradeoff time: we store this search status even if no virtual results + // were found. This causes a little extra cost here and in NtClose every + // time a non-virtual dir is being searched. However if we don't, + // whenever NtQueryDirectoryFile is called another time on the same handle, + // this (expensive) block would be run again. + infoIter = + activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first; + infoIter->second.searchPattern.appendPath(FileName); + + SearchHandleMap& searchMap = context->customData<SearchHandleMap>(SearchHandles); + SearchHandleMap::iterator iter = searchMap.find(FileHandle); + + UnicodeString searchPath; + if (iter != searchMap.end()) { + searchPath = UnicodeString(iter->second.c_str()); + infoIter->second.currentSearchHandle = CreateFileW( + iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + searchPath = ntdllHandleTracker.lookup(FileHandle); + } + gatherVirtualEntries(searchPath, context->redirectionTable(), FileName, + infoIter->second); + } + + ULONG dataRead = Length; + PVOID FileInformationCurrent = FileInformation; + + // add regular search results, skipping those files we have in a virtual + // location + bool moreRegular = !infoIter->second.regularComplete; + bool dataReturned = false; + while (moreRegular && !dataReturned) { + dataRead = Length; + + HANDLE handle = infoIter->second.currentSearchHandle; + if (handle == INVALID_HANDLE_VALUE) { + handle = FileHandle; + } + NTSTATUS subRes = addNtSearchData( + handle, FileName, L"", FileInformationClass, FileInformationCurrent, dataRead, + infoIter->second.foundFiles, Event, ApcRoutine, ApcContext, ReturnSingleEntry); + moreRegular = subRes == STATUS_SUCCESS; + if (moreRegular) { + dataReturned = dataRead != 0; + } else { + infoIter->second.regularComplete = true; + infoIter->second.foundFiles.clear(); + if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + if (!moreRegular) { + // add virtual results + while (!dataReturned && infoIter->second.virtualMatches.size() > 0) { + auto match = infoIter->second.virtualMatches.front(); + if (match.realPath.size() != 0) { + dataRead = Length; + if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass, + infoIter->second, match.realPath, match.virtualName, + ReturnSingleEntry, dataRead)) { + // a positive result here means the call returned data and there may + // be further objects to be retrieved by repeating the call + dataReturned = true; + } else { + // proceed to next search handle + + // TODO: doesn't append search results from more than one redirection + // per call. This is bad for performance but otherwise we'd need to + // re-write the offsets between information objects + infoIter->second.virtualMatches.pop(); + CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + } + + if (!dataReturned) { + if (firstSearch) { + res = STATUS_NO_SUCH_FILE; + } else { + res = STATUS_NO_MORE_FILES; + } + } else { + res = STATUS_SUCCESS; + } + IoStatusBlock->Status = res; + IoStatusBlock->Information = dataRead; + + size_t numVirtualFiles = infoIter->second.virtualMatches.size(); + if ((numVirtualFiles > 0)) { + LOG_CALL() + .addParam("path", ntdllHandleTracker.lookup(FileHandle)) + .PARAM(FileInformationClass) + .PARAM(FileName) + .PARAM(numVirtualFiles) + .PARAMWRAP(res); + } + + HOOK_END + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFileEx( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags, + PUNICODE_STRING FileName) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + NTSTATUS res = STATUS_NO_MORE_FILES; + + // this is quite messy... + // first, this will gather the virtual locations mapping to the iterated one + // then we return results from the real location, skipping those that exist + // in the virtual locations, as those take precedence + // finally the virtual results are returned, adding each result to a skip + // list, so they don't get added twice + // + // if we don't add the regular files first, "." and ".." wouldn't be in the + // first search result of wildcard searches which may confuse the caller + HOOK_START_GROUP(MutExHookGroup::FIND_FILES) + if (!callContext.active()) { + return ::NtQueryDirectoryFileEx(FileHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, FileInformation, Length, + FileInformationClass, QueryFlags, FileName); + } + + // std::unique_lock<std::recursive_mutex> queryLock; + std::map<HANDLE, Searches::Info>::iterator infoIter; + bool firstSearch = false; + + { // scope to limit context lifetime + HookContext::ConstPtr context = READ_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // queryLock = std::unique_lock<std::recursive_mutex>(activeSearches.queryMutex); + + if (QueryFlags & SL_RESTART_SCAN) { + auto iter = activeSearches.info.find(FileHandle); + if (iter != activeSearches.info.end()) { + activeSearches.info.erase(iter); + } + } + + // see if we already have a running search + infoIter = activeSearches.info.find(FileHandle); + firstSearch = (infoIter == activeSearches.info.end()); + } + + if (firstSearch) { + HookContext::Ptr context = WRITE_CONTEXT(); + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // tradeoff time: we store this search status even if no virtual results + // were found. This causes a little extra cost here and in NtClose every + // time a non-virtual dir is being searched. However if we don't, + // whenever NtQueryDirectoryFile is called another time on the same handle, + // this (expensive) block would be run again. + infoIter = + activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first; + infoIter->second.searchPattern.appendPath(FileName); + + SearchHandleMap& searchMap = context->customData<SearchHandleMap>(SearchHandles); + SearchHandleMap::iterator iter = searchMap.find(FileHandle); + + UnicodeString searchPath; + if (iter != searchMap.end()) { + searchPath = UnicodeString(iter->second.c_str()); + infoIter->second.currentSearchHandle = CreateFileW( + iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + searchPath = ntdllHandleTracker.lookup(FileHandle); + } + gatherVirtualEntries(searchPath, context->redirectionTable(), FileName, + infoIter->second); + } + + ULONG dataRead = Length; + PVOID FileInformationCurrent = FileInformation; + + // add regular search results, skipping those files we have in a virtual + // location + bool moreRegular = !infoIter->second.regularComplete; + bool dataReturned = false; + while (moreRegular && !dataReturned) { + dataRead = Length; + + HANDLE handle = infoIter->second.currentSearchHandle; + if (handle == INVALID_HANDLE_VALUE) { + handle = FileHandle; + } + NTSTATUS subRes = addNtSearchData(handle, FileName, L"", FileInformationClass, + FileInformationCurrent, dataRead, + infoIter->second.foundFiles, Event, ApcRoutine, + ApcContext, QueryFlags & SL_RETURN_SINGLE_ENTRY); + moreRegular = subRes == STATUS_SUCCESS; + if (moreRegular) { + dataReturned = dataRead != 0; + } else { + infoIter->second.regularComplete = true; + infoIter->second.foundFiles.clear(); + if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + if (!moreRegular) { + // add virtual results + while (!dataReturned && infoIter->second.virtualMatches.size() > 0) { + auto match = infoIter->second.virtualMatches.front(); + if (match.realPath.size() != 0) { + dataRead = Length; + if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass, + infoIter->second, match.realPath, match.virtualName, + QueryFlags & SL_RETURN_SINGLE_ENTRY, dataRead)) { + // a positive result here means the call returned data and there may + // be further objects to be retrieved by repeating the call + dataReturned = true; + } else { + // proceed to next search handle + + // TODO: doesn't append search results from more than one redirection + // per call. This is bad for performance but otherwise we'd need to + // re-write the offsets between information objects + infoIter->second.virtualMatches.pop(); + CloseHandle(infoIter->second.currentSearchHandle); + infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE; + } + } + } + } + + if (!dataReturned) { + if (firstSearch) { + res = STATUS_NO_SUCH_FILE; + } else { + res = STATUS_NO_MORE_FILES; + } + } else { + res = STATUS_SUCCESS; + } + IoStatusBlock->Status = res; + IoStatusBlock->Information = dataRead; + + size_t numVirtualFiles = infoIter->second.virtualMatches.size(); + if ((numVirtualFiles > 0)) { + LOG_CALL() + .addParam("path", ntdllHandleTracker.lookup(FileHandle)) + .PARAM(FileInformationClass) + .PARAM(FileName) + .PARAM(QueryFlags) + .PARAM(numVirtualFiles) + .PARAMWRAP(res); + } + + HOOK_END + return res; +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryObject( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active()) { + return ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation, + ObjectInformationLength, ReturnLength); + } + + PRE_REALCALL + res = ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation, + ObjectInformationLength, ReturnLength); + POST_REALCALL + + // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be + // smaller than the original one + // + // we only handle STATUS_INFO_LENGTH_MISMATCH if ReturnLength is not NULL since + // this is only returned if the length is too small to hold the structure itself + // (regardless of the name), in which case, we need to compute our own ReturnLength + // + if ((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW || + (res == STATUS_INFO_LENGTH_MISMATCH && ReturnLength)) && + (ObjectInformationClass == ObjectNameInformation)) { + const auto trackerInfo = ntdllHandleTracker.lookup(Handle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + + OBJECT_NAME_INFORMATION* info = + reinterpret_cast<OBJECT_NAME_INFORMATION*>(ObjectInformation); + + if (redir.redirected) { + // https://learn.microsoft.com/en-us/windows/win32/fileio/displaying-volume-paths + // + + // TODO: is that always true? + // path should start with \??\X: - we need to replace this by device name + // + WCHAR deviceName[MAX_PATH]; + std::wstring buffer(static_cast<LPCWSTR>(trackerInfo)); + buffer[6] = L'\0'; + + QueryDosDeviceW(buffer.data() + 4, deviceName, ARRAYSIZE(deviceName)); + + buffer = std::wstring(deviceName) + L'\\' + + std::wstring(buffer.data() + 7, buffer.size() - 7); + + // the name is put in the buffer AFTER the struct, so the required size if + // sizeof(OBJECT_NAME_INFORMATION) + the number of bytes for the name + 2 bytes + // for a wide null character + const auto requiredLength = + sizeof(OBJECT_NAME_INFORMATION) + buffer.size() * 2 + 2; + if (ObjectInformationLength < requiredLength) { + // if the status was info length mismatch, we keep it, we are just going to + // update *ReturnLength + if (res != STATUS_INFO_LENGTH_MISMATCH) { + res = STATUS_BUFFER_OVERFLOW; + } + + if (ReturnLength) { + *ReturnLength = static_cast<ULONG>(requiredLength); + } + } else { + // put the unicode buffer at the end of the object + const USHORT unicodeBufferLength = static_cast<USHORT>(std::min( + static_cast<unsigned long long>(std::numeric_limits<USHORT>::max()), + static_cast<unsigned long long>(ObjectInformationLength - + sizeof(OBJECT_NAME_INFORMATION)))); + LPWSTR unicodeBuffer = reinterpret_cast<LPWSTR>( + static_cast<LPSTR>(ObjectInformation) + sizeof(OBJECT_NAME_INFORMATION)); + + // copy the path into the buffer + wmemcpy_s(unicodeBuffer, unicodeBufferLength, buffer.data(), buffer.size()); + + // set the null character + unicodeBuffer[buffer.size()] = L'\0'; + + // update the actual unicode string + info->Name.Buffer = unicodeBuffer; + info->Name.Length = static_cast<USHORT>(buffer.size() * 2); + info->Name.MaximumLength = unicodeBufferLength; + + res = STATUS_SUCCESS; + } + } + + auto logger = LOG_CALL() + .PARAMWRAP(res) + .PARAM(ObjectInformationLength) + .addParam("return_length", ReturnLength ? *ReturnLength : -1) + .addParam("tracker_path", trackerInfo) + .PARAM(ObjectInformationClass) + .PARAM(redir.redirected) + .PARAM(redir.path); + + if (res == STATUS_SUCCESS) { + logger.addParam("name_info", info->Name); + } else { + logger.addParam("name_info", ""); + } + } + + HOOK_END + return res; +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + if (!callContext.active()) { + return ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + } + + PRE_REALCALL + res = ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + POST_REALCALL + + // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be + // smaller than the original one + // + // we do not handle STATUS_INFO_LENGTH_MISMATCH because this is only returned if + // the length is too small to old the structure itself (regardless of the name) + // + // TODO: currently, this does not handle STATUS_BUFFER_OVERLOW for ALL information + // because most of the structures would need to be manually filled, which is very + // complicated - this should not pose huge issue + // + if (((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW) && + (FileInformationClass == FileNameInformation || + FileInformationClass == FileNormalizedNameInformation)) || + (res == STATUS_SUCCESS && FileInformationClass == FileAllInformation)) { + + const auto trackerInfo = ntdllHandleTracker.lookup(FileHandle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + + // TODO: difference between FileNameInformation and FileNormalizedNameInformation + + // maximum length in bytes - the required length is + // - for ALL, 100 + the number of bytes in the name (not account for null character) + // - for NAME, 4 + the number of bytes in the name (not accounting for null + // character) + // + // it is close to the sizeof the structure + the number of bytes in the name, except + // for the alignment that gives us a bit more space + // + ULONG prefixStructLength; + FILE_NAME_INFORMATION* info; + if (FileInformationClass == FileAllInformation) { + info = &reinterpret_cast<FILE_ALL_INFORMATION*>(FileInformation)->NameInformation; + prefixStructLength = sizeof(FILE_ALL_INFORMATION) - 4; + } else { + info = reinterpret_cast<FILE_NAME_INFORMATION*>(FileInformation); + prefixStructLength = sizeof(FILE_NAME_INFORMATION) - 4; + } + + if (redir.redirected) { + auto requiredLength = prefixStructLength + 2 * (trackerInfo.size() - 6); + if (Length < requiredLength) { + res = STATUS_BUFFER_OVERFLOW; + } else { + // strip the \??\X: prefix (X being the drive name) + LPCWSTR filenameFixed = static_cast<LPCWSTR>(trackerInfo) + 6; + + // not using SetInfoFilename because the length is not set and we do not need to + // 0-out the memory here + info->FileNameLength = static_cast<ULONG>((trackerInfo.size() - 6) * 2); + wmemcpy(info->FileName, filenameFixed, trackerInfo.size() - 6); + res = STATUS_SUCCESS; + + // update status block, Information is the number of bytes written, basically + // the required length + IoStatusBlock->Status = STATUS_SUCCESS; + IoStatusBlock->Information = static_cast<ULONG_PTR>(requiredLength); + } + } + + LOG_CALL() + .PARAMWRAP(res) + .addParam("tracker_path", trackerInfo) + .PARAM(FileInformationClass) + .PARAM(redir.redirected) + .PARAM(redir.path) + .addParam("name_info", res == STATUS_SUCCESS + ? std::wstring{info->FileName, + info->FileNameLength / sizeof(WCHAR)} + : std::wstring{}); + } + + HOOK_END + return res; +} + +unique_ptr_deleter<OBJECT_ATTRIBUTES> +makeObjectAttributes(RedirectionInfo& redirInfo, POBJECT_ATTRIBUTES attributeTemplate) +{ + if (redirInfo.redirected) { + unique_ptr_deleter<OBJECT_ATTRIBUTES> result(new OBJECT_ATTRIBUTES, + [](OBJECT_ATTRIBUTES* ptr) { + delete ptr; + }); + memcpy(result.get(), attributeTemplate, sizeof(OBJECT_ATTRIBUTES)); + result->RootDirectory = nullptr; + result->ObjectName = static_cast<PUNICODE_STRING>(redirInfo.path); + return result; + } else { + // just reuse the template with a dummy deleter + return unique_ptr_deleter<OBJECT_ATTRIBUTES>(attributeTemplate, + [](OBJECT_ATTRIBUTES*) {}); + } +} + +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationByName( + POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass) +{ + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + + if (!callContext.active()) { + res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation, + Length, FileInformationClass); + callContext.updateLastError(); + return res; + } + + RedirectionInfo redir = + applyReroute(READ_CONTEXT(), callContext, CreateUnicodeString(ObjectAttributes)); + + if (redir.redirected) { + auto newObjectAttributes = makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryInformationByName(newObjectAttributes.get(), IoStatusBlock, + FileInformation, Length, FileInformationClass); + POST_REALCALL + + LOG_CALL() + .PARAMWRAP(res) + .addParam("input_path", *ObjectAttributes->ObjectName) + .addParam("reroute_path", redir.path); + } else { + PRE_REALCALL + res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation, + Length, FileInformationClass); + POST_REALCALL + } + + HOOK_END + return res; +} + +NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, ULONG ShareAccess, + ULONG OpenOptions) +{ + using namespace usvfs; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + // Why is the usual if (!callContext.active()... check missing? + + bool storePath = false; + if (((OpenOptions & FILE_DIRECTORY_FILE) != 0UL) && + ((OpenOptions & FILE_OPEN_FOR_BACKUP_INTENT) != 0UL)) { + // this may be an attempt to open a directory handle for iterating. + // If so we need to treat it a little bit differently + /* usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::FILE_ATTRIBUTES); + FILE_BASIC_INFORMATION dummy; + storePath = FAILED(NtQueryAttributesFile(ObjectAttributes, &dummy));*/ + storePath = true; + } + + UnicodeString fullName = CreateUnicodeString(ObjectAttributes); + + if (isDeviceFile(static_cast<LPCWSTR>(fullName))) { + return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + } + + UnicodeString Path = ntdllHandleTracker.lookup(ObjectAttributes->RootDirectory); + + std::wstring checkpath = + ush::string_cast<std::wstring>(static_cast<LPCWSTR>(Path), ush::CodePage::UTF8); + + if ((fullName.size() == 0) || + (GetFileSize(ObjectAttributes->RootDirectory, nullptr) != INVALID_FILE_SIZE)) { + // //relative paths that we don't have permission over will fail here due that we + // can't get the filesize of the root directory + // //We should try again to see if it is a directory using another method + if ((fullName.size() == 0) || + (GetFileAttributesW((LPCWSTR)checkpath.c_str()) == INVALID_FILE_ATTRIBUTES)) { + return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + } + } + + try { + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, fullName); + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtOpenFile(FileHandle, DesiredAccess, adjustedAttributes.get(), + IoStatusBlock, ShareAccess, OpenOptions); + POST_REALCALL + if (SUCCEEDED(res) && storePath) { + // store the original search path for use during iteration + READ_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[*FileHandle] = + static_cast<LPCWSTR>(fullName); +#pragma message("need to clean up this handle in CloseHandle call") + } + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAM(*FileHandle) + .PARAM(OpenOptions) + .PARAMWRAP(res); + } + } catch (const std::exception&) { + PRE_REALCALL + res = ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + ShareAccess, OpenOptions); + POST_REALCALL + } + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, ULONG OpenOptions) +{ + NTSTATUS res = ntdll_mess_NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, + IoStatusBlock, ShareAccess, OpenOptions); + if (res >= 0 && ObjectAttributes && FileHandle && + GetFileType(*FileHandle) == FILE_TYPE_DISK) + ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes)); + return res; +} + +NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, ULONG FileAttributes, + ULONG ShareAccess, ULONG CreateDisposition, + ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength) +{ + using namespace usvfs; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) + if (!callContext.active()) { + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + UnicodeString inPath = CreateUnicodeString(ObjectAttributes); + LPCWSTR inPathW = static_cast<LPCWSTR>(inPath); + + if (inPath.size() == 0) { + spdlog::get("hooks")->info( + "failed to set from handle: {0}", + ush::string_cast<std::string>(ObjectAttributes->ObjectName->Buffer)); + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + if (isDeviceFile(inPathW)) { + return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + } + + DWORD convertedDisposition = OPEN_EXISTING; + switch (CreateDisposition) { + case FILE_SUPERSEDE: + convertedDisposition = CREATE_ALWAYS; + break; + case FILE_OPEN: + convertedDisposition = OPEN_EXISTING; + break; + case FILE_CREATE: + convertedDisposition = CREATE_NEW; + break; + case FILE_OPEN_IF: + convertedDisposition = OPEN_ALWAYS; + break; + case FILE_OVERWRITE: + convertedDisposition = TRUNCATE_EXISTING; + break; + case FILE_OVERWRITE_IF: + convertedDisposition = CREATE_ALWAYS; + break; + default: + spdlog::get("hooks")->error("invalid disposition: {0}", CreateDisposition); + break; + } + + DWORD convertedAccess = 0; + if ((DesiredAccess & FILE_GENERIC_READ) == FILE_GENERIC_READ) + convertedAccess |= GENERIC_READ; + if ((DesiredAccess & FILE_GENERIC_WRITE) == FILE_GENERIC_WRITE) + convertedAccess |= GENERIC_WRITE; + if ((DesiredAccess & FILE_GENERIC_EXECUTE) == FILE_GENERIC_EXECUTE) + convertedAccess |= GENERIC_EXECUTE; + if ((DesiredAccess & FILE_ALL_ACCESS) == FILE_ALL_ACCESS) + convertedAccess |= GENERIC_ALL; + + ULONG originalDisposition = CreateDisposition; + CreateRerouter rerouter; + if (rerouter.rerouteCreate( + READ_CONTEXT(), callContext, inPathW, convertedDisposition, convertedAccess, + (LPSECURITY_ATTRIBUTES)ObjectAttributes->SecurityDescriptor)) { + switch (convertedDisposition) { + case CREATE_NEW: + CreateDisposition = FILE_CREATE; + break; + case CREATE_ALWAYS: + if (CreateDisposition != FILE_SUPERSEDE) + CreateDisposition = FILE_OVERWRITE_IF; + break; + case OPEN_EXISTING: + CreateDisposition = FILE_OPEN; + break; + case OPEN_ALWAYS: + CreateDisposition = FILE_OPEN_IF; + break; + case TRUNCATE_EXISTING: + CreateDisposition = FILE_OVERWRITE; + break; + } + + RedirectionInfo redir = applyReroute(rerouter); + + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtCreateFile(FileHandle, DesiredAccess, adjustedAttributes.get(), + IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, + CreateDisposition, CreateOptions, EaBuffer, EaLength); + POST_REALCALL + rerouter.updateResult(callContext, res == STATUS_SUCCESS); + + if (res == STATUS_SUCCESS) { + if (rerouter.newReroute()) + rerouter.insertMapping(WRITE_CONTEXT()); + + if (rerouter.isDir() && rerouter.wasRerouted() && + ((FileAttributes & FILE_OPEN_FOR_BACKUP_INTENT) == + FILE_OPEN_FOR_BACKUP_INTENT)) { + // store the original search path for use during iteration + WRITE_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[*FileHandle] = + inPathW; + } + } + + if (rerouter.wasRerouted() || rerouter.changedError() || + originalDisposition != CreateDisposition) { + LOG_CALL() + .PARAM(inPathW) + .PARAM(rerouter.fileName()) + .PARAMHEX(DesiredAccess) + .PARAMHEX(originalDisposition) + .PARAMHEX(CreateDisposition) + .PARAMHEX(FileAttributes) + .PARAMHEX(res) + .PARAMHEX(*FileHandle) + .PARAM(rerouter.originalError()) + .PARAM(rerouter.error()); + } + } else { + // make the original call to set up the proper errors and return statuses + PRE_REALCALL + res = ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, CreateDisposition, + CreateOptions, EaBuffer, EaLength); + POST_REALCALL + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, + ULONG FileAttributes, ULONG ShareAccess, + ULONG CreateDisposition, ULONG CreateOptions, + PVOID EaBuffer, ULONG EaLength) +{ + NTSTATUS res = ntdll_mess_NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, + IoStatusBlock, AllocationSize, FileAttributes, + ShareAccess, CreateDisposition, CreateOptions, + EaBuffer, EaLength); + if (res >= 0 && ObjectAttributes && FileHandle && + GetFileType(*FileHandle) == FILE_TYPE_DISK) + ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes)); + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtClose(HANDLE Handle) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_NO_SUCH_FILE; + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + bool log = false; + + if ((::GetFileType(Handle) == FILE_TYPE_DISK)) { + HookContext::Ptr context = WRITE_CONTEXT(); + + { // clean up search data associated with this handle part 1 + Searches& activeSearches = context->customData<Searches>(SearchInfo); + // std::lock_guard<std::recursive_mutex> lock(activeSearches.queryMutex); + auto iter = activeSearches.info.find(Handle); + if (iter != activeSearches.info.end()) { + if (iter->second.currentSearchHandle != INVALID_HANDLE_VALUE) { + ::CloseHandle(iter->second.currentSearchHandle); + } + + activeSearches.info.erase(iter); + log = true; + } + } + + { + SearchHandleMap& searchHandles = + context->customData<SearchHandleMap>(SearchHandles); + auto iter = searchHandles.find(Handle); + if (iter != searchHandles.end()) { + searchHandles.erase(iter); + log = true; + } + } + } + + if (GetFileType(Handle) == FILE_TYPE_DISK) + ntdllHandleTracker.erase(Handle); + + PRE_REALCALL + res = ::NtClose(Handle); + POST_REALCALL + + if (log) { + LOG_CALL().PARAM(Handle).PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + // Why is the usual if (!callContext.active()... check missing? + + UnicodeString inPath = CreateUnicodeString(ObjectAttributes); + + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath); + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryAttributesFile(adjustedAttributes.get(), FileInformation); + POST_REALCALL + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtQueryFullAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_NETWORK_OPEN_INFORMATION FileInformation) +{ + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES) + + if (!callContext.active()) { + return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation); + } + + UnicodeString inPath; + try { + inPath = CreateUnicodeString(ObjectAttributes); + } catch (const std::exception&) { + return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation); + } + + RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath); + unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes = + makeObjectAttributes(redir, ObjectAttributes); + + PRE_REALCALL + res = ::NtQueryFullAttributesFile(adjustedAttributes.get(), FileInformation); + POST_REALCALL + + if (redir.redirected) { + LOG_CALL() + .addParam("source", ObjectAttributes) + .addParam("rerouted", adjustedAttributes.get()) + .PARAMWRAP(res); + } + + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtTerminateProcess(HANDLE ProcessHandle, + NTSTATUS ExitStatus) +{ + // this hook is normally called when terminating another process, in which + // case there's nothing to do + // + // if the current process exits normally, the ExitProcess() hook is called + // and disconnects from the vfs; if the current process crashes, this hook + // is not called, the process just dies + // + // but a process can also terminate itself, bypassing ExitProcess(), and + // ending up here, in which case the vfs should be disconnected + // + // NtTerminateProcess() can be called two different ways to terminate the + // current process: + // - with a valid handle for the current process, or + // - with -1, because that's what GetCurrentProcess() returns + // + // it's unclear what a NULL handle represents, the behaviour is not + // documented anywhere, but looking at ReactOS, it's also interpreted as the + // current process + + NTSTATUS res = STATUS_SUCCESS; + + HOOK_START + + const bool isCurrentProcess = ProcessHandle == (HANDLE)-1 || ProcessHandle == 0 || + GetProcessId(ProcessHandle) == GetCurrentProcessId(); + + if (isCurrentProcess) { + usvfsDisconnectVFS(); + } + + res = ::NtTerminateProcess(ProcessHandle, ExitStatus); + + HOOK_END + + return res; +} diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.h b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h new file mode 100644 index 0000000..f3db4a6 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h @@ -0,0 +1,57 @@ +#pragma once + +#include <dllimport.h> +#include <loghelpers.h> +#include <ntdll_declarations.h> + +namespace usvfs +{ + +DLLEXPORT NTSTATUS WINAPI +hook_NtQueryFullAttributesFile(POBJECT_ATTRIBUTES ObjectAttributes, + PFILE_NETWORK_OPEN_INFORMATION FileInformation); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFile( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, BOOLEAN RestartScan); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFileEx( + HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags, + PUNICODE_STRING FileName); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryObject( + HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass, + PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationFile( + HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, + ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationByName( + POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); + +DLLEXPORT NTSTATUS WINAPI hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, ULONG OpenOptions); + +DLLEXPORT NTSTATUS WINAPI hook_NtCreateFile( + PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, + ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, + ULONG EaLength); + +DLLEXPORT NTSTATUS WINAPI hook_NtClose(HANDLE Handle); + +DLLEXPORT NTSTATUS WINAPI hook_NtTerminateProcess(HANDLE ProcessHandle, + NTSTATUS ExitStatus); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/hooks/sharedids.h b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h new file mode 100644 index 0000000..9a7531c --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../hookcontext.h" + +typedef std::map<HANDLE, std::wstring> SearchHandleMap; + +// maps handles opened for searching to the original search path, which is +// necessary if the handle creation was rerouted +DATA_ID(SearchHandles); diff --git a/libs/usvfs/src/usvfs_dll/maptracker.h b/libs/usvfs/src/usvfs_dll/maptracker.h new file mode 100644 index 0000000..6ddf471 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/maptracker.h @@ -0,0 +1,691 @@ +#pragma once + +#include "hookcallcontext.h" +#include "hookcontext.h" +#include "stringcast.h" + +namespace usvfs +{ + +// returns true iff the path exists (checks only real paths) +static inline bool pathExists(LPCWSTR fileName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(fileName); + return attrib != INVALID_FILE_ATTRIBUTES; +} + +// returns true iff the path exists and is a file (checks only real paths) +static inline bool pathIsFile(LPCWSTR fileName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(fileName); + return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +// returns true iff the path exists and is a file (checks only real paths) +static inline bool pathIsDirectory(LPCWSTR fileName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(fileName); + return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY); +} + +// returns true iff the path does not exist but it parent directory does (checks only +// real paths) +static inline bool pathDirectlyAvailable(LPCWSTR pathName) +{ + FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES); + DWORD attrib = GetFileAttributesW(pathName); + return attrib == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_FILE_NOT_FOUND; +} + +class MapTracker +{ +public: + std::wstring lookup(const std::wstring& fromPath) const + { + if (!fromPath.empty()) { + std::shared_lock<std::shared_mutex> lock(m_mutex); + auto find = m_map.find(fromPath); + if (find != m_map.end()) + return find->second; + } + return std::wstring(); + } + + bool contains(const std::wstring& fromPath) const + { + if (!fromPath.empty()) { + std::shared_lock<std::shared_mutex> lock(m_mutex); + auto find = m_map.find(fromPath); + if (find != m_map.end()) + return true; + } + return false; + } + + void insert(const std::wstring& fromPath, const std::wstring& toPath) + { + if (fromPath.empty()) + return; + std::unique_lock<std::shared_mutex> lock(m_mutex); + m_map[fromPath] = toPath; + } + + bool erase(const std::wstring& fromPath) + { + if (fromPath.empty()) + return false; + std::unique_lock<std::shared_mutex> lock(m_mutex); + return m_map.erase(fromPath); + } + +private: + mutable std::shared_mutex m_mutex; + std::unordered_map<std::wstring, std::wstring> m_map; +}; + +extern MapTracker k32DeleteTracker; +extern MapTracker k32FakeDirTracker; + +class RerouteW +{ + std::wstring m_Buffer{}; + std::wstring m_RealPath{}; + bool m_Rerouted{false}; + LPCWSTR m_FileName{nullptr}; + bool m_PathCreated{false}; + bool m_NewReroute{false}; + + RedirectionTree::NodePtrT m_FileNode; + +public: + RerouteW() = default; + + RerouteW(RerouteW&& reference) + : m_Buffer(std::move(reference.m_Buffer)), + m_RealPath(std::move(reference.m_RealPath)), m_Rerouted(reference.m_Rerouted), + m_PathCreated(reference.m_PathCreated), m_NewReroute(reference.m_NewReroute), + m_FileNode(std::move(reference.m_FileNode)) + { + m_FileName = reference.m_FileName != nullptr ? m_Buffer.c_str() : nullptr; + reference.m_FileName = nullptr; + } + + RerouteW& operator=(RerouteW&& reference) + { + m_Buffer = std::move(reference.m_Buffer); + m_RealPath = std::move(reference.m_RealPath); + m_Rerouted = reference.m_Rerouted; + m_PathCreated = reference.m_PathCreated; + m_NewReroute = reference.m_NewReroute; + m_FileName = reference.m_FileName != nullptr ? m_Buffer.c_str() : nullptr; + m_FileNode = std::move(reference.m_FileNode); + return *this; + } + + RerouteW(const RerouteW& reference) = delete; + RerouteW& operator=(const RerouteW&) = delete; + + LPCWSTR fileName() const { return m_FileName; } + + const std::wstring& buffer() const { return m_Buffer; } + + bool wasRerouted() const { return m_Rerouted; } + + bool newReroute() const { return m_NewReroute; } + + void insertMapping(const HookContext::Ptr& context, bool directory = false) + { + if (directory) { + addDirectoryMapping(context, m_RealPath, m_FileName); + + // In case we have just created a "fake" directory, it is no longer fake and need + // to remove it and all its parent folders from the fake map: + std::wstring dir = m_FileName; + while (k32FakeDirTracker.erase(dir)) + dir = fs::path(dir).parent_path().wstring(); + } else { + // if (m_PathCreated) + // addDirectoryMapping(context, fs::path(m_RealPath).parent_path(), + // fs::path(m_FileName).parent_path()); + + spdlog::get("hooks")->info( + "mapping file in vfs: {}, {}", + shared::string_cast<std::string>(m_RealPath, shared::CodePage::UTF8), + shared::string_cast<std::string>(m_FileName, shared::CodePage::UTF8)); + m_FileNode = context->redirectionTable().addFile( + m_RealPath, RedirectionDataLocal(shared::string_cast<std::string>( + m_FileName, shared::CodePage::UTF8))); + + k32DeleteTracker.erase(m_RealPath); + } + } + + void removeMapping(const HookContext::ConstPtr& readContext, bool directory = false) + { + bool addToDelete = false; + bool dontAddToDelete = false; + + // We need to track deleted files even if they were not rerouted (i.e. files deleted + // from the real folder which there is a virtualized mapped folder on top of it). + // Since we don't want to add, *every* file which is deleted we check this: + bool found = wasRerouted(); + if (!found) { + FindCreateTarget visitor; + RedirectionTree::VisitorFunction visitorWrapper = + [&](const RedirectionTree::NodePtrT& node) { + visitor(node); + }; + readContext->redirectionTable()->visitPath(m_RealPath, visitorWrapper); + if (visitor.target.get()) + found = true; + } + if (found) + addToDelete = true; + + if (wasRerouted()) { + if (m_FileNode.get()) + m_FileNode->removeFromTree(); + else + spdlog::get("usvfs")->warn("Node not removed: {}", + shared::string_cast<std::string>(m_FileName)); + + if (!directory) { + // check if this file was the last file inside a "fake" directory then remove it + // and possibly also its fake empty parent folders: + std::wstring parent = m_FileName; + while (true) { + parent = fs::path(parent).parent_path().wstring(); + if (k32FakeDirTracker.contains(parent)) { + dontAddToDelete = true; + if (RemoveDirectoryW(parent.c_str())) { + k32FakeDirTracker.erase(parent); + spdlog::get("usvfs")->info("removed empty fake directory: {}", + shared::string_cast<std::string>(parent)); + } else if (GetLastError() != ERROR_DIR_NOT_EMPTY) { + auto error = GetLastError(); + spdlog::get("usvfs")->warn("removing fake directory failed: {}, error={}", + shared::string_cast<std::string>(parent), + error); + break; + } + } else + break; + } + } + } + if (addToDelete && !dontAddToDelete) { + k32DeleteTracker.insert(m_RealPath, m_FileName); + } + } + + static bool createFakePath(fs::path path, LPSECURITY_ATTRIBUTES securityAttributes) + { + // sanity and guaranteed recursion end: + if (!path.has_relative_path()) + throw shared::windows_error( + "createFakePath() refusing to create non-existing top level path: " + + path.string()); + + DWORD attr = GetFileAttributesW(path.c_str()); + DWORD err = GetLastError(); + if (attr != INVALID_FILE_ATTRIBUTES) { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return false; // if directory already exists all is good + else + throw shared::windows_error("createFakePath() called on a file: " + + path.string()); + } + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw shared::windows_error( + "createFakePath() GetFileAttributesW failed on: " + path.string(), err); + + if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory + // already exists + createFakePath( + path.parent_path(), + securityAttributes); // otherwise create parent directory (recursively) + + BOOL res = CreateDirectoryW(path.c_str(), securityAttributes); + if (res) + k32FakeDirTracker.insert(path.wstring(), std::wstring()); + else { + err = GetLastError(); + throw shared::windows_error( + "createFakePath() CreateDirectoryW failed on: " + path.string(), err); + } + return true; + } + + static bool addDirectoryMapping(const HookContext::Ptr& context, + const fs::path& originalPath, + const fs::path& reroutedPath) + { + if (originalPath.empty() || reroutedPath.empty()) { + spdlog::get("hooks")->error("RerouteW::addDirectoryMapping failed: {}, {}", + shared::string_cast<std::string>( + originalPath.wstring(), shared::CodePage::UTF8) + .c_str(), + shared::string_cast<std::string>( + reroutedPath.wstring(), shared::CodePage::UTF8) + .c_str()); + return false; + } + + auto lookupParent = + context->redirectionTable()->findNode(originalPath.parent_path()); + if (!lookupParent.get() || lookupParent->data().linkTarget.empty()) { + if (!addDirectoryMapping(context, originalPath.parent_path(), + reroutedPath.parent_path())) { + spdlog::get("hooks")->error("RerouteW::addDirectoryMapping failed: {}, {}", + shared::string_cast<std::string>( + originalPath.wstring(), shared::CodePage::UTF8) + .c_str(), + shared::string_cast<std::string>( + reroutedPath.wstring(), shared::CodePage::UTF8) + .c_str()); + return false; + } + } + + std::string reroutedU8 = shared::string_cast<std::string>(reroutedPath.wstring(), + shared::CodePage::UTF8); + if (reroutedU8.empty() || reroutedU8[reroutedU8.size() - 1] != '\\') + reroutedU8 += "\\"; + + spdlog::get("hooks")->info("mapping directory in vfs: {}, {}", + shared::string_cast<std::string>(originalPath.wstring(), + shared::CodePage::UTF8), + reroutedU8.c_str()); + + context->redirectionTable().addDirectory( + originalPath, RedirectionDataLocal(reroutedU8), + shared::FLAG_DIRECTORY | shared::FLAG_CREATETARGET); + + fs::directory_iterator end_itr; + + // cycle through the directory + for (fs::directory_iterator itr(reroutedPath); itr != end_itr; ++itr) { + // If it's not a directory, add it to the VFS, if it is recurse into it + if (is_regular_file(itr->path())) { + std::string fileReroutedU8 = shared::string_cast<std::string>( + itr->path().wstring(), shared::CodePage::UTF8); + spdlog::get("hooks")->info( + "mapping file in vfs: {}, {}", + shared::string_cast<std::string>( + (originalPath / itr->path().filename()).wstring(), + shared::CodePage::UTF8), + fileReroutedU8.c_str()); + context->redirectionTable().addFile( + fs::path(originalPath / itr->path().filename()), + RedirectionDataLocal(fileReroutedU8)); + } else { + addDirectoryMapping(context, originalPath / itr->path().filename(), + reroutedPath / itr->path().filename()); + } + } + + return true; + } + + template <class char_t> + static bool interestingPathImpl(const char_t* inPath) + { + if (!inPath || !inPath[0]) + return false; + // ignore \\.\ unless its a \\.\?: + if (inPath[0] == '\\' && inPath[1] == '\\' && inPath[2] == '.' && + inPath[3] == '\\' && (!inPath[4] || inPath[5] != ':')) + return false; + // ignore L"hid#": + if ((inPath[0] == 'h' || inPath[0] == 'H') && + ((inPath[1] == 'i' || inPath[1] == 'I')) && + ((inPath[2] == 'd' || inPath[2] == 'D')) && inPath[3] == '#') + return false; + return true; + } + + static bool interestingPath(const char* inPath) + { + return interestingPathImpl(inPath); + } + static bool interestingPath(const wchar_t* inPath) + { + return interestingPathImpl(inPath); + } + + static fs::path absolutePath(const wchar_t* inPath) + { + if (shared::startswith(inPath, LR"(\\?\)") || + shared::startswith(inPath, LR"(\??\)")) { + inPath += 4; + return inPath; + } else if ((shared::startswith(inPath, LR"(\\localhost\)") || + shared::startswith(inPath, LR"(\\127.0.0.1\)")) && + inPath[13] == L'$') { + std::wstring newPath; + newPath += towupper(inPath[12]); + newPath += L':'; + newPath += &inPath[14]; + return newPath; + } else if (inPath[0] == L'\0' || inPath[1] == L':') { + return inPath; + } else if (inPath[0] == L'\\' || inPath[0] == L'/') { + return fs::path(winapi::wide::getFullPathName(inPath).first); + } + WCHAR currentDirectory[MAX_PATH]; + ::GetCurrentDirectoryW(MAX_PATH, currentDirectory); + fs::path finalPath = fs::path(currentDirectory) / inPath; + return finalPath; + // winapi::wide::getFullPathName(inPath).first; + } + + static fs::path canonizePath(const fs::path& inPath) + { + fs::path p = inPath.lexically_normal(); + if (p.filename_is_dot()) + p = p.remove_filename(); + return p.make_preferred(); + } + + static RerouteW create(const HookContext::ConstPtr& context, + const HookCallContext& callContext, const wchar_t* inPath, + bool inverse = false) + { + RerouteW result; + + if (interestingPath(inPath) && callContext.active()) { + const auto& lookupPath = canonizePath(absolutePath(inPath)); + result.m_RealPath = lookupPath.wstring(); + + result.m_Buffer = k32DeleteTracker.lookup(result.m_RealPath); + bool found = !result.m_Buffer.empty(); + if (found) { + spdlog::get("hooks")->info( + "Rerouting file open to location of deleted file: {}", + shared::string_cast<std::string>(result.m_Buffer)); + result.m_NewReroute = true; + } else { + const RedirectionTreeContainer& table = + inverse ? context->inverseTable() : context->redirectionTable(); + result.m_FileNode = table->findNode(lookupPath); + + if (result.m_FileNode.get() && (!result.m_FileNode->data().linkTarget.empty() || + result.m_FileNode->isDirectory())) { + if (!result.m_FileNode->data().linkTarget.empty()) { + result.m_Buffer = shared::string_cast<std::wstring>( + result.m_FileNode->data().linkTarget.c_str(), shared::CodePage::UTF8); + } else { + result.m_Buffer = result.m_FileNode->path().wstring(); + } + found = true; + } + } + if (found) { + result.m_Rerouted = true; + + wchar_t inIt = inPath[wcslen(inPath) - 1]; + std::wstring::iterator outIt = result.m_Buffer.end() - 1; + if ((*outIt == L'\\' || *outIt == L'/') && !(inIt == L'\\' || inIt == L'/')) + result.m_Buffer.erase(outIt); + std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\'); + } else + result.m_Buffer = inPath; + } else if (inPath) + result.m_Buffer = inPath; + + if (inPath) + result.m_FileName = result.m_Buffer.c_str(); + return result; + } + + static RerouteW createNew(const HookContext::ConstPtr& context, + const HookCallContext& callContext, LPCWSTR inPath, + bool createPath = true, + LPSECURITY_ATTRIBUTES securityAttributes = nullptr) + { + RerouteW result; + + if (interestingPath(inPath) && callContext.active()) { + const auto& lookupPath = canonizePath(absolutePath(inPath)); + result.m_RealPath = lookupPath.wstring(); + + result.m_Buffer = k32DeleteTracker.lookup(result.m_RealPath); + bool found = !result.m_Buffer.empty(); + if (found) + spdlog::get("hooks")->info( + "Rerouting file creation to original location of deleted file: {}", + shared::string_cast<std::string>(result.m_Buffer)); + else { + FindCreateTarget visitor; + RedirectionTree::VisitorFunction visitorWrapper = + [&](const RedirectionTree::NodePtrT& node) { + visitor(node); + }; + context->redirectionTable()->visitPath(lookupPath, visitorWrapper); + if (visitor.target.get()) { + // the visitor has found the last (deepest in the directory hierarchy) + // create-target + fs::path relativePath = + shared::make_relative(visitor.target->path(), lookupPath); + result.m_Buffer = + (fs::path(visitor.target->data().linkTarget.c_str()) / relativePath) + .wstring(); + found = true; + } + } + + if (found) { + if (createPath) { + try { + FunctionGroupLock lock(MutExHookGroup::ALL_GROUPS); + result.m_PathCreated = createFakePath( + fs::path(result.m_Buffer).parent_path(), securityAttributes); + } catch (const std::exception& e) { + spdlog::get("hooks")->error( + "failed to create {}: {}", + shared::string_cast<std::string>(result.m_Buffer), e.what()); + } + } + + wchar_t inIt = inPath[wcslen(inPath) - 1]; + std::wstring::iterator outIt = result.m_Buffer.end() - 1; + if ((*outIt == L'\\' || *outIt == L'/') && !(inIt == L'\\' || inIt == L'/')) + result.m_Buffer.erase(outIt); + std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\'); + result.m_Rerouted = true; + result.m_NewReroute = true; + } else + result.m_Buffer = inPath; + } else if (inPath) + result.m_Buffer = inPath; + + if (inPath) + result.m_FileName = result.m_Buffer.c_str(); + return result; + } + + static RerouteW createOrNew(const HookContext::ConstPtr& context, + const HookCallContext& callContext, LPCWSTR inPath, + bool createPath = true, + LPSECURITY_ATTRIBUTES securityAttributes = nullptr) + { + { + auto res = create(context, callContext, inPath); + if (res.wasRerouted() || !interestingPath(inPath) || !callContext.active() || + pathExists(inPath)) + return std::move(res); + } + return createNew(context, callContext, inPath, createPath, securityAttributes); + } + + static RerouteW noReroute(LPCWSTR inPath) + { + RerouteW result; + if (inPath) + result.m_Buffer = inPath; + if (inPath && inPath[0] && !shared::startswith(inPath, L"hid#")) + std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\'); + result.m_FileName = result.m_Buffer.c_str(); + return result; + } + +private: + struct FindCreateTarget + { + RedirectionTree::NodePtrT target; + void operator()(RedirectionTree::NodePtrT node) + { + if (node->hasFlag(shared::FLAG_CREATETARGET)) { + target = node; + } + } + }; +}; + +class CreateRerouter +{ +public: + bool rerouteCreate(const HookContext::ConstPtr& context, + const HookCallContext& callContext, LPCWSTR lpFileName, + DWORD& dwCreationDisposition, DWORD dwDesiredAccess, + LPSECURITY_ATTRIBUTES lpSecurityAttributes) + { + enum class Open + { + existing, + create, + empty + }; + Open open = Open::existing; + + // Notice since we are calling our patched GetFileAttributesW here this will also + // check virtualized paths + DWORD virtAttr = GetFileAttributesW(lpFileName); + bool isFile = virtAttr != INVALID_FILE_ATTRIBUTES && + (virtAttr & FILE_ATTRIBUTE_DIRECTORY) == 0; + m_isDir = + virtAttr != INVALID_FILE_ATTRIBUTES && (virtAttr & FILE_ATTRIBUTE_DIRECTORY); + + switch (dwCreationDisposition) { + case CREATE_ALWAYS: + open = Open::create; + if (isFile || m_isDir) { + m_error = ERROR_ALREADY_EXISTS; + } + break; + + case CREATE_NEW: + if (isFile || m_isDir) { + m_error = ERROR_FILE_EXISTS; + return false; + } else { + open = Open::create; + } + break; + + case OPEN_ALWAYS: + if (isFile || m_isDir) { + m_error = ERROR_ALREADY_EXISTS; + } else { + open = Open::create; + } + break; + + case TRUNCATE_EXISTING: + if ((dwDesiredAccess & GENERIC_WRITE) == 0) { + m_error = ERROR_INVALID_PARAMETER; + return false; + } + if (isFile || m_isDir) + open = Open::empty; + break; + } + + if (m_isDir && pathIsDirectory(lpFileName)) + m_reroute = RerouteW::noReroute(lpFileName); + else + m_reroute = RerouteW::create(context, callContext, lpFileName); + + if (m_reroute.wasRerouted() && open == Open::create && + pathIsDirectory(m_reroute.fileName())) + m_reroute = RerouteW::createNew(context, callContext, lpFileName, true, + lpSecurityAttributes); + + if (!m_isDir && !isFile && !m_reroute.wasRerouted() && + (open == Open::create || open == Open::empty)) { + m_reroute = RerouteW::createNew(context, callContext, lpFileName, true, + lpSecurityAttributes); + + bool newFile = + !m_reroute.wasRerouted() && pathDirectlyAvailable(m_reroute.fileName()); + if (newFile && open == Open::empty) + // TRUNCATE_EXISTING will fail since the new file doesn't exist, so change + // disposition: + dwCreationDisposition = CREATE_ALWAYS; + } + + return true; + } + + // rerouteNew is used for rerouting the destination of copy/move operations. Assumes + // that the call will be skipped if false is returned. + bool rerouteNew(const HookContext::ConstPtr& context, HookCallContext& callContext, + LPCWSTR lpFileName, bool replaceExisting, const char* hookName) + { + DWORD disposition = replaceExisting ? CREATE_ALWAYS : CREATE_NEW; + if (!rerouteCreate(context, callContext, lpFileName, disposition, GENERIC_WRITE, + nullptr)) { + spdlog::get("hooks")->info( + "{} guaranteed failure, skipping original call: {}, replaceExisting={}, " + "error={}", + hookName, + shared::string_cast<std::string>(lpFileName, shared::CodePage::UTF8), + replaceExisting ? "true" : "false", error()); + + callContext.updateLastError(error()); + return false; + } + return true; + } + + void updateResult(HookCallContext& callContext, bool success) + { + m_originalError = callContext.lastError(); + if (success) { + // m_error != ERROR_SUCCESS means we are overriding the error on success + if (m_error == ERROR_SUCCESS) + m_error = m_originalError; + } else if (m_originalError == ERROR_PATH_NOT_FOUND && m_directlyAvailable) + m_error = ERROR_FILE_NOT_FOUND; + else + m_error = m_originalError; + if (m_error != m_originalError) + callContext.updateLastError(m_error); + } + + DWORD error() const { return m_error; } + DWORD originalError() const { return m_originalError; } + bool changedError() const { return m_error != m_originalError; } + + bool isDir() const { return m_isDir; } + bool newReroute() const { return m_reroute.newReroute(); } + bool wasRerouted() const { return m_reroute.wasRerouted(); } + LPCWSTR fileName() const { return m_reroute.fileName(); } + + void insertMapping(const HookContext::Ptr& context, bool directory = false) + { + m_reroute.insertMapping(context, directory); + } + +private: + DWORD m_error = ERROR_SUCCESS; + DWORD m_originalError = ERROR_SUCCESS; + bool m_directlyAvailable = false; + bool m_isDir = false; + RerouteW m_reroute; +}; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/pch.cpp b/libs/usvfs/src/usvfs_dll/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/usvfs/src/usvfs_dll/redirectiontree.cpp b/libs/usvfs/src/usvfs_dll/redirectiontree.cpp new file mode 100644 index 0000000..e521365 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/redirectiontree.cpp @@ -0,0 +1,27 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "redirectiontree.h" + +std::ostream& usvfs::operator<<(std::ostream& stream, const RedirectionData& data) +{ + stream << data.linkTarget; + return stream; +} diff --git a/libs/usvfs/src/usvfs_dll/redirectiontree.h b/libs/usvfs/src/usvfs_dll/redirectiontree.h new file mode 100644 index 0000000..6a2238a --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/redirectiontree.h @@ -0,0 +1,104 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <directory_tree.h> + +namespace usvfs +{ + +// typedef boost::interprocess::basic_string<char, std::char_traits<char>, +// shared::CharAllocatorT> StringT; + +namespace shared +{ + static const TreeFlags FLAG_CREATETARGET = FLAG_FIRSTUSERFLAG + 0x00; +} + +struct RedirectionDataLocal +{ + + RedirectionDataLocal(const char* target) : linkTarget(target) {} + + RedirectionDataLocal(const std::string& target) : linkTarget(target) {} + + std::string linkTarget; +}; + +struct RedirectionData +{ + + RedirectionData(const RedirectionData& reference, + const shared::VoidAllocatorT& allocator) + : linkTarget(reference.linkTarget.c_str(), allocator) + {} + + RedirectionData(const RedirectionDataLocal& reference, + const shared::VoidAllocatorT& allocator) + : linkTarget(reference.linkTarget.c_str(), allocator) + {} + + RedirectionData(const char* target, const shared::VoidAllocatorT& allocator) + : linkTarget(target, allocator) + {} + + shared::StringT linkTarget; +}; + +std::ostream& operator<<(std::ostream& stream, const RedirectionData& data); + +template <> +inline void shared::dataAssign<RedirectionData>(RedirectionData& destination, + const RedirectionData& source) +{ + destination.linkTarget.assign(source.linkTarget.c_str()); +} + +template <> +inline RedirectionData +shared::createDataEmpty<RedirectionData>(const VoidAllocatorT& allocator) +{ + return RedirectionData("", allocator); +} + +template <typename T> +struct shared::SHMDataCreator<RedirectionData, T> +{ + static RedirectionData create(T source, const VoidAllocatorT& allocator) + { + return RedirectionData(source, allocator); + } +}; + +template <> +struct shared::SHMDataCreator<RedirectionData, RedirectionData> +{ + static RedirectionData create(const RedirectionData& source, + const VoidAllocatorT& allocator) + { + return RedirectionData(source, allocator); + } +}; + +using RedirectionTree = shared::DirectoryTree<RedirectionData>; +using RedirectionTreeContainer = shared::TreeContainer<RedirectionTree>; + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/semaphore.cpp b/libs/usvfs/src/usvfs_dll/semaphore.cpp new file mode 100644 index 0000000..609b2f5 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/semaphore.cpp @@ -0,0 +1,57 @@ +#include "semaphore.h" +#include "exceptionex.h" + +RecursiveBenaphore::RecursiveBenaphore() : m_Counter(0), m_OwnerId(0UL), m_Recursion(0) +{ + m_Semaphore = ::CreateSemaphore(nullptr, 1, 1, nullptr); +} + +RecursiveBenaphore::~RecursiveBenaphore() +{ + ::CloseHandle(m_Semaphore); +} + +void RecursiveBenaphore::wait(DWORD timeout) +{ + DWORD tid = ::GetCurrentThreadId(); + + if (::_InterlockedIncrement(&m_Counter) > 1) { + if (tid != m_OwnerId) { + int tries = 3; + while (::WaitForSingleObject(m_Semaphore, timeout) != WAIT_OBJECT_0) { + HANDLE owner = ::OpenThread(SYNCHRONIZE, FALSE, m_OwnerId); + ON_BLOCK_EXIT([owner]() { + ::CloseHandle(owner); + }); + if ((tries <= 0) || (::WaitForSingleObject(owner, 0) == WAIT_OBJECT_0)) { + // owner has quit without releasing the semaphore! + m_Recursion = 0; + spdlog::get("usvfs")->error("thread {} never released the mutex", m_OwnerId); + break; + } else { + --tries; + } + } + } + } + m_OwnerId = tid; + ++m_Recursion; +} + +void RecursiveBenaphore::signal() +{ + if (m_Recursion == 0) { + return; + } + // no validation the signaling thread is the one owning the lock + DWORD recursion = --m_Recursion; + if (recursion == 0) { + m_OwnerId = 0; + } + DWORD result = ::_InterlockedDecrement(&m_Counter); + if (result > 0) { + if (recursion == 0) { + ::ReleaseSemaphore(m_Semaphore, 1, nullptr); + } + } +} diff --git a/libs/usvfs/src/usvfs_dll/semaphore.h b/libs/usvfs/src/usvfs_dll/semaphore.h new file mode 100644 index 0000000..1048c47 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/semaphore.h @@ -0,0 +1,27 @@ +#pragma once + +// based on code by Jeff Preshing + +// this is a synchronization class that prefers +// undefined behaviour over deadlock. It's utterly broken +// and needs to be replaced in time. + +class RecursiveBenaphore +{ + +public: + RecursiveBenaphore(); + ~RecursiveBenaphore(); + + // wait on the semaphore. after timeout this will check if the current owner + // thread is still alive and steal the semaphore if it isn't. Otherwise this + // will continue to wait. + void wait(DWORD timeout = INFINITE); + void signal(); + +private: + LONG m_Counter; + DWORD m_OwnerId; + int m_Recursion; + HANDLE m_Semaphore; +}; diff --git a/libs/usvfs/src/usvfs_dll/sharedparameters.cpp b/libs/usvfs/src/usvfs_dll/sharedparameters.cpp new file mode 100644 index 0000000..95cd0b2 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/sharedparameters.cpp @@ -0,0 +1,264 @@ +#include <logging.h> +#include <sharedparameters.h> +#include <usvfsparametersprivate.h> + +namespace usvfs +{ + +ForcedLibrary::ForcedLibrary(const std::string& process, const std::string& path, + const shared::VoidAllocatorT& alloc) + : m_processName(process.begin(), process.end(), alloc), + m_libraryPath(path.begin(), path.end(), alloc) +{} + +std::string ForcedLibrary::processName() const +{ + return {m_processName.begin(), m_processName.end()}; +} + +std::string ForcedLibrary::libraryPath() const +{ + return {m_libraryPath.begin(), m_libraryPath.end()}; +} + +SharedParameters::SharedParameters(const usvfsParameters& reference, + const shared::VoidAllocatorT& allocator) + : m_instanceName(reference.instanceName, allocator), + m_currentSHMName(reference.currentSHMName, allocator), + m_currentInverseSHMName(reference.currentInverseSHMName, allocator), + m_debugMode(reference.debugMode), m_logLevel(reference.logLevel), + m_crashDumpsType(reference.crashDumpsType), + m_crashDumpsPath(reference.crashDumpsPath, allocator), + m_delayProcess(reference.delayProcessMs), m_userCount(1), + m_processBlacklist(allocator), m_processList(allocator), + m_fileSuffixSkipList(allocator), m_directorySkipList(allocator), + m_forcedLibraries(allocator) +{} + +usvfsParameters SharedParameters::makeLocal() const +{ + bi::scoped_lock lock(m_mutex); + + return usvfsParameters(m_instanceName.c_str(), m_currentSHMName.c_str(), + m_currentInverseSHMName.c_str(), m_debugMode, m_logLevel, + m_crashDumpsType, m_crashDumpsPath.c_str(), + m_delayProcess.count()); +} + +std::string SharedParameters::instanceName() const +{ + bi::scoped_lock lock(m_mutex); + return {m_instanceName.begin(), m_instanceName.end()}; +} + +std::string SharedParameters::currentSHMName() const +{ + bi::scoped_lock lock(m_mutex); + return {m_currentSHMName.begin(), m_currentSHMName.end()}; +} + +std::string SharedParameters::currentInverseSHMName() const +{ + bi::scoped_lock lock(m_mutex); + return {m_currentInverseSHMName.begin(), m_currentInverseSHMName.end()}; +} + +void SharedParameters::setSHMNames(const std::string& current, + const std::string& inverse) +{ + bi::scoped_lock lock(m_mutex); + + m_currentSHMName.assign(current.begin(), current.end()); + m_currentInverseSHMName.assign(inverse.begin(), inverse.end()); +} + +void SharedParameters::setDebugParameters(LogLevel level, CrashDumpsType dumpType, + const std::string& dumpPath, + std::chrono::milliseconds delayProcess) +{ + bi::scoped_lock lock(m_mutex); + + m_logLevel = level; + m_crashDumpsType = dumpType; + m_crashDumpsPath.assign(dumpPath.begin(), dumpPath.end()); + m_delayProcess = delayProcess; +} + +std::size_t SharedParameters::userConnected() +{ + bi::scoped_lock lock(m_mutex); + return ++m_userCount; +} + +std::size_t SharedParameters::userDisconnected() +{ + bi::scoped_lock lock(m_mutex); + return --m_userCount; +} + +std::size_t SharedParameters::userCount() +{ + bi::scoped_lock lock(m_mutex); + return m_userCount; +} + +std::size_t SharedParameters::registeredProcessCount() const +{ + bi::scoped_lock lock(m_mutex); + return m_processList.size(); +} + +std::vector<DWORD> SharedParameters::registeredProcesses() const +{ + bi::scoped_lock lock(m_mutex); + return {m_processList.begin(), m_processList.end()}; +} + +void SharedParameters::registerProcess(DWORD pid) +{ + bi::scoped_lock lock(m_mutex); + m_processList.insert(pid); +} + +void SharedParameters::unregisterProcess(DWORD pid) +{ + { + bi::scoped_lock lock(m_mutex); + + auto itor = m_processList.find(pid); + + if (itor != m_processList.end()) { + m_processList.erase(itor); + return; + } + } + + spdlog::get("usvfs")->error("cannot unregister process {}, not in list", pid); +} + +void SharedParameters::blacklistExecutable(const std::string& name) +{ + bi::scoped_lock lock(m_mutex); + + m_processBlacklist.insert( + shared::StringT(name.begin(), name.end(), m_processBlacklist.get_allocator())); +} + +void SharedParameters::clearExecutableBlacklist() +{ + bi::scoped_lock lock(m_mutex); + m_processBlacklist.clear(); +} + +bool SharedParameters::executableBlacklisted(const std::string& appName, + const std::string& cmdLine) const +{ + bool blacklisted = false; + std::string log; + + { + bi::scoped_lock lock(m_mutex); + + for (const shared::StringT& sitem : m_processBlacklist) { + const auto item = "\\" + std::string(sitem.begin(), sitem.end()); + + if (!appName.empty()) { + if (boost::algorithm::iends_with(appName, item)) { + blacklisted = true; + log = std::format("application {} is blacklisted", appName); + break; + } + } + + if (!cmdLine.empty()) { + if (boost::algorithm::icontains(cmdLine, item)) { + blacklisted = true; + log = std::format("command line {} is blacklisted", cmdLine); + break; + } + } + } + } + + if (blacklisted) { + spdlog::get("usvfs")->info(log); + return true; + } + + return false; +} + +void SharedParameters::addSkipFileSuffix(const std::string& fileSuffix) +{ + bi::scoped_lock lock(m_mutex); + + m_fileSuffixSkipList.insert(shared::StringT(fileSuffix.begin(), fileSuffix.end(), + m_fileSuffixSkipList.get_allocator())); +} + +void SharedParameters::clearSkipFileSuffixes() +{ + bi::scoped_lock lock(m_mutex); + m_fileSuffixSkipList.clear(); +} + +std::vector<std::string> SharedParameters::skipFileSuffixes() const +{ + bi::scoped_lock lock(m_mutex); + return {m_fileSuffixSkipList.begin(), m_fileSuffixSkipList.end()}; +} + +void SharedParameters::addSkipDirectory(const std::string& directory) +{ + bi::scoped_lock lock(m_mutex); + + m_directorySkipList.insert(shared::StringT(directory.begin(), directory.end(), + m_directorySkipList.get_allocator())); +} + +void SharedParameters::clearSkipDirectories() +{ + bi::scoped_lock lock(m_mutex); + m_directorySkipList.clear(); +} + +std::vector<std::string> SharedParameters::skipDirectories() const +{ + bi::scoped_lock lock(m_mutex); + return {m_directorySkipList.begin(), m_directorySkipList.end()}; +} + +void SharedParameters::addForcedLibrary(const std::string& processName, + const std::string& libraryPath) +{ + bi::scoped_lock lock(m_mutex); + + m_forcedLibraries.push_front( + ForcedLibrary(processName, libraryPath, m_forcedLibraries.get_allocator())); +} + +std::vector<std::string> +SharedParameters::forcedLibraries(const std::string& processName) +{ + std::vector<std::string> v; + + { + bi::scoped_lock lock(m_mutex); + + for (const auto& lib : m_forcedLibraries) { + if (boost::algorithm::iequals(processName, lib.processName())) { + v.push_back(lib.libraryPath()); + } + } + } + + return v; +} + +void SharedParameters::clearForcedLibraries() +{ + bi::scoped_lock lock(m_mutex); + m_forcedLibraries.clear(); +} + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/stringcast_boost.h b/libs/usvfs/src/usvfs_dll/stringcast_boost.h new file mode 100644 index 0000000..d2544ec --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/stringcast_boost.h @@ -0,0 +1,42 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include <stringcast.h> + +namespace usvfs +{ +namespace shared +{ + + template <typename ToT, typename CharT, typename Traits, typename Allocator> + class string_cast_impl<ToT, boost::container::basic_string<CharT, Traits, Allocator>> + { + public: + static ToT + cast(const boost::container::basic_string<CharT, Traits, Allocator>& source, + CodePage codePage, size_t sourceLength) + { + return string_cast_impl<ToT, const CharT*>::cast(source.c_str(), codePage, + sourceLength); + } + }; + +} // namespace shared +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_dll/usvfs.cpp b/libs/usvfs/src/usvfs_dll/usvfs.cpp new file mode 100644 index 0000000..56d9f71 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/usvfs.cpp @@ -0,0 +1,965 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "usvfs.h" +#include "hookmanager.h" +#include "loghelpers.h" +#include "redirectiontree.h" +#include "usvfs_version.h" +#include "usvfsparametersprivate.h" +#include <inject.h> +#include <shmlogger.h> +#include <spdlog/sinks/null_sink.h> +#include <spdlog/sinks/stdout_sinks.h> +#include <stringcast.h> +#include <ttrampolinepool.h> +#include <winapi.h> + +// note that there's a mix of boost and std filesystem stuff in this file and +// that they're not completely compatible +#include <filesystem> + +namespace bfs = boost::filesystem; +namespace ush = usvfs::shared; +namespace bip = boost::interprocess; +namespace ba = boost::algorithm; + +using usvfs::log::ConvertLogLevel; + +usvfs::HookManager* manager = nullptr; +usvfs::HookContext* context = nullptr; +HMODULE dllModule = nullptr; +PVOID exceptionHandler = nullptr; +CrashDumpsType usvfs_dump_type = CrashDumpsType::None; +std::wstring usvfs_dump_path; + +// this is called for every single file, so it's a bit long winded, but it's +// as fast as it gets, probably +// +template <std::size_t LongestExtension, std::size_t ExtensionsCount> +bool extensionMatchesCI( + std::string_view name, + const std::array<std::string_view, ExtensionsCount>& extensionsLC, + const std::array<std::string_view, ExtensionsCount>& extensionsUC) +{ + constexpr std::size_t longestExtensionWithDot = LongestExtension + 1; + + // quick check + if (name.size() < longestExtensionWithDot) { + return false; + } + + // for each extension + for (std::size_t i = 0; i < ExtensionsCount; ++i) { + const std::size_t extensionLength = extensionsLC[i].size(); + const std::size_t extensionLengthWithDot = extensionLength + 1; + + // check size + if (name.size() < extensionLengthWithDot) { + continue; + } + + // check dot + if (name[name.size() - extensionLengthWithDot] != '.') { + continue; + } + + // starts at one past the dot + const auto* p = name.data() + name.size() - extensionLength; + + // set to false as soon as a character doesn't match + bool found = true; + + // for each character in extension + for (std::size_t c = 0; c < extensionLength; ++c) { + // checking both lowercase and uppercase + if (*p != extensionsLC[i][c] && *p != extensionsUC[i][c]) { + // neither + found = false; + break; + } + + // matches, check next + ++p; + } + + if (found) { + return true; + } + } + + return false; +} + +bool shouldAddToInverseTree(std::string_view name) +{ + static std::array<std::string_view, 3> extensionsLC{"exe", "dll"}; + static std::array<std::string_view, 3> extensionsUC{"EXE", "DLL"}; + + // must be changed if any extension longer than 3 letters is added + constexpr std::size_t longestExtension = 3; + + return extensionMatchesCI<longestExtension>(name, extensionsLC, extensionsUC); +} + +// +// Logging +// + +void InitLoggingInternal(bool toConsole, bool connectExistingSHM) +{ + try { + if (!toConsole && !SHMLogger::isInstantiated()) { + if (connectExistingSHM) { + SHMLogger::open("usvfs"); + } else { + SHMLogger::create("usvfs"); + } + } + + // a temporary logger was created in DllMain + spdlog::drop("usvfs"); +#pragma message("need a customized name for the shm") + auto logger = spdlog::get("usvfs"); + if (logger.get() == nullptr) { + logger = toConsole ? spdlog::create<spdlog::sinks::stdout_sink_mt>("usvfs") + : spdlog::create<usvfs::sinks::shm_sink>("usvfs", "usvfs"); + logger->set_pattern("%H:%M:%S.%e [%L] %v"); + } + logger->set_level(spdlog::level::debug); + + spdlog::drop("hooks"); + logger = spdlog::get("hooks"); + if (logger.get() == nullptr) { + logger = toConsole ? spdlog::create<spdlog::sinks::stdout_sink_mt>("hooks") + : spdlog::create<usvfs::sinks::shm_sink>("hooks", "usvfs"); + logger->set_pattern("%H:%M:%S.%e <%P:%t> [%L] %v"); + } + logger->set_level(spdlog::level::debug); + } catch (const std::exception&) { + // TODO should really report this + // OutputDebugStringA((boost::format("init exception: %1%\n") % + // e.what()).str().c_str()); + if (spdlog::get("usvfs").get() == nullptr) { + spdlog::create<spdlog::sinks::null_sink_mt>("usvfs"); + } + if (spdlog::get("hooks").get() == nullptr) { + spdlog::create<spdlog::sinks::null_sink_mt>("hooks"); + } + } + + spdlog::get("usvfs")->info("usvfs dll {} initialized in process {}", + USVFS_VERSION_STRING, GetCurrentProcessId()); +} + +void WINAPI usvfsInitLogging(bool toConsole) +{ + InitLoggingInternal(toConsole, false); +} + +extern "C" DLLEXPORT bool WINAPI usvfsGetLogMessages(LPSTR buffer, size_t size, + bool blocking) +{ + buffer[0] = '\0'; + try { + if (blocking) { + SHMLogger::instance().get(buffer, size); + return true; + } else { + return SHMLogger::instance().tryGet(buffer, size); + } + } catch (const std::exception& e) { + _snprintf_s(buffer, size, _TRUNCATE, "Failed to retrieve log messages: %s", + e.what()); + return false; + } +} + +void SetLogLevel(LogLevel level) +{ + spdlog::get("usvfs")->set_level(ConvertLogLevel(level)); + spdlog::get("hooks")->set_level(ConvertLogLevel(level)); +} + +void WINAPI usvfsUpdateParameters(usvfsParameters* p) +{ + spdlog::get("usvfs")->info("updating parameters:\n" + " . debugMode: {}\n" + " . log level: {}\n" + " . dump type: {}\n" + " . dump path: {}\n" + " . delay process: {}ms", + p->debugMode, usvfsLogLevelToString(p->logLevel), + usvfsCrashDumpTypeToString(p->crashDumpsType), + p->crashDumpsPath, p->delayProcessMs); + + // update actual values used: + usvfs_dump_type = p->crashDumpsType; + usvfs_dump_path = + ush::string_cast<std::wstring>(p->crashDumpsPath, ush::CodePage::UTF8); + SetLogLevel(p->logLevel); + + // update parameters in context so spawned process will inherit changes: + context->setDebugParameters(p->logLevel, p->crashDumpsType, p->crashDumpsPath, + std::chrono::milliseconds(p->delayProcessMs)); +} + +// +// Structured Exception handling +// + +std::wstring generate_minidump_name(const wchar_t* dumpPath) +{ + DWORD pid = GetCurrentProcessId(); + wchar_t pname[100]; + if (GetModuleBaseName(GetCurrentProcess(), NULL, pname, _countof(pname)) == 0) + return std::wstring(); + + // find an available name: + wchar_t dmpFile[MAX_PATH]; + int count = 0; + _snwprintf_s(dmpFile, _TRUNCATE, L"%s\\%s-%lu.dmp", dumpPath, pname, pid); + while (winapi::ex::wide::fileExists(dmpFile)) { + if (++count > 99) + return std::wstring(); + _snwprintf_s(dmpFile, _TRUNCATE, L"%s\\%s-%lu_%02d.dmp", dumpPath, pname, pid, + count); + } + return dmpFile; +} + +int createMiniDumpImpl(PEXCEPTION_POINTERS exceptionPtrs, CrashDumpsType type, + const wchar_t* dumpPath, HMODULE dbgDLL) +{ + typedef BOOL(WINAPI * FuncMiniDumpWriteDump)( + HANDLE process, DWORD pid, HANDLE file, MINIDUMP_TYPE dumpType, + const PMINIDUMP_EXCEPTION_INFORMATION exceptionParam, + const PMINIDUMP_USER_STREAM_INFORMATION userStreamParam, + const PMINIDUMP_CALLBACK_INFORMATION callbackParam); + + // notice we avoid logging here on purpose because this is called from the VEHandler + // and the logger can crash it in extreme cases. + // additionally it is also called for MO crashes which use it's own logging. + winapi::ex::wide::createPath(dumpPath); + + auto dmpName = generate_minidump_name(dumpPath); + if (dmpName.empty()) + return 4; + + FuncMiniDumpWriteDump funcDump = reinterpret_cast<FuncMiniDumpWriteDump>( + GetProcAddress(dbgDLL, "MiniDumpWriteDump")); + if (!funcDump) + return 5; + + HANDLE dumpFile = winapi::wide::createFile(dmpName) + .createAlways() + .access(GENERIC_WRITE) + .share(FILE_SHARE_WRITE)(); + if (dumpFile != INVALID_HANDLE_VALUE) { + DWORD dumpType = MiniDumpNormal | MiniDumpWithHandleData | + MiniDumpWithUnloadedModules | MiniDumpWithProcessThreadData; + if (type == CrashDumpsType::Data) + dumpType |= MiniDumpWithDataSegs; + if (type == CrashDumpsType::Full) + dumpType |= MiniDumpWithFullMemory; + + _MINIDUMP_EXCEPTION_INFORMATION exceptionInfo; + exceptionInfo.ThreadId = GetCurrentThreadId(); + exceptionInfo.ExceptionPointers = exceptionPtrs; + exceptionInfo.ClientPointers = FALSE; + + BOOL success = funcDump(GetCurrentProcess(), GetCurrentProcessId(), dumpFile, + static_cast<MINIDUMP_TYPE>(dumpType), &exceptionInfo, + nullptr, nullptr); + + CloseHandle(dumpFile); + + return success ? 0 : 7; + } else + return 6; +} + +int WINAPI usvfsCreateMiniDump(PEXCEPTION_POINTERS exceptionPtrs, CrashDumpsType type, + const wchar_t* dumpPath) +{ + if (type == CrashDumpsType::None) + return 0; + + int res = 1; + if (HMODULE dbgDLL = LoadLibraryW(L"dbghelp.dll")) { + try { + res = createMiniDumpImpl(exceptionPtrs, type, dumpPath, dbgDLL); + } catch (...) { + res = 2; + } + FreeLibrary(dbgDLL); + } + return res; +} + +static bool exceptionInUSVFS(PEXCEPTION_POINTERS exceptionPtrs) +{ + if (!dllModule) // shouldn't happen, check just in case + return true; // create dump to better understand how this could happen + + std::pair<uintptr_t, uintptr_t> range = winapi::ex::getSectionRange(dllModule); + + uintptr_t exceptionAddress = + reinterpret_cast<uintptr_t>(exceptionPtrs->ExceptionRecord->ExceptionAddress); + + return range.first <= exceptionAddress && exceptionAddress < range.second; +} + +LONG WINAPI VEHandler(PEXCEPTION_POINTERS exceptionPtrs) +{ + // NOTICE: don't use logger in VEHandler as it can cause another fault causing + // VEHandler to be called again and so on. + + if ((exceptionPtrs->ExceptionRecord->ExceptionCode < 0x80000000) // non-critical + || + (exceptionPtrs->ExceptionRecord->ExceptionCode == 0xe06d7363)) { // cpp exception + // don't report non-critical exceptions + return EXCEPTION_CONTINUE_SEARCH; + } + /* + if (((exceptionPtrs->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) != 0) + || (exceptionPtrs->ExceptionRecord->ExceptionCode == 0xe06d7363)) { + // don't want to break on non-critical exceptions. 0xe06d7363 indicates a C++ + exception. why are those marked non-continuable? return EXCEPTION_CONTINUE_SEARCH; + } + */ + + // VEHandler is called on "first-chance" exceptions which might be caught and handled. + // Ideally we would like to use an UnhandledExceptionFilter but that fails to catch + // crashes inside our hooks at least on x64, which is the main reason why want a crash + // collection from usvfs. As a workaround/compromise we catch vectored exception but + // only ones that originate directly within the usvfs code: + if (!exceptionInUSVFS(exceptionPtrs)) + return EXCEPTION_CONTINUE_SEARCH; + + // disable our hooking mechanism to increase chances the dump writing won't crash + HookLib::TrampolinePool& trampPool = HookLib::TrampolinePool::instance(); + if (&trampPool) { // need to test this in case of crash before TrampolinePool + // initialized + trampPool.forceUnlockBarrier(); + trampPool.setBlock(true); + } + + usvfsCreateMiniDump(exceptionPtrs, usvfs_dump_type, usvfs_dump_path.c_str()); + + return EXCEPTION_CONTINUE_SEARCH; +} + +// +// Exported functions +// + +void __cdecl InitHooks(LPVOID parameters, size_t) +{ + InitLoggingInternal(false, true); + + const usvfsParameters* params = reinterpret_cast<usvfsParameters*>(parameters); + + // there is already a wait in the constructor of HookManager, but this one is useful + // to debug code here (from experience... ), should not wait twice since the second + // will return true immediately + if (params->debugMode) { + while (!::IsDebuggerPresent()) { + // wait for debugger to attach + ::Sleep(100); + } + } + + usvfs_dump_type = params->crashDumpsType; + usvfs_dump_path = + ush::string_cast<std::wstring>(params->crashDumpsPath, ush::CodePage::UTF8); + + if (params->delayProcessMs > 0) { + ::Sleep(static_cast<unsigned long>(params->delayProcessMs)); + } + + SetLogLevel(params->logLevel); + + if (exceptionHandler == nullptr) { + if (usvfs_dump_type != CrashDumpsType::None) + exceptionHandler = ::AddVectoredExceptionHandler(0, VEHandler); + } else { + spdlog::get("usvfs")->info("vectored exception handler already active"); + // how did this happen?? + } + + spdlog::get("usvfs")->info( + "inithooks called {0} in process {1}:{2} (log level {3}, dump type {4}, dump " + "path {5})", + params->instanceName, winapi::ansi::getModuleFileName(nullptr), + ::GetCurrentProcessId(), static_cast<int>(params->logLevel), + static_cast<int>(params->crashDumpsType), params->crashDumpsPath); + + try { + manager = new usvfs::HookManager(*params, dllModule); + + auto context = manager->context(); + auto exePath = boost::dll::program_location(); + auto libraries = context->librariesToForceLoad(exePath.filename().c_str()); + for (auto library : libraries) { + if (std::filesystem::exists(library)) { + const auto ret = LoadLibraryExW(library.c_str(), NULL, 0); + if (ret) { + spdlog::get("usvfs")->info("inithooks succeeded to force load {0}", + ush::string_cast<std::string>(library).c_str()); + } else { + spdlog::get("usvfs")->critical( + "inithooks failed to force load {0}", + ush::string_cast<std::string>(library).c_str()); + } + } + } + + spdlog::get("usvfs")->info("inithooks in process {0} successful", + ::GetCurrentProcessId()); + + } catch (const std::exception& e) { + spdlog::get("usvfs")->debug("failed to initialise hooks: {0}", e.what()); + } +} + +void WINAPI usvfsGetCurrentVFSName(char* buffer, size_t size) +{ + ush::strncpy_sz(buffer, context->callParameters().currentSHMName, size); +} + +BOOL WINAPI usvfsCreateVFS(const usvfsParameters* p) +{ + usvfs::HookContext::remove(p->instanceName); + return usvfsConnectVFS(p); +} + +BOOL WINAPI usvfsConnectVFS(const usvfsParameters* params) +{ + if (spdlog::get("usvfs").get() == nullptr) { + // create temporary logger so we don't get null-pointer exceptions + spdlog::create<spdlog::sinks::null_sink_mt>("usvfs"); + } + + try { + usvfsDisconnectVFS(); + context = new usvfs::HookContext(*params, dllModule); + + return TRUE; + } catch (const std::exception& e) { + spdlog::get("usvfs")->debug("failed to connect to vfs: {}", e.what()); + return FALSE; + } +} + +void WINAPI usvfsDisconnectVFS() +{ + if (spdlog::get("usvfs").get() == nullptr) { + // create temporary logger so we don't get null-pointer exceptions + spdlog::create<spdlog::sinks::null_sink_mt>("usvfs"); + } + + spdlog::get("usvfs")->debug("remove from process {}", GetCurrentProcessId()); + + if (manager != nullptr) { + delete manager; + manager = nullptr; + } + + if (context != nullptr) { + delete context; + context = nullptr; + spdlog::get("usvfs")->debug("vfs unloaded"); + } +} + +bool processStillActive(DWORD pid) +{ + HANDLE proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + + if (proc == nullptr) { + return false; + } + + ON_BLOCK_EXIT([&]() { + if (proc != INVALID_HANDLE_VALUE) + ::CloseHandle(proc); + }); + + DWORD exitCode; + if (!GetExitCodeProcess(proc, &exitCode)) { + spdlog::get("usvfs")->warn("failed to query exit code on process {}: {}", pid, + ::GetLastError()); + return false; + } else { + return exitCode == STILL_ACTIVE; + } +} + +BOOL WINAPI usvfsGetVFSProcessList(size_t* count, LPDWORD processIDs) +{ + if (count == nullptr) { + SetLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + + if (context == nullptr) { + *count = 0; + } else { + std::vector<DWORD> pids = context->registeredProcesses(); + size_t realCount = 0; + for (DWORD pid : pids) { + if (processStillActive(pid)) { + if ((realCount < *count) && (processIDs != nullptr)) { + processIDs[realCount] = pid; + } + + ++realCount; + } // else the process has already ended + } + *count = realCount; + } + return TRUE; +} + +BOOL WINAPI usvfsGetVFSProcessList2(size_t* count, DWORD** buffer) +{ + if (!count || !buffer) { + SetLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + + *count = 0; + *buffer = nullptr; + + std::vector<DWORD> pids = context->registeredProcesses(); + auto last = std::remove_if(pids.begin(), pids.end(), [](DWORD id) { + return !processStillActive(id); + }); + + pids.erase(last, pids.end()); + + if (pids.empty()) { + return TRUE; + } + + *count = pids.size(); + *buffer = static_cast<DWORD*>(std::calloc(pids.size(), sizeof(DWORD))); + + std::copy(pids.begin(), pids.end(), *buffer); + + return TRUE; +} + +void WINAPI usvfsClearVirtualMappings() +{ + context->redirectionTable()->clear(); + context->inverseTable()->clear(); +} + +/// ensure the specified path exists. If a physical path of the same name +/// exists, it is inserted into the virtual directory as an empty reference. If +/// the path doesn't exist virtually and can't be cloned from a physical +/// directory, this returns false +/// \todo if this fails (i.e. not all intermediate directories exists) any +/// intermediate directories already created aren't removed +bool assertPathExists(usvfs::RedirectionTreeContainer& table, LPCWSTR path) +{ + bfs::path p(path); + p = p.parent_path(); + + usvfs::RedirectionTree::NodeT* current = table.get(); + + for (auto iter = p.begin(); iter != p.end(); iter = ush::nextIter(iter, p.end())) { + if (current->exists(iter->string().c_str())) { + // subdirectory exists virtually, all good + usvfs::RedirectionTree::NodePtrT found = current->node(iter->string().c_str()); + current = found.get().get(); + } else { + // targetPath is relative to the last rerouted "real" path. This means + // that if virtual c:/foo maps to real c:/windows then creating virtual + // c:/foo/bar will map to real c:/windows/bar + bfs::path targetPath = current->data().linkTarget.size() > 0 + ? bfs::path(current->data().linkTarget.c_str()) / *iter + : *iter / "\\"; + + // is_directory returns false for symlinks and reparse points, + // which causes this function to fail if the target path contains + // either of those. paths containing reparse points is a common + // scenario when running under Wine, so check for those explicitly. + // this check could have a false positive if the path contains a + // symlink to a file, but such a scenario is extremely unlikely. + if (is_directory(targetPath) || is_symlink(targetPath) || + status(targetPath).type() == bfs::file_type::reparse_file) { + usvfs::RedirectionTree::NodePtrT newNode = + table.addDirectory(current->path() / *iter, targetPath.string().c_str(), + ush::FLAG_DUMMY, false); + current = newNode.get().get(); + } else { + spdlog::get("usvfs")->info("{} doesn't exist", targetPath.c_str()); + return false; + } + } + } + + return true; +} + +static bool fileNameInSkipSuffixes(const std::string& fileNameUtf8, + const std::vector<std::string>& skipFileSuffixes) +{ + for (const auto& skipFileSuffix : skipFileSuffixes) { + if (boost::algorithm::iends_with(fileNameUtf8, skipFileSuffix)) { + spdlog::get("usvfs")->debug( + "file '{}' should be skipped, matches file suffix '{}'", fileNameUtf8, + skipFileSuffix); + return true; + } + } + return false; +} + +static bool fileNameInSkipDirectories(const std::string& directoryNameUtf8, + const std::vector<std::string>& skipDirectories) +{ + for (const auto& skipDir : skipDirectories) { + if (boost::algorithm::iequals(directoryNameUtf8, skipDir)) { + spdlog::get("usvfs")->debug("directory '{}' should be skipped", + directoryNameUtf8); + return true; + } + } + return false; +} + +BOOL WINAPI usvfsVirtualLinkFile(LPCWSTR source, LPCWSTR destination, + unsigned int flags) +{ + // TODO difference between winapi and ntdll api regarding system32 vs syswow64 + // (and other windows links?) + try { + if (!assertPathExists(context->redirectionTable(), destination)) { + SetLastError(ERROR_PATH_NOT_FOUND); + return FALSE; + } + + const auto skipFileSuffixes = context->skipFileSuffixes(); + + std::string sourceU8 = ush::string_cast<std::string>(source, ush::CodePage::UTF8); + + // Check if the file should be skipped + if (fileNameInSkipSuffixes(sourceU8, skipFileSuffixes)) { + // return false when we want to fail when the file is skipped + return (flags & LINKFLAG_FAILIFSKIPPED) ? FALSE : TRUE; + } + + auto res = context->redirectionTable().addFile( + bfs::path(destination), usvfs::RedirectionDataLocal(sourceU8), + !(flags & LINKFLAG_FAILIFEXISTS)); + + if (shouldAddToInverseTree(sourceU8)) { + std::string destinationU8 = + ush::string_cast<std::string>(destination, ush::CodePage::UTF8); + + context->inverseTable().addFile(bfs::path(source), + usvfs::RedirectionDataLocal(destinationU8), true); + } + + context->updateParameters(); + + if (res.get() == nullptr) { + // the tree structure currently doesn't provide useful error codes but + // this is currently the only reason + // we would return a nullptr. + SetLastError(ERROR_FILE_EXISTS); + return FALSE; + } else { + return TRUE; + } + } catch (const std::exception& e) { + spdlog::get("usvfs")->error("failed to copy file {}", e.what()); + // TODO: no clue what's wrong + SetLastError(ERROR_INVALID_DATA); + return FALSE; + } +} + +/** + * @brief extract the flags relevant to redirection + */ +static usvfs::shared::TreeFlags convertRedirectionFlags(unsigned int flags) +{ + usvfs::shared::TreeFlags result = 0; + if (flags & LINKFLAG_CREATETARGET) { + result |= usvfs::shared::FLAG_CREATETARGET; + } + return result; +} + +BOOL WINAPI usvfsVirtualLinkDirectoryStatic(LPCWSTR source, LPCWSTR destination, + unsigned int flags) +{ + // TODO change notification not yet implemented + try { + if ((flags & LINKFLAG_FAILIFEXISTS) && winapi::ex::wide::fileExists(destination)) { + SetLastError(ERROR_FILE_EXISTS); + return FALSE; + } + + if (!assertPathExists(context->redirectionTable(), destination)) { + SetLastError(ERROR_PATH_NOT_FOUND); + return FALSE; + } + + std::string sourceU8 = + ush::string_cast<std::string>(source, ush::CodePage::UTF8) + "\\"; + + context->redirectionTable().addDirectory( + destination, usvfs::RedirectionDataLocal(sourceU8), + usvfs::shared::FLAG_DIRECTORY | convertRedirectionFlags(flags), + (flags & LINKFLAG_CREATETARGET) != 0); + + const auto skipDirectories = context->skipDirectories(); + const auto skipFileSuffixes = context->skipFileSuffixes(); + + if ((flags & LINKFLAG_RECURSIVE) != 0) { + std::wstring sourceP(source); + std::wstring sourceW = sourceP + L"\\"; + std::wstring destinationW = std::wstring(destination) + L"\\"; + if (sourceP.length() >= MAX_PATH && !ush::startswith(sourceP.c_str(), LR"(\\?\)")) + sourceP = LR"(\\?\)" + sourceP; + + for (winapi::ex::wide::FileResult file : + winapi::ex::wide::quickFindFiles(sourceP.c_str(), L"*")) { + if (file.attributes & FILE_ATTRIBUTE_DIRECTORY) { + if ((file.fileName != L".") && (file.fileName != L"..")) { + + const auto nameU8 = ush::string_cast<std::string>(file.fileName.c_str(), + ush::CodePage::UTF8); + // Check if the directory should be skipped + if (fileNameInSkipDirectories(nameU8, skipDirectories)) { + // Fail if we desire to fail when a dir/file is skipped + if (flags & LINKFLAG_FAILIFSKIPPED) { + spdlog::get("usvfs")->debug( + "directory '{}' skipped, failing as defined by link flags", nameU8); + return FALSE; + } + + continue; + } + + usvfsVirtualLinkDirectoryStatic((sourceW + file.fileName).c_str(), + (destinationW + file.fileName).c_str(), + flags); + } + } else { + const auto nameU8 = + ush::string_cast<std::string>(file.fileName.c_str(), ush::CodePage::UTF8); + + // Check if the file should be skipped + if (fileNameInSkipSuffixes(nameU8, skipFileSuffixes)) { + // Fail if we desire to fail when a dir/file is skipped + if (flags & LINKFLAG_FAILIFSKIPPED) { + spdlog::get("usvfs")->debug( + "file '{}' skipped, failing as defined by link flags", nameU8); + return FALSE; + } + + continue; + } + + // TODO could save memory here by storing only the file name for the + // source and constructing the full name using the parent directory + context->redirectionTable().addFile( + bfs::path(destination) / nameU8, + usvfs::RedirectionDataLocal(sourceU8 + nameU8), true); + + if (shouldAddToInverseTree(nameU8)) { + std::string destinationU8 = + ush::string_cast<std::string>(destination, ush::CodePage::UTF8) + "\\"; + + context->inverseTable().addFile( + bfs::path(source) / nameU8, + usvfs::RedirectionDataLocal(destinationU8 + nameU8), true); + } + } + } + } + + context->updateParameters(); + + return TRUE; + } catch (const std::exception& e) { + spdlog::get("usvfs")->error("failed to copy file {}", e.what()); + // TODO: no clue what's wrong + SetLastError(ERROR_INVALID_DATA); + return FALSE; + } +} + +BOOL WINAPI usvfsCreateProcessHooked(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, DWORD dwCreationFlags, + LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation) +{ + BOOL susp = dwCreationFlags & CREATE_SUSPENDED; + DWORD flags = dwCreationFlags | CREATE_SUSPENDED; + + BOOL blacklisted = context->executableBlacklisted(lpApplicationName, lpCommandLine); + + BOOL res = CreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, + lpThreadAttributes, bInheritHandles, flags, lpEnvironment, + lpCurrentDirectory, lpStartupInfo, lpProcessInformation); + if (!res) { + spdlog::get("usvfs")->error("failed to spawn {}", + ush::string_cast<std::string>(lpCommandLine)); + return FALSE; + } + + if (!blacklisted) { + std::wstring applicationDirPath = winapi::wide::getModuleFileName(dllModule); + boost::filesystem::path p(applicationDirPath); + try { + usvfs::injectProcess(p.parent_path().wstring(), context->callParameters(), + *lpProcessInformation); + } catch (const std::exception& e) { + spdlog::get("usvfs")->error("failed to inject: {}", e.what()); + logExtInfo(e, LogLevel::Error); + ::TerminateProcess(lpProcessInformation->hProcess, 1); + ::SetLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + } + + if (!susp) { + ResumeThread(lpProcessInformation->hThread); + } + + return TRUE; +} + +BOOL WINAPI usvfsCreateVFSDump(LPSTR buffer, size_t* size) +{ + assert(size != nullptr); + std::ostringstream output; + usvfs::shared::dumpTree(output, *context->redirectionTable().get()); + std::string str = output.str(); + if ((buffer != NULL) && (*size > 0)) { + strncpy_s(buffer, *size, str.c_str(), _TRUNCATE); + } + bool success = *size >= str.length(); + *size = str.length(); + return success ? TRUE : FALSE; +} + +VOID WINAPI usvfsBlacklistExecutable(LPCWSTR executableName) +{ + context->blacklistExecutable(executableName); +} + +VOID WINAPI usvfsClearExecutableBlacklist() +{ + context->clearExecutableBlacklist(); +} + +VOID WINAPI usvfsAddSkipFileSuffix(LPCWSTR fileSuffix) +{ + context->addSkipFileSuffix(fileSuffix); +} + +VOID WINAPI usvfsClearSkipFileSuffixes() +{ + context->clearSkipFileSuffixes(); +} + +VOID WINAPI usvfsAddSkipDirectory(LPCWSTR directory) +{ + context->addSkipDirectory(directory); +} + +VOID WINAPI usvfsClearSkipDirectories() +{ + context->clearSkipDirectories(); +} + +VOID WINAPI usvfsForceLoadLibrary(LPCWSTR processName, LPCWSTR libraryPath) +{ + context->forceLoadLibrary(processName, libraryPath); +} + +VOID WINAPI usvfsClearLibraryForceLoads() +{ + context->clearLibraryForceLoads(); +} + +VOID WINAPI usvfsPrintDebugInfo() +{ + spdlog::get("usvfs")->warn("===== debug {} =====", + context->redirectionTable().shmName()); + void* buffer = nullptr; + size_t bufferSize = 0; + context->redirectionTable().getBuffer(buffer, bufferSize); + std::ostringstream temp; + for (size_t i = 0; i < bufferSize; ++i) { + temp << std::hex << std::setfill('0') << std::setw(2) + << (unsigned)reinterpret_cast<char*>(buffer)[i] << " "; + if ((i % 16) == 15) { + spdlog::get("usvfs")->info("{}", temp.str()); + temp.str(""); + temp.clear(); + } + } + if (!temp.str().empty()) { + spdlog::get("usvfs")->info("{}", temp.str()); + } + spdlog::get("usvfs")->warn("===== / debug {} =====", + context->redirectionTable().shmName()); +} + +const char* WINAPI usvfsVersionString() +{ + return USVFS_VERSION_STRING; +} + +// +// DllMain +// + +BOOL APIENTRY DllMain(HMODULE module, DWORD reasonForCall, LPVOID) +{ + switch (reasonForCall) { + case DLL_PROCESS_ATTACH: { + dllModule = module; + } break; + case DLL_PROCESS_DETACH: { + if (exceptionHandler) + ::RemoveVectoredExceptionHandler(exceptionHandler); + } break; + case DLL_THREAD_ATTACH: { + } break; + case DLL_THREAD_DETACH: { + } break; + } + + return TRUE; +} diff --git a/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp b/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp new file mode 100644 index 0000000..7059ec2 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp @@ -0,0 +1,193 @@ +#include "usvfsparametersprivate.h" +#include <algorithm> + +usvfsParameters::usvfsParameters() + : debugMode(false), logLevel(LogLevel::Debug), crashDumpsType(CrashDumpsType::None), + delayProcessMs(0) +{ + std::fill(std::begin(instanceName), std::end(instanceName), 0); + std::fill(std::begin(currentSHMName), std::end(currentSHMName), 0); + std::fill(std::begin(currentInverseSHMName), std::end(currentInverseSHMName), 0); + std::fill(std::begin(crashDumpsPath), std::end(crashDumpsPath), 0); +} + +usvfsParameters::usvfsParameters(const char* instanceName, const char* currentSHMName, + const char* currentInverseSHMName, bool debugMode, + LogLevel logLevel, CrashDumpsType crashDumpsType, + const char* crashDumpsPath, int delayProcessMs) + : usvfsParameters() +{ + strncpy_s(this->instanceName, instanceName, _TRUNCATE); + strncpy_s(this->currentSHMName, currentSHMName, _TRUNCATE); + strncpy_s(this->currentInverseSHMName, currentInverseSHMName, _TRUNCATE); + this->debugMode = debugMode; + this->logLevel = logLevel; + this->crashDumpsType = crashDumpsType; + strncpy_s(this->crashDumpsPath, crashDumpsPath, _TRUNCATE); + this->delayProcessMs = delayProcessMs; +} + +usvfsParameters::usvfsParameters(const USVFSParameters& oldParams) + : usvfsParameters(oldParams.instanceName, oldParams.currentSHMName, + oldParams.currentInverseSHMName, oldParams.debugMode, + oldParams.logLevel, oldParams.crashDumpsType, + oldParams.crashDumpsPath, 0) +{} + +void usvfsParameters::setInstanceName(const char* name) +{ + strncpy_s(instanceName, name, _TRUNCATE); + strncpy_s(currentSHMName, 60, name, _TRUNCATE); + memset(currentInverseSHMName, '\0', _countof(currentInverseSHMName)); + _snprintf(currentInverseSHMName, 60, "inv_%s", name); +} + +void usvfsParameters::setDebugMode(bool b) +{ + debugMode = b; +} + +void usvfsParameters::setLogLevel(LogLevel level) +{ + logLevel = level; +} + +void usvfsParameters::setCrashDumpType(CrashDumpsType type) +{ + crashDumpsType = type; +} + +void usvfsParameters::setCrashDumpPath(const char* path) +{ + if (path && *path && strlen(path) < _countof(crashDumpsPath)) { + memcpy(crashDumpsPath, path, strlen(path) + 1); + } else { + // crashDumpsPath invalid or overflow of USVFSParameters variable so disable + // crash dumps: + crashDumpsPath[0] = 0; + crashDumpsType = CrashDumpsType::None; + } +} + +void usvfsParameters::setProcessDelay(int milliseconds) +{ + delayProcessMs = milliseconds; +} + +extern "C" +{ + + const char* usvfsLogLevelToString(LogLevel lv) + { + switch (lv) { + case LogLevel::Debug: + return "debug"; + + case LogLevel::Info: + return "info"; + + case LogLevel::Warning: + return "warning"; + + case LogLevel::Error: + return "error"; + + default: + return "unknown"; + } + } + + const char* usvfsCrashDumpTypeToString(CrashDumpsType t) + { + switch (t) { + case CrashDumpsType::None: + return "none"; + + case CrashDumpsType::Mini: + return "mini"; + + case CrashDumpsType::Data: + return "data"; + + case CrashDumpsType::Full: + return "full"; + + default: + return "unknown"; + } + } + + usvfsParameters* usvfsCreateParameters() + { + return new (std::nothrow) usvfsParameters; + } + + usvfsParameters* usvfsDupeParameters(usvfsParameters* p) + { + if (!p) { + return nullptr; + } + + auto* dupe = usvfsCreateParameters(); + if (!dupe) { + return nullptr; + } + + *dupe = *p; + + return dupe; + } + + void usvfsCopyParameters(const usvfsParameters* source, usvfsParameters* dest) + { + *dest = *source; + } + + void usvfsFreeParameters(usvfsParameters* p) + { + delete p; + } + + void usvfsSetInstanceName(usvfsParameters* p, const char* name) + { + if (p) { + p->setInstanceName(name); + } + } + + void usvfsSetDebugMode(usvfsParameters* p, BOOL debugMode) + { + if (p) { + p->setDebugMode(debugMode); + } + } + + void usvfsSetLogLevel(usvfsParameters* p, LogLevel level) + { + if (p) { + p->setLogLevel(level); + } + } + + void usvfsSetCrashDumpType(usvfsParameters* p, CrashDumpsType type) + { + if (p) { + p->setCrashDumpType(type); + } + } + + void usvfsSetCrashDumpPath(usvfsParameters* p, const char* path) + { + if (p) { + p->setCrashDumpPath(path); + } + } + + void usvfsSetProcessDelay(usvfsParameters* p, int milliseconds) + { + if (p) { + p->setProcessDelay(milliseconds); + } + } + +} // extern "C" diff --git a/libs/usvfs/src/usvfs_dll/version.rc b/libs/usvfs/src/usvfs_dll/version.rc new file mode 100644 index 0000000..7dbf8e3 --- /dev/null +++ b/libs/usvfs/src/usvfs_dll/version.rc @@ -0,0 +1,40 @@ +#include "Winver.h" +#include "..\..\include\usvfs\usvfs_version.h" + +#define VER_FILEVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_FILEVERSION_STR USVFS_VERSION_STRING + +#define VER_PRODUCTVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_PRODUCTVERSION_STR USVFS_VERSION_STRING + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_PRODUCTVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "USVFS\0" +#ifdef _WIN64 + VALUE "OriginalFilename", "usvfs_x64.dll\0" +#else + VALUE "OriginalFilename", "usvfs_x86.dll\0" +#endif + VALUE "ProductName", "USVFS\0" + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409L, 1200 + END +END diff --git a/libs/usvfs/src/usvfs_helper/CMakeLists.txt b/libs/usvfs/src/usvfs_helper/CMakeLists.txt new file mode 100644 index 0000000..5e083d9 --- /dev/null +++ b/libs/usvfs/src/usvfs_helper/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(usvfs_helper STATIC inject.h inject.cpp) +target_link_libraries(usvfs_helper PUBLIC shared PRIVATE tinjectlib) +target_include_directories(usvfs_helper PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(usvfs_helper PROPERTIES FOLDER injection) diff --git a/libs/usvfs/src/usvfs_helper/inject.cpp b/libs/usvfs/src/usvfs_helper/inject.cpp new file mode 100644 index 0000000..bb1cb58 --- /dev/null +++ b/libs/usvfs/src/usvfs_helper/inject.cpp @@ -0,0 +1,193 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include <string> +#include <utility> + +#include <spdlog/spdlog.h> + +#include <boost/filesystem.hpp> + +#include "inject.h" +#include <exceptionex.h> +#include <formatters.h> +#include <injectlib.h> +#include <loghelpers.h> +#include <pch.h> +#include <stringcast.h> +#include <stringutils.h> +#include <usvfsparametersprivate.h> +#include <winapi.h> + +namespace ush = usvfs::shared; + +using namespace winapi; + +void usvfs::injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, + const PROCESS_INFORMATION& processInfo) +{ + injectProcess(applicationPath, parameters, processInfo.hProcess, processInfo.hThread); +} + +void usvfs::injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, HANDLE processHandle, + HANDLE threadHandle) +{ + bool proc64 = false; + bool sameBitness = false; + { + SYSTEM_INFO info; + GetSystemInfo(&info); + BOOL wow64; + IsWow64Process(processHandle, &wow64); + if (wow64) { + // process is running under wow64 so it has to be a 32bit process running on 64bit + // windows + proc64 = false; + BOOL temp; + IsWow64Process(GetCurrentProcess(), &temp); + sameBitness = temp == TRUE; + } else { + BOOL selfWow64; + IsWow64Process(GetCurrentProcess(), &selfWow64); + if (selfWow64) { + // WE are a 32 bit process running on 64bit windows. the other process isn't, so + // its 64bit + proc64 = true; + } else { + sameBitness = true; + // we have the same bitness as that other process, but which is it? +#ifdef _WIN64 + proc64 = true; +#else + proc64 = false; +#endif + } + } + } + boost::filesystem::path binPath = boost::filesystem::path(applicationPath); + spdlog::get("usvfs")->info("injecting to process {} with {} bitness", + ::GetProcessId(processHandle), + sameBitness ? "same" : "different"); + + if (sameBitness) { + static constexpr auto USVFS_DLL = +#ifdef _WIN64 + L"usvfs_x64.dll"; +#else + L"usvfs_x86.dll"; +#endif + const auto& preferedDll = binPath / USVFS_DLL; + boost::filesystem::path dllPath = preferedDll; + bool dllFound = boost::filesystem::exists(dllPath); + // support for runing tests using a usvfs dll in lib folder (and proxy under bin): + if (!dllFound && binPath.filename() == L"bin") { + dllPath = binPath.parent_path() / L"lib" / USVFS_DLL; + dllFound = boost::filesystem::exists(dllPath); + } + if (!dllFound) { + USVFS_THROW_EXCEPTION( + file_not_found_error() + << ex_msg(std::string("dll missing: ") + + ush::string_cast<std::string>(preferedDll.wstring()).c_str())); + } + + spdlog::get("usvfs")->info("dll path: {}", dllPath.wstring()); + + InjectLib::InjectDLL(processHandle, threadHandle, dllPath.c_str(), "InitHooks", + ¶meters, sizeof(parameters)); + + spdlog::get("usvfs")->info("injection to same bitness process {} successful", + ::GetProcessId(processHandle)); + } else { + // first try platform specific proxy exe: + static constexpr auto USVFS_PREFERED_EXE = +#ifdef _WIN64 + L"usvfs_proxy_x86.exe"; +#else + L"usvfs_proxy_x64.exe"; +#endif + const auto& preferedExe = binPath / USVFS_PREFERED_EXE; + boost::filesystem::path exePath = preferedExe; + bool exeFound = boost::filesystem::exists(exePath); + // support for runing tests using a usvfs dll in lib folder (and proxy under bin): + if (!exeFound && binPath.filename() == L"lib") { + exePath = binPath.parent_path() / L"bin" / USVFS_PREFERED_EXE; + exeFound = boost::filesystem::exists(exePath); + } + // finally fallback to old proxy naming (but only for 64bit as we don't have a 64bit + // proxy in this case): +#ifdef _WIN64 + if (!exeFound) { + exePath = binPath / L"usvfs_proxy.exe"; + exeFound = boost::filesystem::exists(exePath); + } +#endif + if (!exeFound) { + USVFS_THROW_EXCEPTION(file_not_found_error() << ex_msg( + std::string("usvfs proxy not found: ") + + ush::string_cast<std::string>(preferedExe.wstring()))); + } else + spdlog::get("usvfs")->info("using usvfs proxy: {}", + ush::string_cast<std::string>(preferedExe.wstring())); + // need to use proxy aplication to inject + auto proxyProcess = + std::move(wide::createProcess(exePath.wstring()) + .arg(L"--instance") + .arg(ush::string_cast<std::wstring>(parameters.instanceName)) + .arg(L"--pid") + .arg(GetProcessId(processHandle))); + + if (threadHandle != INVALID_HANDLE_VALUE) { + proxyProcess.arg("--tid").arg(GetThreadId(threadHandle)); + } + process::Result result = proxyProcess(); + if (!result.valid) { + USVFS_THROW_EXCEPTION(unknown_error() + << ex_msg(std::string("failed to start proxy ") + + ush::string_cast<std::string>(exePath.wstring())) + << ex_win_errcode(result.errorCode)); + } else { + // wait for proxy completion. this shouldn't take long, 15 seconds is very + // generous + switch (WaitForSingleObject(result.processInfo.hProcess, 15000)) { + case WAIT_TIMEOUT: { + spdlog::get("usvfs")->debug("proxy timeout"); + TerminateProcess(result.processInfo.hProcess, 1); + USVFS_THROW_EXCEPTION(timeout_error() + << ex_msg(std::string("proxy didn't complete in time"))); + } break; + case WAIT_FAILED: { + spdlog::get("usvfs")->debug("proxy wait failed"); + TerminateProcess(result.processInfo.hProcess, 1); + USVFS_THROW_EXCEPTION(unknown_error() + << ex_msg( + std::string("failed to wait for proxy completion")) + << ex_win_errcode(result.errorCode)); + } break; + default: { + spdlog::get("usvfs")->debug("proxy run successful"); + // nop + } break; + } + } + } +} diff --git a/libs/usvfs/src/usvfs_helper/inject.h b/libs/usvfs/src/usvfs_helper/inject.h new file mode 100644 index 0000000..10f179f --- /dev/null +++ b/libs/usvfs/src/usvfs_helper/inject.h @@ -0,0 +1,51 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "usvfsparameters.h" +#include <string> +#include <windows_sane.h> + +namespace usvfs +{ + +/** + * @brief inject usvfs to a process + * @param applicationPath + * @param parameters + * @param processInfo + */ +void injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, + const PROCESS_INFORMATION& processInfo); + +/** + * @brief inject usvfs to a process + * @param applicationPath path to usvfs + * @param parameters + * @param process process handle to inject to + * @param thread main thread inside that process. This can be set to + * INVALID_HANDLE_VALUE in which case a new thread is created in the process + */ +void injectProcess(const std::wstring& applicationPath, + const usvfsParameters& parameters, HANDLE process, HANDLE thread); + +} // namespace usvfs diff --git a/libs/usvfs/src/usvfs_proxy/CMakeLists.txt b/libs/usvfs/src/usvfs_proxy/CMakeLists.txt new file mode 100644 index 0000000..47614ee --- /dev/null +++ b/libs/usvfs/src/usvfs_proxy/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Boost CONFIG REQUIRED COMPONENTS filesystem) + +add_executable(usvfs_proxy main.cpp version.rc) +target_link_libraries(usvfs_proxy PRIVATE usvfs_dll shared usvfs_helper) +set_target_properties(usvfs_proxy + PROPERTIES + RUNTIME_OUTPUT_NAME usvfs_proxy${ARCH_POSTFIX} + RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_BINDIR} + RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_BINDIR} +) + +install(TARGETS usvfs_proxy EXPORT usvfs${ARCH_POSTFIX}Targets) +install(FILES $<TARGET_PDB_FILE:usvfs_proxy> DESTINATION pdb OPTIONAL) diff --git a/libs/usvfs/src/usvfs_proxy/main.cpp b/libs/usvfs/src/usvfs_proxy/main.cpp new file mode 100644 index 0000000..88d4696 --- /dev/null +++ b/libs/usvfs/src/usvfs_proxy/main.cpp @@ -0,0 +1,221 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "pch.h" +#include <../usvfs_dll/hookcontext.h> +#include <Psapi.h> +#include <WinUser.h> +#include <boost/algorithm/string/predicate.hpp> +#include <boost/filesystem.hpp> +#include <boost/lexical_cast.hpp> +#include <inject.h> +#include <shared_memory.h> +#include <sharedparameters.h> +#include <shmlogger.h> +#include <spdlog/spdlog.h> +#include <usvfsparameters.h> +#include <winapi.h> + +namespace bi = boost::interprocess; +namespace bfs = boost::filesystem; +using usvfs::SharedParameters; +using usvfs::shared::SharedMemoryT; + +template <typename T> +T getParameter(std::vector<std::string>& arguments, const std::string& key, + bool consume) +{ + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) { + T result = boost::lexical_cast<T>(*(iter + 1)); + if (consume) { + arguments.erase(iter, iter + 2); + } + return result; + } else { + throw std::runtime_error(std::string("argument missing " + key)); + } +} + +template <typename T> +T getParameter(std::vector<std::string>& arguments, const std::string& key, + const T& def, bool consume) +{ + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) { + T result = boost::lexical_cast<T>(*(iter + 1)); + if (consume) { + arguments.erase(iter, iter + 2); + } + return result; + } else { + return def; + } +} + +static void exceptionDialog(int line, int num, ...) +{ + va_list args; + va_start(args, num); + + std::wstring wstr; + WCHAR buf[256]; + wstr.append(L"Unhandled USVFS proxy exception (line "); + wsprintf(buf, L"%d): ", line); + wstr.append(buf); + for (int i = 0; i < num; i++) { + wsprintf(buf, L"%S", va_arg(args, const char*)); + if (i < num - 1) + wsprintf(buf, L", "); + wstr.append(buf); + } + + MessageBox(NULL, wstr.data(), NULL, MB_OK); + + va_end(args); +} + +int main(int argc, char** argv) +{ + std::shared_ptr<spdlog::logger> logger; + + std::vector<std::string> arguments; + std::copy(argv + 1, argv + argc, std::back_inserter(arguments)); + + std::string instance; + try { + SHMLogger::open("usvfs"); + logger = spdlog::create<usvfs::sinks::shm_sink>("usvfs", "usvfs"); + logger->set_pattern("%H:%M:%S.%e [%L] (proxy) %v"); + + instance = getParameter<std::string>(arguments, "instance", true); + } catch (const std::exception& e) { + if (logger.get() == nullptr) { + exceptionDialog(__LINE__, 1, e.what()); + return 1; + } + try { + logger->critical("{}", e.what()); + } catch (const spdlog::spdlog_ex& e2) { + exceptionDialog(__LINE__, 2, e.what(), e2.what()); + // no way to log this + } catch (const std::exception&) { + logger->critical(e.what()); + } + return 1; + } + + try { + std::string executable = + getParameter<std::string>(arguments, "executable", "", true); + int pid = getParameter<int>(arguments, "pid", 0, true); + int tid = getParameter<int>(arguments, "tid", 0, true); + + logger->info("instance: {}", instance); + logger->info("exe: {}", executable); + logger->info("pid: {}", pid); + + if (executable.empty() && (pid == 0)) { + logger->warn("not all required settings set"); + return 1; + } + + SharedMemoryT configurationSHM(bi::open_only, instance.c_str()); + if (!configurationSHM.check_sanity()) { + logger->warn("failed to connect to vfs"); + return 1; + } + logger->info("size: {}", configurationSHM.get_size()); + logger->info("addr: {0:p}", configurationSHM.get_address()); + logger->info("objs: {}", configurationSHM.get_num_named_objects()); + std::pair<SharedParameters*, SharedMemoryT::size_type> params = + configurationSHM.find<SharedParameters>("parameters"); + if (params.first == nullptr) { + logger->error("failed to open shared configuration for {}", instance); + return 1; + } + + boost::filesystem::path p(winapi::wide::getModuleFileName(nullptr)); + + if (executable.empty()) { + HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + HANDLE threadHandle = INVALID_HANDLE_VALUE; + if (tid != 0) { + threadHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, tid); + } + + BOOL blacklisted = FALSE; + TCHAR szModName[MAX_PATH]; + + if (GetModuleFileNameEx(processHandle, NULL, szModName, + sizeof(szModName) / sizeof(TCHAR))) { + const auto appName = usvfs::shared::string_cast<std::string>(szModName); + + if (params.first->executableBlacklisted(appName, {})) { + logger->info("not injecting {} as application is blacklisted", appName); + blacklisted = TRUE; + } + } + + if (!blacklisted) { + usvfs::injectProcess(p.parent_path().wstring(), params.first->makeLocal(), + processHandle, threadHandle); + } + } else { + winapi::process::Result process = + winapi::ansi::createProcess(executable) + .arguments(arguments.begin(), arguments.end()) + .workingDirectory(bfs::path(executable).parent_path().string()) + .suspended()(); + + if (!process.valid) { + return 1; + } + + BOOL blacklisted = FALSE; + + if (params.first->executableBlacklisted(executable, {})) { + logger->info("not injecting {} as application is blacklisted", executable); + + blacklisted = TRUE; + } + + if (!blacklisted) { + usvfs::injectProcess(p.parent_path().wstring(), params.first->makeLocal(), + process.processInfo); + } + + ResumeThread(process.processInfo.hThread); + } + + return 0; + } catch (const std::exception& e) { + try { + logger->critical("unhandled exception: {}", e.what()); + logExtInfo(e); + } catch (const spdlog::spdlog_ex& e2) { + // no way to log this + exceptionDialog(__LINE__, 2, e.what(), e2.what()); + } catch (const std::exception&) { + logger->critical(e.what()); + } + } +} diff --git a/libs/usvfs/src/usvfs_proxy/version.rc b/libs/usvfs/src/usvfs_proxy/version.rc new file mode 100644 index 0000000..03652c2 --- /dev/null +++ b/libs/usvfs/src/usvfs_proxy/version.rc @@ -0,0 +1,40 @@ +#include "Winver.h" +#include "..\..\include\usvfs\usvfs_version.h" + +#define VER_FILEVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_FILEVERSION_STR USVFS_VERSION_STRING + +#define VER_PRODUCTVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION +#define VER_PRODUCTVERSION_STR USVFS_VERSION_STRING + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_PRODUCTVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "USVFS Proxy\0" +#ifdef _WIN64 + VALUE "OriginalFilename", "usvfs_proxy_x64.exe\0" +#else + VALUE "OriginalFilename", "usvfs_proxy_x86.exe\0" +#endif + VALUE "ProductName", "USVFS\0" + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409L, 1200 + END +END |
