/* 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 . */ #include "ttrampolinepool.h" #include #include // #include #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(pow(2, 30)) - 1; m_AddressMask = std::numeric_limits::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( 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(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( original))); // push original function, as parameter to barrier assembler.mov(ecx, (Ptr) static_cast(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(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(original))); // open the barrier again assembler.mov(eax, (Ptr) static_cast(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(reinterpret_cast(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()}; 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(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(returnAddress)); size_t codeSize = assembler.getCodeSize(); m_MaxTrampolineSize = std::max(m_MaxTrampolineSize, static_cast(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(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(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(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(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(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(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(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(original) + preambleSize); // adjust relative jumps for move to buffer size_t codeSize = assembler.getCodeSize(); m_MaxTrampolineSize = std::max(m_MaxTrampolineSize, static_cast(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(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(preambleSize)); addAbsoluteJump(assembler, reinterpret_cast(original) + preambleSize); // adjust relative jumps for move to buffer size_t codeSize = assembler.getCodeSize(); m_MaxTrampolineSize = std::max(m_MaxTrampolineSize, static_cast(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(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(rounded); if (iter->second.buffers.size() > 0) { // start searching were we last found a buffer lowerEnd = reinterpret_cast(*iter->second.buffers.rbegin()) + sysInfo.dwPageSize; } uintptr_t start = std::max(std::max(lowerEnd, MIN_ALLOC_ADDR), reinterpret_cast(sysInfo.lpMinimumApplicationAddress)); uintptr_t upperEnd = reinterpret_cast(rounded) + m_SearchRange; uintptr_t end = std::min( upperEnd, reinterpret_cast(sysInfo.lpMaximumApplicationAddress)); LPVOID buffer = nullptr; for (uintptr_t cur = start; cur < end; cur += sysInfo.dwPageSize) { buffer = VirtualAlloc(reinterpret_cast(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(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(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