From a5daa6b8d4e6dbf2354d59baf22b7ee208f83a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 20 Jun 2025 17:37:28 +0200 Subject: [PATCH 1/4] Capture build-id in module load events on Linux Module load events on Windows carry a GUID of the matching PDB. This is used to verify the PDB that is used to analyze traces matches the one of the program. On Linux, we don't have PDBs and PDB GUID. Instead, we have build-id. Build-id is a binary blob of an unspecified length that is stored both in .debug files and in the program binary. Typically, it is 20 bytes (SHA-1). Since 20 is more than 16, it unfortunately doesn't fit in the space we have. The event however has two GUIDs. So use the space from the other event to store rest of the bits. Should be futureproof up to SHA-256. --- src/coreclr/nativeaot/Runtime/Pal.h | 2 +- src/coreclr/nativeaot/Runtime/eventtrace.cpp | 9 +- .../nativeaot/Runtime/unix/PalUnix.cpp | 110 +++++++++++++++++- .../nativeaot/Runtime/windows/PalCommon.cpp | 3 +- 4 files changed, 116 insertions(+), 8 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/Pal.h b/src/coreclr/nativeaot/Runtime/Pal.h index dc80f8d32ab61e..8e2dcb4301f16b 100644 --- a/src/coreclr/nativeaot/Runtime/Pal.h +++ b/src/coreclr/nativeaot/Runtime/Pal.h @@ -161,7 +161,7 @@ bool PalInit(); // Given the OS handle of a loaded module, compute the upper and lower virtual address bounds (inclusive). void PalGetModuleBounds(HANDLE hOsHandle, _Out_ uint8_t ** ppLowerBound, _Out_ uint8_t ** ppUpperBound); -void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath); +void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, GUID * pManagedGuidSignature); struct NATIVE_CONTEXT; diff --git a/src/coreclr/nativeaot/Runtime/eventtrace.cpp b/src/coreclr/nativeaot/Runtime/eventtrace.cpp index bc0e48e4d40d77..a39168afa6ca27 100644 --- a/src/coreclr/nativeaot/Runtime/eventtrace.cpp +++ b/src/coreclr/nativeaot/Runtime/eventtrace.cpp @@ -377,12 +377,11 @@ void ETW::LoaderLog::SendModuleEvent(HANDLE pModule, uint32_t dwEventOptions) wszModuleFileName = (const WCHAR *)&wszModuleFileNameUnicode[0]; #endif + GUID managedGuid; GUID nativeGuid; uint32_t dwAge; WCHAR wszPath[1024]; - PalGetPDBInfo(pModule, &nativeGuid, &dwAge, wszPath, ARRAY_SIZE(wszPath)); - - GUID zeroGuid = { 0 }; + PalGetPDBInfo(pModule, &nativeGuid, &dwAge, wszPath, ARRAY_SIZE(wszPath), &managedGuid); if (dwEventOptions & ETW::EnumerationLog::EnumerationStructs::DomainAssemblyModuleLoad) { @@ -394,7 +393,7 @@ void ETW::LoaderLog::SendModuleEvent(HANDLE pModule, uint32_t dwEventOptions) wszModuleILFileName, // ModuleILPath, wszModuleFileName, // ModuleNativePath, GetClrInstanceId(), - &zeroGuid, // ManagedPdbSignature, + &managedGuid, // ManagedPdbSignature, 0, // ManagedPdbAge, NULL, // ManagedPdbBuildPath, &nativeGuid, // NativePdbSignature, @@ -412,7 +411,7 @@ void ETW::LoaderLog::SendModuleEvent(HANDLE pModule, uint32_t dwEventOptions) wszModuleILFileName, // ModuleILPath, wszModuleFileName, // ModuleNativePath, GetClrInstanceId(), - &zeroGuid, // ManagedPdbSignature, + &managedGuid, // ManagedPdbSignature, 0, // ManagedPdbAge, NULL, // ManagedPdbBuildPath, &nativeGuid, // NativePdbSignature, diff --git a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp index 00334ae945be0f..ea10388e8c4050 100644 --- a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp +++ b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp @@ -44,6 +44,8 @@ #ifdef TARGET_LINUX #include +#include +#include #endif #if HAVE_PTHREAD_GETTHREADID_NP @@ -95,13 +97,119 @@ void RhFailFast() abort(); } -void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath) +#if TARGET_LINUX + +struct PalGetPDBInfoPhdrCallbackData +{ + void* Base; + void* BuildID; + uint32_t BuildIDLength; +}; + +static int PalGetPDBInfoPhdrCallback(struct dl_phdr_info *info, size_t size, void* pData) +{ + struct PalGetPDBInfoPhdrCallbackData* pCallbackData = (struct PalGetPDBInfoPhdrCallbackData*)pData; + + // Find the module of interest + void* loadAddress = NULL; + for (ElfW(Half) i = 0; i < info->dlpi_phnum; i++) + { + if (info->dlpi_phdr[i].p_type == PT_LOAD) + { + loadAddress = (void*)(info->dlpi_addr + info->dlpi_phdr[i].p_vaddr); + if (loadAddress == pCallbackData->Base) + break; + } + } + + if (loadAddress != pCallbackData->Base) + { + return 0; + } + + // Got the module of interest. Now iterate program headers and try to find the GNU build ID note + for (ElfW(Half) i = 0; i < info->dlpi_phnum; i++) + { + // Must be a note section. We don't check the name because while there's a convention for the name, + // the convention is not mandatory. + if (info->dlpi_phdr[i].p_type != PT_NOTE) + continue; + + // Got a note section, iterate over the contents and find the GNU build id one + ElfW(Nhdr) *note = (ElfW(Nhdr)*)(info->dlpi_addr + info->dlpi_phdr[i].p_vaddr); + ElfW(Addr) align = info->dlpi_phdr[i].p_align; + ElfW(Addr) size = info->dlpi_phdr[i].p_memsz; + ElfW(Addr) start = (ElfW(Addr))note; + + while ((ElfW(Addr)) (note + 1) - start < size) + { + if (note->n_namesz == 4 + && note->n_type == NT_GNU_BUILD_ID + && memcmp(note + 1, "GNU", 4) == 0) + { + // Got the note, fill out the callback data and return. + pCallbackData->BuildID = (uint8_t*)note + sizeof(ElfW(Nhdr)) + ALIGN_UP(note->n_namesz, align); + pCallbackData->BuildIDLength = note->n_descsz; + return 1; + } + + // Skip over the note. Size of the note is determined by the header and payload (aligned) + size_t offset = sizeof(ElfW(Nhdr)) + + ALIGN_UP(note->n_namesz, align) + + ALIGN_UP(note->n_descsz, align); + note = (ElfW(Nhdr)*)((uint8_t*)note + offset); + } + } + + return 0; +} +#endif + +void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, GUID * pManagedGuidSignature) { memset(pGuidSignature, 0, sizeof(*pGuidSignature)); + memset(pManagedGuidSignature, 0, sizeof(*pManagedGuidSignature)); *pdwAge = 0; if (cchPath <= 0) return; wszPath[0] = L'\0'; + +#if TARGET_LINUX + // Since on Linux the debug information is not stored in PDBs and we don't have a PDB GUID, + // we'll use the GNU build-id instead if available. + // Since build-id doesn't have a predefined length but is typically 20 bytes, we need more bytes + // than what a GUID can store. We'll misuse the managedGuid signature to store the overflow. + // PDB age will store the actual number of bytes used. If the build-id is longer than what + // we can store in two GUIDs, we'll truncate. The consumer can decide what to do about it + // based on seeing dwAge > 2*sizeof(GUID). + + Dl_info info; + if (!dladdr((void*)&PalGetPDBInfo, &info) + || !info.dli_fbase) + { + return; + } + + struct PalGetPDBInfoPhdrCallbackData data; + data.Base = info.dli_fbase; + + if (!dl_iterate_phdr(&PalGetPDBInfoPhdrCallback, &data)) + { + return; + } + + *pdwAge = data.BuildIDLength; + + uint8_t* src = (uint8_t*)data.BuildID; + uint32_t count = data.BuildIDLength; + memcpy(pGuidSignature, src, count < sizeof(*pGuidSignature) ? count : sizeof(*pGuidSignature)); + if (count > sizeof(*pGuidSignature)) + { + count -= sizeof(*pGuidSignature); + src += sizeof(*pGuidSignature); + memcpy(pManagedGuidSignature, src, count < sizeof(*pManagedGuidSignature) ? count : sizeof(*pManagedGuidSignature)); + } +#endif } static void UnmaskActivationSignal() diff --git a/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp b/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp index 5983da31147ee7..cd0dc01c71c4dd 100644 --- a/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp +++ b/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp @@ -50,10 +50,11 @@ void PalGetModuleBounds(HANDLE hOsHandle, _Out_ uint8_t ** ppLowerBound, _Out_ u // // This is a simplification of similar code in CLR's GetCodeViewInfo // in eventtrace.cpp. -void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath) +void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, GUID * pManagedGuidSignature) { // Zero-init [out]-params ZeroMemory(pGuidSignature, sizeof(*pGuidSignature)); + ZeroMemory(pManagedGuidSignature, sizeof(*pManagedGuidSignature)); *pdwAge = 0; if (cchPath <= 0) return; From e36790babd058307bdacd5a735820a680eae7a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Tue, 24 Jun 2025 13:08:06 +0200 Subject: [PATCH 2/4] FB --- src/coreclr/nativeaot/Runtime/Pal.h | 2 +- .../eventpipe/gen-eventing-event-inc.lst | 4 +- src/coreclr/nativeaot/Runtime/eventtrace.cpp | 35 ++++++-- .../nativeaot/Runtime/unix/PalUnix.cpp | 35 ++------ .../nativeaot/Runtime/windows/PalCommon.cpp | 5 +- src/coreclr/vm/ClrEtwAll.man | 81 +++++++++++++++++++ 6 files changed, 120 insertions(+), 42 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/Pal.h b/src/coreclr/nativeaot/Runtime/Pal.h index 8e2dcb4301f16b..6a4c57ce7307a1 100644 --- a/src/coreclr/nativeaot/Runtime/Pal.h +++ b/src/coreclr/nativeaot/Runtime/Pal.h @@ -161,7 +161,7 @@ bool PalInit(); // Given the OS handle of a loaded module, compute the upper and lower virtual address bounds (inclusive). void PalGetModuleBounds(HANDLE hOsHandle, _Out_ uint8_t ** ppLowerBound, _Out_ uint8_t ** ppUpperBound); -void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, GUID * pManagedGuidSignature); +void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, _Out_ uint32_t * pcbBuildId, _Out_ void ** ppBuildId); struct NATIVE_CONTEXT; diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/gen-eventing-event-inc.lst b/src/coreclr/nativeaot/Runtime/eventpipe/gen-eventing-event-inc.lst index 40ba078c965e49..6047675921ff13 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/gen-eventing-event-inc.lst +++ b/src/coreclr/nativeaot/Runtime/eventpipe/gen-eventing-event-inc.lst @@ -83,8 +83,8 @@ GenAwareBegin GenAwareEnd IncreaseMemoryPressure LockCreated -ModuleDCEnd_V2 -ModuleLoad_V2 +ModuleDCEnd_V3 +ModuleLoad_V3 PinObjectAtGCTime PinPlugAtGCTime PrvDestroyGCHandle diff --git a/src/coreclr/nativeaot/Runtime/eventtrace.cpp b/src/coreclr/nativeaot/Runtime/eventtrace.cpp index a39168afa6ca27..0a05c9376bb343 100644 --- a/src/coreclr/nativeaot/Runtime/eventtrace.cpp +++ b/src/coreclr/nativeaot/Runtime/eventtrace.cpp @@ -377,15 +377,32 @@ void ETW::LoaderLog::SendModuleEvent(HANDLE pModule, uint32_t dwEventOptions) wszModuleFileName = (const WCHAR *)&wszModuleFileNameUnicode[0]; #endif - GUID managedGuid; GUID nativeGuid; uint32_t dwAge; WCHAR wszPath[1024]; - PalGetPDBInfo(pModule, &nativeGuid, &dwAge, wszPath, ARRAY_SIZE(wszPath), &managedGuid); + uint32_t cbBuildId; + void* pBuildId; + PalGetPDBInfo(pModule, &nativeGuid, &dwAge, wszPath, ARRAY_SIZE(wszPath), &cbBuildId, &pBuildId); + + WCHAR wszBuildId[65]; + size_t written = 0; + wszBuildId[0] = 0; + for (size_t i = 0; i < cbBuildId; i++) + { + if (written + 3 <= ARRAY_SIZE(wszBuildId)) { // 2 hex digits + 1 null terminator + // Convert each byte to hex and append to the output string + written += swprintf_s(wszBuildId + written, ARRAY_SIZE(wszBuildId) - written, L"%02X", ((uint8_t*)pBuildId)[i]); + } else { + // If buffer not enough to fit, truncate + break; + } + } + + GUID zeroGuid = { 0 }; if (dwEventOptions & ETW::EnumerationLog::EnumerationStructs::DomainAssemblyModuleLoad) { - FireEtwModuleLoad_V2( + FireEtwModuleLoad_V3( ULONGLONG(pModule), 0, // AssemblyID ETW::LoaderLog::LoaderStructs::NativeModule, // Module Flags @@ -393,17 +410,18 @@ void ETW::LoaderLog::SendModuleEvent(HANDLE pModule, uint32_t dwEventOptions) wszModuleILFileName, // ModuleILPath, wszModuleFileName, // ModuleNativePath, GetClrInstanceId(), - &managedGuid, // ManagedPdbSignature, + &zeroGuid, // ManagedPdbSignature, 0, // ManagedPdbAge, NULL, // ManagedPdbBuildPath, &nativeGuid, // NativePdbSignature, dwAge, // NativePdbAge, - wszPath // NativePdbBuildPath, + wszPath, // NativePdbBuildPath, + wszBuildId // NativeBuildId, ); } else if (dwEventOptions & ETW::EnumerationLog::EnumerationStructs::DomainAssemblyModuleDCEnd) { - FireEtwModuleDCEnd_V2( + FireEtwModuleDCEnd_V3( ULONGLONG(pModule), 0, // AssemblyID ETW::LoaderLog::LoaderStructs::NativeModule, // Module Flags @@ -411,12 +429,13 @@ void ETW::LoaderLog::SendModuleEvent(HANDLE pModule, uint32_t dwEventOptions) wszModuleILFileName, // ModuleILPath, wszModuleFileName, // ModuleNativePath, GetClrInstanceId(), - &managedGuid, // ManagedPdbSignature, + &zeroGuid, // ManagedPdbSignature, 0, // ManagedPdbAge, NULL, // ManagedPdbBuildPath, &nativeGuid, // NativePdbSignature, dwAge, // NativePdbAge, - wszPath // NativePdbBuildPath, + wszPath, // NativePdbBuildPath, + wszBuildId // NativeBuildId, ); } else diff --git a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp index ea10388e8c4050..b6045f6f74d5ac 100644 --- a/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp +++ b/src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp @@ -165,50 +165,27 @@ static int PalGetPDBInfoPhdrCallback(struct dl_phdr_info *info, size_t size, voi } #endif -void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, GUID * pManagedGuidSignature) +void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, _Out_ uint32_t * pcbBuildId, _Out_ void ** ppBuildId) { memset(pGuidSignature, 0, sizeof(*pGuidSignature)); - memset(pManagedGuidSignature, 0, sizeof(*pManagedGuidSignature)); *pdwAge = 0; + *ppBuildId = NULL; + *pcbBuildId = 0; if (cchPath <= 0) return; wszPath[0] = L'\0'; #if TARGET_LINUX - // Since on Linux the debug information is not stored in PDBs and we don't have a PDB GUID, - // we'll use the GNU build-id instead if available. - // Since build-id doesn't have a predefined length but is typically 20 bytes, we need more bytes - // than what a GUID can store. We'll misuse the managedGuid signature to store the overflow. - // PDB age will store the actual number of bytes used. If the build-id is longer than what - // we can store in two GUIDs, we'll truncate. The consumer can decide what to do about it - // based on seeing dwAge > 2*sizeof(GUID). - - Dl_info info; - if (!dladdr((void*)&PalGetPDBInfo, &info) - || !info.dli_fbase) - { - return; - } - struct PalGetPDBInfoPhdrCallbackData data; - data.Base = info.dli_fbase; + data.Base = hOsHandle; if (!dl_iterate_phdr(&PalGetPDBInfoPhdrCallback, &data)) { return; } - *pdwAge = data.BuildIDLength; - - uint8_t* src = (uint8_t*)data.BuildID; - uint32_t count = data.BuildIDLength; - memcpy(pGuidSignature, src, count < sizeof(*pGuidSignature) ? count : sizeof(*pGuidSignature)); - if (count > sizeof(*pGuidSignature)) - { - count -= sizeof(*pGuidSignature); - src += sizeof(*pGuidSignature); - memcpy(pManagedGuidSignature, src, count < sizeof(*pManagedGuidSignature) ? count : sizeof(*pManagedGuidSignature)); - } + *pcbBuildId = data.BuildIDLength; + *ppBuildId = data.BuildID; #endif } diff --git a/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp b/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp index cd0dc01c71c4dd..8e4e582c5cec00 100644 --- a/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp +++ b/src/coreclr/nativeaot/Runtime/windows/PalCommon.cpp @@ -50,11 +50,12 @@ void PalGetModuleBounds(HANDLE hOsHandle, _Out_ uint8_t ** ppLowerBound, _Out_ u // // This is a simplification of similar code in CLR's GetCodeViewInfo // in eventtrace.cpp. -void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, GUID * pManagedGuidSignature) +void PalGetPDBInfo(HANDLE hOsHandle, GUID * pGuidSignature, _Out_ uint32_t * pdwAge, _Out_writes_z_(cchPath) WCHAR * wszPath, int32_t cchPath, _Out_ uint32_t * pcbBuildId, _Out_ void ** ppBuildId) { // Zero-init [out]-params ZeroMemory(pGuidSignature, sizeof(*pGuidSignature)); - ZeroMemory(pManagedGuidSignature, sizeof(*pManagedGuidSignature)); + *ppBuildId = NULL; + *pcbBuildId = 0; *pdwAge = 0; if (cchPath <= 0) return; diff --git a/src/coreclr/vm/ClrEtwAll.man b/src/coreclr/vm/ClrEtwAll.man index b2010b111e1c78..c04c58cfdac656 100644 --- a/src/coreclr/vm/ClrEtwAll.man +++ b/src/coreclr/vm/ClrEtwAll.man @@ -1932,6 +1932,40 @@ + + + +