From fcec9060d925d03dac8b3ec75143bb3e62d58549 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Wed, 7 Oct 2026 11:48:08 +0530 Subject: [PATCH] Windows: ARM64EC support, long paths, and indirect OpenCL import patching - Detect ARM64/ARM64EC platforms in CMake via CMAKE_GENERATOR_PLATFORM. - cliloader: compare PE header machine types instead of IsWow64Process, which cannot distinguish ARM64EC from ARM64. Warn and continue if a type is unknown. - cliloader: while loading the intercept DLL into the loader process, disable the controls that create log, trace, and report files, then restore them so the target process does not inherit the overrides. - Skip the report at shutdown if no enqueues occurred. - Patch the IAT of every loaded module so DLLs that import opencl.dll indirectly (e.g. SYCL runtimes) are also intercepted. Links Psapi. - Add Utils::OpenOutputFile() and use it for all dump files. Together with CreateDirectoryW for dump directories, this uses the \?\ prefix on Windows to support paths longer than MAX_PATH. --- CMakeLists.txt | 6 +- cliloader/cliloader.cpp | 148 ++++++++++++++++++++++++++++----- intercept/CMakeLists.txt | 2 +- intercept/OS/OS_windows.h | 15 +++- intercept/src/cliprof_init.cpp | 49 +++++++++-- intercept/src/intercept.cpp | 47 +++++++---- intercept/src/utils.cpp | 28 +++++++ intercept/src/utils.h | 8 ++ 8 files changed, 257 insertions(+), 46 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f9e2039..da62e44b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,11 @@ if(NOT CMAKE_BUILD_TYPE) endif() if(WIN32) - if(CMAKE_SIZEOF_VOID_P EQUAL 8) + if(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64") + set(CLINTERCEPT_PLATFORM_NAME "ARM64") + elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64EC") + set(CLINTERCEPT_PLATFORM_NAME "ARM64EC") + elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) set(CLINTERCEPT_PLATFORM_NAME "x64") elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) set(CLINTERCEPT_PLATFORM_NAME "Win32") diff --git a/cliloader/cliloader.cpp b/cliloader/cliloader.cpp index 0afac23f..51a62aa0 100644 --- a/cliloader/cliloader.cpp +++ b/cliloader/cliloader.cpp @@ -20,6 +20,58 @@ bool debug = false; static std::string commandLine = ""; +// Read the PE header machine type from a file on disk. +static USHORT getFileMachineType(const char* filePath) +{ + HANDLE hFile = CreateFileA( + filePath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, 0, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + return IMAGE_FILE_MACHINE_UNKNOWN; + } + + USHORT machine = IMAGE_FILE_MACHINE_UNKNOWN; + IMAGE_DOS_HEADER dosHeader = {}; + DWORD bytesRead = 0; + + if (ReadFile(hFile, &dosHeader, sizeof(dosHeader), &bytesRead, NULL) && + bytesRead == sizeof(dosHeader) && + dosHeader.e_magic == IMAGE_DOS_SIGNATURE) + { + if (SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN) != INVALID_SET_FILE_POINTER) + { + DWORD ntSignature = 0; + IMAGE_FILE_HEADER fileHeader = {}; + if (ReadFile(hFile, &ntSignature, sizeof(ntSignature), &bytesRead, NULL) && + bytesRead == sizeof(ntSignature) && + ntSignature == IMAGE_NT_SIGNATURE && + ReadFile(hFile, &fileHeader, sizeof(fileHeader), &bytesRead, NULL) && + bytesRead == sizeof(fileHeader)) + { + machine = fileHeader.Machine; + } + } + } + + CloseHandle(hFile); + return machine; +} + +// Get the PE header machine type of a running process by querying its image path. +static USHORT getProcessMachineType(HANDLE hProcess) +{ + char imagePath[MAX_PATH] = {}; + DWORD size = MAX_PATH; + if (!QueryFullProcessImageNameA(hProcess, 0, imagePath, &size)) + { + return IMAGE_FILE_MACHINE_UNKNOWN; + } + DEBUG("process image path: %s\n", imagePath); + return getFileMachineType(imagePath); +} + static bool checkWow64(HANDLE parent, HANDLE child) { BOOL parentWow64 = FALSE; @@ -37,6 +89,42 @@ static bool checkWow64(HANDLE parent, HANDLE child) return false; } + // IsWow64Process cannot distinguish ARM64EC (PE machine type x64) from pure + // ARM64, so compare PE machine types instead. Injecting across the two fails + // because the LoadLibraryA address is not valid in the other process. + USHORT parentMachineType = getProcessMachineType(parent); + USHORT childMachineType = getProcessMachineType(child); + + DEBUG("parent PE machine type: 0x%04X, child PE machine type: 0x%04X\n", + parentMachineType, childMachineType); + + if (parentMachineType != IMAGE_FILE_MACHINE_UNKNOWN && + childMachineType != IMAGE_FILE_MACHINE_UNKNOWN && + parentMachineType != childMachineType) + { + auto machineStr = [](USHORT machine) -> const char* { + switch (machine) { + case IMAGE_FILE_MACHINE_AMD64: return "x64/ARM64EC"; + case IMAGE_FILE_MACHINE_ARM64: return "ARM64"; + case IMAGE_FILE_MACHINE_I386: return "x86"; + default: return "unknown"; + } + }; + fprintf(stderr, "This is the %s version of cliloader, but the target application is a %s application.\n", + machineStr(parentMachineType), + machineStr(childMachineType)); + fprintf(stderr, "Execution will continue, but intercepting and profiling will be disabled.\n"); + return false; + } + else if (parentMachineType == IMAGE_FILE_MACHINE_UNKNOWN || + childMachineType == IMAGE_FILE_MACHINE_UNKNOWN) + { + // Could be false positive, so we deliberately fail open: proceed with injection + // and let any real incompatibility surface as a CreateRemoteThread / + // LoadLibrary failure later + fprintf(stderr, "Warning: could not determine PE machine type for one or both processes, architecture compatibility check was not applied.\n"); + } + return true; } @@ -678,33 +766,51 @@ int main(int argc, char *argv[]) #if defined(_WIN32) - // Get the existing value of the "SuppressLogging" control. - // We will suppress logging while loading the intercept DLL - // into this process, to avoid seeing loading twice. - char* envVal = NULL; - size_t len = 0; - errno_t err = _dupenv_s( &envVal, &len, "CLI_SuppressLogging" ); - DEBUG("CLI_SuppressLogging is currently: %s\n", envVal ? envVal : ""); - - SETENV("CLI_SuppressLogging", "1"); + // Used below to suppress controls that would cause the intercept DLL to + // create dumpdirectories/files while it is loaded into this (cliloader) process; + // the child (profiled) process must not inherit these overrides + class ScopedEnvOverride + { + public: + ScopedEnvOverride(const char* name, const char* overrideVal) : + m_name(name) + { + size_t len = 0; + _dupenv_s(&m_savedValue, &len, name); + SETENV(name, overrideVal); + } + ~ScopedEnvOverride() + { + SETENV(m_name.c_str(), m_savedValue ? m_savedValue : ""); + if (m_savedValue) free(m_savedValue); + } + ScopedEnvOverride(const ScopedEnvOverride&) = delete; + ScopedEnvOverride& operator=(const ScopedEnvOverride&) = delete; + private: + std::string m_name; + char* m_savedValue = NULL; + }; std::string dllpath = path + "\\opencl.dll"; DEBUG("path to OpenCL.dll is: %s\n", dllpath.c_str()); - // First things first. Load the intercept DLL into this process, and - // try to get the function pointer to the init function. If we can't - // do this, there's no need to go further. - HMODULE dll = LoadLibraryA(dllpath.c_str()); - if( dll == NULL ) + HMODULE dll = NULL; { - die("loading DLL"); - } - DEBUG("loaded DLL\n"); + ScopedEnvOverride suppressLogging("CLI_SuppressLogging", "1"); + ScopedEnvOverride logToFile("CLI_LogToFile", "0"); + ScopedEnvOverride chromeCallLogging("CLI_ChromeCallLogging", "0"); + ScopedEnvOverride chromePerformanceTiming("CLI_ChromePerformanceTiming", "0"); + ScopedEnvOverride reportToFile("CLI_ReportToFile", "0"); + + // Load the intercept DLL into this process, and + // try to get the function pointer to the init function + dll = LoadLibraryA(dllpath.c_str()); + if( dll == NULL ) + { + die("loading DLL"); + } + DEBUG("loaded DLL\n"); - SETENV("CLI_SuppressLogging", envVal ? envVal : ""); - if( envVal ) - { - free( envVal ); } LPTHREAD_START_ROUTINE cliprof_init = (LPTHREAD_START_ROUTINE)GetProcAddress( diff --git a/intercept/CMakeLists.txt b/intercept/CMakeLists.txt index fc163848..2d3c6ef1 100644 --- a/intercept/CMakeLists.txt +++ b/intercept/CMakeLists.txt @@ -204,7 +204,7 @@ endif() if(WIN32) target_compile_options(OpenCL PRIVATE /EHsc) - target_link_libraries(OpenCL SetupAPI Shlwapi) + target_link_libraries(OpenCL SetupAPI Shlwapi Psapi) elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_compile_options(OpenCL PRIVATE -Wall) set_target_properties(OpenCL PROPERTIES LINK_FLAGS "-Wl,--version-script -Wl,${CMAKE_CURRENT_SOURCE_DIR}/src/clIntercept.map") diff --git a/intercept/OS/OS_windows.h b/intercept/OS/OS_windows.h index 15324b97..a12282ea 100644 --- a/intercept/OS/OS_windows.h +++ b/intercept/OS/OS_windows.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -306,9 +307,17 @@ inline void Services::MakeDumpDirectories( pos = fileName.find( "/", ++pos ); while( pos != std::string::npos ) { - CreateDirectoryA( - fileName.substr( 0, pos ).c_str(), - NULL ); + // Use the \\?\ prefix with the wide-character API to support paths + // longer than MAX_PATH (260 characters). The \\?\ prefix requires + // backslashes, so convert any forward slashes in the path. + std::string dirPath = "\\\\?\\" + fileName.substr( 0, pos ); + std::replace( dirPath.begin(), dirPath.end(), '/', '\\' ); + int wLen = MultiByteToWideChar( + CP_UTF8, 0, dirPath.c_str(), -1, NULL, 0 ); + std::wstring wDirPath( wLen, 0 ); + MultiByteToWideChar( + CP_UTF8, 0, dirPath.c_str(), -1, &wDirPath[0], wLen ); + CreateDirectoryW( wDirPath.c_str(), NULL ); pos = fileName.find( "/", ++pos ); } diff --git a/intercept/src/cliprof_init.cpp b/intercept/src/cliprof_init.cpp index d30efe5a..6a5863fd 100644 --- a/intercept/src/cliprof_init.cpp +++ b/intercept/src/cliprof_init.cpp @@ -43,23 +43,33 @@ static void replaceFunction( #define REPLACE_FUNCTION( _name, _fname ) if( !strcmp(_name, #_fname) ) { replaceFunction( firstThunk, _fname ); } -extern "C" __declspec(dllexport) -DWORD cliprof_init( void* dummy ) +// Patch the IAT of a single loaded module for all opencl.dll imports. +static void patchModuleIAT( char* base ) { - char *base = (char*)GetModuleHandle(NULL); + // Validate DOS header: + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)(base); + if( dosHeader->e_magic != IMAGE_DOS_SIGNATURE ) + { + return; + } // Get pointer to NT headers: - PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)(base); PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(base + dosHeader->e_lfanew); if( ntHeaders->Signature != IMAGE_NT_SIGNATURE ) { - return false; + return; } // Get pointer to import directory: DWORD importOffset = ntHeaders->OptionalHeader. DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + + if( importOffset == 0 ) + { + return; + } + PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(base + importOffset); @@ -220,6 +230,35 @@ DWORD cliprof_init( void* dummy ) } importDesc++; } +} + +extern "C" __declspec(dllexport) +DWORD cliprof_init( void* dummy ) +{ + // Patch the IAT of all loaded modules, not just the main executable. + // This is necessary for applications that use OpenCL indirectly through + // another DLL (e.g., SYCL runtime DLLs like sycl7.dll) rather than + // importing opencl.dll directly in the main executable. + HMODULE hModules[1024] = {}; + DWORD cbNeeded = 0; + + if( EnumProcessModules( + GetCurrentProcess(), + hModules, + sizeof(hModules), + &cbNeeded ) ) + { + DWORD numModules = cbNeeded / sizeof(HMODULE); + for( DWORD i = 0; i < numModules; i++ ) + { + patchModuleIAT( (char*)hModules[i] ); + } + } + else + { + // Fallback: patch only the main executable. + patchModuleIAT( (char*)GetModuleHandle(NULL) ); + } return 0; } diff --git a/intercept/src/intercept.cpp b/intercept/src/intercept.cpp index 6f5a8c61..687e2f9e 100644 --- a/intercept/src/intercept.cpp +++ b/intercept/src/intercept.cpp @@ -180,7 +180,10 @@ CLIntercept::CLIntercept( void* pGlobalData ) CLIntercept::~CLIntercept() { stopAubCapture( NULL ); - report(); + if( m_EnqueueCounter.load(std::memory_order_relaxed) > 0 ) + { + report(); + } std::lock_guard lock(m_Mutex); @@ -8138,7 +8141,8 @@ void CLIntercept::dumpCaptureReplayKernelSource( errorCode = dispatch().clGetProgramInfo(program, CL_PROGRAM_SOURCE, sourceSize, source.data(), nullptr); if( errorCode == CL_SUCCESS ) { - std::ofstream output(dumpDirectory + "kernel.cl", std::ios::out | std::ios::binary); + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "kernel.cl", std::ios::out | std::ios::binary); output.write(source.data(), sourceSize); } } @@ -8153,7 +8157,8 @@ void CLIntercept::dumpCaptureReplayKernelSource( errorCode = dispatch().clGetProgramInfo(program, CL_PROGRAM_IL, ilSize, il.data(), nullptr); if( errorCode == CL_SUCCESS ) { - std::ofstream output(dumpDirectory + "kernel.spv", std::ios::out | std::ios::binary); + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "kernel.spv", std::ios::out | std::ios::binary); output.write(il.data(), ilSize); } } @@ -8184,7 +8189,8 @@ void CLIntercept::dumpCaptureReplayKernelSource( { for (size_t device = 0; device != num_devices; ++device) { - std::ofstream output(dumpDirectory + "DeviceBinary" + std::to_string(device) + ".bin", std::ios::out | std::ios::binary); + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "DeviceBinary" + std::to_string(device) + ".bin", std::ios::out | std::ios::binary); output.write(reinterpret_cast(binaries[device].data()), binaries[device].size()); } } @@ -8201,7 +8207,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( const size_t* gws, const size_t* lws ) { - std::ofstream output{dumpDirectory + "worksizes.txt"}; + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "worksizes.txt"); // Print the values of the worksizes and offsets on a line in the order: // gws @@ -8236,7 +8243,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( dispatch().clGetContextInfo(context, CL_CONTEXT_DEVICES, sizeof(cl_device_id), &device, nullptr); { - std::ofstream outputBuildOptions{dumpDirectory + "buildOptions.txt"}; + std::ofstream outputBuildOptions; + Utils::OpenOutputFile(outputBuildOptions, dumpDirectory + "buildOptions.txt"); size_t buildOptionsSize = 0; dispatch().clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_OPTIONS, 0, nullptr, &buildOptionsSize); @@ -8251,7 +8259,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( } std::string kernelName = getShortKernelName(kernel); - std::ofstream outputKernelName{dumpDirectory + "kernelName.txt"}; + std::ofstream outputKernelName; + Utils::OpenOutputFile(outputKernelName, dumpDirectory + "kernelName.txt"); outputKernelName << kernelName; cl_uint numArgs = 0; @@ -8259,7 +8268,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( if( numArgs ) { - std::ofstream outputArgTypes{dumpDirectory + "ArgumentDataTypes.txt"}; + std::ofstream outputArgTypes; + Utils::OpenOutputFile(outputArgTypes, dumpDirectory + "ArgumentDataTypes.txt"); for( cl_uint idx = 0; idx != numArgs; ++idx ) { std::string argTypeName; @@ -8287,7 +8297,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( const auto index = arg.first; const auto& value = arg.second; std::string fileName{dumpDirectory + "Argument" + std::to_string(index) + ".bin"}; - std::ofstream out{fileName, std::ios::out | std::ios::binary}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName, std::ios::out | std::ios::binary); out.write(reinterpret_cast(value.data()), value.size()); } @@ -8297,7 +8308,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( const auto index = arg.first; const auto value = arg.second; std::string fileName{dumpDirectory + "Local" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << std::to_string(value); } @@ -8310,7 +8322,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( { const SImageInfo& info = m_ImageInfoMap[ value ]; std::string fileName{dumpDirectory + "Image_MetaData_" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << info.Region[0] << '\n' << info.Region[1] << '\n' << info.Region[2] << '\n' @@ -8324,7 +8337,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( else { std::string fileName{dumpDirectory + "SVM_Arg_Offset_" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << arg.second.Offset << '\n'; } } @@ -8335,7 +8349,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( const auto index = arg.first; const auto& value = arg.second; std::string fileName{dumpDirectory + "Sampler" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << value; } } @@ -9759,12 +9774,14 @@ void CLIntercept::startCaptureReplay( pPythonScript, pythonScriptLength ) ) { - std::ofstream outputPythonScript{fileNamePrefix + "run.py", std::ios::out | std::ios::binary}; + std::ofstream outputPythonScript; + Utils::OpenOutputFile(outputPythonScript, fileNamePrefix + "run.py", std::ios::out | std::ios::binary); outputPythonScript.write(pPythonScript, pythonScriptLength); } { - std::ofstream outputKernelNumber{fileNamePrefix + "enqueueNumber.txt"}; + std::ofstream outputKernelNumber; + Utils::OpenOutputFile(outputKernelNumber, fileNamePrefix + "enqueueNumber.txt"); outputKernelNumber << std::to_string(enqueueCounter) << '\n'; } diff --git a/intercept/src/utils.cpp b/intercept/src/utils.cpp index 69c5d8af..34ab9eac 100644 --- a/intercept/src/utils.cpp +++ b/intercept/src/utils.cpp @@ -10,6 +10,10 @@ #include #include +#if defined(_WIN32) +#include +#endif + #ifdef __cpp_lib_bitops #include #endif @@ -110,4 +114,28 @@ uint32_t CountLeadingZeroes(uint64_t value) #endif } +void OpenOutputFile( + std::ofstream& os, + const std::string& fileName, + std::ios::openmode mode ) +{ +#if defined(_WIN32) + // Use the \\?\ prefix with the wide-character API to support file paths + // longer than MAX_PATH (260 characters). The \\?\ prefix requires + // backslashes, so convert any forward slashes in the path. + { + std::string longPath = "\\\\?\\" + fileName; + std::replace( longPath.begin(), longPath.end(), '/', '\\' ); + int wLen = MultiByteToWideChar( + CP_UTF8, 0, longPath.c_str(), -1, NULL, 0 ); + std::wstring wFileName( wLen, 0 ); + MultiByteToWideChar( + CP_UTF8, 0, longPath.c_str(), -1, &wFileName[0], wLen ); + os.open( wFileName.c_str(), mode ); + } +#else + os.open( fileName.c_str(), mode ); +#endif +} + } // namespace Utils \ No newline at end of file diff --git a/intercept/src/utils.h b/intercept/src/utils.h index 68f1157f..86a40694 100644 --- a/intercept/src/utils.h +++ b/intercept/src/utils.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include namespace Utils @@ -15,4 +16,11 @@ namespace Utils std::string GetUniqueFileName(const std::string& fileName); uint32_t CountLeadingZeroes(uint64_t value); +// Opens an output file stream, using the \\?\ long-path prefix on Windows +// to support paths longer than MAX_PATH (260 characters). +void OpenOutputFile( + std::ofstream& os, + const std::string& fileName, + std::ios::openmode mode = std::ios::out ); + }