From 0eca6771338376f12776a0ea3b478bcde739564d Mon Sep 17 00:00:00 2001 From: Rhynier Myburgh Date: Wed, 15 Oct 2025 17:11:31 -0700 Subject: [PATCH 1/2] Add ability to inject startup hook assembly if DOTNET_STARTUP_HOOKS is not configured --- .../cor_profiler.cpp | 417 +++++++++++++++++- .../cor_profiler.h | 6 + .../otel_profiler_constants.h | 5 + .../pal.h | 50 +++ 4 files changed, 477 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp b/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp index fca639ef8d..606704ac2c 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp +++ b/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp @@ -177,7 +177,10 @@ HRESULT STDMETHODCALLTYPE CorProfiler::Initialize(IUnknown* cor_profiler_info_un const auto startup_hooks = GetEnvironmentValues(environment::dotnet_startup_hooks, ENV_VAR_PATH_SEPARATOR); if (!IsStartupHookValid(startup_hooks, home_path)) { - FailProfiler(Error, "The required StartupHook was not configured correctly. No telemetry will be captured.") + Logger::Info("The StartupHook was not configured. Will patch ProcessStartupHooks."); + // TODO: Instead of failing, instrument corelib to add the profiler path to the startup hook + // initialization code. + startup_fix_required = true; } } @@ -692,6 +695,35 @@ HRESULT STDMETHODCALLTYPE CorProfiler::ModuleLoadFinished(ModuleID module_id, HR } #endif + if (module_info.assembly.name == system_private_corelib_assemblyName && startup_fix_required) + { + WSTRING startup_hook_assembly_path = GetModuleFilePath(opentelemetry_autoinstrumentation_startuphook_filepath); + mdTypeDef fixup_type = mdTokenNil; + mdMethodDef patch_startup_hook_method = mdTokenNil; + hr = GenerateHookFixup(module_id, &fixup_type, &patch_startup_hook_method, startup_hook_assembly_path); + + if (FAILED(hr)) + { + Logger::Error("Failed to inject startup hook patch in System.Private.CoreLib, module id ", module_id, ", result ", hr); + } + else + { + hr = ModifyProcessStartupHooks(module_id, patch_startup_hook_method); + + if (FAILED(hr)) + { + Logger::Warn("Failed to patch ProcessStartupHooks injection, module id ", module_id, ", result ", hr); + } + + Logger::Info("Patched ProcessStartupHooks to inject loading ", startup_hook_assembly_path); + } + + if (FAILED(hr)) + { + FailProfiler(Info, "The required StartupHook was not configured correctly. No telemetry will be captured."); + } + } + return S_OK; } @@ -2048,6 +2080,94 @@ HRESULT CorProfiler::ModifyAppDomainCreate(const ModuleID module_id, mdMethodDef return S_OK; } +// Add at the start of System.StartupHookProvider::ProcessStartupHooks(string) +// call to __DDLoaderFixup__::__DDPatchStartupHookValue__ passing the startupHooks argument by ref there. +HRESULT CorProfiler::ModifyProcessStartupHooks(const ModuleID module_id, mdMethodDef patch_startup_hook_method) +{ + // Expects to be called on System.Private.CoreLib only + // patch_startup_hook_method should be pre-injected in System.Private.CoreLib + ComPtr metadata_interfaces; + auto hr = this->info_->GetModuleMetaData(module_id, ofRead | ofWrite, IID_IMetaDataImport2, + metadata_interfaces.GetAddressOf()); + if (FAILED(hr)) + { + Logger::Warn("ModifyProcessStartupHooks: failed to get metadata interface for ", module_id); + return hr; + } + + const auto& metadata_import = metadata_interfaces.As(IID_IMetaDataImport); + + mdTypeDef system_startup_hook_provider_token; + { + hr = metadata_import->FindTypeDefByName(WStr("System.StartupHookProvider"), mdTokenNil, &system_startup_hook_provider_token); + if (FAILED(hr)) + { + Logger::Warn("ModifyProcessStartupHooks: FindTypeDefByName System.StartupHookProvider failed"); + return hr; + } + } + + mdMethodDef system_startup_hook_provider_process_startup_hooks_token; + { + SignatureBuilder::StaticMethod + system_startup_hook_provider_process_startup_hooks_signature{SignatureBuilder::BuiltIn::Void, + {SignatureBuilder::BuiltIn::String}}; + + hr = metadata_import->FindMethod(system_startup_hook_provider_token, WStr("ProcessStartupHooks"), + system_startup_hook_provider_process_startup_hooks_signature.Head(), + system_startup_hook_provider_process_startup_hooks_signature.Size(), + &system_startup_hook_provider_process_startup_hooks_token); + if (FAILED(hr)) + { + Logger::Warn("ModifyProcessStartupHooks: FindMethod System.StartupHookProvider::ProcessStartupHooks failed"); + return hr; + } + + ILRewriter rewriter(this->info_, nullptr, module_id, system_startup_hook_provider_process_startup_hooks_token); + hr = rewriter.Import(); + + if (FAILED(hr)) + { + Logger::Warn("ModifyProcessStartupHooks: ILRewriter.Import System.StartupHookProvider::ProcessStartupHooks failed"); + return hr; + } + + ILInstr* pFirstInstr = rewriter.GetILList()->m_pNext; + ILInstr* pNewInstr = NULL; + + // ldarga.s 0 ; Load the address of the first argument (startupHooks string) + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDARGA_S; + pNewInstr->m_Arg8 = 0; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // call __DDLoaderFixup__::__DDPatchStartupHookValue__ + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_CALL; + pNewInstr->m_Arg32 = patch_startup_hook_method; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + hr = rewriter.Export(); + + if (FAILED(hr)) + { + Logger::Warn("ModifyProcessStartupHooks: ILRewriter.Export System.StartupHookProvider::ProcessStartupHooks failed"); + return hr; + } + + if (IsDumpILRewriteEnabled()) + { + mdToken token = 0; + TypeInfo typeInfo{}; + WSTRING methodName = WStr("ProcessStartupHooks"); + FunctionInfo caller(token, methodName, typeInfo, MethodSignature(), FunctionMethodSignature()); + Logger::Info(GetILCodes("*** ModifyProcessStartupHooks: Modified Code: ", &rewriter, caller, metadata_import)); + } + } + + return S_OK; +} + // clang-format off // This method will generate new type __DDVoidMethodType__ in target module. // C# code for created class: @@ -3450,6 +3570,301 @@ HRESULT CorProfiler::AddIISPreStartInitFlags(const ModuleID module_id, const mdT return S_OK; } +// clang-format off +// This method will generate new type __DDLoaderFixup__ in target module. +// C# code for created class: +// public static class __DDLoaderFixup__ +// { +// public static void __DDPatchStartupHookValue__(ref System.String startupHooks) +// { +// if (startupHooks == null) +// { +// startupHooks = ""; +// } +// else +// { +// startupHooks += ";"; +// } +// } +// } +// clang-format on +HRESULT CorProfiler::GenerateHookFixup(const ModuleID module_id, + mdTypeDef* hook_fixup_type, + mdMethodDef* patch_startup_hook_method, + const WSTRING& startup_hook_dll_name) +{ + const auto& module_info = GetModuleInfo(this->info_, module_id); + if (!module_info.IsValid()) + { + Logger::Warn("GenerateHookFixup: failed to get module info ", module_id); + return E_FAIL; + } + + ComPtr metadata_interfaces; + auto hr = this->info_->GetModuleMetaData(module_id, ofRead | ofWrite, IID_IMetaDataImport2, + metadata_interfaces.GetAddressOf()); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: failed to get metadata interface for ", module_id); + return hr; + } + + const auto& metadata_import = metadata_interfaces.As(IID_IMetaDataImport); + const auto& metadata_emit = metadata_interfaces.As(IID_IMetaDataEmit); + const auto& assembly_emit = metadata_interfaces.As(IID_IMetaDataAssemblyEmit); + + MemberResolver resolver(metadata_import, metadata_emit); + + mdAssemblyRef corlib_ref = mdTokenNil; + // We need assemblyRef only when we generate type outside of mscorlib + if (module_info.assembly.name != mscorlib_assemblyName) + { + hr = GetCorLibAssemblyRef(assembly_emit, corAssemblyProperty, &corlib_ref); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: failed to define AssemblyRef to mscorlib"); + return hr; + } + } + + // TypeDef/TypeRef for System.Object + mdToken system_object_token; + { + hr = resolver.GetTypeRefOrDefByName(corlib_ref, WStr("System.Object"), &system_object_token); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: GetTypeRefOrDefByName System.Object failed"); + return hr; + } + } + + // .class public abstract auto ansi sealed __DDLoaderFixup__ + // extends[mscorlib] System.Object + { + hr = metadata_emit->DefineTypeDef(WStr("__DDLoaderFixup__"), tdAbstract | tdSealed | tdPublic, + system_object_token, NULL, hook_fixup_type); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: DefineTypeDef __DDLoaderFixup__ failed"); + return hr; + } + } + + // TypeDef/TypeRef for System.String + mdToken system_string_token; + { + hr = metadata_import->FindTypeDefByName(WStr("System.String"), mdTokenNil, &system_string_token); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: FindTypeDefByName System.String failed"); + return hr; + } + } + + // .method public hidebysig static void __DDPatchStartupHookValue__(string& startupHooks) cil managed + { + SignatureBuilder::StaticMethod + patch_startup_hook_method_signature{SignatureBuilder::BuiltIn::Void, + {SignatureBuilder::ByRef{SignatureBuilder::BuiltIn::String}}}; + + hr = metadata_emit->DefineMethod(*hook_fixup_type, WStr("__DDPatchStartupHookValue__"), + mdPublic | mdHideBySig | mdStatic, + patch_startup_hook_method_signature.Head(), + patch_startup_hook_method_signature.Size(), 0, 0, + patch_startup_hook_method); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: DefineMethod __DDPatchStartupHookValue__ failed"); + return hr; + } + } + + // Add IL instructions into __DDPatchStartupHookValue__ + { + // MethodRef/MethodDef for string.Concat(string, string) + mdToken string_concat_token; + { + SignatureBuilder::StaticMethod + string_concat_signature{SignatureBuilder::BuiltIn::String, + {SignatureBuilder::BuiltIn::String, + SignatureBuilder::BuiltIn::String}}; + + hr = metadata_import->FindMethod(system_string_token, WStr("Concat"), + string_concat_signature.Head(), + string_concat_signature.Size(), + &string_concat_token); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: FindMethod System.String::Concat failed"); + return hr; + } + } + + // Create a string representing the startup hook DLL name + mdString startup_hook_dll_token; + { + LPCWSTR startup_hook_dll_str = startup_hook_dll_name.c_str(); + auto startup_hook_dll_str_size = startup_hook_dll_name.length(); + + hr = metadata_emit->DefineUserString(startup_hook_dll_str, + (ULONG)startup_hook_dll_str_size, + &startup_hook_dll_token); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: DefineUserString failed for startup hook DLL name"); + return hr; + } + } + + // Create a string representing a path separator (Unix = ":", Windows = ";") + mdString path_separator_token; + { + LPCWSTR path_separator_str = ENV_VAR_PATH_SEPARATOR_STR; + auto path_separator_str_size = wcslen(path_separator_str); + + hr = metadata_emit->DefineUserString(path_separator_str, + (ULONG)path_separator_str_size, + &path_separator_token); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: DefineUserString ; failed"); + return hr; + } + } + + // Generate IL for the method body + // C# equivalent: + // if (startupHooks == null) + // { + // startupHooks = ""; + // } + // else + // { + // startupHooks += ""; + // } + + ILRewriter rewriter(this->info_, nullptr, module_id, *patch_startup_hook_method); + rewriter.InitializeTiny(); + + ILInstr* pFirstInstr = rewriter.GetILList()->m_pNext; + ILInstr* pNewInstr = NULL; + + // Labels for branching + ILInstr* elseLabel = NULL; + ILInstr* endLabel = NULL; + + // ldarg.0 ; Load startupHooks by reference + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDARG_0; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // ldind.ref ; Dereference to get the string value + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDIND_REF; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // brtrue.s elseLabel ; Branch to else if startupHooks is not null + elseLabel = rewriter.NewILInstr(); + elseLabel->m_opcode = CEE_NOP; // Placeholder, will be replaced + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_BRTRUE_S; + pNewInstr->m_pTarget = elseLabel; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // If block: startupHooks = ""; + // ldarg.0 ; Load startupHooks by reference + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDARG_0; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // ldstr "" + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDSTR; + pNewInstr->m_Arg32 = startup_hook_dll_token; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // stind.ref ; Store the string value + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_STIND_REF; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // br.s endLabel ; Branch to end + endLabel = rewriter.NewILInstr(); + endLabel->m_opcode = CEE_NOP; // Placeholder, will be replaced + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_BR_S; + pNewInstr->m_pTarget = endLabel; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // Else block: startupHooks += ""; + // Replace the placeholder else label + elseLabel->m_opcode = CEE_LDARG_0; + rewriter.InsertBefore(pFirstInstr, elseLabel); + + // ldarg.0 ; Load startupHooks by reference for the assignment + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDARG_0; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // ldind.ref ; Dereference to get the current string value + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDIND_REF; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // ldstr "" + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDSTR; + pNewInstr->m_Arg32 = path_separator_token; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // call string.Concat(string, string) - concatenate original with path separator + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_CALL; + pNewInstr->m_Arg32 = string_concat_token; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // ldstr "" + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_LDSTR; + pNewInstr->m_Arg32 = startup_hook_dll_token; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // call string.Concat(string, string) - concatenate result with dll name + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_CALL; + pNewInstr->m_Arg32 = string_concat_token; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // stind.ref ; Store the concatenated string + pNewInstr = rewriter.NewILInstr(); + pNewInstr->m_opcode = CEE_STIND_REF; + rewriter.InsertBefore(pFirstInstr, pNewInstr); + + // End label + // Replace the placeholder end label + endLabel->m_opcode = CEE_RET; + rewriter.InsertBefore(pFirstInstr, endLabel); + + if (IsDumpILRewriteEnabled()) + { + mdToken token = 0; + TypeInfo typeInfo{}; + WSTRING methodName = WStr("__DDPatchStartupHookValue__"); + FunctionInfo caller(token, methodName, typeInfo, MethodSignature(), FunctionMethodSignature()); + Logger::Info(GetILCodes("*** GenerateHookFixup: Modified Code: ", &rewriter, caller, metadata_import)); + } + + hr = rewriter.Export(); + if (FAILED(hr)) + { + Logger::Warn("GenerateHookFixup: Call to ILRewriter.Export() failed for ModuleID=", module_id); + return hr; + } + } + + return S_OK; +} + void CorProfiler::GetAssemblyAndSymbolsBytes(BYTE** pAssemblyArray, int* assemblySize, BYTE** pSymbolsArray, diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.h b/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.h index 2b54636aff..99e5778ac7 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.h +++ b/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.h @@ -45,6 +45,7 @@ class CorProfiler : public CorProfilerBase // Startup helper variables bool first_jit_compilation_completed = false; + bool startup_fix_required = false; bool corlib_module_loaded = false; AppDomainID corlib_app_domain_id = 0; @@ -110,7 +111,12 @@ class CorProfiler : public CorProfilerBase mdTypeDef* loader_type, mdMethodDef* init_method, mdMethodDef* patch_app_domain_setup_method); + HRESULT GenerateHookFixup(const ModuleID module_id, + mdTypeDef* hook_fixup_type, + mdMethodDef* patch_startup_hook_method, + const WSTRING& startup_hook_dll_name); HRESULT ModifyAppDomainCreate(const ModuleID module_id, mdMethodDef patch_app_domain_setup_method); + HRESULT ModifyProcessStartupHooks(const ModuleID module_id, mdMethodDef patch_startup_hook_method); HRESULT AddIISPreStartInitFlags(const ModuleID module_id, const mdToken function_token); #endif diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/otel_profiler_constants.h b/src/OpenTelemetry.AutoInstrumentation.Native/otel_profiler_constants.h index 560d0ffd1f..881f00e429 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/otel_profiler_constants.h +++ b/src/OpenTelemetry.AutoInstrumentation.Native/otel_profiler_constants.h @@ -66,6 +66,11 @@ const WSTRING skip_assemblies[]{WStr("mscorlib"), const WSTRING mscorlib_assemblyName = WStr("mscorlib"); const WSTRING system_private_corelib_assemblyName = WStr("System.Private.CoreLib"); const WSTRING opentelemetry_autoinstrumentation_loader_assemblyName = WStr("OpenTelemetry.AutoInstrumentation.Loader"); +#ifdef _WIN32 +const WSTRING opentelemetry_autoinstrumentation_startuphook_filepath = WStr("net\\OpenTelemetry.AutoInstrumentation.StartupHook.dll"); +#else +const WSTRING opentelemetry_autoinstrumentation_startuphook_filepath = WStr("net/OpenTelemetry.AutoInstrumentation.StartupHook.dll"); +#endif const WSTRING managed_profiler_name = WStr("OpenTelemetry.AutoInstrumentation"); diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/pal.h b/src/OpenTelemetry.AutoInstrumentation.Native/pal.h index 1a408a5b6e..53b086c1bf 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/pal.h +++ b/src/OpenTelemetry.AutoInstrumentation.Native/pal.h @@ -31,9 +31,13 @@ #ifdef _WIN32 #define DIR_SEPARATOR WStr('\\') #define ENV_VAR_PATH_SEPARATOR WStr(';') +#define DIR_SEPARATOR_STR WStr("\\") +#define ENV_VAR_PATH_SEPARATOR_STR WStr(";") #else #define DIR_SEPARATOR WStr('/') #define ENV_VAR_PATH_SEPARATOR WStr(':') +#define DIR_SEPARATOR_STR WStr("/") +#define ENV_VAR_PATH_SEPARATOR_STR WStr(":") #endif namespace trace @@ -132,6 +136,52 @@ inline WSTRING GetCurrentModuleFileName() return EmptyWStr; } +inline WSTRING GetModuleFilePath(const WSTRING& moduleName) +{ + const WSTRING currentModuleFileName = GetCurrentModuleFileName(); + if (currentModuleFileName == EmptyWStr) + { + return EmptyWStr; + } + +#ifdef _WIN32 + // Use std::filesystem for path manipulation on Windows + std::filesystem::path currentPath(currentModuleFileName); + + // Get the parent directory of the parent directory (strip filename and its folder) + std::filesystem::path parentParentDir = currentPath.parent_path().parent_path(); + + // Convert moduleName to string and replace forward slashes with backslashes on Windows + std::string moduleNameStr = ToString(moduleName); + std::replace(moduleNameStr.begin(), moduleNameStr.end(), '/', '\\'); + + // Combine the parent parent directory with the module name + std::filesystem::path resultPath = parentParentDir / moduleNameStr; + + return resultPath.wstring(); +#else + // Manual path manipulation for Unix-like systems + size_t lastSeparator = currentModuleFileName.find_last_of(WStr('/')); + if (lastSeparator == WSTRING::npos) + { + return EmptyWStr; + } + + // Find the second-to-last separator to get the parent directory of the parent directory + size_t secondLastSeparator = currentModuleFileName.find_last_of(WStr('/'), lastSeparator - 1); + if (secondLastSeparator == WSTRING::npos) + { + return EmptyWStr; + } + + // Extract the parent parent directory path + WSTRING parentParentDir = currentModuleFileName.substr(0, secondLastSeparator); + + // Append the module name + return parentParentDir + WStr("/") + moduleName; +#endif +} + } // namespace trace #endif // OTEL_CLR_PROFILER_PAL_H_ From d770791690677f0c0492584ac3a158113d553a5b Mon Sep 17 00:00:00 2001 From: Rhynier Myburgh Date: Wed, 15 Oct 2025 17:18:51 -0700 Subject: [PATCH 2/2] Remove unnecessary todo --- src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp b/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp index 606704ac2c..47ad067506 100644 --- a/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp +++ b/src/OpenTelemetry.AutoInstrumentation.Native/cor_profiler.cpp @@ -178,8 +178,6 @@ HRESULT STDMETHODCALLTYPE CorProfiler::Initialize(IUnknown* cor_profiler_info_un if (!IsStartupHookValid(startup_hooks, home_path)) { Logger::Info("The StartupHook was not configured. Will patch ProcessStartupHooks."); - // TODO: Instead of failing, instrument corelib to add the profiler path to the startup hook - // initialization code. startup_fix_required = true; } }