Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines -20 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an OK change, and getting this "wrong" isn't a huge problem (everything still builds correctly, it just gets installed into a mis-named directory), but it's also a touch fragile since CMAKE_GENERATOR_PLATFORM is only set for Visual Studio builds and only when the -A compiler option is passed.

If we want to make this more robust, one option is to check CMAKE_CXX_COMPILER_ARCHITECTURE_ID instead:

    string(TOUPPER "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}" CLINTERCEPT_ARCH_ID)
    if(CLINTERCEPT_ARCH_ID STREQUAL "ARM64EC")
        set(CLINTERCEPT_PLATFORM_NAME "ARM64EC")
    elseif(CLINTERCEPT_ARCH_ID STREQUAL "ARM64")
        set(CLINTERCEPT_PLATFORM_NAME "ARM64")
    elseif(CMAKE_SIZEOF_VOID_P EQUAL 8)
    ...

Longer-term, I'd like to remove this Windows-specific handling, and just install into an "install" directory by default, similar to the other OSes.

set(CLINTERCEPT_PLATFORM_NAME "x64")
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(CLINTERCEPT_PLATFORM_NAME "Win32")
Expand Down
148 changes: 127 additions & 21 deletions cliloader/cliloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion intercept/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
15 changes: 12 additions & 3 deletions intercept/OS/OS_windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#pragma once

#include <Windows.h>
#include <algorithm>
#include <string>
#include <stdint.h>

Expand Down Expand Up @@ -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 );
Comment on lines +310 to +313

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the \\?\ prefix doesn't work with relative paths or with network paths (\\server\share\folder). Can you please check these cases? We may want to go down this \\?\ codepath selectively, only when path names are too long.

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 );
Comment on lines +318 to +320

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a potential issue here, at least in theory, with the current implementation: The "dump directories" are created by converting the path to UTF-8, but there are still places that open the file with standard C++ APIs. Some possible solutions:

  1. Switch every place that opens a file to use the new utility function, then we will use UTF-8 consistently.
  2. Convert to the "ANSI code page" (CP_ACP) instead, which is consistent with the standard C++ APIs.

(1) is probably the better solution, but it's more code to change than (2).


pos = fileName.find( "/", ++pos );
}
Expand Down
49 changes: 44 additions & 5 deletions intercept/src/cliprof_init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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] = {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we allocate this dynamically (say as an std::vector)? Or, at the very least, handle the case when there are more than 1024 modules? I guess since EnumProcessModules takes the size of the array we won't overwrite memory, but we would silently leave some modules un-patched, which could be very difficult to debug.

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;
}
Expand Down
Loading
Loading