// Copyright Epic Games, Inc. All Rights Reserved. #define UBA_IS_DETOURED_INCLUDE 1 #include "UbaDetoursFunctionsWin.h" #include "UbaDetoursFileMappingTable.h" #include "UbaDetoursApi.h" #if !defined(UBA_USE_MIMALLOC) #define True_malloc malloc #else // Taken from mimalloc/types.h #define MI_ZU(x) x##ULL #define MI_SEGMENT_MASK ((uintptr_t)(MI_SEGMENT_ALIGN - 1)) #define MI_SEGMENT_ALIGN MI_SEGMENT_SIZE #define MI_SEGMENT_SIZE (MI_ZU(1)< #define WIN32_NO_STATUS #include NTSTATUS NtCopyFileChunk(HANDLE Source, HANDLE Dest, HANDLE Event, PIO_STATUS_BLOCK IoStatusBlock, ULONG Length, PULONG SourceOffset, PULONG DestOffset, PULONG SourceKey, PULONG DestKey, ULONG Flags) { return 0; } #define NtCurrentProcess() ((HANDLE)-1) #define DETOURED_FUNCTION(Func) decltype(Func)* True_##Func = ::Func; DETOURED_FUNCTIONS DETOURED_FUNCTIONS_MEMORY DETOURED_FUNCTIONS_MEMORY_NON_WINE #undef DETOURED_FUNCTION #define DETOURED_FUNCTION(Func) void* Local_##Func = ::Func; DETOURED_FUNCTIONS_MEMORY DETOURED_FUNCTIONS_MEMORY_NON_WINE #undef DETOURED_FUNCTION #include "UbaBinaryParser.h" #include "UbaDirectoryTable.h" #include "UbaProcessStats.h" #include "UbaProtocol.h" #include "UbaDetoursPayload.h" #include "UbaApplicationRules.h" #include "UbaDetoursShared.h" #include "UbaDetoursUtilsWin.h" #include "Shlwapi.h" #include #include namespace uba { bool g_useMiMalloc; constexpr u64 g_pageSize = 64*1024; thread_local u32 t_disallowCreateFileDetour = 0; // Set this to 1 to disallow file detour.. note that this will prevent directory cache from properly being updated // Beautiful! cl.exe needs an exact address in that range to be able to map in pch file // So we'll reserve a bigger range than will be requested and give it back when needed. constexpr uintptr_t g_clExeBaseAddress = 0x6bb00000000; constexpr u64 g_clExeBaseAddressSize = 0x400000000; void* g_clExeBaseReservedMemory = 0; HANDLE PseudoHandle = (HANDLE)0xfffffffffffffffe; constexpr int StdOutFd = -2; #if UBA_DEBUG_LOG_ENABLED bool g_debugFileFlushOnWrite = false; void WriteDebug(const char* s, u32 strLen) { DWORD toWrite = (DWORD)strLen; DWORD lastError = GetLastError(); while (true) { DWORD written; if (True_WriteFile((HANDLE)g_debugFile, s, toWrite, &written, NULL)) break; if (GetLastError() != ERROR_IO_PENDING) break;// ExitProcess(1340); // During shutdown this might actually cause error and we just ignore that and break out s += written; toWrite -= written; } if (g_debugFileFlushOnWrite) True_FlushFileBuffers((HANDLE)g_debugFile); SetLastError(lastError); } void FlushDebugLog() { if (isLogging()) True_FlushFileBuffers((HANDLE)g_debugFile); } #endif //#define UBA_PROFILE_DETOURED_CALLS #if defined(UBA_PROFILE_DETOURED_CALLS) #define DETOURED_FUNCTION(name) Timer timer##name; DETOURED_FUNCTIONS DETOURED_FUNCTIONS_MEMORY DETOURED_FUNCTIONS_MEMORY_NON_WINE #undef DETOURED_FUNCTION #define DETOURED_CALL(name) TimerScope _(timer##name) #else #define DETOURED_CALL(name) //DEBUG_LOG(TC(#name)); #endif bool g_isDetachedProcess; bool g_isRunningWine; int g_uiLanguage; u32 g_processId; StringBuffer<256> g_exeDir; const wchar_t* g_virtualCommandLineW; char* g_virtualCommandLineA; constexpr u32 TrackInputsMemCapacity = 512 * 1024; u8* g_trackInputsMem; u32 g_trackInputsBufPos; void SendInput() { u32 left = g_trackInputsBufPos; u32 reserveSize = left; u32 pos = 0; while (left) { RPC_MESSAGE(InputDependencies, log) writer.Write7BitEncoded(reserveSize); reserveSize = 0; u32 toWrite = Min(left, u32(writer.GetCapacityLeft() - sizeof(u32))); writer.WriteU32(toWrite); writer.WriteBytes(g_trackInputsMem + pos, toWrite); writer.Flush(); left -= toWrite; pos += toWrite; } g_trackInputsBufPos = 0; } void TrackInput(const wchar_t* file) { if (!g_trackInputsMem) return; if (g_trackInputsBufPos > TrackInputsMemCapacity - 2048) SendInput(); BinaryWriter w(g_trackInputsMem, g_trackInputsBufPos, TrackInputsMemCapacity); w.WriteString(file); g_trackInputsBufPos = u32(w.GetPosition()); } void SkipTrackInput(const wchar_t* file) { // Just here to easily log out what we are ignoring in terms of input } u8 g_emptyMemoryFileMem; MemoryFile& g_emptyMemoryFile = *new MemoryFile(&g_emptyMemoryFileMem, true); constexpr u64 DetouredHandleMaxCount = 200*1024; // ~200000 handles enough? constexpr u64 DetouredHandleStart = 300000; // Let's hope noone uses the handles starting at 300000! :) constexpr u64 DetouredHandleEnd = DetouredHandleStart + DetouredHandleMaxCount; constexpr u64 DetouredHandlesMemReserve = AlignUp(DetouredHandleMaxCount*sizeof(DetouredHandle), 64*1024); constexpr u64 DetouredHandlesMemStart = 0; MemoryBlock& g_detouredHandleMemoryBlock = *new MemoryBlock(DetouredHandlesMemReserve, (void*)DetouredHandlesMemStart); u64 g_detouredHandlesStart = u64(g_detouredHandleMemoryBlock.memory); u64 g_detouredHandlesEnd = g_detouredHandlesStart + g_detouredHandleMemoryBlock.reserveSize; BlockAllocator g_detouredHandleAllocator(g_detouredHandleMemoryBlock); void* DetouredHandle::operator new(size_t size) { return g_detouredHandleAllocator.Allocate(); } void DetouredHandle::operator delete(void* p) { g_detouredHandleAllocator.Free(p); } inline bool isDetouredHandle(HANDLE h) { return u64(h) >= DetouredHandleStart && u64(h) < DetouredHandleEnd; } inline HANDLE makeDetouredHandle(DetouredHandle* p) { u64 index = (u64(p) - g_detouredHandlesStart) / sizeof(DetouredHandle); UBA_ASSERT(index < DetouredHandleMaxCount); return HANDLE(DetouredHandleStart + index); } inline DetouredHandle& asDetouredHandle(HANDLE h) { u64 index = u64(h) - DetouredHandleStart; u64 p = (index * sizeof(DetouredHandle)) + g_detouredHandlesStart; return *(DetouredHandle*)p; } HANDLE g_stdHandle[3]; HANDLE g_nullFile; struct ListDirectoryHandle { void* operator new(size_t size); void operator delete(void* p); StringKey dirNameKey; DirectoryTable::Directory& dir; int it; Vector fileTableOffsets; HANDLE validateHandle; TString wildcard; const wchar_t* originalName = nullptr; }; constexpr u64 ListDirHandlesRange = 4*1024*1024; MemoryBlock& g_listDirHandleMemoryBlock = *new MemoryBlock(ListDirHandlesRange); constexpr u64 ListDirHandlesStart = DetouredHandleEnd; constexpr u64 ListDirHandlesEnd = ListDirHandlesStart + ListDirHandlesRange/sizeof(ListDirectoryHandle); BlockAllocator& g_listDirectoryHandleAllocator = *new BlockAllocator(g_listDirHandleMemoryBlock); void* ListDirectoryHandle::operator new(size_t size) { return g_listDirectoryHandleAllocator.Allocate(); } void ListDirectoryHandle::operator delete(void* p) { g_listDirectoryHandleAllocator.Free(p); } inline bool isListDirectoryHandle(HANDLE h) { return (u64)h >= ListDirHandlesStart && (u64)h < ListDirHandlesEnd; } inline HANDLE makeListDirectoryHandle(ListDirectoryHandle* p) { return HANDLE(ListDirHandlesStart + (p - (ListDirectoryHandle*)g_listDirHandleMemoryBlock.memory)); } inline ListDirectoryHandle& asListDirectoryHandle(HANDLE h) { return *(((ListDirectoryHandle*)g_listDirHandleMemoryBlock.memory) + (u64(h) - ListDirHandlesStart)); } ReaderWriterLock& g_loadedModulesLock = *new ReaderWriterLock(); UnorderedMap& g_loadedModules = *new UnorderedMap(); u64 g_memoryFileIndexCounter = ~u64(0) - 1000000; // I really hope this will not collide with anything bool g_filesCouldBeCompressed; bool CouldBeCompressedFile(const StringView& fileName) { return g_filesCouldBeCompressed && g_globalRules.FileCanBeCompressed(fileName); } bool CanDetour(const tchar* path) { if (t_disallowDetour || !path) return false; if (path[0] == '\\') { if (path[1] == '\\') { if (path[2] == '.' && path[3] == '\\') // \\.\ - Win32 namespace for files and devices { if (path[5] != ':') // Not file return false; path += 4; } else if (path[2] == '?') // \\?\ - Win32 path prefix to send through unmodified to nt layer { if (path[3] != '\\') return false; path += 4; } else { if (path[2] == '\\' || path[2] == '/' || path[2] == ':' || path[2] == '*' || path[2] == '?' || path[2] == '\"' || path[2] == '<' || path[2] == '>' || path[2] == '|') return false; // Unknown } } else if (path[1] == '?' && path[2] == '?' && path[3] == '\\') { if (path[4] == 'U' && path[5] == 'N' && path[6] == 'C') // All network paths ok? return true; if (path[5] == ':') path += 4; else return false; // Unknown } } if (Equals(path, TC("nul"))) return false; return g_rules->CanDetour(path, g_runningRemote); } struct SuppressCreateFileDetourScope { SuppressCreateFileDetourScope() { ++t_disallowCreateFileDetour; } ~SuppressCreateFileDetourScope() { --t_disallowCreateFileDetour; } }; const wchar_t* HandleToName(const DetouredHandle& dh) { if (dh.fileObject) if (const wchar_t* name = dh.fileObject->fileInfo->name) return name; return L"Unknown"; } BlockAllocator& g_fileObjectAllocator = *new BlockAllocator(g_memoryBlock); MemoryFile::MemoryFile(u8* data, bool localOnly) : baseAddress(data) , isLocalOnly(localOnly) { } MemoryFile::MemoryFile(bool localOnly, u64 reserveSize_, bool isThrowAway_, const tchar* fileName) : isLocalOnly(localOnly) , isThrowAway(isThrowAway_) { if (!isThrowAway) Reserve(reserveSize_, fileName); } void MemoryFile::Reserve(u64 reserveSize_, const tchar* fileName) { UBA_ASSERT(!isThrowAway); reserveSize = reserveSize_; if (isLocalOnly) { baseAddress = (u8*)VirtualAlloc(NULL, reserveSize, MEM_RESERVE, PAGE_READWRITE); if (!baseAddress) FatalError(1354, L"VirtualAlloc failed trying to reserve %llu for %s. (Error code: %u)", reserveSize, fileName, GetLastError()); mappedSize = reserveSize; } #if UBA_ENABLE_ON_DISK_FILE_MAPPINGS else if (fileName && !g_runningRemote) { StringBuffer<> tempFileName; tempFileName.Appendf(L"\\??\\").Append(fileName); if (false) tempFileName.Append(L".uba.tmp"); UNICODE_STRING us; us.Length = USHORT(tempFileName.count*sizeof(tchar)); us.MaximumLength = us.Length; us.Buffer = tempFileName.data; SuppressDetourScope _; { TimerScope ts(g_kernelStats.createFile); OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(&oa, &us, OBJ_CASE_INSENSITIVE, NULL, NULL); IO_STATUS_BLOCK iosb; ACCESS_MASK desiredAccess = GENERIC_READ | GENERIC_WRITE | DELETE | SYNCHRONIZE | FILE_WRITE_ATTRIBUTES; ULONG shareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; ULONG createOptions = FILE_SYNCHRONOUS_IO_NONALERT;// | FILE_DELETE_ON_CLOSE; NTSTATUS status = ZwCreateFile(&mappingHandle.fh, desiredAccess, &oa, &iosb, 0, FILE_ATTRIBUTE_NORMAL, shareAccess, FILE_OVERWRITE_IF, createOptions, 0, 0); if (status != STATUS_SUCCESS) FatalError(1347, L"ZwCreateFile failed to create %s. (Error code: 0x%x)", us.Buffer, status); } FILE_DISPOSITION_INFO info; info.DeleteFile = true; if (!::SetFileInformationByHandle(mappingHandle.fh, FileDispositionInfo, &info, sizeof(info))) FatalError(1347, L"SetFileInformationByHandle failed to set delete-on-close on %s. (Error code: 0x%x)", us.Buffer, GetLastError()); { TimerScope ts(g_kernelStats.createFileMapping); LARGE_INTEGER liMaxSize; liMaxSize.QuadPart = 2; NTSTATUS status = ZwCreateSection(&mappingHandle.mh, SECTION_ALL_ACCESS, 0, &liMaxSize, PAGE_READWRITE, SEC_COMMIT, mappingHandle.fh); if (status != STATUS_SUCCESS) FatalError(1348, L"NtCreateSection failed to reserve %llu. (Error code: 0x%x)", reserveSize, status); } TimerScope ts(g_kernelStats.mapViewOfFile); NTSTATUS status = ZwMapViewOfSection(mappingHandle.mh, NtCurrentProcess(), (void**)&baseAddress, 0, 0, NULL, &reserveSize, 2, MEM_RESERVE, PAGE_READWRITE); if (status != STATUS_SUCCESS) FatalError(1348, L"ZwMapViewOfSection failed trying to reserve %llu. (Error code: 0x%x)", reserveSize, status); mappedSize = reserveSize; } #endif else { HANDLE handle; { mappedSize = 32 * 1024 * 1024; TimerScope ts(g_kernelStats.createFileMapping); handle = True_CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE | SEC_RESERVE, ToHigh(reserveSize), ToLow(reserveSize), NULL); if (!handle) FatalError(1348, L"CreateFileMappingW failed trying to reserve %llu for %s. (Error code: %u)", reserveSize, fileName, GetLastError()); } TimerScope ts(g_kernelStats.mapViewOfFile); baseAddress = (u8*)True_MapViewOfFile(handle, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, mappedSize); if (!baseAddress) FatalError(1353, L"MapViewOfFile failed trying to map %llu for %s. ReservedSize: %llu (Error code: %u)", mappedSize, fileName, reserveSize, GetLastError()); mappingHandle = { 0, handle }; } } void MemoryFile::Unreserve() { if (isLocalOnly) { VirtualFree(baseAddress, 0, MEM_RELEASE); } else { True_UnmapViewOfFile(baseAddress); CloseHandle(mappingHandle.mh); mappingHandle.mh = nullptr; } baseAddress = nullptr; committedSize = 0; } void MemoryFile::Write(DetouredHandle& handle, LPCVOID lpBuffer, u64 nNumberOfBytesToWrite) { u64 newPos = handle.pos + nNumberOfBytesToWrite; if (isThrowAway) { writtenSize = newPos; return; } EnsureCommitted(handle, newPos); memcpy(baseAddress + handle.pos, lpBuffer, nNumberOfBytesToWrite); handle.pos += nNumberOfBytesToWrite; if (writtenSize < newPos) { writtenSize = newPos; isReported = false; } } void MemoryFile::EnsureCommitted(const DetouredHandle& handle, u64 size) { if (isThrowAway) return; if (committedSize >= size) return; if (size > mappedSize) { bool shouldRemap = true; if (size > reserveSize) { if (writtenSize == 0 && !isReported) { u64 newReserve = AlignUp(size, g_pageSize); if (reserveSize) Rpc_WriteLogf(L"TODO: RE-RESERVING MemoryFile. Initial reserve: %llu, New reserve: %llu. Please fix application rules", reserveSize, newReserve); Unreserve(); Reserve(newReserve); shouldRemap = false; } else FatalError(1347, L"Reserved size of %ls is smaller than what is requested to be. ReserveSize: %llu Written: %llu Requested: %llu", HandleToName(handle), reserveSize, writtenSize, size); } if (shouldRemap) Remap(handle, size); } #if UBA_ENABLE_ON_DISK_FILE_MAPPINGS if (mappingHandle.fh) { if (!committedSize) committedSize = size; else committedSize = Min(reserveSize, Max(size, committedSize * 2)); auto lg = ToLargeInteger(committedSize); NtExtendSection(mappingHandle.mh, &lg); } else #endif { u64 toCommit = Min(reserveSize, AlignUp(size - committedSize, g_pageSize)); if (!VirtualAlloc(baseAddress + committedSize, toCommit, MEM_COMMIT, PAGE_READWRITE)) FatalError(1347, L"Failed to ensure virtual memory for %ls trying to commit %llu at %llx. MappedSize: %llu, CommittedSize: %llu RequestedSize: %llu. (%u)", HandleToName(handle), toCommit, baseAddress + committedSize, mappedSize, committedSize, size, GetLastError()); committedSize += toCommit; } } void MemoryFile::Remap(const DetouredHandle& handle, u64 size) { UBA_ASSERT(!mappingHandle.fh); True_UnmapViewOfFile(baseAddress); mappedSize = Min(reserveSize, AlignUp(Max(size, mappedSize * 4), g_pageSize)); TimerScope ts(g_kernelStats.mapViewOfFile); baseAddress = (u8*)True_MapViewOfFile(mappingHandle.mh, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, mappedSize); if (!baseAddress) FatalError(1347, L"MapViewOfFile failed trying to map %llu for %ls. ReservedSize: %llu (Error code: %u)", mappedSize, HandleToName(handle), reserveSize, GetLastError()); } void ToInvestigate(const wchar_t* format, ...) { #if UBA_DEBUG_LOG_ENABLED va_list arg; va_start (arg, format); StringBuffer<> buffer; buffer.Append(format, arg); va_end (arg); DEBUG_LOG(buffer.data); FlushDebugLog(); Rpc_WriteLogf(L"%ls\n", buffer.data); #endif } UBA_NOINLINE void UbaAssert(const tchar* text, const char* file, u32 line, const char* expr, bool allowTerminate, u32 terminateCode, void* context, u32 skipCallstackCount) { SuppressDetourScope _; static CriticalSection cs; ScopedCriticalSection s(cs); #if UBA_DEBUG_LOG_ENABLED FlushDebugLog(); #endif static auto& sb = *new StringBuffer<16*1024>(); WriteAssertInfo(sb.Clear(), text, file, line, expr); Rpc_ResolveCallstack(sb, 3 + skipCallstackCount, context); Rpc_WriteLog(sb.data, sb.count, true, true); #if UBA_DEBUG_LOG_ENABLED FlushDebugLog(); #endif #if UBA_ASSERT_MESSAGEBOX StringBuffer<> title; title.Appendf(L"Assert %ls - pid %u", GetApplicationShortName(), GetCurrentProcessId()); int ret = MessageBoxW(GetConsoleWindow(), sb.data, title.data, MB_ABORTRETRYIGNORE|MB_SYSTEMMODAL); if (ret == IDABORT) ExitProcess(terminateCode); else if (ret == IDRETRY && IsDebuggerPresent()) DebugBreak(); #else if (allowTerminate) ExitProcess(terminateCode); #endif } extern HANDLE g_hostProcess; const wchar_t* HandleToName(HANDLE handle) { if (handle == INVALID_HANDLE_VALUE) return L"INVALID"; if (isListDirectoryHandle(handle)) { #if UBA_DEBUG return asListDirectoryHandle(handle).originalName; #else return L"DIRECTORY"; #endif } if (!isDetouredHandle(handle)) return L"UNKNOWN"; DetouredHandle& dh = asDetouredHandle(handle); if (FileObject* fo = dh.fileObject) if (FileInfo* fi = fo->fileInfo) if (const wchar_t* name = fi->name) return name; return L"DETOURED"; } bool NeedsSharedMemory(const wchar_t* file) { return g_allowKeepFilesInMemory && g_rules->NeedsSharedMemory(file); } u64 FileTypeMaxSize(const StringBufferBase& file, bool isSystemOrTempFile) { return g_rules->FileTypeMaxSize(file, isSystemOrTempFile); } bool EnsureMapped(DetouredHandle& handle, DWORD dwFileOffsetHigh = 0, DWORD dwFileOffsetLow = 0, SIZE_T numberOfBytesToMap = 0, void* baseAddress = nullptr) { FileInfo& info = *handle.fileObject->fileInfo; if (info.memoryFile || info.fileMapMem) return true; u64 offset = ToLargeInteger(dwFileOffsetHigh, dwFileOffsetLow).QuadPart; if (!numberOfBytesToMap) { UBA_ASSERTF(info.size && info.size != InvalidValue || (info.isFileMap && info.size == 0), L"FileInfo file size is bad: %llu", info.size); numberOfBytesToMap = info.size; } u64 alignedOffsetStart = 0; if (info.trueFileMapOffset) { offset += info.trueFileMapOffset; u64 endOffset = offset + numberOfBytesToMap; alignedOffsetStart = AlignUp(offset - (g_pageSize - 1), g_pageSize); u64 alignedOffsetEnd = AlignUp(endOffset, g_pageSize); u64 mapSize = alignedOffsetEnd - alignedOffsetStart; TimerScope ts(g_kernelStats.mapViewOfFile); info.fileMapMem = (u8*)True_MapViewOfFileEx(info.trueFileMapHandle, info.fileMapViewDesiredAccess, ToHigh(alignedOffsetStart), ToLow(alignedOffsetStart), mapSize, baseAddress); // TODO: This is ugly.. but in some cases we have created virtual files pointing into segments of real file // and real file size is not aligned to page size so we need to set map size to 0 to allow it to map up to size of file if (!info.fileMapMem) info.fileMapMem = (u8*)True_MapViewOfFileEx(info.trueFileMapHandle, info.fileMapViewDesiredAccess, ToHigh(alignedOffsetStart), ToLow(alignedOffsetStart), 0, baseAddress); } else { UBA_ASSERTF(!info.freeFileMapOnClose, TC("File %s has been freed because of earlier close and is now reopened (%s)"), info.name, info.originalName); TimerScope ts(g_kernelStats.mapViewOfFile); info.fileMapMem = (u8*)True_MapViewOfFileEx(info.trueFileMapHandle, info.fileMapViewDesiredAccess, 0, 0, numberOfBytesToMap, baseAddress); } if (info.fileMapMem == nullptr || (baseAddress && info.fileMapMem != baseAddress)) { ToInvestigate(L"MapViewOfFileEx failed trying to map %llu bytes on address 0x%llx with offset %llu, using %llu with access %u (%u)", numberOfBytesToMap, u64(baseAddress), alignedOffsetStart, u64(info.trueFileMapHandle), info.fileMapViewDesiredAccess, GetLastError()); return false; } info.fileMapMem += (offset - alignedOffsetStart); info.fileMapMemSize = info.size; DEBUG_LOG_TRUE(L"INTERNAL MapViewOfFileEx", L"(%ls) (size: %llu) (%ls) -> 0x%llx", info.name, numberOfBytesToMap, info.originalName, uintptr_t(info.fileMapMem)); return true; } ReaderWriterLock& g_longPathNameCacheLock = *new ReaderWriterLock(); using LongPathMap = GrowingUnorderedMap; LongPathMap& g_longPathNameCache = *new LongPathMap(g_memoryBlock); void Rpc_AllocFailed(const wchar_t* allocType, u32 error) { RPC_MESSAGE(VirtualAllocFailed, virtualAllocFailed) writer.WriteString(allocType); writer.WriteU32(error); writer.Flush(); Sleep(5*1000); } void CloseCaches() { for (auto& it : g_mappedFileTable.m_lookup) { FileInfo& info = it.second; if (info.fileMapMem) { DEBUG_LOG_TRUE(L"INTERNAL UnmapViewOfFile", L"0x%llx (%ls) (%ls)", uintptr_t(info.fileMapMem), info.name, info.originalName); True_UnmapViewOfFile(info.fileMapMem); } if (info.trueFileMapHandle != 0) { DEBUG_LOG_TRUE(L"INTERNAL CloseHandle", L"%llu (%ls) (%ls)", uintptr_t(info.trueFileMapHandle), info.name, info.originalName); CloseHandle(info.trueFileMapHandle); } // Let them leak if (info.memoryFile && !info.memoryFile->isLocalOnly) { UnmapViewOfFile(info.memoryFile->baseAddress); CloseHandle(info.memoryFile->mappingHandle.mh); CloseHandle(info.memoryFile->mappingHandle.fh); } //if (info.memoryFile && info.memoryFile != &g_emptyMemoryFile) // delete info.memoryFile; } } bool g_exitMessageSent; void SendExitMessage(DWORD exitCode, u64 startTime); void OnModuleLoaded(HMODULE moduleHandle, const StringView& name); // Variables used to communicate state from kernelbase functions to ntdll functions thread_local const wchar_t* t_renameFileNewName; thread_local const wchar_t* t_createFileFileName; const wchar_t* ToString(BOOL b) { return b ? L"Success" : L"Error"; } #include "UbaDetoursFunctionsMiMalloc.inl" #include "UbaDetoursFunctionsNtDll.inl" #include "UbaDetoursFunctionsKernelBase.inl" #include "UbaDetoursFunctionsUcrtBase.inl" #include "UbaDetoursFunctionsImagehlp.inl" #include "UbaDetoursFunctionsDbgHelp.inl" #include "UbaDetoursFunctionsShell32.inl" #include "UbaDetoursFunctionsRpcrt4.inl" extern u32 g_consoleStringIndex; void SendExitMessage(DWORD exitCode, u64 startTime) { if (g_exitMessageSent) return; g_exitMessageSent = true; if (g_consoleStringIndex) Shared_WriteConsole(L"\n", 1, 0); if (g_trackInputsMem) SendInput(); g_stats.detoursMemory = g_memoryBlock.writtenSize; // + g_directoryTable.m_memorySize + g_mappedFileTable.m_memPosition; RPC_MESSAGE(Exit, log) writer.WriteU32(exitCode); writer.WriteString(g_logName); g_stats.detach.time += GetTime() - startTime; g_stats.detach.count = 1; g_stats.Write(writer); g_kernelStats.Write(writer); // We must flush here if this is a child because, // if there is a parent process waiting for this to finish, // the parent might move on before Exit message has been processed on session side writer.Flush(g_isChild); } void DetourAttachFunction(void** trueFunc, void* detouredFunc, const char* funcName) { if (!*trueFunc) return; auto error = DetourAttach(trueFunc, detouredFunc); if (error == NO_ERROR) return; const char* errorString = "Unknown error"; switch (error) { case ERROR_INVALID_BLOCK: errorString = "The function referenced is too small to be detoured."; break; case ERROR_INVALID_HANDLE: errorString = "The ppPointer parameter is NULL or points to a NULL pointer."; break; case ERROR_INVALID_OPERATION: errorString = "No pending transaction exists."; break; case ERROR_NOT_ENOUGH_MEMORY: errorString = "Not enough memory exists to complete the operation."; break; } Rpc_WriteLogf(L"Failed to detour %hs (%hs)", funcName, errorString); ExitProcess(error); } void DetourDetachFunction(void** trueFunc, void* detouredFunc, const char* funcName) { if (!*trueFunc) return; auto error = DetourDetach(trueFunc, detouredFunc); if (error == NO_ERROR) return; Rpc_WriteLogf(L"Failed to detach detoured %hs", funcName); } void DetourTransactionBegin() { LONG error = ::DetourTransactionBegin(); if (error != NO_ERROR) FatalError(1357, L"DetourTransactionBegin failed (%ld)", error); error = ::DetourUpdateThread(GetCurrentThread()); if (error != NO_ERROR) FatalError(1358, L"DetourUpdateThread failed (%ld)", error); } void DetourTransactionCommit() { LONG error = ::DetourTransactionCommit(); if (error != NO_ERROR) FatalError(1343, L"DetourTransactionCommit failed (%ld)", error); } int DetourAttachFunctions(bool runningRemote) { DetourTransactionBegin(); #define DETOURED_FUNCTION(Func) True_##Func = (decltype(True_##Func))GetProcAddress(moduleHandle, #Func); if (HMODULE moduleHandle = GetModuleHandleW(L"kernelbase.dll")) { DETOURED_FUNCTIONS_KERNELBASE } if (HMODULE moduleHandle = GetModuleHandleW(L"kernel32.dll")) { DETOURED_FUNCTIONS_KERNEL32 } if (HMODULE moduleHandle = GetModuleHandleW(L"ntdll.dll")) { DETOURED_FUNCTIONS_NTDLL } if (HMODULE moduleHandle = GetModuleHandleW(L"ucrtbase.dll")) { DETOURED_FUNCTIONS_UCRTBASE if (g_useMiMalloc) { DETOURED_FUNCTIONS_MEMORY if (!g_isRunningWine) { DETOURED_FUNCTIONS_MEMORY_NON_WINE } } } if (HMODULE moduleHandle = GetModuleHandleW(L"shlwapi.dll")) { DETOURED_FUNCTIONS_SHLWAPI } #if UBA_SUPPORT_MSPDBSRV if (HMODULE moduleHandle = GetModuleHandleW(L"rpcrt4.dll")) { DETOURED_FUNCTIONS_RPCRT4 } #endif #undef DETOURED_FUNCTION // Can't attach to these when running through debugger with some vs extensions (Microsoft child process debugging) #if UBA_DEBUG if (IsDebuggerPresent()) { True_CreateProcessW = nullptr; #if defined(DETOURED_INCLUDE_DEBUG) True_CreateProcessA = nullptr; True_CreateProcessAsUserW = nullptr; #endif } #endif #define DETOURED_FUNCTION(Func) DetourAttachFunction((PVOID*)&True_##Func, Detoured_##Func, #Func); DETOURED_FUNCTIONS if (g_useMiMalloc) { DETOURED_FUNCTIONS_MEMORY if (!g_isRunningWine) { DETOURED_FUNCTIONS_MEMORY_NON_WINE } } #undef DETOURED_FUNCTION DetourTransactionCommit(); #if UBA_SUPPORT_MSPDBSRV True2_NdrClientCall2 = True_NdrClientCall2; #endif return 0; } void OnModuleLoaded(HMODULE moduleHandle, const StringView& name) { // SymLoadModuleExW do something bad that cause remote wine to fail everything after this call.. TODO: Revisit if (g_isRunningWine && !True_SymLoadModuleExW && name.Contains(L"dbghelp.dll")) { True_SymLoadModuleExW = (SymLoadModuleExWFunc*)GetProcAddress(moduleHandle, "SymLoadModuleExW"); UBA_ASSERT(True_SymLoadModuleExW); DetourTransactionBegin(); DetourAttachFunction((PVOID*)&True_SymLoadModuleExW, Detoured_SymLoadModuleExW, "SymLoadModuleExW"); DetourTransactionCommit(); } // ImageGetDigestStream is buggy in wine so we have to detour it for ShaderCompileWorker if (g_isRunningWine && !True_ImageGetDigestStream && name.Contains(L"imagehlp.dll")) { True_ImageGetDigestStream = (ImageGetDigestStreamFunc*)GetProcAddress(moduleHandle, "ImageGetDigestStream"); UBA_ASSERT(True_ImageGetDigestStream); DetourTransactionBegin(); DetourAttachFunction((PVOID*)&True_ImageGetDigestStream, Detoured_ImageGetDigestStream, "ImageGetDigestStream"); DetourTransactionCommit(); } // SHGetKnownFolderPath is used by Metal.exe and must always execute on host if (!True_SHGetKnownFolderPath && name.Contains(L"shell32.dll")) { True_SHGetKnownFolderPath = (SHGetKnownFolderPathFunc*)GetProcAddress(moduleHandle, "SHGetKnownFolderPath"); UBA_ASSERT(True_SHGetKnownFolderPath); DetourTransactionBegin(); DetourAttachFunction((PVOID*)&True_SHGetKnownFolderPath, Detoured_SHGetKnownFolderPath, "SHGetKnownFolderPath"); DetourTransactionCommit(); } } int DetourDetachFunctions() { if (g_directoryTable.m_memory) True_UnmapViewOfFile(g_directoryTable.m_memory); if (g_mappedFileTable.m_mem) True_UnmapViewOfFile(g_mappedFileTable.m_mem); //assert(g_wantsOnCloseLookup.empty()); //UBA_ASSERT(g_mappedFileTable.m_memLookup.empty()); CloseCaches(); #define DETOURED_FUNCTION(Func) DetourDetachFunction((PVOID*)&True_##Func, Detoured_##Func, #Func); if (g_useMiMalloc) { DETOURED_FUNCTIONS_MEMORY if (!g_isRunningWine) { DETOURED_FUNCTIONS_MEMORY_NON_WINE } } DETOURED_FUNCTIONS #undef DETOURED_FUNCTION return 0; } extern bool g_reportAllExceptions; void PreInit(const DetoursPayload& payload) { #if UBA_USE_MIMALLOC //mi_option_enable(mi_option_large_os_pages); mi_option_disable(mi_option_abandoned_page_reset); #endif InitSharedVariables(); g_reportAllExceptions = payload.reportAllExceptions; g_rulesIndex = payload.rulesIndex; g_rules = GetApplicationRules()[payload.rulesIndex].rules; g_useMiMalloc = payload.useCustomAllocator; g_runningRemote = payload.runningRemote; g_isChild = payload.isChild; g_allowKeepFilesInMemory = payload.allowKeepFilesInMemory; g_allowOutputFiles = g_allowKeepFilesInMemory && payload.allowOutputFiles; g_suppressLogging = payload.suppressLogging; g_isDetachedProcess = g_rules->AllowDetach(); g_isRunningWine = payload.isRunningWine; g_uiLanguage = payload.uiLanguage; if (g_isRunningWine) // There are crashes when running in Wine and really hard to debug { //g_useMiMalloc = false; //g_checkRtlHeap = false; } #if UBA_DEBUG_VALIDATE if (g_runningRemote) g_validateFileAccess = false; #endif { if (!payload.logFile.IsEmpty()) { g_logName.Append(payload.logFile); HANDLE debugFile = CreateFileW(payload.logFile.data, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); #if UBA_DEBUG_LOG_ENABLED g_debugFile = (FileHandle)(u64)debugFile; #else if (debugFile != INVALID_HANDLE_VALUE) { const char str[] = "Run in debug to get this file populated"; DWORD written = 0; WriteFile(debugFile, str, sizeof(str), &written, NULL); CloseHandle(debugFile); } #endif } } if (g_runningRemote) { ULONG languageCount = 1; wchar_t languageBuffer[6]; swprintf_s(languageBuffer, 6, L"%04x", g_uiLanguage); languageBuffer[5] = 0; if (!SetProcessPreferredUILanguages(MUI_LANGUAGE_ID, languageBuffer, &languageCount)) { DEBUG_LOG(L"Failed to set locale"); } } { wchar_t exeFullName[256]; if (!GetModuleFileNameW(NULL, exeFullName, sizeof_array(exeFullName))) FatalError(1350, L"GetModuleFileNameW failed (%u)", GetLastError()); wchar_t* lastSlash = wcsrchr(exeFullName, '\\'); *lastSlash = 0; FixPath(g_exeDir, exeFullName); g_exeDir.Append('\\'); } // Special cl.exe handling.. this is needed for compiles using pch files where this address _must_ be available. if (payload.rulesIndex == SpecialRulesIndex_ClExe) { g_clExeBaseReservedMemory = VirtualAlloc((void*)g_clExeBaseAddress, g_clExeBaseAddressSize, MEM_RESERVE, PAGE_READWRITE); DEBUG_LOG(L"Reserving %llu bytes at 0x%llx for cl.exe", g_clExeBaseAddressSize, g_clExeBaseAddress); if (!g_clExeBaseReservedMemory) FatalError(1349, L"Failed to reserve memory for cl.exe (%u)", GetLastError()); } if (const tchar* const* preloads = g_rules->LibrariesToPreload()) for (auto it = preloads; *it; ++it) if (!LoadLibraryExW(*it, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)) FatalError(1351, L"Failed to preload %s (%u)", *it, GetLastError()); } void Init(const DetoursPayload& payload, u64 startTime) { AddExceptionHandler(); DetourAttachFunctions(g_runningRemote); if (!g_isDetachedProcess) { // If GetStdHandle returns 0 it is likely that there is a parent process and that one has detached process set (which means now conhost is created) // .. solve this by detaching this process too HANDLE stdoutHandle = True_GetStdHandle(STD_OUTPUT_HANDLE); if (stdoutHandle == 0) { g_isDetachedProcess = true; } else { HANDLE stderrHandle = True_GetStdHandle(STD_ERROR_HANDLE); g_stdHandle[0] = GetFileType(stderrHandle) == FILE_TYPE_CHAR ? stderrHandle : 0; g_stdHandle[1] = GetFileType(stdoutHandle) == FILE_TYPE_CHAR ? stdoutHandle : 0; } } if (g_isDetachedProcess) { g_stdHandle[0] = makeDetouredHandle(new DetouredHandle(HandleType_StdErr)); // STD_ERR g_stdHandle[1] = makeDetouredHandle(new DetouredHandle(HandleType_StdOut)); // STD_OUT g_stdHandle[2] = makeDetouredHandle(new DetouredHandle(HandleType_StdIn)); // STD_IN } g_processId = payload.processId; if (payload.trackInputs) g_trackInputsMem = (u8*)g_memoryBlock.Allocate(TrackInputsMemCapacity, 1, L"TrackInputs"); g_systemRoot.count = GetEnvironmentVariableW(L"SystemRoot", g_systemRoot.data, g_systemRoot.capacity); g_systemRoot.MakeLower(); wchar_t systemTemp[256]; GetEnvironmentVariableW(L"TEMP", systemTemp, 256); FixPath(g_systemTemp, systemTemp); StringBuffer<512> applicationBuffer; StringBuffer<512> workingDirBuffer; HANDLE directoryTableHandle; u32 directoryTableSize; u32 directoryTableCount; HANDLE mappedFileTableHandle; u32 mappedFileTableSize; u32 mappedFileTableCount; { RPC_MESSAGE(Init, init) writer.Flush(); BinaryReader reader; g_echoOn = reader.ReadBool(); g_isChild = reader.ReadBool(); reader.ReadString(applicationBuffer); reader.ReadString(workingDirBuffer); directoryTableHandle = FileMappingHandle::FromU64(reader.ReadU64()).mh; directoryTableSize = reader.ReadU32(); directoryTableCount = reader.ReadU32(); mappedFileTableHandle = FileMappingHandle::FromU64(reader.ReadU64()).mh; mappedFileTableSize = reader.ReadU32(); mappedFileTableCount = reader.ReadU32(); if (u16 vfsSize = reader.ReadU16()) { BinaryReader vfsReader(reader.GetPositionData(), 0, vfsSize); PopulateVfs(vfsReader); } DEBUG_LOG_PIPE(L"Init", L""); } TrackInput(applicationBuffer.data); VirtualizePath(applicationBuffer); VirtualizePath(workingDirBuffer); VirtualizePath(g_exeDir); Shared_SetCurrentDirectory(workingDirBuffer.data); { FixPath(applicationBuffer.data, g_virtualWorkingDir.data, g_virtualWorkingDir.count, g_virtualApplication); if (const wchar_t* lastBackslash = g_virtualApplication.Last('\\')) g_virtualApplicationDir.Append(g_virtualApplication.data, (lastBackslash + 1 - g_virtualApplication.data)); else FatalError(4444, L"What the heck: %s (%s)", g_virtualApplication.data, applicationBuffer.data); } const wchar_t* cmdLine = True_GetCommandLineW(); const wchar_t* exePos; if (g_runningRemote && Contains(cmdLine, g_exeDir.data, true, &exePos)) { StringBuffer<> buf; buf.Append(cmdLine, exePos - cmdLine); buf.Append(g_virtualApplicationDir); TString realCmdLine(buf.data); realCmdLine += (cmdLine + g_exeDir.count + 1); g_virtualCommandLineW = g_memoryBlock.Strdup(realCmdLine).data; } //else // g_virtualCommandLineW = g_memoryBlock.Strdup(cmdLine); if (g_virtualCommandLineW) { u64 len = wcslen(g_virtualCommandLineW); g_virtualCommandLineA = (char*)g_memoryBlock.Allocate(len + 1, 1, L""); size_t res; wcstombs_s(&res, g_virtualCommandLineA, len + 1, g_virtualCommandLineW, len); } #if UBA_DEBUG_LOG_ENABLED if (isLogging()) { if (g_virtualCommandLineW) cmdLine = g_virtualCommandLineW; u64 cmdLineLen = wcslen(cmdLine); wchar_t temp[LogBufSize - 10]; if (cmdLineLen > sizeof_array(temp)) { memcpy(temp, cmdLine, sizeof(temp)); temp[sizeof_array(temp)-1] = 0; cmdLine = temp; } DEBUG_LOG(L"ProcessId: %u", payload.processId); DEBUG_LOG(L"Cmdline: %ls", cmdLine); DEBUG_LOG(L"WorkingDir: %ls", g_virtualWorkingDir.data); DEBUG_LOG(L"ExeDir: %ls", g_virtualApplicationDir.data); DEBUG_LOG(L"ExeDir (true): %ls", g_exeDir.data); DEBUG_LOG(L"Rules: %u (%u)", g_rules->index, GetApplicationRules()[g_rules->index].hash); if (g_runningRemote) { StringBuffer<256> computerName; GetComputerNameW(computerName); DEBUG_LOG(L"Remote: %s", computerName.data); } LogVfsInfo(); } #endif if (!True_DuplicateHandle(g_hostProcess, mappedFileTableHandle, GetCurrentProcess(), &mappedFileTableHandle, 0, FALSE, DUPLICATE_SAME_ACCESS)) UBA_ASSERTF(false, L"Failed to duplicate filetable handle (%u)", GetLastError()); u8* mappedFileTableMem; { TimerScope ts(g_kernelStats.mapViewOfFile); mappedFileTableMem = (u8*)True_MapViewOfFile(mappedFileTableHandle, FILE_MAP_READ, 0, 0, 0); UBA_ASSERT(mappedFileTableMem); } { TimerScope ts2(g_stats.fileTable); g_mappedFileTable.Init(mappedFileTableMem, mappedFileTableCount, mappedFileTableSize); } if (!True_DuplicateHandle(g_hostProcess, directoryTableHandle, GetCurrentProcess(), &directoryTableHandle, 0, FALSE, DUPLICATE_SAME_ACCESS)) UBA_ASSERTF(false, L"Failed to duplicate directorytable handle (%u)", GetLastError()); u8* directoryTableMem; { TimerScope ts(g_kernelStats.mapViewOfFile); directoryTableMem = (u8*)True_MapViewOfFile(directoryTableHandle, FILE_MAP_READ, 0, 0, 0); UBA_ASSERT(directoryTableMem); } { TimerScope ts2(g_stats.dirTable); g_directoryTable.Init(directoryTableMem, directoryTableCount, directoryTableSize); } if (g_isChild) Rpc_GetWrittenFiles(); g_stats.attach.time += GetTime() - startTime; g_stats.attach.count = 1; g_filesCouldBeCompressed = payload.readIntermediateFilesCompressed && g_rules->CanDependOnCompressedFiles(); } void Deinit(u64 startTime) { if (g_isRunningWine) // mt.exe etc fails if detaching is not done during shutdown { DetourTransactionBegin(); DetourDetachFunctions(); ::DetourTransactionCommit(); // Ignore errors } #if defined(UBA_PROFILE_DETOURED_CALLS) #define DETOURED_FUNCTION(name) if (timer##name.count != 0) { char sb[1024]; sprintf_s(sb, sizeof(sb), "%s: %u %llu\n", #name, timer##name.count.load(), TimeToMs(timer##name.time.load())); WriteDebug(sb); } DETOURED_FUNCTIONS DETOURED_FUNCTIONS_MEMORY DETOURED_FUNCTIONS_MEMORY_NON_WINE #undef DETOURED_FUNCTION #endif DWORD exitCode = STILL_ACTIVE; if (!True_GetExitCodeProcess(GetCurrentProcess(), &exitCode)) exitCode = STILL_ACTIVE; if (!g_exitMessageSent) SendExitMessage(exitCode, startTime); // This should never happen? ExitProcess is always called after main function } void PostDeinit() { DEBUG_LOG(L"Finished"); #if UBA_DEBUG_LOG_ENABLED if (isLogging()) { FlushDebugLog(); HANDLE debugFile = (HANDLE)g_debugFile; g_debugFile = InvalidFileHandle; CloseHandle(debugFile); } #endif } } // namespace uba extern "C" { using namespace uba; UBA_DETOURED_API u32 UbaSendCustomMessage(const void* send, u32 sendSize, void* recv, u32 recvCapacity) { RPC_MESSAGE(Custom, log) writer.WriteU32(sendSize); writer.WriteBytes(send, sendSize); writer.Flush(); BinaryReader reader; u32 recvSize = reader.ReadU32(); UBA_ASSERT(recvSize < recvCapacity); reader.ReadBytes(recv, recvSize); return recvSize; } UBA_DETOURED_API bool UbaFlushWrittenFiles() { RPC_MESSAGE(FlushWrittenFiles, log) writer.Flush(); BinaryReader reader; return reader.ReadBool(); } UBA_DETOURED_API bool UbaUpdateEnvironment(const wchar_t* reason, bool resetStats) { { RPC_MESSAGE(UpdateEnvironment, log) writer.WriteString(reason ? reason : L""); writer.WriteBool(resetStats); writer.Flush(); BinaryReader reader; if (!reader.ReadBool()) return false; } Rpc_UpdateTables(); return true; } UBA_DETOURED_API bool UbaRunningRemote() { return g_runningRemote; } UBA_DETOURED_API bool UbaRequestNextProcess(u32 prevExitCode, wchar_t* outArguments, u32 outArgumentsCapacity) { #if UBA_DEBUG_LOG_ENABLED FlushDebugLog(); #endif *outArguments = 0; bool newProcess; { RPC_MESSAGE(GetNextProcess, log) writer.WriteU32(prevExitCode); g_stats.Write(writer); g_kernelStats.Write(writer); writer.Flush(); BinaryReader reader; newProcess = reader.ReadBool(); if (newProcess) { reader.ReadString(outArguments, outArgumentsCapacity); reader.SkipString(); // workingDir reader.SkipString(); // description reader.ReadString(g_logName.Clear()); } } if (newProcess) { g_kernelStats = {}; g_stats = {}; #if UBA_DEBUG_LOG_ENABLED SuppressCreateFileDetourScope scope; HANDLE debugFile = (HANDLE)g_debugFile; g_debugFile = InvalidFileHandle; CloseHandle(debugFile); debugFile = CreateFileW(g_logName.data, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); g_debugFile = (FileHandle)(u64)debugFile; #endif } Rpc_UpdateTables(); return newProcess; } }