From 6f991009e3636edb4c25402e9dd68df3c95dd68e Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Tue, 24 Mar 2026 16:59:07 -0700 Subject: [PATCH 01/10] Add ELF symbol resolution with SymbolInfo type hierarchy Introduce a TraceModuleFileSymbolInfo type hierarchy (PESymbolInfo, ElfSymbolInfo) to cleanly separate Windows PE and Linux ELF module metadata. Migrate existing PDB and R2R fields from TraceModuleFile into PESymbolInfo with MatchOrInit pattern for type-safe access. Add end-to-end ELF symbol resolution: - ElfSymbolModule: add ReadBuildId, fix RVA computation using page-aligned p_vaddr with SystemPageSize from trace headers - SymbolReader: add FindElfSymbolFilePath with SSQP symbol server support, ElfBuildIdMatches for build-id validation, OpenElfSymbolFile - TraceLog: add OpenElfSymbolsForModuleFile, update LookupSymbolsForModule to dispatch by ModuleBinaryFormat - Populate ELF metadata (BuildId, VirtualAddress, FileOffset, PageSize) from trace events Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../EventPipe/EventPipeEventSource.cs | 5 + .../UniversalSystemTraceEventParser.cs | 41 ++ .../NettraceUniversalConverter.cs | 9 + src/TraceEvent/Symbols/ElfSymbolModule.cs | 292 +++++++- src/TraceEvent/Symbols/SymbolReader.cs | 185 +++++- .../TraceEvent.Tests/Symbols/ElfBuilder.cs | 139 +++- .../Symbols/ElfSymbolModuleTests.cs | 264 ++++++++ .../Symbols/SymbolReaderTests.cs | 627 +++++++++++++++++- src/TraceEvent/TraceLog.cs | 581 +++++++++++++--- 9 files changed, 2037 insertions(+), 106 deletions(-) diff --git a/src/TraceEvent/EventPipe/EventPipeEventSource.cs b/src/TraceEvent/EventPipe/EventPipeEventSource.cs index 24bdb382e..af9e855c3 100644 --- a/src/TraceEvent/EventPipe/EventPipeEventSource.cs +++ b/src/TraceEvent/EventPipe/EventPipeEventSource.cs @@ -240,6 +240,10 @@ internal void ReadTraceBlockV6OrGreater(Block block) { _expectedCPUSamplingRate = intVal3; } + else if (key == "SystemPageSize" && ulong.TryParse(value, out ulong ulongVal) && ulongVal > 0) + { + _systemPageSize = ulongVal; + } } } @@ -656,6 +660,7 @@ private DynamicTraceEventData CreateTemplate(EventPipeMetadata metadata) private int _lastLabelListId; internal int _processId; internal int _expectedCPUSamplingRate; + internal ulong _systemPageSize; private RewindableStream _stream; private bool _isStreaming; private ThreadCache _threadCache; diff --git a/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs b/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs index f39c478ad..307da3edd 100644 --- a/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs +++ b/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs @@ -481,8 +481,10 @@ internal sealed class ELFProcessMappingSymbolMetadata : ProcessMappingSymbolMeta [JsonPropertyName("build_id")] public string BuildId { get; set; } [JsonPropertyName("p_vaddr")] + [JsonConverter(typeof(HexUInt64Converter))] public ulong VirtualAddress { get; set; } [JsonPropertyName("p_offset")] + [JsonConverter(typeof(HexUInt64Converter))] public ulong FileOffset { get; set; } } @@ -535,4 +537,43 @@ public override void Write(Utf8JsonWriter writer, ProcessMappingSymbolMetadata v JsonSerializer.Serialize(writer, value, value.GetType(), options); } } + + internal class HexUInt64Converter : JsonConverter + { + public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + return reader.GetUInt64(); + } + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException($"Cannot convert token of type {reader.TokenType} to ulong."); + } + + string text = reader.GetString(); + if (text != null && text.StartsWith("0x", StringComparison.OrdinalIgnoreCase) && text.Length > 2) + { + if (ulong.TryParse(text.Substring(2), System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, out ulong hexValue)) + { + return hexValue; + } + } + + if (ulong.TryParse(text, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out ulong value)) + { + return value; + } + + throw new JsonException($"Cannot convert \"{text}\" to ulong."); + } + + public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) + { + writer.WriteStringValue("0x" + value.ToString("X")); + } + } } diff --git a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs index f6f686f7e..6fbc5c0a2 100644 --- a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs +++ b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs @@ -27,6 +27,15 @@ public static void RegisterParsers(TraceLog traceLog) public void BeforeProcess(TraceLog traceLog, TraceEventDispatcher source) { + // Extract EventPipe-specific header values (e.g., SystemPageSize for ELF RVA calculations). + if (source is EventPipeEventSource eventPipeSource) + { + eventPipeSource.HeadersDeserialized += delegate () + { + traceLog.systemPageSize = eventPipeSource._systemPageSize; + }; + } + UniversalSystemTraceEventParser universalSystemParser = new UniversalSystemTraceEventParser(source); universalSystemParser.ExistingProcess += delegate (ProcessCreateTraceData data) { diff --git a/src/TraceEvent/Symbols/ElfSymbolModule.cs b/src/TraceEvent/Symbols/ElfSymbolModule.cs index 3ab108767..96e0b0f24 100644 --- a/src/TraceEvent/Symbols/ElfSymbolModule.cs +++ b/src/TraceEvent/Symbols/ElfSymbolModule.cs @@ -101,6 +101,172 @@ public string FindNameForRva(uint rva, ref uint symbolStart) return string.Empty; } + /// + /// Reads the GNU build-id from an ELF file by scanning PT_NOTE program headers. + /// Uses program headers (not section headers) because they are always present + /// even when section headers have been stripped. + /// + /// Path to the ELF file. + /// Lowercase hex string of the build-id, or null if not found or on any error. + internal static string ReadBuildId(string filePath) + { + try + { + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + // Read the ELF header (max 64 bytes for 64-bit). + byte[] header = new byte[Elf64EhdrSize]; + int headerRead = ReadFully(stream, header, 0, header.Length); + if (headerRead < EI_NIDENT) + { + Debug.WriteLine("ReadBuildId: File too small."); + return null; + } + + // Verify ELF magic bytes: 0x7f 'E' 'L' 'F'. + if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) + { + Debug.WriteLine("ReadBuildId: Invalid ELF magic."); + return null; + } + + byte eiClass = header[EI_CLASS]; + byte eiData = header[EI_DATA]; + + bool is64Bit = (eiClass == ElfClass64); + bool bigEndian = (eiData == ElfDataMsb); + + if (eiClass != ElfClass32 && eiClass != ElfClass64) + { + Debug.WriteLine("ReadBuildId: Unknown ELF class " + eiClass + "."); + return null; + } + + int ehSize = is64Bit ? Elf64EhdrSize : Elf32EhdrSize; + if (headerRead < ehSize) + { + Debug.WriteLine("ReadBuildId: Header too small."); + return null; + } + + // Parse program header table location from ELF header. + // Layout after e_ident(16): e_type(2), e_machine(2), e_version(4), e_entry(4/8), e_phoff(4/8). + int pos = EI_NIDENT + 2 + 2 + 4; // skip e_ident, e_type, e_machine, e_version + ulong ePhoff; + if (is64Bit) + { + pos += 8; // skip e_entry + ePhoff = ReadU64Static(header, pos, bigEndian); pos += 8; + } + else + { + pos += 4; // skip e_entry + ePhoff = ReadU32Static(header, pos, bigEndian); pos += 4; + } + + // Skip to e_phentsize and e_phnum. + // After e_phoff: e_shoff(4/8), e_flags(4), e_ehsize(2). + if (is64Bit) + { + pos += 8 + 4 + 2; // e_shoff(8), e_flags(4), e_ehsize(2) + } + else + { + pos += 4 + 4 + 2; // e_shoff(4), e_flags(4), e_ehsize(2) + } + + ushort ePhentsize = ReadU16Static(header, pos, bigEndian); pos += 2; + ushort ePhnum = ReadU16Static(header, pos, bigEndian); + + if (ePhoff == 0 || ePhentsize == 0 || ePhnum == 0) + { + Debug.WriteLine("ReadBuildId: No program headers found."); + return null; + } + + // Validate minimum program header entry size to avoid out-of-bounds reads. + int minPhentsize = is64Bit ? 56 : 32; // Elf64_Phdr = 56 bytes, Elf32_Phdr = 32 bytes + if (ePhentsize < minPhentsize) + { + Debug.WriteLine("ReadBuildId: ePhentsize too small: " + ePhentsize); + return null; + } + + // Guard against corrupt ELF headers with unreasonably large program header counts. + if (ePhnum > 4096) + { + Debug.WriteLine("ReadBuildId: Program header count too large: " + ePhnum); + return null; + } + + // Read all program headers in one bulk read. + int phTableSize = ePhnum * ePhentsize; + byte[] phTable = new byte[phTableSize]; + stream.Seek((long)ePhoff, SeekOrigin.Begin); + if (ReadFully(stream, phTable, 0, phTableSize) < phTableSize) + { + Debug.WriteLine("ReadBuildId: Could not read program headers."); + return null; + } + + // Iterate program headers looking for PT_NOTE segments. + for (int i = 0; i < ePhnum; i++) + { + int phPos = i * ePhentsize; + uint pType = ReadU32Static(phTable, phPos, bigEndian); + + if (pType != PT_NOTE) + { + continue; + } + + // Parse p_offset and p_filesz from the program header. + ulong pOffset, pFilesz; + if (is64Bit) + { + // 64-bit: p_type(4) + p_flags(4) + p_offset(8) + p_vaddr(8) + p_paddr(8) + p_filesz(8). + pOffset = ReadU64Static(phTable, phPos + 8, bigEndian); + pFilesz = ReadU64Static(phTable, phPos + 8 + 8 + 8 + 8, bigEndian); + } + else + { + // 32-bit: p_type(4) + p_offset(4) + p_vaddr(4) + p_paddr(4) + p_filesz(4). + pOffset = ReadU32Static(phTable, phPos + 4, bigEndian); + pFilesz = ReadU32Static(phTable, phPos + 4 + 4 + 4 + 4, bigEndian); + } + + if (pFilesz == 0 || pFilesz > int.MaxValue) + { + continue; + } + + // Read the PT_NOTE segment data. + byte[] noteData = new byte[(int)pFilesz]; + stream.Seek((long)pOffset, SeekOrigin.Begin); + if (ReadFully(stream, noteData, 0, noteData.Length) < noteData.Length) + { + continue; + } + + // Iterate notes within the segment looking for GNU build-id. + string buildId = ExtractBuildId(noteData, bigEndian); + if (buildId != null) + { + return buildId; + } + } + + Debug.WriteLine("ReadBuildId: No GNU build-id note found."); + return null; + } + } + catch (Exception ex) + { + Debug.WriteLine("ReadBuildId: Error reading file: " + ex.Message); + return null; + } + } + #region private // ELF identification (e_ident) constants. @@ -123,10 +289,24 @@ public string FindNameForRva(uint rva, ref uint symbolStart) private const int Elf32EhdrSize = 52; private const int Elf64EhdrSize = 64; + // Section header entry sizes. + private const int Elf32ShdrSize = 40; + private const int Elf64ShdrSize = 64; + private const int MaxShentsize = 256; + + // Maximum section count to accept from ELF headers. + private const uint MaxSectionCount = 65535; + // Section header types. private const uint SHT_SYMTAB = 2; private const uint SHT_DYNSYM = 11; + // Program header types. + private const uint PT_NOTE = 4; // Note segment. + + // Note types. + private const uint NT_GNU_BUILD_ID = 3; // GNU build-id note type. + // Symbol table constants. private const byte STT_FUNC = 2; // Symbol type: function. private const byte STT_MASK = 0xf; // Mask to extract symbol type from st_info. @@ -161,7 +341,7 @@ public string FindNameForRva(uint rva, ref uint symbolStart) /// private struct ElfSymbolEntry : IComparable { - public uint Start; // Adjusted RVA: (st_value - pVaddr) + pOffset. + public uint Start; // RVA: (st_value - pVaddr) + pOffset. public uint End; // Start + size - 1 (inclusive). public uint StrtabOffset; // Offset into m_strtab for lazy name decode. public string Name; // Null until first lookup, then cached. @@ -236,6 +416,16 @@ private void ParseElf(Stream stream) return; } + // Valid ELF section header sizes are 40 (32-bit) or 64 (64-bit). + // Reject values below the minimum struct size (would cause out-of-bounds reads) + // and cap at 256 to guard against overflow in sectionCount * eShentsize. + int minShentsize = m_is64Bit ? Elf64ShdrSize : Elf32ShdrSize; + if (eShentsize < minShentsize || eShentsize > MaxShentsize) + { + Debug.WriteLine("ElfSymbolModule: Invalid section header entry size: " + eShentsize); + return; + } + // Handle extended section count. uint sectionCount = eShnum; if (eShnum == 0) @@ -261,6 +451,13 @@ private void ParseElf(Stream stream) return; } + // Guard against corrupt ELF headers with unreasonably large section counts. + if (sectionCount > MaxSectionCount) + { + Debug.WriteLine("ElfSymbolModule: Section count too large: " + sectionCount); + return; + } + // Read all section headers in one bulk read. int shTableSize = (int)sectionCount * eShentsize; byte[] shTable = new byte[shTableSize]; @@ -291,6 +488,11 @@ private void ParseElf(Stream stream) } // Get the linked string table size. + if (shLink == 0 || shLink >= sectionCount) + { + continue; + } + int strtabShPos = (int)shLink * eShentsize; uint strtabType; long strtabOffset, strtabSize, strtabLink, strtabEntsize; @@ -317,6 +519,11 @@ private void ParseElf(Stream stream) } // Load the linked string table into the SegmentedList. + if (shLink == 0 || shLink >= sectionCount) + { + continue; + } + int strtabShPos = (int)shLink * eShentsize; uint strtabType; long strtabOffset, strtabSize, strtabLink, strtabEntsize; @@ -492,6 +699,61 @@ private void ReadSymbolTable(byte[] symData, long size, long entsize, long strta } } + /// + /// Searches a PT_NOTE segment's raw bytes for a GNU build-id note. + /// Note format: namesz(4) + descsz(4) + type(4) + name(aligned to 4) + desc(aligned to 4). + /// + /// Raw bytes of the PT_NOTE segment. + /// True if the ELF file uses big-endian encoding. + /// Lowercase hex string of the build-id, or null if not found. + private static string ExtractBuildId(byte[] noteData, bool bigEndian) + { + int pos = 0; + int length = noteData.Length; + + while (pos + 12 <= length) // Minimum note header: namesz(4) + descsz(4) + type(4). + { + uint namesz = ReadU32Static(noteData, pos, bigEndian); + uint descsz = ReadU32Static(noteData, pos + 4, bigEndian); + uint type = ReadU32Static(noteData, pos + 8, bigEndian); + pos += 12; + + // Align name and desc sizes to 4-byte boundaries. + uint nameAligned = (namesz + 3) & ~3u; + uint descAligned = (descsz + 3) & ~3u; + + // Validate that the note fits within the segment data. + if (pos + nameAligned + descAligned > length) + { + break; + } + + // Check for GNU build-id: name == "GNU\0" (namesz == 4) and type == NT_GNU_BUILD_ID (3). + if (type == NT_GNU_BUILD_ID && namesz == 4 && + noteData[pos] == (byte)'G' && noteData[pos + 1] == (byte)'N' && + noteData[pos + 2] == (byte)'U' && noteData[pos + 3] == 0) + { + if (descsz == 0) + { + return null; + } + + // Extract the build-id descriptor bytes as lowercase hex. + int descStart = pos + (int)nameAligned; + var sb = new StringBuilder((int)descsz * 2); + for (int j = 0; j < (int)descsz; j++) + { + sb.Append(noteData[descStart + j].ToString("x2")); + } + return sb.ToString(); + } + + pos += (int)nameAligned + (int)descAligned; + } + + return null; + } + /// /// Attempts to demangle a symbol name using available demanglers. /// Supports Itanium C++ ABI (_Z prefix) and Rust v0 (_R prefix) mangling. @@ -611,6 +873,34 @@ private ulong ReadU64(byte[] data, int offset) return BitConverter.ToUInt64(data, offset); } + // Static overloads for use in ReadBuildId (which has no instance state). + + /// Static uint16 read with explicit endianness. + private static ushort ReadU16Static(byte[] data, int offset, bool bigEndian) + { + return bigEndian ? ReadU16BE(data, offset) : ReadU16LE(data, offset); + } + + /// Static uint32 read with explicit endianness. + private static uint ReadU32Static(byte[] data, int offset, bool bigEndian) + { + if (bigEndian) + { + return (uint)(data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]); + } + return BitConverter.ToUInt32(data, offset); + } + + /// Static uint64 read with explicit endianness. + private static ulong ReadU64Static(byte[] data, int offset, bool bigEndian) + { + if (bigEndian) + { + return ((ulong)ReadU32Static(data, offset, bigEndian) << 32) | ReadU32Static(data, offset + 4, bigEndian); + } + return BitConverter.ToUInt64(data, offset); + } + #endregion #endregion diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index 7f354b2b4..1332e0d65 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -34,6 +34,8 @@ public SymbolReader(TextWriter log, string nt_symbol_path = null, DelegatingHand m_symbolModuleCache = new Cache(10); m_pdbPathCache = new Cache(10); m_r2rPerfMapPathCache = new Cache(10); + m_elfPathCache = new Cache(10); + m_elfModuleCache = new Cache(10); m_symbolPath = nt_symbol_path; if (m_symbolPath == null) @@ -380,6 +382,115 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig return perfMapPath; } + /// + /// Given an ELF module's filename and GNU build-id, attempts to find the corresponding + /// debug symbol file (.debug) or the binary itself from symbol servers and local paths. + /// Tries debug symbols (_.debug/elf-buildid-sym-{buildId}/_.debug) first, then falls + /// back to the binary ({filename}/elf-buildid-{buildId}/{filename}). + /// + /// The simple filename of the ELF module (e.g., "libcoreclr.so") + /// The GNU build-id as a lowercase hex string + /// The local file path to the downloaded symbol file, or null if not found. + public string FindElfSymbolFilePath(string fileName, string buildId) + { + m_log.WriteLine("FindElfSymbolFilePath: *{{ Searching for {0} with BuildId {1}", fileName, buildId); + + string simpleFileName = Path.GetFileName(fileName); + + // Normalize the build ID to lowercase. Build IDs vary in length depending on the + // hash algorithm (e.g., SHA-1 = 40 hex chars, MD5/UUID = 32), so we use the exact + // value without padding. + string normalizedBuildId = buildId.ToLowerInvariant(); + + ElfBuildIdSignature cacheKey = new ElfBuildIdSignature() { FileName = simpleFileName, BuildId = normalizedBuildId }; + if (m_elfPathCache.TryGet(cacheKey, out string cachedPath)) + { + m_log.WriteLine("FindElfSymbolFilePath: }} Hit Cache, returning {0}", cachedPath ?? "NULL"); + return cachedPath; + } + + // SSQP key conventions for ELF debug symbols and binaries. + // Use forward slashes — these are URL path segments for SSQP servers and + // Path.Combine handles mixed separators when joining with a local root. + string debugIndexPath = $"_.debug/elf-buildid-sym-{normalizedBuildId}/_.debug"; + string binaryIndexPath = $"{simpleFileName}/elf-buildid-{normalizedBuildId}/{simpleFileName}"; + + string resultPath = null; + + SymbolPath path = new SymbolPath(SymbolPath); + foreach (SymbolPathElement element in path.Elements) + { + if (element.IsSymServer) + { + string cache = element.Cache; + if (cache == null) + { + cache = path.DefaultSymbolCache(); + } + + // Try debug symbols first (preferred — has .symtab with full symbols). + resultPath = GetFileFromServer(element.Target, debugIndexPath, Path.Combine(cache, debugIndexPath)); + if (resultPath != null) + { + break; + } + + // Fall back to the binary (may only have .dynsym). + resultPath = GetFileFromServer(element.Target, binaryIndexPath, Path.Combine(cache, binaryIndexPath)); + if (resultPath != null) + { + break; + } + } + else + { + string target = element.Target; + if (target != null) + { + if ((Options & SymbolReaderOptions.CacheOnly) != 0 && element.IsRemote) + { + m_log.WriteLine("FindElfSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", target); + continue; + } + + // Try SSQP-structured debug symbols first. + string debugPath = Path.Combine(target, debugIndexPath); + if (File.Exists(debugPath)) + { + resultPath = debugPath; + break; + } + + // Try SSQP-structured binary. + string binaryPath = Path.Combine(target, binaryIndexPath); + if (File.Exists(binaryPath)) + { + resultPath = binaryPath; + break; + } + } + } + } + + if (resultPath != null) + { + m_log.WriteLine("FindElfSymbolFilePath: *}} Successfully found ELF symbols for {0} BuildId {1} at {2}", simpleFileName, normalizedBuildId, resultPath); + } + else + { + string where = ""; + if ((Options & SymbolReaderOptions.CacheOnly) != 0) + { + where = " in local cache"; + } + + m_log.WriteLine("FindElfSymbolFilePath: *}} Failed to find ELF symbols for {0}{1} BuildId {2}", simpleFileName, where, normalizedBuildId); + } + + m_elfPathCache.Add(cacheKey, resultPath); + return resultPath; + } + // Find an executable file path (not a PDB) based on information about the file image. /// /// This API looks up an executable file, by its build-timestamp and size (on a symbol server), 'fileName' should be @@ -495,9 +606,54 @@ public NativeSymbolModule OpenNativeSymbolFile(string pdbFileName) internal R2RPerfMapSymbolModule OpenR2RPerfMapSymbolFile(string filePath, uint loadedLayoutTextOffset) { + if (!CheckSecurity(filePath)) + { + m_log.WriteLine("OpenR2RPerfMapSymbolFile: Security check failed for {0}", filePath); + return null; + } return new R2RPerfMapSymbolModule(filePath, loadedLayoutTextOffset); } + /// + /// Opens an ELF symbol module, returning a cached instance if the same file and load + /// parameters have been seen before. This avoids re-parsing large ELF debug files when + /// the same binary is loaded across multiple processes in a trace. + /// + internal ElfSymbolModule OpenElfSymbolFile(string filePath, ulong pVaddr, ulong pOffset, string expectedBuildId = null) + { + var cacheKey = new ElfModuleSignature() { FilePath = filePath, VAddr = pVaddr, Offset = pOffset }; + if (m_elfModuleCache.TryGet(cacheKey, out ElfSymbolModule cached)) + { + m_log.WriteLine("OpenElfSymbolFile: Cache hit for {0}", filePath); + return cached; + } + + if (!CheckSecurity(filePath)) + { + m_log.WriteLine("OpenElfSymbolFile: Security check failed for {0}", filePath); + return null; + } + + // Validate build-id before parsing the full symbol table. + if (!string.IsNullOrEmpty(expectedBuildId)) + { + string actualBuildId = ElfSymbolModule.ReadBuildId(filePath); + if (actualBuildId == null) + { + m_log.WriteLine("OpenElfSymbolFile: Warning: Could not read build-id from {0} (may be stripped). Accepting without validation.", filePath); + } + else if (!string.Equals(actualBuildId, expectedBuildId, StringComparison.OrdinalIgnoreCase)) + { + m_log.WriteLine("OpenElfSymbolFile: ************ ELF file {0} has build-id {1} != expected {2}", filePath, actualBuildId, expectedBuildId); + return null; + } + } + + var module = new ElfSymbolModule(filePath, pVaddr, pOffset); + m_elfModuleCache.Add(cacheKey, module); + return module; + } + // Various state that controls symbol and source file lookup. /// /// The symbol path used to look up PDB symbol files. Set when the reader is initialized. @@ -510,6 +666,9 @@ public string SymbolPath m_symbolPath = value; m_symbolModuleCache.Clear(); m_pdbPathCache.Clear(); + m_r2rPerfMapPathCache.Clear(); + m_elfPathCache.Clear(); + m_elfModuleCache.Clear(); m_log.WriteLine("Symbol Path Updated to {0}", m_symbolPath); m_log.WriteLine("Symbol Path update forces clearing Pdb lookup cache"); } @@ -583,6 +742,9 @@ public SymbolReaderOptions Options { _Options = value; m_pdbPathCache.Clear(); + m_r2rPerfMapPathCache.Clear(); + m_elfPathCache.Clear(); + m_elfModuleCache.Clear(); m_log.WriteLine("Setting SymbolReaderOptions forces clearing Pdb lookup cache"); } } @@ -942,8 +1104,9 @@ private bool PdbMatches(string filePath, Guid pdbGuid, int pdbAge, bool checkSec return false; } + /// - /// Fetches a file from the server 'serverPath' with pdb signature path 'pdbSigPath' (concatenate them with a / or \ separator + /// Fetches a file from the server 'serverPath' with pdb signature path 'pdbSigPath'(concatenate them with a / or \ separator /// to form a complete URL or path name). It will place the file in 'fullDestPath' It will return true if successful /// If 'contentTypeFilter is present, this predicate is called with the URL content type (e.g. application/octet-stream) /// and if it returns false, it fails. This ensures that things that are the wrong content type (e.g. redirects to @@ -1642,12 +1805,32 @@ private struct R2RPerfMapSignature : IEquatable public int Version; } + // Used as the key to the m_elfPathCache. + private struct ElfBuildIdSignature : IEquatable + { + public override int GetHashCode() { return FileName.GetHashCode() + BuildId.GetHashCode(); } + public bool Equals(ElfBuildIdSignature other) { return FileName == other.FileName && BuildId == other.BuildId; } + public string FileName; + public string BuildId; + } + + private struct ElfModuleSignature : IEquatable + { + public override int GetHashCode() { return FilePath.GetHashCode() ^ VAddr.GetHashCode() ^ Offset.GetHashCode(); } + public bool Equals(ElfModuleSignature other) { return FilePath == other.FilePath && VAddr == other.VAddr && Offset == other.Offset; } + public string FilePath; + public ulong VAddr; + public ulong Offset; + } + internal TextWriter m_log; private string m_SymbolCacheDirectory; private string m_SourceCacheDirectory; private Cache m_symbolModuleCache; private Cache m_pdbPathCache; private Cache m_r2rPerfMapPathCache; + private Cache m_elfPathCache; + private Cache m_elfModuleCache; private string m_symbolPath; #endregion diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs index d1e1578f3..92c3e7ff7 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs @@ -15,6 +15,7 @@ internal class ElfBuilder private bool m_bigEndian = false; private ulong m_pVaddr = 0x400000; private ulong m_pOffset = 0; + private byte[] m_buildId = null; private readonly List m_symtabSymbols = new List(); private readonly List m_dynsymSymbols = new List(); @@ -32,10 +33,16 @@ private struct SymbolDef private const uint SHT_SYMTAB = 2; private const uint SHT_DYNSYM = 11; + // ELF program header types. + private const uint PT_NOTE = 4; + // Symbol type helpers. private const byte STT_FUNC = 2; private const byte STB_GLOBAL = 1; + // GNU build-id note type. + private const uint NT_GNU_BUILD_ID = 3; + public ElfBuilder Set64Bit(bool is64Bit) { m_is64Bit = is64Bit; @@ -58,6 +65,15 @@ public ElfBuilder SetPTLoad(ulong pVaddr, ulong pOffset) return this; } + /// + /// Sets the GNU build-id that will be embedded as a PT_NOTE program header. + /// + public ElfBuilder SetBuildId(byte[] buildId) + { + m_buildId = buildId; + return this; + } + /// /// Adds a STT_FUNC symbol to the .symtab section. /// @@ -105,7 +121,7 @@ public ElfBuilder AddDynFunction(string name, ulong virtualAddress, ulong size) /// /// Builds a complete ELF binary and returns it as a byte array. - /// Layout: [ELF Header] [Section Data...] [Section Headers] + /// Layout: [ELF Header] [Section Data...] [Note Data] [Program Headers] [Section Headers] /// public byte[] Build() { @@ -119,10 +135,12 @@ public byte[] Build() // [3] .dynstr (string table for .dynsym) — only if dynsym symbols exist // [4] .dynsym — only if dynsym symbols exist bool hasDynsym = m_dynsymSymbols.Count > 0; + bool hasBuildId = m_buildId != null; int sectionCount = hasDynsym ? 5 : 3; int ehSize = m_is64Bit ? 64 : 52; int shEntSize = m_is64Bit ? 64 : 40; + int phEntSize = m_is64Bit ? 56 : 32; // Build string table for .symtab. byte[] strtab = BuildStringTable(m_symtabSymbols, out int[] strtabOffsets); @@ -140,6 +158,13 @@ public byte[] Build() dynsym = BuildSymbolTable(m_dynsymSymbols, dynstrOffsets); } + // Build note data for GNU build-id if requested. + byte[] noteData = null; + if (hasBuildId) + { + noteData = BuildBuildIdNote(m_buildId); + } + // Section data starts right after the ELF header. long dataStart = ehSize; @@ -148,16 +173,38 @@ public byte[] Build() long symtabOffset = strtabOffset + strtab.Length; long dynstrOffset = symtabOffset + symtab.Length; long dynsymOffset = hasDynsym ? dynstrOffset + dynstr.Length : dynstrOffset; - long sectionHeadersOffset = hasDynsym ? dynsymOffset + dynsym.Length : dynstrOffset; + long afterSections = hasDynsym ? dynsymOffset + dynsym.Length : dynstrOffset; + + // Write note data after sections. + long noteOffset = afterSections; + long afterNote = hasBuildId ? noteOffset + noteData.Length : afterSections; - // Align section headers to 8-byte boundary. + // Write program headers after note data (align to 8 bytes). + long phOffset = 0; + ushort phNum = 0; + if (hasBuildId) + { + phOffset = afterNote; + if (phOffset % 8 != 0) + { + phOffset += 8 - (phOffset % 8); + } + phNum = 1; + } + + long afterPh = hasBuildId ? phOffset + phEntSize : afterNote; + + // Section headers follow everything else (align to 8 bytes). + long sectionHeadersOffset = afterPh; if (sectionHeadersOffset % 8 != 0) { sectionHeadersOffset += 8 - (sectionHeadersOffset % 8); } // Write ELF header. - WriteElfHeader(writer, (ulong)sectionHeadersOffset, (ushort)sectionCount, (ushort)shEntSize); + ushort headerPhEntSize = hasBuildId ? (ushort)phEntSize : (ushort)0; + WriteElfHeader(writer, (ulong)sectionHeadersOffset, (ushort)sectionCount, (ushort)shEntSize, + (ulong)phOffset, headerPhEntSize, phNum); // Write section data. writer.BaseStream.Seek(strtabOffset, SeekOrigin.Begin); @@ -169,6 +216,20 @@ public byte[] Build() writer.Write(dynsym); } + // Write note data. + if (hasBuildId) + { + writer.BaseStream.Seek(noteOffset, SeekOrigin.Begin); + writer.Write(noteData); + } + + // Write program headers. + if (hasBuildId) + { + writer.BaseStream.Seek(phOffset, SeekOrigin.Begin); + WriteProgramHeader(writer, PT_NOTE, (ulong)noteOffset, (ulong)noteData.Length); + } + // Pad to section header offset. while (writer.BaseStream.Position < sectionHeadersOffset) { @@ -210,7 +271,8 @@ public void GetPTLoadParams(out ulong pVaddr, out ulong pOffset) #region Private helpers - private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, ushort eShentsize) + private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, ushort eShentsize, + ulong ePhoff, ushort ePhentsize, ushort ePhnum) { // e_ident: magic + class + data + version + padding (16 bytes total). writer.Write((byte)0x7f); @@ -230,20 +292,20 @@ private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, us if (m_is64Bit) { WriteUInt64(writer, 0); // e_entry - WriteUInt64(writer, 0); // e_phoff + WriteUInt64(writer, ePhoff); // e_phoff WriteUInt64(writer, eShoff); // e_shoff } else { WriteUInt32(writer, 0); // e_entry - WriteUInt32(writer, 0); // e_phoff + WriteUInt32(writer, (uint)ePhoff); // e_phoff WriteUInt32(writer, (uint)eShoff); // e_shoff } WriteUInt32(writer, 0); // e_flags WriteUInt16(writer, (ushort)(m_is64Bit ? 64 : 52)); // e_ehsize - WriteUInt16(writer, 0); // e_phentsize - WriteUInt16(writer, 0); // e_phnum + WriteUInt16(writer, ePhentsize); // e_phentsize + WriteUInt16(writer, ePhnum); // e_phnum WriteUInt16(writer, eShentsize); // e_shentsize WriteUInt16(writer, eShnum); // e_shnum WriteUInt16(writer, 0); // e_shstrndx @@ -347,6 +409,65 @@ private void WriteSymbolEntry(BinaryWriter writer, uint stName, ulong stValue, u } } + /// + /// Builds a .note.gnu.build-id note: namesz(4) + descsz(4) + type(4) + "GNU\0" + buildId. + /// + private byte[] BuildBuildIdNote(byte[] buildId) + { + using (var ms = new MemoryStream()) + using (var writer = new BinaryWriter(ms)) + { + WriteUInt32(writer, 4); // namesz: length of "GNU\0" + WriteUInt32(writer, (uint)buildId.Length); // descsz: length of build-id + WriteUInt32(writer, NT_GNU_BUILD_ID); // type: NT_GNU_BUILD_ID (3) + writer.Write((byte)'G'); // name: "GNU\0" (already 4-byte aligned) + writer.Write((byte)'N'); + writer.Write((byte)'U'); + writer.Write((byte)0); + writer.Write(buildId); // desc: build-id bytes + + // Pad descriptor to 4-byte alignment. + int descPadding = ((buildId.Length + 3) & ~3) - buildId.Length; + for (int i = 0; i < descPadding; i++) + { + writer.Write((byte)0); + } + + return ms.ToArray(); + } + } + + /// + /// Writes a single ELF program header entry (PT_NOTE). + /// + private void WriteProgramHeader(BinaryWriter writer, uint pType, ulong pOffset, ulong pFilesz) + { + if (m_is64Bit) + { + // Elf64_Phdr: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8), p_memsz(8), p_align(8) + WriteUInt32(writer, pType); // p_type + WriteUInt32(writer, 0); // p_flags + WriteUInt64(writer, pOffset); // p_offset + WriteUInt64(writer, 0); // p_vaddr + WriteUInt64(writer, 0); // p_paddr + WriteUInt64(writer, pFilesz); // p_filesz + WriteUInt64(writer, pFilesz); // p_memsz + WriteUInt64(writer, 4); // p_align + } + else + { + // Elf32_Phdr: p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4), p_memsz(4), p_flags(4), p_align(4) + WriteUInt32(writer, pType); // p_type + WriteUInt32(writer, (uint)pOffset); // p_offset + WriteUInt32(writer, 0); // p_vaddr + WriteUInt32(writer, 0); // p_paddr + WriteUInt32(writer, (uint)pFilesz); // p_filesz + WriteUInt32(writer, (uint)pFilesz); // p_memsz + WriteUInt32(writer, 0); // p_flags + WriteUInt32(writer, 4); // p_align + } + } + #region Endianness helpers private void WriteUInt16(BinaryWriter writer, ushort val) diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs index 68f10a6ea..53cafd7b8 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs @@ -1,6 +1,8 @@ using System; +using System.Diagnostics; using System.IO; using Microsoft.Diagnostics.Symbols; +using Microsoft.Diagnostics.Tracing.Etlx; using Xunit; using Xunit.Abstractions; @@ -443,6 +445,81 @@ public void NonZeroPOffset_AdjustsRvaCorrectly() Assert.Equal(string.Empty, module.FindNameForRva(0x1000, ref symbolStart)); } + /// + /// Regression test for the libcoreclr.so bug where p_vaddr != p_offset. + /// In the real trace: p_vaddr=0x1c9060, p_offset=0x1c8060, pageSize=4096. + /// The Linux loader maps at PAGE_DOWN(p_vaddr) = 0x1c9000. + /// The caller (OpenElfSymbolsForModuleFile) page-aligns pVaddr and passes + /// the actual pOffset. LookupSymbolsForModule adds pOffset to + /// (address - ImageBase) so that the lookup RVA matches the ElfSymbolModule + /// formula (st_value - alignedPVaddr) + pOffset. + /// + [Fact] + public void NonPageAlignedPVaddr_CallerPageAligns() + { + // These values are from a real libcoreclr.so trace. + ulong rawPVaddr = 0x1c9060; + ulong rawPOffset = 0x1c8060; + // Note: rawPOffset (0x1c8060) differs from rawPVaddr — this is the root cause of the bug. + ulong pageSize = 4096; + + // The caller (OpenElfSymbolsForModuleFile) page-aligns before passing to ElfSymbolModule. + ulong alignedPVaddr = rawPVaddr & ~(pageSize - 1); // 0x1c9000 + Assert.Equal((ulong)0x1c9000, alignedPVaddr); + + // Symbol at virtual address 0x1D0000 (inside the executable segment). + ulong symbolAddr = 0x1D0000; + ulong symbolSize = 0x100; + + var builder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(alignedPVaddr, rawPOffset) + .AddFunction("coreclr_execute_assembly", symbolAddr, symbolSize); + + byte[] data = builder.Build(); + + // The caller passes (alignedPVaddr, rawPOffset) — the actual p_offset. + var module = CreateModule(data, alignedPVaddr, rawPOffset); + + uint symbolStart = 0; + // The ElfSymbolModule RVA formula: (st_value - pVaddr) + pOffset + // = (0x1D0000 - 0x1c9000) + 0x1c8060 = 0x7000 + 0x1c8060 = 0x1CF060 + // The caller (LookupSymbolsForModule) computes: (address - ImageBase) + pOffset + // = (st_value - alignedPVaddr) + pOffset — same value. + uint lookupRva = (uint)(symbolAddr - alignedPVaddr + rawPOffset); + Assert.Equal("coreclr_execute_assembly", module.FindNameForRva(lookupRva, ref symbolStart)); + Assert.Equal(lookupRva, symbolStart); + } + + /// + /// Verifies that ElfSymbolInfo.PageAlignedVirtualAddress correctly page-aligns p_vaddr. + /// + [Fact] + public void ElfSymbolInfo_PageAlignedVirtualAddress() + { + var info = new Microsoft.Diagnostics.Tracing.Etlx.ElfSymbolInfo(); + + // With page size set, non-aligned p_vaddr gets aligned. + info.VirtualAddress = 0x1c9060; + info.PageSize = 4096; + Assert.Equal((ulong)0x1c9000, info.PageAlignedVirtualAddress); + + // Already-aligned p_vaddr stays the same. + info.VirtualAddress = 0x400000; + info.PageSize = 4096; + Assert.Equal((ulong)0x400000, info.PageAlignedVirtualAddress); + + // PageSize=0 (unknown) returns raw VirtualAddress. + info.VirtualAddress = 0x1c9060; + info.PageSize = 0; + Assert.Equal((ulong)0x1c9060, info.PageAlignedVirtualAddress); + + // 64K pages (ARM64). + info.VirtualAddress = 0x1c9060; + info.PageSize = 65536; + Assert.Equal((ulong)0x1c0000, info.PageAlignedVirtualAddress); + } + #endregion #region Demangling Integration @@ -636,6 +713,193 @@ public void FilePathConstructor_LoadsSymbols() #endregion + #region ReadBuildId + + [Fact] + public void ReadBuildId_ValidElf64_ReturnsBuildId() + { + byte[] buildId = new byte[] { 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, + 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, + 0xab, 0xcd, 0xef, 0x01 }; + var builder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(0x400000, 0) + .SetBuildId(buildId); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadBuildId(path); + Assert.Equal("abcdef0123456789abcdef0123456789abcdef01", result); + }); + } + + [Fact] + public void ReadBuildId_ValidElf32_ReturnsBuildId() + { + byte[] buildId = new byte[] { 0xde, 0xad, 0xbe, 0xef }; + var builder = new ElfBuilder() + .Set64Bit(false) + .SetPTLoad(0x400000, 0) + .SetBuildId(buildId); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadBuildId(path); + Assert.Equal("deadbeef", result); + }); + } + + [Fact] + public void ReadBuildId_NoBuildId_ReturnsNull() + { + // ELF with no build-id note (no program headers). + var builder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(0x400000, 0) + .AddFunction("test", 0x401000, 0x100); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadBuildId(path); + Assert.Null(result); + }); + } + + [Fact] + public void ReadBuildId_NotElfFile_ReturnsNull() + { + byte[] data = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 }; + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadBuildId(path); + Assert.Null(result); + }); + } + + [Fact] + public void ReadBuildId_EmptyFile_ReturnsNull() + { + RunWithTempFile(Array.Empty(), (path) => + { + string result = ElfSymbolModule.ReadBuildId(path); + Assert.Null(result); + }); + } + + [Fact] + public void ReadBuildId_NonExistentFile_ReturnsNull() + { + string result = ElfSymbolModule.ReadBuildId(@"C:\nonexistent\path\fake.so"); + Assert.Null(result); + } + + [Fact] + public void ReadBuildId_BigEndianElf64_ReturnsBuildId() + { + byte[] buildId = new byte[] { 0x11, 0x22, 0x33, 0x44 }; + var builder = new ElfBuilder() + .Set64Bit(true) + .SetBigEndian(true) + .SetPTLoad(0x400000, 0) + .SetBuildId(buildId); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadBuildId(path); + Assert.Equal("11223344", result); + }); + } + + #endregion + + #region MatchOrInit tests + + [Fact] + public void MatchOrInitPE_WhenNull_CreatesPESymbolInfo() + { + var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0); + var pe = moduleFile.MatchOrInitPE(); + Assert.NotNull(pe); + Assert.IsType(pe); + Assert.Equal(ModuleBinaryFormat.PE, moduleFile.BinaryFormat); + } + + [Fact] + public void MatchOrInitPE_WhenAlreadyPE_ReturnsSame() + { + var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0); + var pe1 = moduleFile.MatchOrInitPE(); + var pe2 = moduleFile.MatchOrInitPE(); + Assert.Same(pe1, pe2); + } + + [Fact] + public void MatchOrInitPE_WhenElf_ReturnsNull() + { + var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0); + moduleFile.MatchOrInitElf(); // Set as ELF first + + // Suppress Debug.Assert so we can verify the return value. + var listeners = new TraceListener[Trace.Listeners.Count]; + Trace.Listeners.CopyTo(listeners, 0); + Trace.Listeners.Clear(); + try + { + var pe = moduleFile.MatchOrInitPE(); + Assert.Null(pe); + } + finally + { + Trace.Listeners.AddRange(listeners); + } + } + + [Fact] + public void MatchOrInitElf_WhenNull_CreatesElfSymbolInfo() + { + var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0); + var elf = moduleFile.MatchOrInitElf(); + Assert.NotNull(elf); + Assert.IsType(elf); + Assert.Equal(ModuleBinaryFormat.ELF, moduleFile.BinaryFormat); + } + + [Fact] + public void MatchOrInitElf_WhenAlreadyElf_ReturnsSame() + { + var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0); + var elf1 = moduleFile.MatchOrInitElf(); + var elf2 = moduleFile.MatchOrInitElf(); + Assert.Same(elf1, elf2); + } + + [Fact] + public void MatchOrInitElf_WhenPE_ReturnsNull() + { + var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0); + moduleFile.MatchOrInitPE(); // Set as PE first + + // Suppress Debug.Assert so we can verify the return value. + var listeners = new TraceListener[Trace.Listeners.Count]; + Trace.Listeners.CopyTo(listeners, 0); + Trace.Listeners.Clear(); + try + { + var elf = moduleFile.MatchOrInitElf(); + Assert.Null(elf); + } + finally + { + Trace.Listeners.AddRange(listeners); + } + } + + #endregion + #region Helpers /// diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs index 2a17ca2cc..0abfa501c 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Diagnostics.Symbols; +using Microsoft.Diagnostics.Symbols; using PerfView.TestUtilities; using System; using System.Collections.Generic; @@ -543,6 +543,629 @@ public void HttpRequestIncludesMsfzAcceptHeader() } } + #region FindElfSymbolFilePath Tests + + [Fact] + public void FindElfSymbolFilePath_DebugSymbolsFoundLocally() + { + string tempDir = Path.Combine(OutputDir, "elf-local-debug"); + try + { + string buildId = "abc123"; + string normalizedBuildId = buildId.ToLowerInvariant(); + + // Create SSQP debug symbol directory structure. + string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); + Directory.CreateDirectory(debugDir); + string debugFile = Path.Combine(debugDir, "_.debug"); + File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); // ELF magic + + _symbolReader.SymbolPath = tempDir; + string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", buildId); + + Assert.NotNull(result); + Assert.Equal(Path.GetFullPath(debugFile), Path.GetFullPath(result)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_BinaryFallbackLocally() + { + string tempDir = Path.Combine(OutputDir, "elf-local-binary"); + try + { + string buildId = "def456"; + string normalizedBuildId = buildId.ToLowerInvariant(); + + // Create only the binary directory structure (no debug symbols). + string binaryDir = Path.Combine(tempDir, "libcoreclr.so", "elf-buildid-" + normalizedBuildId); + Directory.CreateDirectory(binaryDir); + string binaryFile = Path.Combine(binaryDir, "libcoreclr.so"); + File.WriteAllBytes(binaryFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + + _symbolReader.SymbolPath = tempDir; + string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", buildId); + + Assert.NotNull(result); + Assert.Equal(Path.GetFullPath(binaryFile), Path.GetFullPath(result)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_DebugPreferredOverBinary() + { + string tempDir = Path.Combine(OutputDir, "elf-local-prefer-debug"); + try + { + string buildId = "aabbcc"; + string normalizedBuildId = buildId.ToLowerInvariant(); + + // Create both debug and binary directory structures. + string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); + Directory.CreateDirectory(debugDir); + string debugFile = Path.Combine(debugDir, "_.debug"); + File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + + string binaryDir = Path.Combine(tempDir, "libtest.so", "elf-buildid-" + normalizedBuildId); + Directory.CreateDirectory(binaryDir); + string binaryFile = Path.Combine(binaryDir, "libtest.so"); + File.WriteAllBytes(binaryFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + + _symbolReader.SymbolPath = tempDir; + string result = _symbolReader.FindElfSymbolFilePath("libtest.so", buildId); + + Assert.NotNull(result); + Assert.Equal(Path.GetFullPath(debugFile), Path.GetFullPath(result)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_NotFoundLocally() + { + string tempDir = Path.Combine(OutputDir, "elf-local-empty"); + try + { + Directory.CreateDirectory(tempDir); + + _symbolReader.SymbolPath = tempDir; + string result = _symbolReader.FindElfSymbolFilePath("libmissing.so", "deadbeef"); + + Assert.Null(result); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Theory] + [InlineData("abc", "abc")] + [InlineData("ABC123", "abc123")] + [InlineData("aabbccdd00112233445566778899aabbccddeeff", "aabbccdd00112233445566778899aabbccddeeff")] + public void FindElfSymbolFilePath_BuildIdNormalization(string inputBuildId, string expectedNormalized) + { + string tempDir = Path.Combine(OutputDir, "elf-buildid-norm"); + try + { + // Create debug symbol directory structure with normalized build ID. + string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + expectedNormalized); + Directory.CreateDirectory(debugDir); + string debugFile = Path.Combine(debugDir, "_.debug"); + File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + + _symbolReader.SymbolPath = tempDir; + string result = _symbolReader.FindElfSymbolFilePath("libnorm.so", inputBuildId); + + Assert.NotNull(result); + Assert.Equal(Path.GetFullPath(debugFile), Path.GetFullPath(result)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_AbsolutePathExtractsFilename() + { + string tempDir = Path.Combine(OutputDir, "elf-abspath"); + try + { + string buildId = "1122334455"; + string normalizedBuildId = buildId.ToLowerInvariant(); + + // Create binary directory structure using just the simple filename. + string binaryDir = Path.Combine(tempDir, "libc.so.6", "elf-buildid-" + normalizedBuildId); + Directory.CreateDirectory(binaryDir); + string binaryFile = Path.Combine(binaryDir, "libc.so.6"); + File.WriteAllBytes(binaryFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + + _symbolReader.SymbolPath = tempDir; + // Pass an absolute path — only the filename portion should be used for lookup. + string result = _symbolReader.FindElfSymbolFilePath("/usr/lib/x86_64-linux-gnu/libc.so.6", buildId); + + Assert.NotNull(result); + Assert.Equal(Path.GetFullPath(binaryFile), Path.GetFullPath(result)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_CacheOnlySkipsRemotePaths() + { + // Use a UNC-style path that is "remote" but won't actually be accessed. + _symbolReader.SymbolPath = @"\\nonexistent-server\symbols"; + _symbolReader.Options = SymbolReaderOptions.CacheOnly; + + string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", "aabbccdd"); + + Assert.Null(result); + } + + [Fact] + public void FindElfSymbolFilePath_CacheHitSkipsSearch() + { + string tempDir = Path.Combine(OutputDir, "elf-cache-hit"); + try + { + string buildId = "cachedid123"; + string normalizedBuildId = buildId.ToLowerInvariant(); + + string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); + Directory.CreateDirectory(debugDir); + string debugFile = Path.Combine(debugDir, "_.debug"); + File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + + _symbolReader.SymbolPath = tempDir; + + // First call populates the cache. + string result1 = _symbolReader.FindElfSymbolFilePath("libcache.so", buildId); + Assert.NotNull(result1); + + // Remove the file so only cache can return it. + File.Delete(debugFile); + Directory.Delete(debugDir); + + string result2 = _symbolReader.FindElfSymbolFilePath("libcache.so", buildId); + Assert.Equal(result1, result2); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_NegativeCacheReturnsNull() + { + string tempDir = Path.Combine(OutputDir, "elf-negative-cache"); + try + { + Directory.CreateDirectory(tempDir); + _symbolReader.SymbolPath = tempDir; + + // First call: nothing found, null is cached. + string result1 = _symbolReader.FindElfSymbolFilePath("libnocache.so", "ffffffff"); + Assert.Null(result1); + + // Now create the file — but the negative cache should still return null. + string normalizedBuildId = "ffffffff"; + string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); + Directory.CreateDirectory(debugDir); + File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), new byte[] { 0x7F }); + + string result2 = _symbolReader.FindElfSymbolFilePath("libnocache.so", "ffffffff"); + Assert.Null(result2); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_DifferentBuildIdsAreDifferentCacheKeys() + { + string tempDir = Path.Combine(OutputDir, "elf-diff-keys"); + try + { + string buildId1 = "aaaa"; + string buildId2 = "bbbb"; + string norm1 = buildId1; + string norm2 = buildId2; + + // Only create debug symbols for the second build ID. + string debugDir2 = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + norm2); + Directory.CreateDirectory(debugDir2); + string debugFile2 = Path.Combine(debugDir2, "_.debug"); + File.WriteAllBytes(debugFile2, new byte[] { 0x7F }); + + _symbolReader.SymbolPath = tempDir; + + string result1 = _symbolReader.FindElfSymbolFilePath("lib.so", buildId1); + Assert.Null(result1); + + string result2 = _symbolReader.FindElfSymbolFilePath("lib.so", buildId2); + Assert.NotNull(result2); + Assert.Equal(Path.GetFullPath(debugFile2), Path.GetFullPath(result2)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + #endregion + + #region FindR2RPerfMapSymbolFilePath Tests + + [Fact] + public void FindR2RPerfMapSymbolFilePath_FoundLocally() + { + string tempDir = Path.Combine(OutputDir, "r2r-local"); + try + { + Directory.CreateDirectory(tempDir); + string perfMapFile = Path.Combine(tempDir, "CoreLib.r2rmap"); + File.WriteAllBytes(perfMapFile, new byte[] { 0x01, 0x02 }); + + _symbolReader.SymbolPath = tempDir; + var sig = new Guid("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"); + string result = _symbolReader.FindR2RPerfMapSymbolFilePath("CoreLib.r2rmap", sig, 1); + + Assert.NotNull(result); + Assert.Equal(perfMapFile, result); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindR2RPerfMapSymbolFilePath_NotFound() + { + string tempDir = Path.Combine(OutputDir, "r2r-empty"); + try + { + Directory.CreateDirectory(tempDir); + + _symbolReader.SymbolPath = tempDir; + var sig = new Guid("11111111-2222-3333-4444-555555555555"); + string result = _symbolReader.FindR2RPerfMapSymbolFilePath("Missing.r2rmap", sig, 1); + + Assert.Null(result); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindR2RPerfMapSymbolFilePath_CacheOnlySkipsRemotePaths() + { + _symbolReader.SymbolPath = @"\\nonexistent-server\symbols"; + _symbolReader.Options = SymbolReaderOptions.CacheOnly; + + var sig = new Guid("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"); + string result = _symbolReader.FindR2RPerfMapSymbolFilePath("CoreLib.r2rmap", sig, 1); + + Assert.Null(result); + } + + [Fact] + public void FindR2RPerfMapSymbolFilePath_CacheHitSkipsSearch() + { + string tempDir = Path.Combine(OutputDir, "r2r-cache-hit"); + try + { + Directory.CreateDirectory(tempDir); + string perfMapFile = Path.Combine(tempDir, "Cached.r2rmap"); + File.WriteAllBytes(perfMapFile, new byte[] { 0x01 }); + + _symbolReader.SymbolPath = tempDir; + var sig = new Guid("cc000000-0000-0000-0000-000000000000"); + + // First call populates the cache. + string result1 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, 1); + Assert.NotNull(result1); + + // Remove the file so only cache can return it. + File.Delete(perfMapFile); + + string result2 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, 1); + Assert.Equal(result1, result2); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindR2RPerfMapSymbolFilePath_DifferentSignaturesAreDifferentCacheKeys() + { + string tempDir = Path.Combine(OutputDir, "r2r-diff-keys"); + try + { + Directory.CreateDirectory(tempDir); + // No file on disk — both lookups will miss the file system. + + _symbolReader.SymbolPath = tempDir; + var sig1 = new Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + var sig2 = new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); + + string result1 = _symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig1, 1); + Assert.Null(result1); + + // Now create the file — sig2 should find it (not negatively cached). + string perfMapFile = Path.Combine(tempDir, "Test.r2rmap"); + File.WriteAllBytes(perfMapFile, new byte[] { 0x01 }); + + string result2 = _symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig2, 1); + Assert.NotNull(result2); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + #endregion + + #region FindSymbolFilePathForModule Tests + + [Fact] + public void FindSymbolFilePathForModule_FileDoesNotExist() + { + string result = _symbolReader.FindSymbolFilePathForModule(@"C:\nonexistent\path\fake.dll"); + + Assert.Null(result); + } + + [Fact] + public void FindSymbolFilePathForModule_InvalidPeFile() + { + string tempDir = Path.Combine(OutputDir, "module-invalid-pe"); + Directory.CreateDirectory(tempDir); + string invalidDll = Path.Combine(tempDir, "invalid.dll"); + File.WriteAllText(invalidDll, "This is not a valid PE file"); + + // Should not throw — exception is caught internally and null returned. + string result = _symbolReader.FindSymbolFilePathForModule(invalidDll); + + Assert.Null(result); + } + + [Fact] + public void FindSymbolFilePathForModule_FindsPdbNextToDll() + { + PrepareTestData(); + + // The test data has PDB files. We need a DLL that references one of those PDBs. + // Since we may not have a matching DLL in test data, verify the basic "file exists" path + // by testing that a DLL file that exists but has no CodeView signature returns null gracefully. + string tempDir = Path.Combine(OutputDir, "module-no-codeview"); + Directory.CreateDirectory(tempDir); + // Create a minimal valid PE file (just MZ header + PE signature) that lacks CodeView info. + // The DOS stub points to PE signature at offset 0x80. + byte[] minimalPe = new byte[0x100]; + minimalPe[0] = 0x4D; // 'M' + minimalPe[1] = 0x5A; // 'Z' + minimalPe[0x3C] = 0x80; // e_lfanew + minimalPe[0x80] = 0x50; // 'P' + minimalPe[0x81] = 0x45; // 'E' + minimalPe[0x82] = 0x00; + minimalPe[0x83] = 0x00; + string minimalDll = Path.Combine(tempDir, "minimal.dll"); + File.WriteAllBytes(minimalDll, minimalPe); + + // This PE file has no CodeView debug directory, so FindSymbolFilePathForModule + // should return null (either via no PDB signature or PE parsing gracefully failing). + string result = _symbolReader.FindSymbolFilePathForModule(minimalDll); + Assert.Null(result); + } + + #endregion + + #region Cache Invalidation Tests + + [Fact] + public void ElfCache_ClearedWhenSymbolPathChanges() + { + string tempDir1 = Path.Combine(OutputDir, "elf-cache-inv1"); + string tempDir2 = Path.Combine(OutputDir, "elf-cache-inv2"); + try + { + // Set up: first path has nothing, second path has the file. + Directory.CreateDirectory(tempDir1); + + string buildId = "cachetest1"; + string normalizedBuildId = buildId; + string debugDir = Path.Combine(tempDir2, "_.debug", "elf-buildid-sym-" + normalizedBuildId); + Directory.CreateDirectory(debugDir); + File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), new byte[] { 0x7F }); + + // First search against empty path — null is cached. + _symbolReader.SymbolPath = tempDir1; + Assert.Null(_symbolReader.FindElfSymbolFilePath("lib.so", buildId)); + + // Change SymbolPath — cache should be cleared, so the new path is searched. + _symbolReader.SymbolPath = tempDir2; + Assert.NotNull(_symbolReader.FindElfSymbolFilePath("lib.so", buildId)); + } + finally + { + if (Directory.Exists(tempDir1)) Directory.Delete(tempDir1, true); + if (Directory.Exists(tempDir2)) Directory.Delete(tempDir2, true); + } + } + + [Fact] + public void R2RCache_ClearedWhenSymbolPathChanges() + { + string tempDir1 = Path.Combine(OutputDir, "r2r-cache-inv1"); + string tempDir2 = Path.Combine(OutputDir, "r2r-cache-inv2"); + try + { + Directory.CreateDirectory(tempDir1); + Directory.CreateDirectory(tempDir2); + File.WriteAllBytes(Path.Combine(tempDir2, "Test.r2rmap"), new byte[] { 0x01 }); + + var sig = new Guid("12345678-1234-1234-1234-123456789abc"); + + // First search against empty path — null is cached. + _symbolReader.SymbolPath = tempDir1; + Assert.Null(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, 1)); + + // Change SymbolPath — cache should be cleared, so the new path is searched. + _symbolReader.SymbolPath = tempDir2; + Assert.NotNull(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, 1)); + } + finally + { + if (Directory.Exists(tempDir1)) Directory.Delete(tempDir1, true); + if (Directory.Exists(tempDir2)) Directory.Delete(tempDir2, true); + } + } + + [Fact] + public void ElfCache_ClearedWhenOptionsChange() + { + string tempDir = Path.Combine(OutputDir, "elf-cache-opt"); + try + { + string buildId = "opttest1"; + string normalizedBuildId = buildId; + string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); + Directory.CreateDirectory(debugDir); + File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), new byte[] { 0x7F }); + + // First: find it successfully and cache it. + _symbolReader.SymbolPath = tempDir; + Assert.NotNull(_symbolReader.FindElfSymbolFilePath("lib.so", buildId)); + + // Remove the file. + Directory.Delete(debugDir, true); + + // Without cache invalidation the cached path would still be returned. + // Changing Options should clear the cache, forcing a fresh lookup. + _symbolReader.Options = SymbolReaderOptions.CacheOnly; + Assert.Null(_symbolReader.FindElfSymbolFilePath("lib.so", buildId)); + } + finally + { + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + } + + #endregion + + #region ELF Module Cache Tests + + [Fact] + public void OpenElfSymbolFile_CacheHitReturnsSameInstance() + { + string tempDir = Path.Combine(OutputDir, "elf-mod-cache-hit"); + try + { + Directory.CreateDirectory(tempDir); + // Create a dummy file — ElfSymbolModule gracefully handles non-ELF content. + string elfFile = Path.Combine(tempDir, "libtest.so"); + File.WriteAllBytes(elfFile, new byte[] { 0x00 }); + + _symbolReader.SecurityCheck = _ => true; + var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); + var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); + + Assert.Same(module1, module2); + } + finally + { + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + } + + [Fact] + public void OpenElfSymbolFile_DifferentParamsAreDifferentCacheEntries() + { + string tempDir = Path.Combine(OutputDir, "elf-mod-cache-diff"); + try + { + Directory.CreateDirectory(tempDir); + string elfFile = Path.Combine(tempDir, "libtest.so"); + File.WriteAllBytes(elfFile, new byte[] { 0x00 }); + + _symbolReader.SecurityCheck = _ => true; + var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); + var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x2000, 0x0); + + Assert.NotSame(module1, module2); + } + finally + { + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + } + + [Fact] + public void OpenElfSymbolFile_CacheClearedOnSymbolPathChange() + { + string tempDir = Path.Combine(OutputDir, "elf-mod-cache-clear"); + try + { + Directory.CreateDirectory(tempDir); + string elfFile = Path.Combine(tempDir, "libtest.so"); + File.WriteAllBytes(elfFile, new byte[] { 0x00 }); + + _symbolReader.SecurityCheck = _ => true; + var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); + + // Changing SymbolPath clears all caches including the module cache. + _symbolReader.SymbolPath = tempDir; + + var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); + + // Should be a different instance because cache was cleared. + Assert.NotSame(module1, module2); + } + finally + { + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + } + + #endregion + protected void PrepareTestData() { lock (s_fileLock) @@ -658,4 +1281,4 @@ protected override IEnumerable GetSourceLinkJson() } } } -} \ No newline at end of file +} diff --git a/src/TraceEvent/TraceLog.cs b/src/TraceEvent/TraceLog.cs index b7957ae87..08635f118 100644 --- a/src/TraceEvent/TraceLog.cs +++ b/src/TraceEvent/TraceLog.cs @@ -1557,12 +1557,15 @@ private unsafe void SetupCallbacks(TraceEventDispatcher rawEvents) // TODO review: is using the timestamp the best way to make the association if (lastDbgData != null && data.TimeStampQPC == lastDbgData.TimeStampQPC) { - moduleFile.pdbName = lastDbgData.PdbFileName; - moduleFile.pdbSignature = lastDbgData.GuidSig; - moduleFile.pdbAge = lastDbgData.Age; - // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time - // We tolerate the exceptions, because it is a useful check most of the time - Debug.Assert(RoughDllPdbMatch(moduleFile.fileName, moduleFile.pdbName)); + if (moduleFile.MatchOrInitPE() is { } peInfo) + { + peInfo.PdbName = lastDbgData.PdbFileName; + peInfo.PdbSignature = lastDbgData.GuidSig; + peInfo.PdbAge = lastDbgData.Age; + // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time + // We tolerate the exceptions, because it is a useful check most of the time + Debug.Assert(RoughDllPdbMatch(moduleFile.fileName, moduleFile.PdbName)); + } } moduleFile.timeDateStamp = data.TimeDateStamp; moduleFile.imageChecksum = data.ImageChecksum; @@ -1593,14 +1596,17 @@ private unsafe void SetupCallbacks(TraceEventDispatcher rawEvents) hasPdbInfo = true; // The ImageIDDbgID_RSDS may be after the ImageLoad - if (lastTraceModuleFile != null && lastTraceModuleFileQPC == data.TimeStampQPC && string.IsNullOrEmpty(lastTraceModuleFile.pdbName)) - { - lastTraceModuleFile.pdbName = data.PdbFileName; - lastTraceModuleFile.pdbSignature = data.GuidSig; - lastTraceModuleFile.pdbAge = data.Age; - // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time - // We tolerate the exceptions, because it is a useful check most of the time - Debug.Assert(RoughDllPdbMatch(lastTraceModuleFile.fileName, lastTraceModuleFile.pdbName)); + if (lastTraceModuleFile != null && lastTraceModuleFileQPC == data.TimeStampQPC && string.IsNullOrEmpty(lastTraceModuleFile.PdbName)) + { + if (lastTraceModuleFile.MatchOrInitPE() is { } peInfo) + { + peInfo.PdbName = data.PdbFileName; + peInfo.PdbSignature = data.GuidSig; + peInfo.PdbAge = data.Age; + // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time + // We tolerate the exceptions, because it is a useful check most of the time + Debug.Assert(RoughDllPdbMatch(lastTraceModuleFile.fileName, lastTraceModuleFile.PdbName)); + } lastDbgData = null; } else // Or before (it is handled in ImageGroup callback above) @@ -4140,7 +4146,7 @@ void IFastSerializable.FromStream(Deserializer deserializer) } int IFastSerializableVersion.Version { - get { return 76; } + get { return 77; } } int IFastSerializableVersion.MinimumVersionCanRead { @@ -4165,6 +4171,7 @@ int IFastSerializableVersion.MinimumReaderVersion private string etlxFilePath; private int memorySizeMeg; private int eventsLost; + internal ulong systemPageSize; private string osName; private string osBuild; private long bootTime100ns; // This is a windows FILETIME object @@ -7069,11 +7076,14 @@ internal void ManagedModuleLoadOrUnload(ModuleLoadUnloadTraceData data, bool isL process.Log.ModuleFiles.SetModuleFileName(module.ModuleFile, ilModulePath); } - if (module.ModuleFile.pdbSignature == Guid.Empty && data.ManagedPdbSignature != Guid.Empty) + if (module.ModuleFile.PdbSignature == Guid.Empty && data.ManagedPdbSignature != Guid.Empty) { - module.ModuleFile.pdbSignature = data.ManagedPdbSignature; - module.ModuleFile.pdbAge = data.ManagedPdbAge; - module.ModuleFile.pdbName = data.ManagedPdbBuildPath; + if (module.ModuleFile.MatchOrInitPE() is { } peInfo) + { + peInfo.PdbSignature = data.ManagedPdbSignature; + peInfo.PdbAge = data.ManagedPdbAge; + peInfo.PdbName = data.ManagedPdbBuildPath; + } } if (module.NativeModule != null) @@ -7082,11 +7092,14 @@ internal void ManagedModuleLoadOrUnload(ModuleLoadUnloadTraceData data, bool isL module.NativeModule.ModuleFile.managedModule.FilePath == module.ModuleFile.FilePath); module.NativeModule.ModuleFile.managedModule = module.ModuleFile; - if (nativePdbSignature != Guid.Empty && module.NativeModule.ModuleFile.pdbSignature == Guid.Empty) + if (nativePdbSignature != Guid.Empty && module.NativeModule.ModuleFile.PdbSignature == Guid.Empty) { - module.NativeModule.ModuleFile.pdbSignature = nativePdbSignature; - module.NativeModule.ModuleFile.pdbAge = data.NativePdbAge; - module.NativeModule.ModuleFile.pdbName = data.NativePdbBuildPath; + if (module.NativeModule.ModuleFile.MatchOrInitPE() is { } nativePeInfo) + { + nativePeInfo.PdbSignature = nativePdbSignature; + nativePeInfo.PdbAge = data.NativePdbAge; + nativePeInfo.PdbName = data.NativePdbBuildPath; + } } module.InitializeNativeModuleIsReadyToRun(); @@ -7128,7 +7141,6 @@ internal TraceModuleFile UniversalMapping(string fileName, Address startAddress, // A loaded and managed modules depend on a module file, so get or create one. // The key is the file name. For jitted code on Linux, this will be a memfd with a static name, which is OK // because this path will use the StartAddress to ensure that we get the right one. - // TODO: We'll need to store FileOffset as well to handle elf images. TraceModuleFile moduleFile = process.Log.ModuleFiles.GetOrCreateModuleFile(fileName, startAddress); long newImageSize = (long)(endAddress - startAddress); @@ -7169,16 +7181,31 @@ internal TraceModuleFile UniversalMapping(string fileName, Address startAddress, Debug.Assert(moduleFile != null); CheckClassInvarients(); - PEProcessMappingSymbolMetadata symbolMetadata = metadata?.ParsedSymbolMetadata as PEProcessMappingSymbolMetadata; - if (symbolMetadata != null) + PEProcessMappingSymbolMetadata peMetadata = metadata?.ParsedSymbolMetadata as PEProcessMappingSymbolMetadata; + if (peMetadata != null) { - moduleFile.pdbName = symbolMetadata.PdbName; - moduleFile.pdbAge = symbolMetadata.PdbAge; - moduleFile.pdbSignature = symbolMetadata.PdbSignature; - moduleFile.r2rPerfMapSignature = symbolMetadata.PerfmapSignature; - moduleFile.r2rPerfMapVersion = symbolMetadata.PerfmapVersion; - moduleFile.r2rPerfMapName = symbolMetadata.PerfmapName; - moduleFile.r2rImageTextVirtualOffset = (uint)symbolMetadata.TextOffset; + if (moduleFile.MatchOrInitPE() is { } peInfo) + { + peInfo.PdbName = peMetadata.PdbName; + peInfo.PdbAge = peMetadata.PdbAge; + peInfo.PdbSignature = peMetadata.PdbSignature; + peInfo.R2RPerfMapSignature = peMetadata.PerfmapSignature; + peInfo.R2RPerfMapVersion = peMetadata.PerfmapVersion; + peInfo.R2RPerfMapName = peMetadata.PerfmapName; + peInfo.R2RImageTextVirtualOffset = (uint)peMetadata.TextOffset; + } + } + + ELFProcessMappingSymbolMetadata elfMetadata = metadata?.ParsedSymbolMetadata as ELFProcessMappingSymbolMetadata; + if (elfMetadata != null) + { + if (moduleFile.MatchOrInitElf() is { } elfInfo) + { + elfInfo.BuildId = elfMetadata.BuildId; + elfInfo.VirtualAddress = elfMetadata.VirtualAddress; + elfInfo.FileOffset = elfMetadata.FileOffset; + elfInfo.PageSize = process.Log.systemPageSize; + } } return moduleFile; @@ -7614,7 +7641,10 @@ internal void InitializeNativeModuleIsReadyToRun() { if (NativeModule != null && (flags & ModuleFlags.ReadyToRunModule) != ModuleFlags.None) { - NativeModule.ModuleFile.isReadyToRun = true; + if (NativeModule.ModuleFile.MatchOrInitPE() is { } pe) + { + pe.IsReadyToRun = true; + } } } @@ -8898,26 +8928,72 @@ private void LookupSymbolsForModule(SymbolReader reader, TraceModuleFile moduleF reader.m_log.WriteLine("[Loading symbols for " + moduleFile.FilePath + "]"); - // There is where we hook up R2R symbol lookup for Linux. These symbol modules are .r2rmap files. - // The R2R modules that are used on Linux still have a pointer to the IL PDB, so we need to look for a R2R symbol module - // before attempting to lookup a PDB, or we may end up with an IL PDB, which won't be helpful for symbol lookup. - // For Windows traces this call will always return immediately because the module won't have any R2R perfmap information. + // Dispatch symbol lookup by binary format. ISymbolLookup symbolLookup = null; - R2RPerfMapSymbolModule r2rSymbolModule = OpenR2RPerfMapForModuleFile(reader, moduleFile); - if (r2rSymbolModule != null) + Func computeRva = null; + switch (moduleFile.BinaryFormat) { - symbolLookup = r2rSymbolModule; + case ModuleBinaryFormat.ELF: + { + ElfSymbolModule elfModule = OpenElfSymbolsForModuleFile(reader, moduleFile); + if (elfModule != null) + { + symbolLookup = elfModule; + // ELF RVA = (address - ImageBase) + FileOffset, matching ElfSymbolModule's + // (st_value - pVaddr) + pOffset formula. + ulong fileOffset = moduleFile.ElfInfo.FileOffset; + computeRva = (address) => (uint)(address - moduleFile.ImageBase) + (uint)fileOffset; + } + } + break; + + case ModuleBinaryFormat.PE: + { + // Try R2R perfmap first (Linux managed with precompiled code), + // then fall back to PDB. + R2RPerfMapSymbolModule r2rSymbolModule = OpenR2RPerfMapForModuleFile(reader, moduleFile); + if (r2rSymbolModule != null) + { + symbolLookup = r2rSymbolModule; + } + else + { + NativeSymbolModule moduleReader = OpenPdbForModuleFile(reader, moduleFile) as NativeSymbolModule; + if (moduleReader != null) + { + symbolLookup = moduleReader; + } + } + // PE RVA = address - ImageBase (standard Windows convention). + computeRva = (address) => (uint)(address - moduleFile.ImageBase); + } + break; + + default: + { + // Unknown format — try all paths: R2R, then PDB. + R2RPerfMapSymbolModule r2rSymbolModule = OpenR2RPerfMapForModuleFile(reader, moduleFile); + if (r2rSymbolModule != null) + { + symbolLookup = r2rSymbolModule; + } + else + { + NativeSymbolModule moduleReader = OpenPdbForModuleFile(reader, moduleFile) as NativeSymbolModule; + if (moduleReader != null) + { + symbolLookup = moduleReader; + } + } + computeRva = (address) => (uint)(address - moduleFile.ImageBase); + } + break; } - else - { - NativeSymbolModule moduleReader = OpenPdbForModuleFile(reader, moduleFile) as NativeSymbolModule; - if (moduleReader == null) - { - reader.m_log.WriteLine("Could not find PDB file."); - return; - } - symbolLookup = moduleReader; + if (symbolLookup == null) + { + reader.m_log.WriteLine("Could not find symbols."); + return; } reader.m_log.WriteLine("Loaded, resolving symbols"); @@ -8948,7 +9024,9 @@ private void LookupSymbolsForModule(SymbolReader reader, TraceModuleFile moduleF else { uint symbolStart = 0; - var newMethodName = symbolLookup.FindNameForRva((uint)(address - moduleFile.ImageBase), ref symbolStart); + uint rva = computeRva(address); + + var newMethodName = symbolLookup.FindNameForRva(rva, ref symbolStart); if (newMethodName.Length > 0) { // TODO FIX NOW @@ -9118,7 +9196,7 @@ private unsafe ManagedSymbolModule OpenPdbForModuleFile(SymbolReader symReader, var nativePdb = symbolReaderModule as NativeSymbolModule; if (nativePdb != null) { - nativePdb.LogManagedInfo(managed.PdbName, managed.PdbSignature, managed.pdbAge); + nativePdb.LogManagedInfo(managed.PdbName, managed.PdbSignature, managed.PdbAge); } } } @@ -9132,20 +9210,22 @@ private unsafe ManagedSymbolModule OpenPdbForModuleFile(SymbolReader symReader, /// private unsafe R2RPerfMapSymbolModule OpenR2RPerfMapForModuleFile(SymbolReader symReader, TraceModuleFile moduleFile) { + Debug.Assert(moduleFile.PEInfo != null, "OpenR2RPerfMapForModuleFile called with null PEInfo"); + var peInfo = moduleFile.PEInfo; // If we have a signature, use it - if (moduleFile.r2rPerfMapSignature != Guid.Empty) + if (peInfo != null && peInfo.R2RPerfMapSignature != Guid.Empty) { - string filePath = symReader.FindR2RPerfMapSymbolFilePath(moduleFile.R2RPerfMapName, moduleFile.R2RPerfMapSignature, moduleFile.R2RPerfMapVersion); + string filePath = symReader.FindR2RPerfMapSymbolFilePath(peInfo.R2RPerfMapName, peInfo.R2RPerfMapSignature, peInfo.R2RPerfMapVersion); if (filePath != null) { - R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(filePath, moduleFile.R2RImageTextVirtualOffset); - if (symbolModule != null && symbolModule.Signature == moduleFile.R2RPerfMapSignature && symbolModule.Version == moduleFile.R2RPerfMapVersion) + R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(filePath, peInfo.R2RImageTextVirtualOffset); + if (symbolModule != null && symbolModule.Signature == peInfo.R2RPerfMapSignature && symbolModule.Version == peInfo.R2RPerfMapVersion) { return symbolModule; } else { - symReader.m_log.WriteLine("ERROR: The R2R perfmap does not match the loaded module. Actual Signature = " + symbolModule.Signature + " Requested Signature = " + moduleFile.R2RPerfMapSignature); + symReader.m_log.WriteLine("ERROR: The R2R perfmap does not match the loaded module. Actual Signature = " + symbolModule.Signature + " Requested Signature = " + peInfo.R2RPerfMapSignature); throw new Exception("ERROR: The R2R perfmap does not match the loaded module."); } } @@ -9155,9 +9235,141 @@ private unsafe R2RPerfMapSymbolModule OpenR2RPerfMapForModuleFile(SymbolReader s symReader.m_log.WriteLine("No R2R perfmap signature for {0} in trace.", moduleFile.FilePath); } + // Fallback: look for an R2R perfmap file next to the binary. + if (peInfo != null && !string.IsNullOrEmpty(peInfo.R2RPerfMapName) && peInfo.R2RPerfMapSignature != Guid.Empty) + { + string moduleDir = Path.GetDirectoryName(moduleFile.FilePath); + if (!string.IsNullOrEmpty(moduleDir)) + { + // Sanitize the perfmap name to prevent path traversal from trace data. + string candidatePath = Path.Combine(moduleDir, Path.GetFileName(peInfo.R2RPerfMapName)); + if (File.Exists(candidatePath)) + { + try + { + R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(candidatePath, peInfo.R2RImageTextVirtualOffset); + if (symbolModule != null && symbolModule.Signature == peInfo.R2RPerfMapSignature && symbolModule.Version == peInfo.R2RPerfMapVersion) + { + return symbolModule; + } + + // Module doesn't match the expected signature; skip it. + symReader.m_log.WriteLine("R2R perfmap adjacent to binary {0} does not match signature. Actual = {1}, Expected = {2}", + candidatePath, symbolModule?.Signature, peInfo.R2RPerfMapSignature); + } + catch (Exception e) + { + symReader.m_log.WriteLine("Error opening R2R perfmap adjacent to binary {0}: {1}", candidatePath, e.Message); + } + } + } + } + + return null; + } + + /// + /// Attempts to find and open ELF debug symbols for the given module file. + /// Returns an ElfSymbolModule if symbols are found, null otherwise. + /// + private ElfSymbolModule OpenElfSymbolsForModuleFile(SymbolReader reader, TraceModuleFile moduleFile) + { + var elfInfo = moduleFile.ElfInfo; + if (elfInfo == null || string.IsNullOrEmpty(elfInfo.BuildId)) + { + return null; + } + + Debug.Assert(moduleFile.ElfInfo != null, "OpenElfSymbolsForModuleFile called with null ElfInfo"); + + ulong alignedVAddr = elfInfo.PageAlignedVirtualAddress; + + // Try symbol server / symbol path first. + string symbolFilePath = reader.FindElfSymbolFilePath(moduleFile.Name, elfInfo.BuildId); + if (symbolFilePath != null) + { + try + { + reader.m_log.WriteLine("Opening ELF symbols from {0} (pVaddr=0x{1:x}, aligned=0x{2:x}, pOffset=0x{3:x}, pageSize={4})", + symbolFilePath, elfInfo.VirtualAddress, alignedVAddr, elfInfo.FileOffset, elfInfo.PageSize); + ElfSymbolModule module = reader.OpenElfSymbolFile(symbolFilePath, alignedVAddr, elfInfo.FileOffset, elfInfo.BuildId); + if (module != null) + { + return module; + } + } + catch (Exception e) + { + reader.m_log.WriteLine("Error opening ELF symbol file {0}: {1}", symbolFilePath, e.Message); + } + } + else + { + reader.m_log.WriteLine("Could not find ELF symbol file for {0} (BuildId: {1})", moduleFile.Name, elfInfo.BuildId); + } + + // Fallback: look for ELF symbols adjacent to the binary. + // Prefer debug symbol files before falling back to the binary itself. + string moduleDir = Path.GetDirectoryName(moduleFile.FilePath); + if (!string.IsNullOrEmpty(moduleDir)) + { + string basePath = moduleFile.FilePath; + + // Try {path}.debug + string candidate = basePath + ".debug"; + ElfSymbolModule result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); + if (result != null) return result; + + // Try {path}.dbg + candidate = basePath + ".dbg"; + result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); + if (result != null) return result; + + // If the path contains .so (possibly with version suffix like .so.1.2.3), strip it and try again. + int soIndex = basePath.IndexOf(".so", StringComparison.OrdinalIgnoreCase); + if (soIndex >= 0) + { + string pathWithoutSo = basePath.Substring(0, soIndex); + + candidate = pathWithoutSo + ".debug"; + result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); + if (result != null) return result; + + candidate = pathWithoutSo + ".dbg"; + result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); + if (result != null) return result; + } + + // Last resort: try the binary itself (has .dynsym at minimum). + result = TryOpenElfFallback(reader, basePath, alignedVAddr, elfInfo); + if (result != null) return result; + } + return null; } + /// + /// Helper to try opening an ELF file as a symbol source with build-id validation. + /// Returns null if the file doesn't exist or doesn't match. + /// + private ElfSymbolModule TryOpenElfFallback(SymbolReader reader, string candidatePath, ulong alignedVAddr, ElfSymbolInfo elfInfo) + { + if (!File.Exists(candidatePath)) + { + return null; + } + + try + { + return reader.OpenElfSymbolFile(candidatePath, alignedVAddr, elfInfo.FileOffset, elfInfo.BuildId); + } + catch (Exception e) + { + reader.m_log.WriteLine("Error opening ELF fallback {0}: {1}", candidatePath, e.Message); + return null; + } + } + /// /// Returns true if 'moduleFile' seems to be unchanged from the time the information about it /// was generated. Logs messages to 'log' if it fails. @@ -9429,7 +9641,7 @@ internal void SetModuleFileIndex(TraceModuleFile moduleFile) moduleFileIndex = moduleFile.ModuleFileIndex; if (optimizationTier == Parsers.Clr.OptimizationTier.Unknown && - moduleFile.IsReadyToRun && + (moduleFile.PEInfo?.IsReadyToRun ?? false) && moduleFile.ImageBase <= Address && Address < moduleFile.ImageEnd) { @@ -10447,35 +10659,76 @@ public string Name /// /// The name of the symbol file (PDB file) associated with the DLL /// - public string PdbName { get { return pdbName; } } + public string PdbName { get { return PEInfo?.PdbName ?? ""; } } /// /// Returns the GUID that uniquely identifies the symbol file (PDB file) for this DLL /// - public Guid PdbSignature { get { return pdbSignature; } } + public Guid PdbSignature { get { return PEInfo?.PdbSignature ?? Guid.Empty; } } /// /// Returns the age (which is a small integer), that is also needed to look up the symbol file (PDB file) on a symbol server. /// - public int PdbAge { get { return pdbAge; } } + public int PdbAge { get { return PEInfo?.PdbAge ?? 0; } } + + /// + /// The binary format of this module file (PE, ELF, or Unknown). + /// + public ModuleBinaryFormat BinaryFormat { get { return symbolInfo?.Format ?? ModuleBinaryFormat.Unknown; } } /// - /// Returns the GUID that uniquely identifies the R2R perfmap file for this DLL + /// PE-specific symbol info (PDB identity + R2R). Null if this is not a PE module. /// - public Guid R2RPerfMapSignature { get { return r2rPerfMapSignature; } } + public PESymbolInfo PEInfo { get { return symbolInfo as PESymbolInfo; } } /// - /// Returns the version number of the R2R perfmap file format. + /// ELF-specific symbol info (BuildId + load header). Null if this is not an ELF module. /// - public int R2RPerfMapVersion { get { return r2rPerfMapVersion; } } + public ElfSymbolInfo ElfInfo { get { return symbolInfo as ElfSymbolInfo; } } /// - /// Returns the name of the R2R perfmap file. + /// Returns PESymbolInfo if this module's symbolInfo is already PE, creates one if null. + /// Returns null if symbolInfo is a different type (e.g. ELF) — prevents silent overwrites. + /// Use with pattern matching: if (moduleFile.MatchOrInitPE() is { } pe) { pe.Field = value; } /// - public string R2RPerfMapName { get { return r2rPerfMapName; } } + internal PESymbolInfo MatchOrInitPE() + { + if (symbolInfo is PESymbolInfo pe) + { + return pe; + } + + if (symbolInfo != null) + { + Debug.Assert(false, $"MatchOrInitPE called but symbolInfo is {symbolInfo.GetType().Name}, not PESymbolInfo. This is a bug — module metadata is being set for the wrong binary format."); + return null; + } + + pe = new PESymbolInfo(); + symbolInfo = pe; + return pe; + } /// - /// Returns the offset in bytes between the beginning of the PE image and the beginning of the text section according to the loaded layout. + /// Returns ElfSymbolInfo if this module's symbolInfo is already ELF, creates one if null. + /// Returns null if symbolInfo is a different type (e.g. PE) — prevents silent overwrites. + /// Use with pattern matching: if (moduleFile.MatchOrInitElf() is { } elf) { elf.Field = value; } /// - public uint R2RImageTextVirtualOffset { get { return r2rImageTextVirtualOffset; } } + internal ElfSymbolInfo MatchOrInitElf() + { + if (symbolInfo is ElfSymbolInfo elf) + { + return elf; + } + + if (symbolInfo != null) + { + Debug.Assert(false, $"MatchOrInitElf called but symbolInfo is {symbolInfo.GetType().Name}, not ElfSymbolInfo. This is a bug — module metadata is being set for the wrong binary format."); + return null; + } + + elf = new ElfSymbolInfo(); + symbolInfo = elf; + return elf; + } /// /// Returns the file version string that is optionally embedded in the DLL's resources. Returns the empty string if not present. @@ -10508,7 +10761,7 @@ public string Name /// /// Tells if the module file is ReadyToRun (the has precompiled code for some managed methods) /// - public bool IsReadyToRun { get { return isReadyToRun; } } + public bool IsReadyToRun { get { return PEInfo?.IsReadyToRun ?? false; } } /// /// If the Product Version fields has a GIT Commit Hash component, this returns it, Otherwise it is empty. @@ -10605,7 +10858,6 @@ internal TraceModuleFile(string fileName, Address imageBase, ModuleFileIndex mod this.moduleFileIndex = moduleFileIndex; fileVersion = ""; productVersion = ""; - pdbName = ""; } internal string fileName; @@ -10613,16 +10865,8 @@ internal TraceModuleFile(string fileName, Address imageBase, ModuleFileIndex mod internal Address imageBase; internal string name; private ModuleFileIndex moduleFileIndex; - internal bool isReadyToRun; internal TraceModuleFile next; // Chain of modules that have the same path (But different image bases) - internal string pdbName; - internal Guid pdbSignature; - internal int pdbAge; - internal Guid r2rPerfMapSignature; - internal int r2rPerfMapVersion; - internal string r2rPerfMapName; - internal uint r2rImageTextVirtualOffset; internal string fileVersion; internal string productName; internal string productVersion; @@ -10630,6 +10874,7 @@ internal TraceModuleFile(string fileName, Address imageBase, ModuleFileIndex mod internal int imageChecksum; // used to validate if the local file is the same as the one from the trace. internal int codeAddressesInModule; internal TraceModuleFile managedModule; + internal TraceModuleFileSymbolInfo symbolInfo; void IFastSerializable.ToStream(Serializer serializer) @@ -10638,13 +10883,14 @@ void IFastSerializable.ToStream(Serializer serializer) serializer.Write(imageSize); serializer.WriteAddress(imageBase); - serializer.Write(pdbName); - serializer.Write(pdbSignature); - serializer.Write(pdbAge); - serializer.Write(r2rPerfMapSignature); - serializer.Write(r2rPerfMapVersion); - serializer.Write(r2rPerfMapName); - serializer.Write((int)r2rImageTextVirtualOffset); + // Write symbol info with format discriminator + byte format = (byte)(symbolInfo?.Format ?? ModuleBinaryFormat.Unknown); + serializer.Write(format); + if (symbolInfo != null) + { + symbolInfo.ToStream(serializer); + } + serializer.Write(fileVersion); serializer.Write(productVersion); serializer.Write(timeDateStamp); @@ -10659,13 +10905,25 @@ void IFastSerializable.FromStream(Deserializer deserializer) deserializer.Read(out imageSize); deserializer.ReadAddress(out imageBase); - deserializer.Read(out pdbName); - deserializer.Read(out pdbSignature); - deserializer.Read(out pdbAge); - deserializer.Read(out r2rPerfMapSignature); - deserializer.Read(out r2rPerfMapVersion); - deserializer.Read(out r2rPerfMapName); - r2rImageTextVirtualOffset = (uint)deserializer.ReadInt(); + // Read symbol info with format discriminator + byte format = deserializer.ReadByte(); + switch ((ModuleBinaryFormat)format) + { + case ModuleBinaryFormat.PE: + var pe = new PESymbolInfo(); + pe.FromStream(deserializer); + symbolInfo = pe; + break; + case ModuleBinaryFormat.ELF: + var elf = new ElfSymbolInfo(); + elf.FromStream(deserializer); + symbolInfo = elf; + break; + default: + symbolInfo = null; + break; + } + deserializer.Read(out fileVersion); deserializer.Read(out productVersion); deserializer.Read(out timeDateStamp); @@ -10677,6 +10935,143 @@ void IFastSerializable.FromStream(Deserializer deserializer) #endregion } + /// + /// Identifies the binary format of a module file. + /// + public enum ModuleBinaryFormat : byte + { + /// The module format is unknown. + Unknown = 0, + /// Windows Portable Executable format. + PE = 1, + /// Linux ELF (Executable and Linkable Format). + ELF = 2, + } + + /// + /// Holds symbol identity metadata for a TraceModuleFile, discriminated by binary format. + /// Subclasses contain the format-specific fields needed for symbol server lookup and resolution. + /// + public abstract class TraceModuleFileSymbolInfo + { + /// The binary format this symbol info represents. + public abstract ModuleBinaryFormat Format { get; } + + internal abstract void ToStream(Serializer serializer); + internal abstract void FromStream(Deserializer deserializer); + } + + /// + /// Symbol info for Windows PE modules. Contains PDB identity and optional R2R perfmap info. + /// + public class PESymbolInfo : TraceModuleFileSymbolInfo + { + /// Returns ModuleBinaryFormat.PE. + public override ModuleBinaryFormat Format => ModuleBinaryFormat.PE; + + /// The name of the PDB file associated with this module. + public string PdbName { get; set; } = ""; + /// GUID that uniquely identifies the PDB file. + public Guid PdbSignature { get; set; } + /// Age (small integer) needed along with signature for symbol server lookup. + public int PdbAge { get; set; } + /// Whether this module contains ReadyToRun precompiled code. + public bool IsReadyToRun { get; set; } + /// GUID identifying the R2R perfmap file. + public Guid R2RPerfMapSignature { get; set; } + /// Version number of the R2R perfmap format. + public int R2RPerfMapVersion { get; set; } + /// Name of the R2R perfmap file. + public string R2RPerfMapName { get; set; } + /// Offset in bytes between PE image beginning and text section beginning. + public uint R2RImageTextVirtualOffset { get; set; } + + internal override void ToStream(Serializer serializer) + { + serializer.Write(PdbName); + serializer.Write(PdbSignature); + serializer.Write(PdbAge); + serializer.Write(IsReadyToRun); + serializer.Write(R2RPerfMapSignature); + serializer.Write(R2RPerfMapVersion); + serializer.Write(R2RPerfMapName); + serializer.Write((int)R2RImageTextVirtualOffset); + } + + internal override void FromStream(Deserializer deserializer) + { + deserializer.Read(out string pdbName); + PdbName = pdbName; + deserializer.Read(out Guid pdbSignature); + PdbSignature = pdbSignature; + deserializer.Read(out int pdbAge); + PdbAge = pdbAge; + IsReadyToRun = deserializer.ReadBool(); + deserializer.Read(out Guid r2rPerfMapSignature); + R2RPerfMapSignature = r2rPerfMapSignature; + deserializer.Read(out int r2rPerfMapVersion); + R2RPerfMapVersion = r2rPerfMapVersion; + deserializer.Read(out string r2rPerfMapName); + R2RPerfMapName = r2rPerfMapName; + R2RImageTextVirtualOffset = (uint)deserializer.ReadInt(); + } + } + + /// + /// Symbol info for Linux ELF modules. Contains BuildId and load header info for symbol resolution. + /// + public class ElfSymbolInfo : TraceModuleFileSymbolInfo + { + /// Returns ModuleBinaryFormat.ELF. + public override ModuleBinaryFormat Format => ModuleBinaryFormat.ELF; + + /// The GNU build-id of the ELF file (lowercase hex string, typically 40 chars). + public string BuildId { get; set; } + /// Virtual address of the first executable PT_LOAD segment (p_vaddr). + public ulong VirtualAddress { get; set; } + /// File offset of the first executable PT_LOAD segment (p_offset). + public ulong FileOffset { get; set; } + /// System page size from the trace header (e.g. 4096 for x86_64). 0 if not available. + public ulong PageSize { get; set; } + + /// + /// Returns the page-aligned virtual address of the executable PT_LOAD segment. + /// This is the base address used for computing symbol RVAs, and matches the + /// "address - ImageBase" coordinate system: the Linux loader maps the executable + /// segment at PAGE_DOWN(p_vaddr) relative to the module load base. + /// Falls back to raw VirtualAddress if PageSize is not set. + /// + public ulong PageAlignedVirtualAddress + { + get + { + if (PageSize > 0) + { + return VirtualAddress & ~(PageSize - 1); + } + + return VirtualAddress; + } + } + + internal override void ToStream(Serializer serializer) + { + serializer.Write(BuildId); + serializer.Write((long)VirtualAddress); + serializer.Write((long)FileOffset); + serializer.Write((long)PageSize); + } + + internal override void FromStream(Deserializer deserializer) + { + deserializer.Read(out string buildId); + BuildId = buildId; + VirtualAddress = (ulong)deserializer.ReadInt64(); + FileOffset = (ulong)deserializer.ReadInt64(); + PageSize = (ulong)deserializer.ReadInt64(); + } + } + /// /// A ActivityIndex uniquely identifies an Activity in the log. Valid values are between /// 0 and Activities.Count-1. From 12964c9d8e2b1f754479dfd537437bfceadb1db1 Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Tue, 24 Mar 2026 16:59:20 -0700 Subject: [PATCH 02/10] Align R2R and ELF with PDB Find-Matches-Open pattern Refactor symbol resolution so all three formats (PDB, R2R, ELF) follow the same Find -> Matches -> Open pipeline: - Add R2RPerfMapMatches and ElfBuildIdMatches mirroring PdbMatches - Move validation from Open methods into Matches classes - Add lightweight R2R header probe to avoid double-parsing - Make CheckSecurity private (only used internally) - Simplify TraceLog callers (fallback logic now in Find methods) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NettraceUniversalConverter.cs | 2 +- .../Symbols/R2RPerfMapSymbolModule.cs | 72 ++++++ src/TraceEvent/Symbols/SymbolReader.cs | 226 +++++++++++++++--- .../Symbols/SymbolReaderTests.cs | 160 ++++++++++--- src/TraceEvent/TraceLog.cs | 163 +++---------- 5 files changed, 418 insertions(+), 205 deletions(-) diff --git a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs index 6fbc5c0a2..846043bf7 100644 --- a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs +++ b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs @@ -27,7 +27,7 @@ public static void RegisterParsers(TraceLog traceLog) public void BeforeProcess(TraceLog traceLog, TraceEventDispatcher source) { - // Extract EventPipe-specific header values (e.g., SystemPageSize for ELF RVA calculations). + // Extract system page size for ELF RVA calculations. if (source is EventPipeEventSource eventPipeSource) { eventPipeSource.HeadersDeserialized += delegate () diff --git a/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs b/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs index 3e6720ac8..7091c1e42 100644 --- a/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs +++ b/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs @@ -168,6 +168,78 @@ private void FinalizeSymbols() _symbols.Sort((a, b) => a.StartAddress.CompareTo(b.StartAddress)); } + /// + /// Reads only the Signature and Version from an R2R perfmap file without parsing + /// the full symbol table. This is used for cheap identity validation (analogous to + /// for ELF files). + /// Returns false if the file cannot be read or does not contain valid header metadata. + /// + internal static bool ReadSignatureAndVersion(string filePath, out Guid signature, out uint version) + { + signature = Guid.Empty; + version = 0; + bool foundSignature = false; + bool foundVersion = false; + + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + using (var reader = new StreamReader(stream)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + int firstSpace = line.IndexOf(' '); + if (firstSpace == -1) continue; + + string addressStr = line.Substring(0, firstSpace); + if (!uint.TryParse(addressStr, System.Globalization.NumberStyles.HexNumber, null, out uint address)) + { + continue; + } + + // Signature marker + if (address == 0xFFFFFFFF) + { + string remainder = line.Substring(firstSpace + 1); + int secondSpace = remainder.IndexOf(' '); + if (secondSpace >= 0) + { + string name = remainder.Substring(secondSpace + 1); + if (Guid.TryParse(name, out signature)) + { + foundSignature = true; + } + } + } + // Version marker + else if (address == 0xFFFFFFFE) + { + string remainder = line.Substring(firstSpace + 1); + int secondSpace = remainder.IndexOf(' '); + if (secondSpace >= 0) + { + string name = remainder.Substring(secondSpace + 1); + if (uint.TryParse(name, out version)) + { + foundVersion = true; + } + } + } + // Once we have both, we can stop — no need to parse the symbol table. + else if (address < 0xFFFFFFFB) + { + break; + } + + if (foundSignature && foundVersion) + { + break; + } + } + } + + return foundSignature && foundVersion; + } + private int BinarySearch(uint rva) { int left = 0; diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index 1332e0d65..99d9f1e46 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -274,7 +274,7 @@ public string FindSymbolFilePath(string pdbFileName, Guid pdbIndexGuid, int pdbI } else { - m_log.WriteLine("FindSymbolFilePath: location {0} is remote and cacheOnly set, giving up.", filePath); + m_log.WriteLine("FindSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", filePath); } } if (pdbPath != null) @@ -308,19 +308,38 @@ public string FindSymbolFilePath(string pdbFileName, Guid pdbIndexGuid, int pdbI return pdbPath; } - internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSignature, int perfMapVersion) + internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSignature, int perfMapVersion, string dllFilePath = null) { m_log.WriteLine("FindR2RPerfMapSymbolFile: *{{ Locating R2R perfmap symbol file {0} Signature {1} Version {2}", perfMapName, perfMapSignature, perfMapVersion); string indexPath = null; string perfMapPath = null; string symbolCacheTargetPath = null; + string perfMapSimpleName = Path.GetFileName(perfMapName); R2RPerfMapSignature cacheKey = new R2RPerfMapSignature() { Name = perfMapName, Signature = perfMapSignature, Version = perfMapVersion }; if (m_r2rPerfMapPathCache.TryGet(cacheKey, out perfMapPath)) { m_log.WriteLine("FindR2RPerfMapSymbolFile: }} Hit Cache, returning {0}", perfMapPath); return perfMapPath; } + + // Check next to the binary first (mirrors PDB local search). + if (perfMapPath == null && dllFilePath != null) + { + string dllDir = Path.GetDirectoryName(dllFilePath); + if (!string.IsNullOrEmpty(dllDir)) + { + string candidate = Path.Combine(dllDir, perfMapSimpleName); + m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Checking relative to DLL path {0}", candidate); + if (R2RPerfMapMatches(candidate, perfMapSignature, perfMapVersion)) + { + perfMapPath = candidate; + } + } + } + + if (perfMapPath == null) + { SymbolPath path = new SymbolPath(SymbolPath); foreach (SymbolPathElement element in path.Elements) { @@ -347,10 +366,10 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig } else { - string filePath = Path.Combine(element.Target, perfMapName); + string filePath = Path.Combine(element.Target, perfMapSimpleName); if ((Options & SymbolReaderOptions.CacheOnly) == 0 || !element.IsRemote) { - if (File.Exists(filePath)) + if (R2RPerfMapMatches(filePath, perfMapSignature, perfMapVersion, checkSecurity: false)) { perfMapPath = filePath; break; @@ -358,10 +377,11 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig } else { - m_log.WriteLine("FindR2RPerfMapSymbolFilePath: location {0} is remote and cacheOnly set, giving up.", filePath); + m_log.WriteLine("FindR2RPerfMapSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", filePath); } } } + } if (perfMapPath != null) { @@ -391,7 +411,7 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig /// The simple filename of the ELF module (e.g., "libcoreclr.so") /// The GNU build-id as a lowercase hex string /// The local file path to the downloaded symbol file, or null if not found. - public string FindElfSymbolFilePath(string fileName, string buildId) + public string FindElfSymbolFilePath(string fileName, string buildId, string elfFilePath = null) { m_log.WriteLine("FindElfSymbolFilePath: *{{ Searching for {0} with BuildId {1}", fileName, buildId); @@ -410,13 +430,76 @@ public string FindElfSymbolFilePath(string fileName, string buildId) } // SSQP key conventions for ELF debug symbols and binaries. - // Use forward slashes — these are URL path segments for SSQP servers and - // Path.Combine handles mixed separators when joining with a local root. string debugIndexPath = $"_.debug/elf-buildid-sym-{normalizedBuildId}/_.debug"; string binaryIndexPath = $"{simpleFileName}/elf-buildid-{normalizedBuildId}/{simpleFileName}"; string resultPath = null; + // Check adjacent to the binary first (mirrors PDB local search). + // Prefer debug symbol files before falling back to the binary itself. + if (elfFilePath != null) + { + string elfDir = Path.GetDirectoryName(elfFilePath); + if (!string.IsNullOrEmpty(elfDir)) + { + m_log.WriteLine("FindElfSymbolFilePath: Checking relative to ELF binary path {0}", elfFilePath); + string basePath = elfFilePath; + + // Try {path}.debug + string candidate = basePath + ".debug"; + if (ElfBuildIdMatches(candidate, normalizedBuildId)) + { + resultPath = candidate; + } + + // Try {path}.dbg + if (resultPath == null) + { + candidate = basePath + ".dbg"; + if (ElfBuildIdMatches(candidate, normalizedBuildId)) + { + resultPath = candidate; + } + } + + // If the path contains .so, strip it and try again. + if (resultPath == null) + { + int soIndex = basePath.IndexOf(".so", StringComparison.OrdinalIgnoreCase); + if (soIndex >= 0) + { + string pathWithoutSo = basePath.Substring(0, soIndex); + + candidate = pathWithoutSo + ".debug"; + if (ElfBuildIdMatches(candidate, normalizedBuildId)) + { + resultPath = candidate; + } + + if (resultPath == null) + { + candidate = pathWithoutSo + ".dbg"; + if (ElfBuildIdMatches(candidate, normalizedBuildId)) + { + resultPath = candidate; + } + } + } + } + + // Last resort: try the binary itself (has .dynsym at minimum). + if (resultPath == null) + { + if (ElfBuildIdMatches(basePath, normalizedBuildId)) + { + resultPath = basePath; + } + } + } + } + + if (resultPath == null) + { SymbolPath path = new SymbolPath(SymbolPath); foreach (SymbolPathElement element in path.Elements) { @@ -455,7 +538,7 @@ public string FindElfSymbolFilePath(string fileName, string buildId) // Try SSQP-structured debug symbols first. string debugPath = Path.Combine(target, debugIndexPath); - if (File.Exists(debugPath)) + if (ElfBuildIdMatches(debugPath, normalizedBuildId, checkSecurity: false)) { resultPath = debugPath; break; @@ -463,7 +546,7 @@ public string FindElfSymbolFilePath(string fileName, string buildId) // Try SSQP-structured binary. string binaryPath = Path.Combine(target, binaryIndexPath); - if (File.Exists(binaryPath)) + if (ElfBuildIdMatches(binaryPath, normalizedBuildId, checkSecurity: false)) { resultPath = binaryPath; break; @@ -471,6 +554,7 @@ public string FindElfSymbolFilePath(string fileName, string buildId) } } } + } if (resultPath != null) { @@ -606,11 +690,6 @@ public NativeSymbolModule OpenNativeSymbolFile(string pdbFileName) internal R2RPerfMapSymbolModule OpenR2RPerfMapSymbolFile(string filePath, uint loadedLayoutTextOffset) { - if (!CheckSecurity(filePath)) - { - m_log.WriteLine("OpenR2RPerfMapSymbolFile: Security check failed for {0}", filePath); - return null; - } return new R2RPerfMapSymbolModule(filePath, loadedLayoutTextOffset); } @@ -619,7 +698,7 @@ internal R2RPerfMapSymbolModule OpenR2RPerfMapSymbolFile(string filePath, uint l /// parameters have been seen before. This avoids re-parsing large ELF debug files when /// the same binary is loaded across multiple processes in a trace. /// - internal ElfSymbolModule OpenElfSymbolFile(string filePath, ulong pVaddr, ulong pOffset, string expectedBuildId = null) + internal ElfSymbolModule OpenElfSymbolFile(string filePath, ulong pVaddr, ulong pOffset) { var cacheKey = new ElfModuleSignature() { FilePath = filePath, VAddr = pVaddr, Offset = pOffset }; if (m_elfModuleCache.TryGet(cacheKey, out ElfSymbolModule cached)) @@ -628,27 +707,6 @@ internal ElfSymbolModule OpenElfSymbolFile(string filePath, ulong pVaddr, ulong return cached; } - if (!CheckSecurity(filePath)) - { - m_log.WriteLine("OpenElfSymbolFile: Security check failed for {0}", filePath); - return null; - } - - // Validate build-id before parsing the full symbol table. - if (!string.IsNullOrEmpty(expectedBuildId)) - { - string actualBuildId = ElfSymbolModule.ReadBuildId(filePath); - if (actualBuildId == null) - { - m_log.WriteLine("OpenElfSymbolFile: Warning: Could not read build-id from {0} (may be stripped). Accepting without validation.", filePath); - } - else if (!string.Equals(actualBuildId, expectedBuildId, StringComparison.OrdinalIgnoreCase)) - { - m_log.WriteLine("OpenElfSymbolFile: ************ ELF file {0} has build-id {1} != expected {2}", filePath, actualBuildId, expectedBuildId); - return null; - } - } - var module = new ElfSymbolModule(filePath, pVaddr, pOffset); m_elfModuleCache.Add(cacheKey, module); return module; @@ -1104,6 +1162,100 @@ private bool PdbMatches(string filePath, Guid pdbGuid, int pdbAge, bool checkSec return false; } + /// + /// Returns true if 'filePath' exists and is an R2R perfmap file whose Signature and Version match. + /// Analogous to for PDB files. + /// + private bool R2RPerfMapMatches(string filePath, Guid expectedSignature, int expectedVersion, bool checkSecurity = true) + { + try + { + if (File.Exists(filePath)) + { + if (checkSecurity && !CheckSecurity(filePath)) + { + m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Aborting, security check failed on {0}", filePath); + return false; + } + + if (R2RPerfMapSymbolModule.ReadSignatureAndVersion(filePath, out Guid actualSignature, out uint actualVersion)) + { + if (actualSignature == expectedSignature && actualVersion == (uint)expectedVersion) + { + return true; + } + else + { + m_log.WriteLine("FindR2RPerfMapSymbolFilePath: ************ FOUND R2R perfmap {0} has Signature {1} Version {2} != Desired Signature {3} Version {4}", + filePath, actualSignature, actualVersion, expectedSignature, expectedVersion); + } + } + else + { + m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Could not read signature/version from {0}", filePath); + } + } + else + { + m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Probed file location {0} does not exist", filePath); + } + } + catch (Exception e) + { + m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Aborting match of {0} Exception thrown: {1}", filePath, e.Message); + } + return false; + } + + /// + /// Returns true if 'filePath' exists and is an ELF file whose GNU build-id matches 'expectedBuildId'. + /// Analogous to for PDB files. + /// + private bool ElfBuildIdMatches(string filePath, string expectedBuildId, bool checkSecurity = true) + { + try + { + if (File.Exists(filePath)) + { + if (checkSecurity && !CheckSecurity(filePath)) + { + m_log.WriteLine("FindElfSymbolFilePath: Aborting, security check failed on {0}", filePath); + return false; + } + + if (string.IsNullOrEmpty(expectedBuildId)) + { + m_log.WriteLine("FindElfSymbolFilePath: No expected build-id provided, assuming unsafe match for {0}", filePath); + return true; + } + + string actualBuildId = ElfSymbolModule.ReadBuildId(filePath); + if (actualBuildId != null && string.Equals(actualBuildId, expectedBuildId, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (actualBuildId == null) + { + m_log.WriteLine("FindElfSymbolFilePath: Could not read build-id from {0} (may be stripped)", filePath); + } + else + { + m_log.WriteLine("FindElfSymbolFilePath: ************ FOUND ELF file {0} has build-id {1} != expected {2}", + filePath, actualBuildId, expectedBuildId); + } + } + else + { + m_log.WriteLine("FindElfSymbolFilePath: Probed file location {0} does not exist", filePath); + } + } + catch (Exception e) + { + m_log.WriteLine("FindElfSymbolFilePath: Aborting match of {0} Exception thrown: {1}", filePath, e.Message); + } + return false; + } + /// /// Fetches a file from the server 'serverPath' with pdb signature path 'pdbSigPath'(concatenate them with a / or \ separator diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs index 0abfa501c..53fc6c1b5 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs @@ -554,11 +554,11 @@ public void FindElfSymbolFilePath_DebugSymbolsFoundLocally() string buildId = "abc123"; string normalizedBuildId = buildId.ToLowerInvariant(); - // Create SSQP debug symbol directory structure. + // Create SSQP debug symbol directory structure with valid ELF build-id. string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); string debugFile = Path.Combine(debugDir, "_.debug"); - File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); // ELF magic + File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(normalizedBuildId)); _symbolReader.SymbolPath = tempDir; string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", buildId); @@ -586,7 +586,7 @@ public void FindElfSymbolFilePath_BinaryFallbackLocally() string binaryDir = Path.Combine(tempDir, "libcoreclr.so", "elf-buildid-" + normalizedBuildId); Directory.CreateDirectory(binaryDir); string binaryFile = Path.Combine(binaryDir, "libcoreclr.so"); - File.WriteAllBytes(binaryFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + File.WriteAllBytes(binaryFile, CreateMinimalElfWithBuildId(normalizedBuildId)); _symbolReader.SymbolPath = tempDir; string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", buildId); @@ -610,16 +610,16 @@ public void FindElfSymbolFilePath_DebugPreferredOverBinary() string buildId = "aabbcc"; string normalizedBuildId = buildId.ToLowerInvariant(); - // Create both debug and binary directory structures. + // Create both debug and binary directory structures with valid ELF build-ids. string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); string debugFile = Path.Combine(debugDir, "_.debug"); - File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(normalizedBuildId)); string binaryDir = Path.Combine(tempDir, "libtest.so", "elf-buildid-" + normalizedBuildId); Directory.CreateDirectory(binaryDir); string binaryFile = Path.Combine(binaryDir, "libtest.so"); - File.WriteAllBytes(binaryFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + File.WriteAllBytes(binaryFile, CreateMinimalElfWithBuildId(normalizedBuildId)); _symbolReader.SymbolPath = tempDir; string result = _symbolReader.FindElfSymbolFilePath("libtest.so", buildId); @@ -655,7 +655,7 @@ public void FindElfSymbolFilePath_NotFoundLocally() } [Theory] - [InlineData("abc", "abc")] + [InlineData("abcd", "abcd")] [InlineData("ABC123", "abc123")] [InlineData("aabbccdd00112233445566778899aabbccddeeff", "aabbccdd00112233445566778899aabbccddeeff")] public void FindElfSymbolFilePath_BuildIdNormalization(string inputBuildId, string expectedNormalized) @@ -663,11 +663,11 @@ public void FindElfSymbolFilePath_BuildIdNormalization(string inputBuildId, stri string tempDir = Path.Combine(OutputDir, "elf-buildid-norm"); try { - // Create debug symbol directory structure with normalized build ID. + // Create debug symbol directory structure with valid ELF build-id. string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + expectedNormalized); Directory.CreateDirectory(debugDir); string debugFile = Path.Combine(debugDir, "_.debug"); - File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(expectedNormalized)); _symbolReader.SymbolPath = tempDir; string result = _symbolReader.FindElfSymbolFilePath("libnorm.so", inputBuildId); @@ -695,7 +695,7 @@ public void FindElfSymbolFilePath_AbsolutePathExtractsFilename() string binaryDir = Path.Combine(tempDir, "libc.so.6", "elf-buildid-" + normalizedBuildId); Directory.CreateDirectory(binaryDir); string binaryFile = Path.Combine(binaryDir, "libc.so.6"); - File.WriteAllBytes(binaryFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + File.WriteAllBytes(binaryFile, CreateMinimalElfWithBuildId(normalizedBuildId)); _symbolReader.SymbolPath = tempDir; // Pass an absolute path — only the filename portion should be used for lookup. @@ -729,13 +729,13 @@ public void FindElfSymbolFilePath_CacheHitSkipsSearch() string tempDir = Path.Combine(OutputDir, "elf-cache-hit"); try { - string buildId = "cachedid123"; + string buildId = "cacced1d12"; string normalizedBuildId = buildId.ToLowerInvariant(); string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); string debugFile = Path.Combine(debugDir, "_.debug"); - File.WriteAllBytes(debugFile, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }); + File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(normalizedBuildId)); _symbolReader.SymbolPath = tempDir; @@ -801,7 +801,7 @@ public void FindElfSymbolFilePath_DifferentBuildIdsAreDifferentCacheKeys() string debugDir2 = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + norm2); Directory.CreateDirectory(debugDir2); string debugFile2 = Path.Combine(debugDir2, "_.debug"); - File.WriteAllBytes(debugFile2, new byte[] { 0x7F }); + File.WriteAllBytes(debugFile2, CreateMinimalElfWithBuildId(norm2)); _symbolReader.SymbolPath = tempDir; @@ -830,12 +830,13 @@ public void FindR2RPerfMapSymbolFilePath_FoundLocally() try { Directory.CreateDirectory(tempDir); + var sig = new Guid("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"); + int version = 1; string perfMapFile = Path.Combine(tempDir, "CoreLib.r2rmap"); - File.WriteAllBytes(perfMapFile, new byte[] { 0x01, 0x02 }); + File.WriteAllBytes(perfMapFile, CreateMinimalR2RPerfMap(sig, version)); _symbolReader.SymbolPath = tempDir; - var sig = new Guid("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"); - string result = _symbolReader.FindR2RPerfMapSymbolFilePath("CoreLib.r2rmap", sig, 1); + string result = _symbolReader.FindR2RPerfMapSymbolFilePath("CoreLib.r2rmap", sig, version); Assert.NotNull(result); Assert.Equal(perfMapFile, result); @@ -887,20 +888,21 @@ public void FindR2RPerfMapSymbolFilePath_CacheHitSkipsSearch() try { Directory.CreateDirectory(tempDir); + var sig = new Guid("cc000000-0000-0000-0000-000000000000"); + int version = 1; string perfMapFile = Path.Combine(tempDir, "Cached.r2rmap"); - File.WriteAllBytes(perfMapFile, new byte[] { 0x01 }); + File.WriteAllBytes(perfMapFile, CreateMinimalR2RPerfMap(sig, version)); _symbolReader.SymbolPath = tempDir; - var sig = new Guid("cc000000-0000-0000-0000-000000000000"); // First call populates the cache. - string result1 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, 1); + string result1 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, version); Assert.NotNull(result1); // Remove the file so only cache can return it. File.Delete(perfMapFile); - string result2 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, 1); + string result2 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, version); Assert.Equal(result1, result2); } finally @@ -926,9 +928,9 @@ public void FindR2RPerfMapSymbolFilePath_DifferentSignaturesAreDifferentCacheKey string result1 = _symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig1, 1); Assert.Null(result1); - // Now create the file — sig2 should find it (not negatively cached). + // Now create the file with sig2's identity — sig2 should find it (not negatively cached). string perfMapFile = Path.Combine(tempDir, "Test.r2rmap"); - File.WriteAllBytes(perfMapFile, new byte[] { 0x01 }); + File.WriteAllBytes(perfMapFile, CreateMinimalR2RPerfMap(sig2, 1)); string result2 = _symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig2, 1); Assert.NotNull(result2); @@ -1009,11 +1011,11 @@ public void ElfCache_ClearedWhenSymbolPathChanges() // Set up: first path has nothing, second path has the file. Directory.CreateDirectory(tempDir1); - string buildId = "cachetest1"; + string buildId = "cace0e0010"; string normalizedBuildId = buildId; string debugDir = Path.Combine(tempDir2, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); - File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), new byte[] { 0x7F }); + File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), CreateMinimalElfWithBuildId(normalizedBuildId)); // First search against empty path — null is cached. _symbolReader.SymbolPath = tempDir1; @@ -1039,17 +1041,17 @@ public void R2RCache_ClearedWhenSymbolPathChanges() { Directory.CreateDirectory(tempDir1); Directory.CreateDirectory(tempDir2); - File.WriteAllBytes(Path.Combine(tempDir2, "Test.r2rmap"), new byte[] { 0x01 }); - var sig = new Guid("12345678-1234-1234-1234-123456789abc"); + int version = 1; + File.WriteAllBytes(Path.Combine(tempDir2, "Test.r2rmap"), CreateMinimalR2RPerfMap(sig, version)); // First search against empty path — null is cached. _symbolReader.SymbolPath = tempDir1; - Assert.Null(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, 1)); + Assert.Null(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, version)); // Change SymbolPath — cache should be cleared, so the new path is searched. _symbolReader.SymbolPath = tempDir2; - Assert.NotNull(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, 1)); + Assert.NotNull(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, version)); } finally { @@ -1064,11 +1066,11 @@ public void ElfCache_ClearedWhenOptionsChange() string tempDir = Path.Combine(OutputDir, "elf-cache-opt"); try { - string buildId = "opttest1"; + string buildId = "00ee0010"; string normalizedBuildId = buildId; string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); - File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), new byte[] { 0x7F }); + File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), CreateMinimalElfWithBuildId(normalizedBuildId)); // First: find it successfully and cache it. _symbolReader.SymbolPath = tempDir; @@ -1103,7 +1105,6 @@ public void OpenElfSymbolFile_CacheHitReturnsSameInstance() string elfFile = Path.Combine(tempDir, "libtest.so"); File.WriteAllBytes(elfFile, new byte[] { 0x00 }); - _symbolReader.SecurityCheck = _ => true; var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); @@ -1125,7 +1126,6 @@ public void OpenElfSymbolFile_DifferentParamsAreDifferentCacheEntries() string elfFile = Path.Combine(tempDir, "libtest.so"); File.WriteAllBytes(elfFile, new byte[] { 0x00 }); - _symbolReader.SecurityCheck = _ => true; var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x2000, 0x0); @@ -1147,7 +1147,6 @@ public void OpenElfSymbolFile_CacheClearedOnSymbolPathChange() string elfFile = Path.Combine(tempDir, "libtest.so"); File.WriteAllBytes(elfFile, new byte[] { 0x00 }); - _symbolReader.SecurityCheck = _ => true; var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0); // Changing SymbolPath clears all caches including the module cache. @@ -1166,6 +1165,101 @@ public void OpenElfSymbolFile_CacheClearedOnSymbolPathChange() #endregion + /// + /// Creates a minimal valid ELF64 little-endian binary with a GNU build-id note. + /// Used by tests that need a file whose build-id can be read by ReadBuildId. + /// + /// Lowercase hex string (e.g., "abc123" → 3 bytes: 0xab, 0xc1, 0x23). + private static byte[] CreateMinimalElfWithBuildId(string buildIdHex) + { + // Convert hex string to bytes. + int byteCount = buildIdHex.Length / 2; + byte[] buildIdBytes = new byte[byteCount]; + for (int i = 0; i < byteCount; i++) + { + buildIdBytes[i] = byte.Parse(buildIdHex.Substring(i * 2, 2), NumberStyles.HexNumber); + } + + // Build the GNU build-id note. + // Note header: namesz(4) + descsz(4) + type(4) = 12 bytes. + // Name: "GNU\0" = 4 bytes (already 4-byte aligned). + // Desc: buildId bytes, padded to 4-byte alignment. + uint descsz = (uint)buildIdBytes.Length; + uint descAligned = (descsz + 3) & ~3u; + int noteSize = 12 + 4 + (int)descAligned; // header + name + aligned desc + + // ELF64 header (64 bytes) + one program header (56 bytes) + note. + int phOffset = 64; + int noteOffset = 64 + 56; + int totalSize = noteOffset + noteSize; + byte[] elf = new byte[totalSize]; + + // ELF header. + elf[0] = 0x7f; elf[1] = (byte)'E'; elf[2] = (byte)'L'; elf[3] = (byte)'F'; // magic + elf[4] = 2; // ELFCLASS64 + elf[5] = 1; // ELFDATA2LSB + elf[6] = 1; // EV_CURRENT + // e_type = ET_EXEC (2) + elf[16] = 2; + // e_machine = EM_X86_64 (0x3e) + elf[18] = 0x3e; + // e_version = 1 + elf[20] = 1; + // e_phoff = 64 (0x40) + elf[32] = 0x40; + // e_ehsize = 64 (0x40) + elf[52] = 0x40; + // e_phentsize = 56 (0x38) + elf[54] = 0x38; + // e_phnum = 1 + elf[56] = 1; + + // Program header (PT_NOTE at offset 64). + // p_type = PT_NOTE (4) + elf[phOffset] = 4; + // p_flags (at +4 for ELF64) + // p_offset (at +8) = noteOffset + elf[phOffset + 8] = (byte)noteOffset; + // p_filesz (at +32) = noteSize + elf[phOffset + 32] = (byte)(noteSize & 0xFF); + elf[phOffset + 33] = (byte)((noteSize >> 8) & 0xFF); + // p_memsz (at +40) = noteSize + elf[phOffset + 40] = (byte)(noteSize & 0xFF); + elf[phOffset + 41] = (byte)((noteSize >> 8) & 0xFF); + + // Note at noteOffset. + int np = noteOffset; + // namesz = 4 + elf[np] = 4; + // descsz + elf[np + 4] = (byte)(descsz & 0xFF); + elf[np + 5] = (byte)((descsz >> 8) & 0xFF); + // type = NT_GNU_BUILD_ID (3) + elf[np + 8] = 3; + // name = "GNU\0" + elf[np + 12] = (byte)'G'; + elf[np + 13] = (byte)'N'; + elf[np + 14] = (byte)'U'; + elf[np + 15] = 0; + // desc = build-id bytes + Array.Copy(buildIdBytes, 0, elf, np + 16, buildIdBytes.Length); + + return elf; + } + + /// + /// Creates a minimal valid R2R perfmap text file with the given Signature and Version. + /// Used by tests that need a file whose Signature/Version can be read by R2RPerfMapSymbolModule. + /// + private static byte[] CreateMinimalR2RPerfMap(Guid signature, int version) + { + // R2R perfmap format: each line is "address size name" + // Signature: FFFFFFFF 0 {guid} + // Version: FFFFFFFE 0 {version} + string content = $"FFFFFFFF 0 {signature:D}\nFFFFFFFE 0 {version}\n"; + return Encoding.UTF8.GetBytes(content); + } + protected void PrepareTestData() { lock (s_fileLock) diff --git a/src/TraceEvent/TraceLog.cs b/src/TraceEvent/TraceLog.cs index 08635f118..dc7eb6d01 100644 --- a/src/TraceEvent/TraceLog.cs +++ b/src/TraceEvent/TraceLog.cs @@ -8971,21 +8971,8 @@ private void LookupSymbolsForModule(SymbolReader reader, TraceModuleFile moduleF default: { - // Unknown format — try all paths: R2R, then PDB. - R2RPerfMapSymbolModule r2rSymbolModule = OpenR2RPerfMapForModuleFile(reader, moduleFile); - if (r2rSymbolModule != null) - { - symbolLookup = r2rSymbolModule; - } - else - { - NativeSymbolModule moduleReader = OpenPdbForModuleFile(reader, moduleFile) as NativeSymbolModule; - if (moduleReader != null) - { - symbolLookup = moduleReader; - } - } - computeRva = (address) => (uint)(address - moduleFile.ImageBase); + Debug.Assert(false, "LookupSymbolsForModule: unknown binary format " + moduleFile.BinaryFormat); + reader.m_log.WriteLine("LookupSymbolsForModule: Unknown binary format {0} for {1}, skipping.", moduleFile.BinaryFormat, moduleFile.FilePath); } break; } @@ -9212,60 +9199,34 @@ private unsafe R2RPerfMapSymbolModule OpenR2RPerfMapForModuleFile(SymbolReader s { Debug.Assert(moduleFile.PEInfo != null, "OpenR2RPerfMapForModuleFile called with null PEInfo"); var peInfo = moduleFile.PEInfo; - // If we have a signature, use it - if (peInfo != null && peInfo.R2RPerfMapSignature != Guid.Empty) + if (peInfo == null || peInfo.R2RPerfMapSignature == Guid.Empty || string.IsNullOrEmpty(peInfo.R2RPerfMapName)) { - string filePath = symReader.FindR2RPerfMapSymbolFilePath(peInfo.R2RPerfMapName, peInfo.R2RPerfMapSignature, peInfo.R2RPerfMapVersion); - if (filePath != null) - { - R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(filePath, peInfo.R2RImageTextVirtualOffset); - if (symbolModule != null && symbolModule.Signature == peInfo.R2RPerfMapSignature && symbolModule.Version == peInfo.R2RPerfMapVersion) - { - return symbolModule; - } - else - { - symReader.m_log.WriteLine("ERROR: The R2R perfmap does not match the loaded module. Actual Signature = " + symbolModule.Signature + " Requested Signature = " + peInfo.R2RPerfMapSignature); - throw new Exception("ERROR: The R2R perfmap does not match the loaded module."); - } - } + symReader.m_log.WriteLine("No R2R perfmap signature for {0} in trace.", moduleFile.FilePath); + return null; } - else + + // Find handles all search: sym server, sym path, and adjacent-to-binary (via dllFilePath). + string filePath = symReader.FindR2RPerfMapSymbolFilePath(peInfo.R2RPerfMapName, peInfo.R2RPerfMapSignature, peInfo.R2RPerfMapVersion, moduleFile.FilePath); + if (filePath == null) { - symReader.m_log.WriteLine("No R2R perfmap signature for {0} in trace.", moduleFile.FilePath); + return null; } - // Fallback: look for an R2R perfmap file next to the binary. - if (peInfo != null && !string.IsNullOrEmpty(peInfo.R2RPerfMapName) && peInfo.R2RPerfMapSignature != Guid.Empty) + R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(filePath, peInfo.R2RImageTextVirtualOffset); + if (symbolModule == null) { - string moduleDir = Path.GetDirectoryName(moduleFile.FilePath); - if (!string.IsNullOrEmpty(moduleDir)) - { - // Sanitize the perfmap name to prevent path traversal from trace data. - string candidatePath = Path.Combine(moduleDir, Path.GetFileName(peInfo.R2RPerfMapName)); - if (File.Exists(candidatePath)) - { - try - { - R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(candidatePath, peInfo.R2RImageTextVirtualOffset); - if (symbolModule != null && symbolModule.Signature == peInfo.R2RPerfMapSignature && symbolModule.Version == peInfo.R2RPerfMapVersion) - { - return symbolModule; - } + return null; + } - // Module doesn't match the expected signature; skip it. - symReader.m_log.WriteLine("R2R perfmap adjacent to binary {0} does not match signature. Actual = {1}, Expected = {2}", - candidatePath, symbolModule?.Signature, peInfo.R2RPerfMapSignature); - } - catch (Exception e) - { - symReader.m_log.WriteLine("Error opening R2R perfmap adjacent to binary {0}: {1}", candidatePath, e.Message); - } - } - } + // Post-open validation (belt and suspenders — Find already validated via R2RPerfMapMatches). + if (symbolModule.Signature != peInfo.R2RPerfMapSignature || symbolModule.Version != peInfo.R2RPerfMapVersion) + { + symReader.m_log.WriteLine("ERROR: R2R perfmap {0} does not match. Actual Signature={1} Version={2}, Expected Signature={3} Version={4}", + filePath, symbolModule.Signature, symbolModule.Version, peInfo.R2RPerfMapSignature, peInfo.R2RPerfMapVersion); + return null; } - return null; + return symbolModule; } /// @@ -9274,98 +9235,32 @@ private unsafe R2RPerfMapSymbolModule OpenR2RPerfMapForModuleFile(SymbolReader s /// private ElfSymbolModule OpenElfSymbolsForModuleFile(SymbolReader reader, TraceModuleFile moduleFile) { + Debug.Assert(moduleFile.ElfInfo != null, "OpenElfSymbolsForModuleFile called with null ElfInfo"); var elfInfo = moduleFile.ElfInfo; if (elfInfo == null || string.IsNullOrEmpty(elfInfo.BuildId)) { return null; } - Debug.Assert(moduleFile.ElfInfo != null, "OpenElfSymbolsForModuleFile called with null ElfInfo"); - ulong alignedVAddr = elfInfo.PageAlignedVirtualAddress; - // Try symbol server / symbol path first. - string symbolFilePath = reader.FindElfSymbolFilePath(moduleFile.Name, elfInfo.BuildId); - if (symbolFilePath != null) - { - try - { - reader.m_log.WriteLine("Opening ELF symbols from {0} (pVaddr=0x{1:x}, aligned=0x{2:x}, pOffset=0x{3:x}, pageSize={4})", - symbolFilePath, elfInfo.VirtualAddress, alignedVAddr, elfInfo.FileOffset, elfInfo.PageSize); - ElfSymbolModule module = reader.OpenElfSymbolFile(symbolFilePath, alignedVAddr, elfInfo.FileOffset, elfInfo.BuildId); - if (module != null) - { - return module; - } - } - catch (Exception e) - { - reader.m_log.WriteLine("Error opening ELF symbol file {0}: {1}", symbolFilePath, e.Message); - } - } - else + // Find handles all search: sym server, sym path, and adjacent-to-binary (via elfFilePath). + string symbolFilePath = reader.FindElfSymbolFilePath(moduleFile.Name, elfInfo.BuildId, moduleFile.FilePath); + if (symbolFilePath == null) { reader.m_log.WriteLine("Could not find ELF symbol file for {0} (BuildId: {1})", moduleFile.Name, elfInfo.BuildId); - } - - // Fallback: look for ELF symbols adjacent to the binary. - // Prefer debug symbol files before falling back to the binary itself. - string moduleDir = Path.GetDirectoryName(moduleFile.FilePath); - if (!string.IsNullOrEmpty(moduleDir)) - { - string basePath = moduleFile.FilePath; - - // Try {path}.debug - string candidate = basePath + ".debug"; - ElfSymbolModule result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); - if (result != null) return result; - - // Try {path}.dbg - candidate = basePath + ".dbg"; - result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); - if (result != null) return result; - - // If the path contains .so (possibly with version suffix like .so.1.2.3), strip it and try again. - int soIndex = basePath.IndexOf(".so", StringComparison.OrdinalIgnoreCase); - if (soIndex >= 0) - { - string pathWithoutSo = basePath.Substring(0, soIndex); - - candidate = pathWithoutSo + ".debug"; - result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); - if (result != null) return result; - - candidate = pathWithoutSo + ".dbg"; - result = TryOpenElfFallback(reader, candidate, alignedVAddr, elfInfo); - if (result != null) return result; - } - - // Last resort: try the binary itself (has .dynsym at minimum). - result = TryOpenElfFallback(reader, basePath, alignedVAddr, elfInfo); - if (result != null) return result; - } - - return null; - } - - /// - /// Helper to try opening an ELF file as a symbol source with build-id validation. - /// Returns null if the file doesn't exist or doesn't match. - /// - private ElfSymbolModule TryOpenElfFallback(SymbolReader reader, string candidatePath, ulong alignedVAddr, ElfSymbolInfo elfInfo) - { - if (!File.Exists(candidatePath)) - { return null; } try { - return reader.OpenElfSymbolFile(candidatePath, alignedVAddr, elfInfo.FileOffset, elfInfo.BuildId); + reader.m_log.WriteLine("Opening ELF symbols from {0} (pVaddr=0x{1:x}, aligned=0x{2:x}, pOffset=0x{3:x}, pageSize={4})", + symbolFilePath, elfInfo.VirtualAddress, alignedVAddr, elfInfo.FileOffset, elfInfo.PageSize); + return reader.OpenElfSymbolFile(symbolFilePath, alignedVAddr, elfInfo.FileOffset); } catch (Exception e) { - reader.m_log.WriteLine("Error opening ELF fallback {0}: {1}", candidatePath, e.Message); + reader.m_log.WriteLine("Error opening ELF symbol file {0}: {1}", symbolFilePath, e.Message); return null; } } From 32b37d543c69047826b3ad99e403ef17f7db3160 Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Tue, 24 Mar 2026 16:59:34 -0700 Subject: [PATCH 03/10] Read .gnu_debuglink from ELF binaries for symbol file discovery Add ReadDebugLink static method to ElfSymbolModule that parses ELF section headers to find the .gnu_debuglink section and extract the debug file name. Update FindElfSymbolFilePath to use debuglink-based probing (same directory and .debug/ subdirectory) instead of only trying hardcoded suffixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TraceEvent/Symbols/ElfSymbolModule.cs | 232 ++++++++++++++++++ src/TraceEvent/Symbols/SymbolReader.cs | 22 +- .../TraceEvent.Tests/Symbols/ElfBuilder.cs | 124 +++++++++- .../Symbols/ElfSymbolModuleTests.cs | 110 +++++++++ .../Symbols/SymbolReaderTests.cs | 106 ++++++++ 5 files changed, 582 insertions(+), 12 deletions(-) diff --git a/src/TraceEvent/Symbols/ElfSymbolModule.cs b/src/TraceEvent/Symbols/ElfSymbolModule.cs index 96e0b0f24..ae8314f9c 100644 --- a/src/TraceEvent/Symbols/ElfSymbolModule.cs +++ b/src/TraceEvent/Symbols/ElfSymbolModule.cs @@ -267,8 +267,240 @@ internal static string ReadBuildId(string filePath) } } + /// + /// Reads the .gnu_debuglink section from an ELF file and returns the debug file name. + /// The .gnu_debuglink section contains a null-terminated filename followed by padding + /// and a CRC32 checksum. Only the filename is returned (CRC is not validated, matching + /// one-collect behavior). + /// + /// Path to the ELF file. + /// The debug link filename (e.g. "libcoreclr.so.dbg"), or null if not found or on any error. + internal static string ReadDebugLink(string filePath) + { + try + { + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + // Read the ELF header (max 64 bytes for 64-bit). + byte[] header = new byte[Elf64EhdrSize]; + int headerRead = ReadFully(stream, header, 0, header.Length); + if (headerRead < EI_NIDENT) + { + Debug.WriteLine("ReadDebugLink: File too small."); + return null; + } + + // Verify ELF magic bytes: 0x7f 'E' 'L' 'F'. + if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) + { + Debug.WriteLine("ReadDebugLink: Invalid ELF magic."); + return null; + } + + byte eiClass = header[EI_CLASS]; + byte eiData = header[EI_DATA]; + + bool is64Bit = (eiClass == ElfClass64); + bool bigEndian = (eiData == ElfDataMsb); + + if (eiClass != ElfClass32 && eiClass != ElfClass64) + { + Debug.WriteLine("ReadDebugLink: Unknown ELF class " + eiClass + "."); + return null; + } + + int ehSize = is64Bit ? Elf64EhdrSize : Elf32EhdrSize; + if (headerRead < ehSize) + { + Debug.WriteLine("ReadDebugLink: Header too small."); + return null; + } + + // Parse section header table location from ELF header. + // Layout after e_ident(16): e_type(2), e_machine(2), e_version(4), + // e_entry(4/8), e_phoff(4/8), e_shoff(4/8), e_flags(4), e_ehsize(2), + // e_phentsize(2), e_phnum(2), e_shentsize(2), e_shnum(2), e_shstrndx(2). + int pos = EI_NIDENT + 2 + 2 + 4; // skip e_ident, e_type, e_machine, e_version + ulong eShoff; + if (is64Bit) + { + pos += 8 + 8; // skip e_entry(8), e_phoff(8) + eShoff = ReadU64Static(header, pos, bigEndian); pos += 8; + } + else + { + pos += 4 + 4; // skip e_entry(4), e_phoff(4) + eShoff = ReadU32Static(header, pos, bigEndian); pos += 4; + } + + pos += 4 + 2 + 2 + 2; // e_flags(4), e_ehsize(2), e_phentsize(2), e_phnum(2) + ushort eShentsize = ReadU16Static(header, pos, bigEndian); pos += 2; + ushort eShnum = ReadU16Static(header, pos, bigEndian); pos += 2; + ushort eShstrndx = ReadU16Static(header, pos, bigEndian); + + if (eShoff == 0 || eShentsize == 0 || eShnum == 0) + { + Debug.WriteLine("ReadDebugLink: No section headers found."); + return null; + } + + int minShentsize = is64Bit ? Elf64ShdrSize : Elf32ShdrSize; + if (eShentsize < minShentsize || eShentsize > MaxShentsize) + { + Debug.WriteLine("ReadDebugLink: Invalid section header entry size: " + eShentsize); + return null; + } + + if (eShnum > MaxSectionCount) + { + Debug.WriteLine("ReadDebugLink: Section count too large: " + eShnum); + return null; + } + + if (eShstrndx >= eShnum) + { + Debug.WriteLine("ReadDebugLink: Invalid shstrndx: " + eShstrndx); + return null; + } + + // Read all section headers in one bulk read. + int shTableSize = eShnum * eShentsize; + byte[] shTable = new byte[shTableSize]; + stream.Seek((long)eShoff, SeekOrigin.Begin); + if (ReadFully(stream, shTable, 0, shTableSize) < shTableSize) + { + Debug.WriteLine("ReadDebugLink: Could not read section headers."); + return null; + } + + // Read the section name string table (shstrtab). + int shstrPos = eShstrndx * eShentsize; + ulong shstrOffset, shstrSize; + ReadSectionOffsetAndSize(shTable, shstrPos, is64Bit, bigEndian, out shstrOffset, out shstrSize); + + if (shstrSize == 0 || shstrSize > 1024 * 1024) + { + Debug.WriteLine("ReadDebugLink: Invalid shstrtab size."); + return null; + } + + byte[] shstrtab = new byte[(int)shstrSize]; + stream.Seek((long)shstrOffset, SeekOrigin.Begin); + if (ReadFully(stream, shstrtab, 0, shstrtab.Length) < shstrtab.Length) + { + Debug.WriteLine("ReadDebugLink: Could not read shstrtab."); + return null; + } + + // Iterate sections looking for .gnu_debuglink by name. + for (int i = 0; i < eShnum; i++) + { + int shPos = i * eShentsize; + uint shName = ReadU32Static(shTable, shPos, bigEndian); + + if (shName >= shstrtab.Length) + { + continue; + } + + // Compare section name against ".gnu_debuglink". + if (!SectionNameEquals(shstrtab, (int)shName, GnuDebugLinkName)) + { + continue; + } + + // Found .gnu_debuglink — read its contents. + ulong secOffset, secSize; + ReadSectionOffsetAndSize(shTable, shPos, is64Bit, bigEndian, out secOffset, out secSize); + + // The section must contain at least a filename byte + null + 4-byte CRC. + if (secSize < 6 || secSize > 4096) + { + Debug.WriteLine("ReadDebugLink: Invalid .gnu_debuglink section size: " + secSize); + return null; + } + + byte[] sectionData = new byte[(int)secSize]; + stream.Seek((long)secOffset, SeekOrigin.Begin); + if (ReadFully(stream, sectionData, 0, sectionData.Length) < sectionData.Length) + { + Debug.WriteLine("ReadDebugLink: Could not read .gnu_debuglink section data."); + return null; + } + + // Extract the null-terminated filename. + int nullPos = Array.IndexOf(sectionData, (byte)0); + if (nullPos <= 0) + { + Debug.WriteLine("ReadDebugLink: Empty or missing filename in .gnu_debuglink."); + return null; + } + + return Encoding.UTF8.GetString(sectionData, 0, nullPos); + } + + Debug.WriteLine("ReadDebugLink: No .gnu_debuglink section found."); + return null; + } + } + catch (Exception ex) + { + Debug.WriteLine("ReadDebugLink: Error reading file: " + ex.Message); + return null; + } + } + #region private + // Name of the .gnu_debuglink section (UTF-8 bytes for fast comparison). + private static readonly byte[] GnuDebugLinkName = Encoding.UTF8.GetBytes(".gnu_debuglink"); + + /// + /// Reads sh_offset and sh_size from a section header at the given position. + /// Layout: sh_name(4), sh_type(4), sh_flags(4/8), sh_addr(4/8), sh_offset(4/8), sh_size(4/8). + /// + private static void ReadSectionOffsetAndSize(byte[] shTable, int shPos, bool is64Bit, bool bigEndian, + out ulong offset, out ulong size) + { + if (is64Bit) + { + // 64-bit: sh_name(4) + sh_type(4) + sh_flags(8) + sh_addr(8) = 24 bytes before sh_offset(8), sh_size(8). + int ofsPos = shPos + 4 + 4 + 8 + 8; + offset = ReadU64Static(shTable, ofsPos, bigEndian); + size = ReadU64Static(shTable, ofsPos + 8, bigEndian); + } + else + { + // 32-bit: sh_name(4) + sh_type(4) + sh_flags(4) + sh_addr(4) = 16 bytes before sh_offset(4), sh_size(4). + int ofsPos = shPos + 4 + 4 + 4 + 4; + offset = ReadU32Static(shTable, ofsPos, bigEndian); + size = ReadU32Static(shTable, ofsPos + 4, bigEndian); + } + } + + /// + /// Compares a null-terminated string in a byte array against an expected byte sequence. + /// + private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected) + { + if (offset + expected.Length > strtab.Length) + { + return false; + } + + for (int i = 0; i < expected.Length; i++) + { + if (strtab[offset + i] != expected[i]) + { + return false; + } + } + + // Ensure the string in strtab is null-terminated right after the match. + int endPos = offset + expected.Length; + return endPos >= strtab.Length || strtab[endPos] == 0; + } + // ELF identification (e_ident) constants. private const byte ElfMagic0 = 0x7f; private const byte ElfMagic1 = (byte)'E'; diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index 99d9f1e46..a483b840b 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -462,23 +462,33 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF } } - // If the path contains .so, strip it and try again. + // Read .gnu_debuglink from the binary if it exists locally. + // The debuglink section contains the exact filename of the companion debug file. if (resultPath == null) { - int soIndex = basePath.IndexOf(".so", StringComparison.OrdinalIgnoreCase); - if (soIndex >= 0) + string debugLink = null; + if (File.Exists(basePath)) { - string pathWithoutSo = basePath.Substring(0, soIndex); + debugLink = ElfSymbolModule.ReadDebugLink(basePath); + if (debugLink != null) + { + m_log.WriteLine("FindElfSymbolFilePath: Binary has .gnu_debuglink = {0}", debugLink); + } + } - candidate = pathWithoutSo + ".debug"; + if (debugLink != null) + { + // Try {bindir}/{debuglink} + candidate = Path.Combine(elfDir, debugLink); if (ElfBuildIdMatches(candidate, normalizedBuildId)) { resultPath = candidate; } + // Try {bindir}/.debug/{debuglink} if (resultPath == null) { - candidate = pathWithoutSo + ".dbg"; + candidate = Path.Combine(elfDir, ".debug", debugLink); if (ElfBuildIdMatches(candidate, normalizedBuildId)) { resultPath = candidate; diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs index 92c3e7ff7..e715f173c 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs @@ -16,6 +16,7 @@ internal class ElfBuilder private ulong m_pVaddr = 0x400000; private ulong m_pOffset = 0; private byte[] m_buildId = null; + private string m_debugLink = null; private readonly List m_symtabSymbols = new List(); private readonly List m_dynsymSymbols = new List(); @@ -29,6 +30,7 @@ private struct SymbolDef // ELF section types. private const uint SHT_NULL = 0; + private const uint SHT_PROGBITS = 1; private const uint SHT_STRTAB = 3; private const uint SHT_SYMTAB = 2; private const uint SHT_DYNSYM = 11; @@ -74,6 +76,16 @@ public ElfBuilder SetBuildId(byte[] buildId) return this; } + /// + /// Sets the .gnu_debuglink section filename. When set, the builder adds a .shstrtab + /// section so ReadDebugLink can find the section by name. + /// + public ElfBuilder SetDebugLink(string filename) + { + m_debugLink = filename; + return this; + } + /// /// Adds a STT_FUNC symbol to the .symtab section. /// @@ -121,7 +133,7 @@ public ElfBuilder AddDynFunction(string name, ulong virtualAddress, ulong size) /// /// Builds a complete ELF binary and returns it as a byte array. - /// Layout: [ELF Header] [Section Data...] [Note Data] [Program Headers] [Section Headers] + /// Layout: [ELF Header] [Section Data...] [DebugLink Data] [ShStrTab Data] [Note Data] [Program Headers] [Section Headers] /// public byte[] Build() { @@ -134,9 +146,16 @@ public byte[] Build() // [2] .symtab (symbol table) // [3] .dynstr (string table for .dynsym) — only if dynsym symbols exist // [4] .dynsym — only if dynsym symbols exist + // [N] .gnu_debuglink — only if debuglink is set + // [N+1] .shstrtab — only if debuglink is set (needed for section names) bool hasDynsym = m_dynsymSymbols.Count > 0; bool hasBuildId = m_buildId != null; + bool hasDebugLink = m_debugLink != null; int sectionCount = hasDynsym ? 5 : 3; + if (hasDebugLink) + { + sectionCount += 2; // .gnu_debuglink + .shstrtab + } int ehSize = m_is64Bit ? 64 : 52; int shEntSize = m_is64Bit ? 64 : 40; @@ -165,6 +184,23 @@ public byte[] Build() noteData = BuildBuildIdNote(m_buildId); } + // Build .gnu_debuglink and .shstrtab section data if requested. + byte[] debugLinkData = null; + byte[] shstrtab = null; + int debugLinkShName = 0; + int shstrtabShName = 0; + int debugLinkSectionIndex = 0; + int shstrtabSectionIndex = 0; + if (hasDebugLink) + { + debugLinkData = BuildDebugLinkSection(m_debugLink); + debugLinkSectionIndex = hasDynsym ? 5 : 3; + shstrtabSectionIndex = debugLinkSectionIndex + 1; + + // Build .shstrtab: "\0.gnu_debuglink\0.shstrtab\0" + shstrtab = BuildShStrTab(out debugLinkShName, out shstrtabShName); + } + // Section data starts right after the ELF header. long dataStart = ehSize; @@ -175,9 +211,14 @@ public byte[] Build() long dynsymOffset = hasDynsym ? dynstrOffset + dynstr.Length : dynstrOffset; long afterSections = hasDynsym ? dynsymOffset + dynsym.Length : dynstrOffset; + // Write debuglink and shstrtab after other section data. + long debugLinkOffset = afterSections; + long shstrtabOffset = hasDebugLink ? debugLinkOffset + debugLinkData.Length : afterSections; + long afterDebugLink = hasDebugLink ? shstrtabOffset + shstrtab.Length : afterSections; + // Write note data after sections. - long noteOffset = afterSections; - long afterNote = hasBuildId ? noteOffset + noteData.Length : afterSections; + long noteOffset = afterDebugLink; + long afterNote = hasBuildId ? noteOffset + noteData.Length : afterDebugLink; // Write program headers after note data (align to 8 bytes). long phOffset = 0; @@ -203,8 +244,9 @@ public byte[] Build() // Write ELF header. ushort headerPhEntSize = hasBuildId ? (ushort)phEntSize : (ushort)0; + ushort eShstrndx = hasDebugLink ? (ushort)shstrtabSectionIndex : (ushort)0; WriteElfHeader(writer, (ulong)sectionHeadersOffset, (ushort)sectionCount, (ushort)shEntSize, - (ulong)phOffset, headerPhEntSize, phNum); + (ulong)phOffset, headerPhEntSize, phNum, eShstrndx); // Write section data. writer.BaseStream.Seek(strtabOffset, SeekOrigin.Begin); @@ -216,6 +258,14 @@ public byte[] Build() writer.Write(dynsym); } + // Write debuglink and shstrtab section data. + if (hasDebugLink) + { + writer.BaseStream.Seek(debugLinkOffset, SeekOrigin.Begin); + writer.Write(debugLinkData); + writer.Write(shstrtab); + } + // Write note data. if (hasBuildId) { @@ -256,6 +306,17 @@ public byte[] Build() WriteSectionHeader(writer, 0, SHT_DYNSYM, (ulong)dynsymOffset, (ulong)dynsym.Length, 3, (ulong)symEntSize); } + if (hasDebugLink) + { + // .gnu_debuglink (SHT_PROGBITS) + WriteSectionHeader(writer, (uint)debugLinkShName, SHT_PROGBITS, + (ulong)debugLinkOffset, (ulong)debugLinkData.Length, 0, 0); + + // .shstrtab (SHT_STRTAB) + WriteSectionHeader(writer, (uint)shstrtabShName, SHT_STRTAB, + (ulong)shstrtabOffset, (ulong)shstrtab.Length, 0, 0); + } + return ms.ToArray(); } } @@ -272,7 +333,7 @@ public void GetPTLoadParams(out ulong pVaddr, out ulong pOffset) #region Private helpers private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, ushort eShentsize, - ulong ePhoff, ushort ePhentsize, ushort ePhnum) + ulong ePhoff, ushort ePhentsize, ushort ePhnum, ushort eShstrndx = 0) { // e_ident: magic + class + data + version + padding (16 bytes total). writer.Write((byte)0x7f); @@ -308,7 +369,7 @@ private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, us WriteUInt16(writer, ePhnum); // e_phnum WriteUInt16(writer, eShentsize); // e_shentsize WriteUInt16(writer, eShnum); // e_shnum - WriteUInt16(writer, 0); // e_shstrndx + WriteUInt16(writer, eShstrndx); // e_shstrndx } private void WriteSectionHeader(BinaryWriter writer, uint shName, uint shType, @@ -468,6 +529,57 @@ private void WriteProgramHeader(BinaryWriter writer, uint pType, ulong pOffset, } } + /// + /// Builds a .gnu_debuglink section: null-terminated filename + padding to 4 bytes + CRC32 (0). + /// + private static byte[] BuildDebugLinkSection(string filename) + { + using (var ms = new MemoryStream()) + using (var writer = new BinaryWriter(ms)) + { + byte[] nameBytes = Encoding.UTF8.GetBytes(filename); + writer.Write(nameBytes); + writer.Write((byte)0); // null terminator + + // Pad to 4-byte alignment. + int nameLen = nameBytes.Length + 1; + int padding = ((nameLen + 3) & ~3) - nameLen; + for (int i = 0; i < padding; i++) + { + writer.Write((byte)0); + } + + // CRC32 (not validated by ReadDebugLink, write 0). + writer.Write((uint)0); + + return ms.ToArray(); + } + } + + /// + /// Builds a section name string table (.shstrtab) containing ".gnu_debuglink" and ".shstrtab". + /// Returns the sh_name offsets for each section. + /// + private static byte[] BuildShStrTab(out int debugLinkShName, out int shstrtabShName) + { + using (var ms = new MemoryStream()) + { + ms.WriteByte(0); // Index 0: empty string + + debugLinkShName = (int)ms.Position; + byte[] dlName = Encoding.UTF8.GetBytes(".gnu_debuglink"); + ms.Write(dlName, 0, dlName.Length); + ms.WriteByte(0); + + shstrtabShName = (int)ms.Position; + byte[] ssName = Encoding.UTF8.GetBytes(".shstrtab"); + ms.Write(ssName, 0, ssName.Length); + ms.WriteByte(0); + + return ms.ToArray(); + } + } + #region Endianness helpers private void WriteUInt16(BinaryWriter writer, ushort val) diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs index 53cafd7b8..2e8e527be 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs @@ -816,6 +816,116 @@ public void ReadBuildId_BigEndianElf64_ReturnsBuildId() #endregion + #region ReadDebugLink + + [Fact] + public void ReadDebugLink_WithDebugLink_ReturnsFilename() + { + var builder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(0x400000, 0) + .SetDebugLink("libcoreclr.so.dbg"); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadDebugLink(path); + Assert.Equal("libcoreclr.so.dbg", result); + }); + } + + [Fact] + public void ReadDebugLink_WithDebugLinkElf32_ReturnsFilename() + { + var builder = new ElfBuilder() + .Set64Bit(false) + .SetPTLoad(0x400000, 0) + .SetDebugLink("mylib.debug"); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadDebugLink(path); + Assert.Equal("mylib.debug", result); + }); + } + + [Fact] + public void ReadDebugLink_WithDebugLinkAndBuildId_ReturnsBoth() + { + byte[] buildId = new byte[] { 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, + 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, + 0xab, 0xcd, 0xef, 0x01 }; + var builder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(0x400000, 0) + .SetBuildId(buildId) + .SetDebugLink("libcoreclr.so.dbg"); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string debugLink = ElfSymbolModule.ReadDebugLink(path); + Assert.Equal("libcoreclr.so.dbg", debugLink); + + string buildIdResult = ElfSymbolModule.ReadBuildId(path); + Assert.Equal("abcdef0123456789abcdef0123456789abcdef01", buildIdResult); + }); + } + + [Fact] + public void ReadDebugLink_WithoutDebugLink_ReturnsNull() + { + var builder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(0x400000, 0) + .AddFunction("test", 0x401000, 0x100); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadDebugLink(path); + Assert.Null(result); + }); + } + + [Fact] + public void ReadDebugLink_InvalidFile_ReturnsNull() + { + byte[] data = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 }; + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadDebugLink(path); + Assert.Null(result); + }); + } + + [Fact] + public void ReadDebugLink_NonExistentFile_ReturnsNull() + { + string result = ElfSymbolModule.ReadDebugLink(@"C:\nonexistent\path\fake.so"); + Assert.Null(result); + } + + [Fact] + public void ReadDebugLink_BigEndianElf64_ReturnsFilename() + { + var builder = new ElfBuilder() + .Set64Bit(true) + .SetBigEndian(true) + .SetPTLoad(0x400000, 0) + .SetDebugLink("libtest.so.debug"); + + byte[] data = builder.Build(); + RunWithTempFile(data, (path) => + { + string result = ElfSymbolModule.ReadDebugLink(path); + Assert.Equal("libtest.so.debug", result); + }); + } + + #endregion + #region MatchOrInit tests [Fact] diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs index 53fc6c1b5..bb0eec08b 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs @@ -819,6 +819,98 @@ public void FindElfSymbolFilePath_DifferentBuildIdsAreDifferentCacheKeys() } } + [Fact] + public void FindElfSymbolFilePath_DebugLinkDiscovery() + { + string tempDir = Path.Combine(OutputDir, "elf-debuglink"); + try + { + string buildId = "aabb0011"; + + // Build an ELF binary with .gnu_debuglink pointing to "libtest.so.dbg". + var binaryBuilder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(0x400000, 0) + .SetBuildId(HexToBytes(buildId)) + .SetDebugLink("libtest.so.dbg"); + byte[] binaryData = binaryBuilder.Build(); + + // Build a debug ELF file with matching build-id. + byte[] debugData = CreateMinimalElfWithBuildId(buildId); + + // Place the binary and debug file in the same directory. + Directory.CreateDirectory(tempDir); + string binaryPath = Path.Combine(tempDir, "libtest.so"); + string debugPath = Path.Combine(tempDir, "libtest.so.dbg"); + File.WriteAllBytes(binaryPath, binaryData); + File.WriteAllBytes(debugPath, debugData); + + // Set symbol path to an empty location (no SSQP match), + // but provide elfFilePath so adjacent search kicks in. + // SecurityCheck is needed because adjacent search uses checkSecurity: true. + string emptyDir = Path.Combine(tempDir, "empty"); + Directory.CreateDirectory(emptyDir); + _symbolReader.SymbolPath = emptyDir; + _symbolReader.SecurityCheck = _ => true; + + string result = _symbolReader.FindElfSymbolFilePath("libtest.so", buildId, elfFilePath: binaryPath); + + Assert.NotNull(result); + Assert.Equal(Path.GetFullPath(debugPath), Path.GetFullPath(result)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_DebugLinkInSubdir() + { + string tempDir = Path.Combine(OutputDir, "elf-debuglink-subdir"); + try + { + string buildId = "ccdd0022"; + + // Build an ELF binary with .gnu_debuglink pointing to "libfoo.debug". + var binaryBuilder = new ElfBuilder() + .Set64Bit(true) + .SetPTLoad(0x400000, 0) + .SetBuildId(HexToBytes(buildId)) + .SetDebugLink("libfoo.debug"); + byte[] binaryData = binaryBuilder.Build(); + + // Build a debug ELF file with matching build-id. + byte[] debugData = CreateMinimalElfWithBuildId(buildId); + + // Place the binary in tempDir, debug file in {tempDir}/.debug/ subdir. + Directory.CreateDirectory(tempDir); + string debugSubDir = Path.Combine(tempDir, ".debug"); + Directory.CreateDirectory(debugSubDir); + + string binaryPath = Path.Combine(tempDir, "libfoo.so"); + string debugPath = Path.Combine(debugSubDir, "libfoo.debug"); + File.WriteAllBytes(binaryPath, binaryData); + File.WriteAllBytes(debugPath, debugData); + + string emptyDir = Path.Combine(tempDir, "empty"); + Directory.CreateDirectory(emptyDir); + _symbolReader.SymbolPath = emptyDir; + _symbolReader.SecurityCheck = _ => true; + + string result = _symbolReader.FindElfSymbolFilePath("libfoo.so", buildId, elfFilePath: binaryPath); + + Assert.NotNull(result); + Assert.Equal(Path.GetFullPath(debugPath), Path.GetFullPath(result)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + #endregion #region FindR2RPerfMapSymbolFilePath Tests @@ -1260,6 +1352,20 @@ private static byte[] CreateMinimalR2RPerfMap(Guid signature, int version) return Encoding.UTF8.GetBytes(content); } + /// + /// Converts a hex string to a byte array. + /// + private static byte[] HexToBytes(string hex) + { + int byteCount = hex.Length / 2; + byte[] bytes = new byte[byteCount]; + for (int i = 0; i < byteCount; i++) + { + bytes[i] = byte.Parse(hex.Substring(i * 2, 2), NumberStyles.HexNumber); + } + return bytes; + } + protected void PrepareTestData() { lock (s_fileLock) From 05284610e6dfe66451f40d7d9df93dbd60e2d905 Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Tue, 24 Mar 2026 16:59:47 -0700 Subject: [PATCH 04/10] Harden ElfSymbolModule - Hoist 20+ magic numbers to named constants for all ELF struct field offsets, sizes, and limits - Cap PT_NOTE allocation at 64KB (MaxNoteSizeBytes) to prevent OOM - Add overflow guard in ExtractBuildId note alignment arithmetic - Make FindNameForRva thread-safe: parallel string[] with Volatile.Read/Interlocked.CompareExchange for lazy name cache - Use ThreadLocal for demanglers (mutable parser state) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TraceEvent/Symbols/ElfSymbolModule.cs | 141 +++++++++++++++------- 1 file changed, 96 insertions(+), 45 deletions(-) diff --git a/src/TraceEvent/Symbols/ElfSymbolModule.cs b/src/TraceEvent/Symbols/ElfSymbolModule.cs index ae8314f9c..5ea588efe 100644 --- a/src/TraceEvent/Symbols/ElfSymbolModule.cs +++ b/src/TraceEvent/Symbols/ElfSymbolModule.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Text; +using System.Threading; namespace Microsoft.Diagnostics.Symbols { @@ -85,17 +86,15 @@ public string FindNameForRva(uint rva, ref uint symbolStart) { symbolStart = m_symbols[hi].Start; - // Lazy name decode on first access. - if (m_symbols[hi].Name == null) + // Thread-safe lazy name decode on first access. + if (Volatile.Read(ref m_symbolNames[hi]) == null) { string name = ReadNullTerminatedString(m_strtab, m_symbols[hi].StrtabOffset); name = TryDemangle(name); - var entry = m_symbols[hi]; - entry.Name = name; - m_symbols[hi] = entry; + Interlocked.CompareExchange(ref m_symbolNames[hi], name, null); } - return m_symbols[hi].Name; + return m_symbolNames[hi]; } return string.Empty; @@ -151,16 +150,16 @@ internal static string ReadBuildId(string filePath) // Parse program header table location from ELF header. // Layout after e_ident(16): e_type(2), e_machine(2), e_version(4), e_entry(4/8), e_phoff(4/8). - int pos = EI_NIDENT + 2 + 2 + 4; // skip e_ident, e_type, e_machine, e_version + int pos = Ehdr_Entry; // skip e_ident, e_type, e_machine, e_version ulong ePhoff; if (is64Bit) { - pos += 8; // skip e_entry + pos += 8; // skip e_entry(8) ePhoff = ReadU64Static(header, pos, bigEndian); pos += 8; } else { - pos += 4; // skip e_entry + pos += 4; // skip e_entry(4) ePhoff = ReadU32Static(header, pos, bigEndian); pos += 4; } @@ -185,7 +184,7 @@ internal static string ReadBuildId(string filePath) } // Validate minimum program header entry size to avoid out-of-bounds reads. - int minPhentsize = is64Bit ? 56 : 32; // Elf64_Phdr = 56 bytes, Elf32_Phdr = 32 bytes + int minPhentsize = is64Bit ? Elf64PhdrSize : Elf32PhdrSize; if (ePhentsize < minPhentsize) { Debug.WriteLine("ReadBuildId: ePhentsize too small: " + ePhentsize); @@ -193,7 +192,7 @@ internal static string ReadBuildId(string filePath) } // Guard against corrupt ELF headers with unreasonably large program header counts. - if (ePhnum > 4096) + if (ePhnum > MaxProgramHeaderCount) { Debug.WriteLine("ReadBuildId: Program header count too large: " + ePhnum); return null; @@ -224,18 +223,16 @@ internal static string ReadBuildId(string filePath) ulong pOffset, pFilesz; if (is64Bit) { - // 64-bit: p_type(4) + p_flags(4) + p_offset(8) + p_vaddr(8) + p_paddr(8) + p_filesz(8). - pOffset = ReadU64Static(phTable, phPos + 8, bigEndian); - pFilesz = ReadU64Static(phTable, phPos + 8 + 8 + 8 + 8, bigEndian); + pOffset = ReadU64Static(phTable, phPos + Phdr64_Offset, bigEndian); + pFilesz = ReadU64Static(phTable, phPos + Phdr64_Filesz, bigEndian); } else { - // 32-bit: p_type(4) + p_offset(4) + p_vaddr(4) + p_paddr(4) + p_filesz(4). - pOffset = ReadU32Static(phTable, phPos + 4, bigEndian); - pFilesz = ReadU32Static(phTable, phPos + 4 + 4 + 4 + 4, bigEndian); + pOffset = ReadU32Static(phTable, phPos + Phdr32_Offset, bigEndian); + pFilesz = ReadU32Static(phTable, phPos + Phdr32_Filesz, bigEndian); } - if (pFilesz == 0 || pFilesz > int.MaxValue) + if (pFilesz == 0 || pFilesz > MaxNoteSizeBytes) { continue; } @@ -320,7 +317,7 @@ internal static string ReadDebugLink(string filePath) // Layout after e_ident(16): e_type(2), e_machine(2), e_version(4), // e_entry(4/8), e_phoff(4/8), e_shoff(4/8), e_flags(4), e_ehsize(2), // e_phentsize(2), e_phnum(2), e_shentsize(2), e_shnum(2), e_shstrndx(2). - int pos = EI_NIDENT + 2 + 2 + 4; // skip e_ident, e_type, e_machine, e_version + int pos = Ehdr_Entry; // skip e_ident, e_type, e_machine, e_version ulong eShoff; if (is64Bit) { @@ -333,7 +330,7 @@ internal static string ReadDebugLink(string filePath) eShoff = ReadU32Static(header, pos, bigEndian); pos += 4; } - pos += 4 + 2 + 2 + 2; // e_flags(4), e_ehsize(2), e_phentsize(2), e_phnum(2) + pos += Ehdr_FlagsToPhnum; // e_flags(4), e_ehsize(2), e_phentsize(2), e_phnum(2) ushort eShentsize = ReadU16Static(header, pos, bigEndian); pos += 2; ushort eShnum = ReadU16Static(header, pos, bigEndian); pos += 2; ushort eShstrndx = ReadU16Static(header, pos, bigEndian); @@ -378,7 +375,7 @@ internal static string ReadDebugLink(string filePath) ulong shstrOffset, shstrSize; ReadSectionOffsetAndSize(shTable, shstrPos, is64Bit, bigEndian, out shstrOffset, out shstrSize); - if (shstrSize == 0 || shstrSize > 1024 * 1024) + if (shstrSize == 0 || shstrSize > MaxShstrtabSize) { Debug.WriteLine("ReadDebugLink: Invalid shstrtab size."); return null; @@ -414,7 +411,7 @@ internal static string ReadDebugLink(string filePath) ReadSectionOffsetAndSize(shTable, shPos, is64Bit, bigEndian, out secOffset, out secSize); // The section must contain at least a filename byte + null + 4-byte CRC. - if (secSize < 6 || secSize > 4096) + if (secSize < MinDebugLinkSectionSize || secSize > MaxDebugLinkSectionSize) { Debug.WriteLine("ReadDebugLink: Invalid .gnu_debuglink section size: " + secSize); return null; @@ -464,17 +461,13 @@ private static void ReadSectionOffsetAndSize(byte[] shTable, int shPos, bool is6 { if (is64Bit) { - // 64-bit: sh_name(4) + sh_type(4) + sh_flags(8) + sh_addr(8) = 24 bytes before sh_offset(8), sh_size(8). - int ofsPos = shPos + 4 + 4 + 8 + 8; - offset = ReadU64Static(shTable, ofsPos, bigEndian); - size = ReadU64Static(shTable, ofsPos + 8, bigEndian); + offset = ReadU64Static(shTable, shPos + Shdr64_OffsetField, bigEndian); + size = ReadU64Static(shTable, shPos + Shdr64_SizeField, bigEndian); } else { - // 32-bit: sh_name(4) + sh_type(4) + sh_flags(4) + sh_addr(4) = 16 bytes before sh_offset(4), sh_size(4). - int ofsPos = shPos + 4 + 4 + 4 + 4; - offset = ReadU32Static(shTable, ofsPos, bigEndian); - size = ReadU32Static(shTable, ofsPos + 4, bigEndian); + offset = ReadU32Static(shTable, shPos + Shdr32_OffsetField, bigEndian); + size = ReadU32Static(shTable, shPos + Shdr32_SizeField, bigEndian); } } @@ -521,11 +514,42 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected private const int Elf32EhdrSize = 52; private const int Elf64EhdrSize = 64; + // Byte offset of e_entry in the ELF header (first pointer-sized field). + // After: e_ident(16) + e_type(2) + e_machine(2) + e_version(4) = 24. + private const int Ehdr_Entry = EI_NIDENT + 2 + 2 + 4; + + // Size of the fixed-width fields between e_shoff and e_shentsize in the ELF header: + // e_flags(4) + e_ehsize(2) + e_phentsize(2) + e_phnum(2) = 10 bytes. + private const int Ehdr_FlagsToPhnum = 4 + 2 + 2 + 2; + + // Program header sizes. + private const int Elf32PhdrSize = 32; + private const int Elf64PhdrSize = 56; + + // Maximum program header count to accept from ELF headers. + private const int MaxProgramHeaderCount = 4096; + + // Elf64_Phdr field offsets: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8). + private const int Phdr64_Offset = 8; // byte offset of p_offset + private const int Phdr64_Filesz = 32; // byte offset of p_filesz + + // Elf32_Phdr field offsets: p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4). + private const int Phdr32_Offset = 4; // byte offset of p_offset + private const int Phdr32_Filesz = 16; // byte offset of p_filesz + // Section header entry sizes. private const int Elf32ShdrSize = 40; private const int Elf64ShdrSize = 64; private const int MaxShentsize = 256; + // Byte offsets of sh_offset and sh_size within the section header structure. + // 64-bit: sh_name(4) + sh_type(4) + sh_flags(8) + sh_addr(8) = 24 to sh_offset, 32 to sh_size. + private const int Shdr64_OffsetField = 24; + private const int Shdr64_SizeField = 32; + // 32-bit: sh_name(4) + sh_type(4) + sh_flags(4) + sh_addr(4) = 16 to sh_offset, 20 to sh_size. + private const int Shdr32_OffsetField = 16; + private const int Shdr32_SizeField = 20; + // Maximum section count to accept from ELF headers. private const uint MaxSectionCount = 65535; @@ -539,6 +563,20 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected // Note types. private const uint NT_GNU_BUILD_ID = 3; // GNU build-id note type. + // Note header: namesz(4) + descsz(4) + type(4) = 12 bytes. + private const int NoteHeaderSize = 12; + // Expected namesz for GNU notes ("GNU\0"). + private const uint GnuNoteNameSize = 4; + + // PT_NOTE segment size limit for ReadBuildId. Real build-id notes are < 100 bytes; + // 64 KB is generous. Prevents OOM from crafted ELF with large p_filesz. + private const int MaxNoteSizeBytes = 64 * 1024; + + // ReadDebugLink section size limits. + private const int MaxShstrtabSize = 1024 * 1024; // 1 MB + private const int MinDebugLinkSectionSize = 6; // 1-char filename + null + 4-byte CRC + private const int MaxDebugLinkSectionSize = 4096; + // Symbol table constants. private const byte STT_FUNC = 2; // Symbol type: function. private const byte STT_MASK = 0xf; // Mask to extract symbol type from st_info. @@ -561,22 +599,25 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected private readonly ulong m_pVaddr; private readonly ulong m_pOffset; private readonly bool m_demangle; - private readonly ItaniumDemangler m_itaniumDemangler = new ItaniumDemangler(); - private readonly RustDemangler m_rustDemangler = new RustDemangler(); + + // Demanglers use mutable parser state and are not thread-safe. ThreadLocal ensures + // each thread gets its own instance for safe concurrent FindNameForRva calls. + private readonly ThreadLocal m_itaniumDemangler = new ThreadLocal(() => new ItaniumDemangler()); + private readonly ThreadLocal m_rustDemangler = new ThreadLocal(() => new RustDemangler()); private SegmentedList m_strtab; // Retained for lazy name resolution. + private string[] m_symbolNames; // Thread-safe lazy name cache (parallel to m_symbols). private bool m_is64Bit; private bool m_bigEndian; /// /// Represents a resolved ELF symbol with its address range. - /// Name is decoded lazily on first FindNameForRva hit. + /// Name is decoded lazily on first FindNameForRva hit via m_symbolNames. /// private struct ElfSymbolEntry : IComparable { public uint Start; // RVA: (st_value - pVaddr) + pOffset. public uint End; // Start + size - 1 (inclusive). public uint StrtabOffset; // Offset into m_strtab for lazy name decode. - public string Name; // Null until first lookup, then cached. public int CompareTo(ElfSymbolEntry other) => Start.CompareTo(other.Start); } @@ -624,7 +665,7 @@ private void ParseElf(Stream stream) } // Parse ELF header fields. - int pos = 16 + 2 + 2 + 4; // skip e_ident(16), e_type(2), e_machine(2), e_version(4) + int pos = Ehdr_Entry; // skip e_ident(16), e_type(2), e_machine(2), e_version(4) ulong eShoff; if (m_is64Bit) @@ -638,7 +679,7 @@ private void ParseElf(Stream stream) eShoff = ReadU32(header, pos); pos += 4; } - pos += 4 + 2 + 2 + 2; // e_flags, e_ehsize, e_phentsize, e_phnum + pos += Ehdr_FlagsToPhnum; // e_flags, e_ehsize, e_phentsize, e_phnum ushort eShentsize = ReadU16(header, pos); pos += 2; ushort eShnum = ReadU16(header, pos); @@ -670,11 +711,11 @@ private void ParseElf(Stream stream) } if (m_is64Bit) { - sectionCount = (uint)ReadU64(firstSh, 8 + 8 + 8 + 8); + sectionCount = (uint)ReadU64(firstSh, Shdr64_SizeField); } else { - sectionCount = ReadU32(firstSh, 8 + 4 + 4 + 4); + sectionCount = ReadU32(firstSh, Shdr32_SizeField); } } @@ -797,6 +838,7 @@ private void ParseElf(Stream stream) // Sort symbols by start address for binary search. m_symbols.Sort(); + m_symbolNames = new string[m_symbols.Count]; } /// @@ -943,25 +985,34 @@ private static string ExtractBuildId(byte[] noteData, bool bigEndian) int pos = 0; int length = noteData.Length; - while (pos + 12 <= length) // Minimum note header: namesz(4) + descsz(4) + type(4). + while (pos + NoteHeaderSize <= length) { uint namesz = ReadU32Static(noteData, pos, bigEndian); uint descsz = ReadU32Static(noteData, pos + 4, bigEndian); uint type = ReadU32Static(noteData, pos + 8, bigEndian); - pos += 12; + pos += NoteHeaderSize; + + // Guard against uint overflow in alignment arithmetic: (x + 3) wraps + // when x >= 0xFFFFFFFD, producing a small aligned value and an infinite loop. + uint remaining = (uint)(length - pos); + if (namesz > remaining || descsz > remaining) + { + break; + } // Align name and desc sizes to 4-byte boundaries. uint nameAligned = (namesz + 3) & ~3u; uint descAligned = (descsz + 3) & ~3u; + uint noteSize = nameAligned + descAligned; // Validate that the note fits within the segment data. - if (pos + nameAligned + descAligned > length) + if (noteSize > remaining) { break; } // Check for GNU build-id: name == "GNU\0" (namesz == 4) and type == NT_GNU_BUILD_ID (3). - if (type == NT_GNU_BUILD_ID && namesz == 4 && + if (type == NT_GNU_BUILD_ID && namesz == GnuNoteNameSize && noteData[pos] == (byte)'G' && noteData[pos + 1] == (byte)'N' && noteData[pos + 2] == (byte)'U' && noteData[pos + 3] == 0) { @@ -980,7 +1031,7 @@ private static string ExtractBuildId(byte[] noteData, bool bigEndian) return sb.ToString(); } - pos += (int)nameAligned + (int)descAligned; + pos += (int)noteSize; } return null; @@ -999,7 +1050,7 @@ private string TryDemangle(string name) if (name.StartsWith("_Z")) { - string demangled = m_itaniumDemangler.Demangle(name); + string demangled = m_itaniumDemangler.Value.Demangle(name); if (demangled != null) { return demangled; @@ -1008,7 +1059,7 @@ private string TryDemangle(string name) if (name.StartsWith("_R")) { - string demangled = m_rustDemangler.Demangle(name); + string demangled = m_rustDemangler.Value.Demangle(name); if (demangled != null) { return demangled; From a3dac399ca31b04943a874e4ac96e873e1ccb745 Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Wed, 25 Mar 2026 13:41:54 -0700 Subject: [PATCH 05/10] Fix FindElfSymbolFilePath to use binary as last resort The binary itself (which typically only has .dynsym exported symbols) was being tried as a fallback before consulting symbol servers that could provide proper debug symbol files (with .symtab full symbols). Restructure the search into three ordered phases: Phase 1: Local debug files adjacent to binary (.debug, .dbg, debuglink) Phase 2: Symbol servers and symbol path directories Phase 3: Binary itself as absolute last resort This ensures we prefer debug symbols from symbol servers over the stripped binary whenever they are available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TraceEvent/Symbols/SymbolReader.cs | 103 +++++++++++++------------ 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index a483b840b..cdfec3b9f 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -435,8 +435,8 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF string resultPath = null; - // Check adjacent to the binary first (mirrors PDB local search). - // Prefer debug symbol files before falling back to the binary itself. + // Phase 1: Check for debug symbol files adjacent to the binary (mirrors PDB local search). + // Only look for dedicated debug files here — the binary itself is deferred to Phase 3. if (elfFilePath != null) { string elfDir = Path.GetDirectoryName(elfFilePath); @@ -496,74 +496,77 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF } } } - - // Last resort: try the binary itself (has .dynsym at minimum). - if (resultPath == null) - { - if (ElfBuildIdMatches(basePath, normalizedBuildId)) - { - resultPath = basePath; - } - } } } + // Phase 2: Search symbol servers and symbol path directories. if (resultPath == null) { - SymbolPath path = new SymbolPath(SymbolPath); - foreach (SymbolPathElement element in path.Elements) - { - if (element.IsSymServer) - { - string cache = element.Cache; - if (cache == null) - { - cache = path.DefaultSymbolCache(); - } - - // Try debug symbols first (preferred — has .symtab with full symbols). - resultPath = GetFileFromServer(element.Target, debugIndexPath, Path.Combine(cache, debugIndexPath)); - if (resultPath != null) - { - break; - } - - // Fall back to the binary (may only have .dynsym). - resultPath = GetFileFromServer(element.Target, binaryIndexPath, Path.Combine(cache, binaryIndexPath)); - if (resultPath != null) - { - break; - } - } - else + SymbolPath path = new SymbolPath(SymbolPath); + foreach (SymbolPathElement element in path.Elements) { - string target = element.Target; - if (target != null) + if (element.IsSymServer) { - if ((Options & SymbolReaderOptions.CacheOnly) != 0 && element.IsRemote) + string cache = element.Cache; + if (cache == null) { - m_log.WriteLine("FindElfSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", target); - continue; + cache = path.DefaultSymbolCache(); } - // Try SSQP-structured debug symbols first. - string debugPath = Path.Combine(target, debugIndexPath); - if (ElfBuildIdMatches(debugPath, normalizedBuildId, checkSecurity: false)) + // Try debug symbols first (preferred — has .symtab with full symbols). + resultPath = GetFileFromServer(element.Target, debugIndexPath, Path.Combine(cache, debugIndexPath)); + if (resultPath != null) { - resultPath = debugPath; break; } - // Try SSQP-structured binary. - string binaryPath = Path.Combine(target, binaryIndexPath); - if (ElfBuildIdMatches(binaryPath, normalizedBuildId, checkSecurity: false)) + // Fall back to the binary (may only have .dynsym). + resultPath = GetFileFromServer(element.Target, binaryIndexPath, Path.Combine(cache, binaryIndexPath)); + if (resultPath != null) { - resultPath = binaryPath; break; } } + else + { + string target = element.Target; + if (target != null) + { + if ((Options & SymbolReaderOptions.CacheOnly) != 0 && element.IsRemote) + { + m_log.WriteLine("FindElfSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", target); + continue; + } + + // Try SSQP-structured debug symbols first. + string debugPath = Path.Combine(target, debugIndexPath); + if (ElfBuildIdMatches(debugPath, normalizedBuildId, checkSecurity: false)) + { + resultPath = debugPath; + break; + } + + // Try SSQP-structured binary. + string binaryPath = Path.Combine(target, binaryIndexPath); + if (ElfBuildIdMatches(binaryPath, normalizedBuildId, checkSecurity: false)) + { + resultPath = binaryPath; + break; + } + } + } } } + + // Phase 3: Last resort — try the binary itself (has .dynsym at minimum). + // This is deferred until after symbol servers so we prefer proper debug symbols + // (.symtab) over the stripped binary whenever a symbol server can provide them. + if (resultPath == null && elfFilePath != null) + { + if (ElfBuildIdMatches(elfFilePath, normalizedBuildId)) + { + resultPath = elfFilePath; + } } if (resultPath != null) From 820650ff953953ad140156b45467be0ce3fc71f5 Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Fri, 27 Mar 2026 08:11:44 -0700 Subject: [PATCH 06/10] Replace ELF offset arithmetic with StructLayout structs Replace manual offset constants and ReadUXX helpers with [StructLayout(Sequential, Pack=1)] structs matching the ELF binary format. Use MemoryMarshal.Read to read structs from byte arrays, following the same pattern as PEFile.cs. Each struct has a SwapEndian() method for big-endian support via BinaryPrimitives.ReverseEndianness. This eliminates ~35 offset constants, 8 ReadUXX helper methods, and collapses 4 symbol parsing branches into 2. Structs defined: Elf32/64_Ehdr, Elf32/64_Phdr, Elf32/64_Shdr, Elf32/64_Sym, Elf_Nhdr. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TraceEvent/Symbols/ElfSymbolModule.cs | 703 +++++++++++++--------- 1 file changed, 415 insertions(+), 288 deletions(-) diff --git a/src/TraceEvent/Symbols/ElfSymbolModule.cs b/src/TraceEvent/Symbols/ElfSymbolModule.cs index 5ea588efe..0c78a9a2f 100644 --- a/src/TraceEvent/Symbols/ElfSymbolModule.cs +++ b/src/TraceEvent/Symbols/ElfSymbolModule.cs @@ -1,7 +1,10 @@ using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using System.Threading; @@ -113,8 +116,8 @@ internal static string ReadBuildId(string filePath) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { - // Read the ELF header (max 64 bytes for 64-bit). - byte[] header = new byte[Elf64EhdrSize]; + // Read the ELF header. + byte[] header = new byte[Unsafe.SizeOf()]; int headerRead = ReadFully(stream, header, 0, header.Length); if (headerRead < EI_NIDENT) { @@ -122,7 +125,6 @@ internal static string ReadBuildId(string filePath) return null; } - // Verify ELF magic bytes: 0x7f 'E' 'L' 'F'. if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) { Debug.WriteLine("ReadBuildId: Invalid ELF magic."); @@ -131,7 +133,6 @@ internal static string ReadBuildId(string filePath) byte eiClass = header[EI_CLASS]; byte eiData = header[EI_DATA]; - bool is64Bit = (eiClass == ElfClass64); bool bigEndian = (eiData == ElfDataMsb); @@ -141,57 +142,46 @@ internal static string ReadBuildId(string filePath) return null; } - int ehSize = is64Bit ? Elf64EhdrSize : Elf32EhdrSize; + int ehSize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); if (headerRead < ehSize) { Debug.WriteLine("ReadBuildId: Header too small."); return null; } - // Parse program header table location from ELF header. - // Layout after e_ident(16): e_type(2), e_machine(2), e_version(4), e_entry(4/8), e_phoff(4/8). - int pos = Ehdr_Entry; // skip e_ident, e_type, e_machine, e_version + // Extract program header fields from the typed struct. ulong ePhoff; + ushort ePhentsize, ePhnum; if (is64Bit) { - pos += 8; // skip e_entry(8) - ePhoff = ReadU64Static(header, pos, bigEndian); pos += 8; - } - else - { - pos += 4; // skip e_entry(4) - ePhoff = ReadU32Static(header, pos, bigEndian); pos += 4; - } - - // Skip to e_phentsize and e_phnum. - // After e_phoff: e_shoff(4/8), e_flags(4), e_ehsize(2). - if (is64Bit) - { - pos += 8 + 4 + 2; // e_shoff(8), e_flags(4), e_ehsize(2) + var ehdr = ReadStruct(header, 0); + if (bigEndian) ehdr.SwapEndian(); + ePhoff = ehdr.e_phoff; + ePhentsize = ehdr.e_phentsize; + ePhnum = ehdr.e_phnum; } else { - pos += 4 + 4 + 2; // e_shoff(4), e_flags(4), e_ehsize(2) + var ehdr = ReadStruct(header, 0); + if (bigEndian) ehdr.SwapEndian(); + ePhoff = ehdr.e_phoff; + ePhentsize = ehdr.e_phentsize; + ePhnum = ehdr.e_phnum; } - ushort ePhentsize = ReadU16Static(header, pos, bigEndian); pos += 2; - ushort ePhnum = ReadU16Static(header, pos, bigEndian); - if (ePhoff == 0 || ePhentsize == 0 || ePhnum == 0) { Debug.WriteLine("ReadBuildId: No program headers found."); return null; } - // Validate minimum program header entry size to avoid out-of-bounds reads. - int minPhentsize = is64Bit ? Elf64PhdrSize : Elf32PhdrSize; + int minPhentsize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); if (ePhentsize < minPhentsize) { Debug.WriteLine("ReadBuildId: ePhentsize too small: " + ePhentsize); return null; } - // Guard against corrupt ELF headers with unreasonably large program header counts. if (ePhnum > MaxProgramHeaderCount) { Debug.WriteLine("ReadBuildId: Program header count too large: " + ePhnum); @@ -212,24 +202,29 @@ internal static string ReadBuildId(string filePath) for (int i = 0; i < ePhnum; i++) { int phPos = i * ePhentsize; - uint pType = ReadU32Static(phTable, phPos, bigEndian); - if (pType != PT_NOTE) - { - continue; - } - - // Parse p_offset and p_filesz from the program header. + uint pType; ulong pOffset, pFilesz; if (is64Bit) { - pOffset = ReadU64Static(phTable, phPos + Phdr64_Offset, bigEndian); - pFilesz = ReadU64Static(phTable, phPos + Phdr64_Filesz, bigEndian); + var phdr = ReadStruct(phTable, phPos); + if (bigEndian) phdr.SwapEndian(); + pType = phdr.p_type; + pOffset = phdr.p_offset; + pFilesz = phdr.p_filesz; } else { - pOffset = ReadU32Static(phTable, phPos + Phdr32_Offset, bigEndian); - pFilesz = ReadU32Static(phTable, phPos + Phdr32_Filesz, bigEndian); + var phdr = ReadStruct(phTable, phPos); + if (bigEndian) phdr.SwapEndian(); + pType = phdr.p_type; + pOffset = phdr.p_offset; + pFilesz = phdr.p_filesz; + } + + if (pType != PT_NOTE) + { + continue; } if (pFilesz == 0 || pFilesz > MaxNoteSizeBytes) @@ -278,8 +273,8 @@ internal static string ReadDebugLink(string filePath) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { - // Read the ELF header (max 64 bytes for 64-bit). - byte[] header = new byte[Elf64EhdrSize]; + // Read the ELF header. + byte[] header = new byte[Unsafe.SizeOf()]; int headerRead = ReadFully(stream, header, 0, header.Length); if (headerRead < EI_NIDENT) { @@ -287,7 +282,6 @@ internal static string ReadDebugLink(string filePath) return null; } - // Verify ELF magic bytes: 0x7f 'E' 'L' 'F'. if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) { Debug.WriteLine("ReadDebugLink: Invalid ELF magic."); @@ -296,7 +290,6 @@ internal static string ReadDebugLink(string filePath) byte eiClass = header[EI_CLASS]; byte eiData = header[EI_DATA]; - bool is64Bit = (eiClass == ElfClass64); bool bigEndian = (eiData == ElfDataMsb); @@ -306,42 +299,42 @@ internal static string ReadDebugLink(string filePath) return null; } - int ehSize = is64Bit ? Elf64EhdrSize : Elf32EhdrSize; + int ehSize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); if (headerRead < ehSize) { Debug.WriteLine("ReadDebugLink: Header too small."); return null; } - // Parse section header table location from ELF header. - // Layout after e_ident(16): e_type(2), e_machine(2), e_version(4), - // e_entry(4/8), e_phoff(4/8), e_shoff(4/8), e_flags(4), e_ehsize(2), - // e_phentsize(2), e_phnum(2), e_shentsize(2), e_shnum(2), e_shstrndx(2). - int pos = Ehdr_Entry; // skip e_ident, e_type, e_machine, e_version + // Extract section header fields from the typed struct. ulong eShoff; + ushort eShentsize, eShnum, eShstrndx; if (is64Bit) { - pos += 8 + 8; // skip e_entry(8), e_phoff(8) - eShoff = ReadU64Static(header, pos, bigEndian); pos += 8; + var ehdr = ReadStruct(header, 0); + if (bigEndian) ehdr.SwapEndian(); + eShoff = ehdr.e_shoff; + eShentsize = ehdr.e_shentsize; + eShnum = ehdr.e_shnum; + eShstrndx = ehdr.e_shstrndx; } else { - pos += 4 + 4; // skip e_entry(4), e_phoff(4) - eShoff = ReadU32Static(header, pos, bigEndian); pos += 4; + var ehdr = ReadStruct(header, 0); + if (bigEndian) ehdr.SwapEndian(); + eShoff = ehdr.e_shoff; + eShentsize = ehdr.e_shentsize; + eShnum = ehdr.e_shnum; + eShstrndx = ehdr.e_shstrndx; } - pos += Ehdr_FlagsToPhnum; // e_flags(4), e_ehsize(2), e_phentsize(2), e_phnum(2) - ushort eShentsize = ReadU16Static(header, pos, bigEndian); pos += 2; - ushort eShnum = ReadU16Static(header, pos, bigEndian); pos += 2; - ushort eShstrndx = ReadU16Static(header, pos, bigEndian); - if (eShoff == 0 || eShentsize == 0 || eShnum == 0) { Debug.WriteLine("ReadDebugLink: No section headers found."); return null; } - int minShentsize = is64Bit ? Elf64ShdrSize : Elf32ShdrSize; + int minShentsize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); if (eShentsize < minShentsize || eShentsize > MaxShentsize) { Debug.WriteLine("ReadDebugLink: Invalid section header entry size: " + eShentsize); @@ -372,8 +365,7 @@ internal static string ReadDebugLink(string filePath) // Read the section name string table (shstrtab). int shstrPos = eShstrndx * eShentsize; - ulong shstrOffset, shstrSize; - ReadSectionOffsetAndSize(shTable, shstrPos, is64Bit, bigEndian, out shstrOffset, out shstrSize); + ReadSectionHeader(shTable, shstrPos, is64Bit, bigEndian, out _, out ulong shstrOffset, out ulong shstrSize, out _, out _); if (shstrSize == 0 || shstrSize > MaxShstrtabSize) { @@ -393,7 +385,24 @@ internal static string ReadDebugLink(string filePath) for (int i = 0; i < eShnum; i++) { int shPos = i * eShentsize; - uint shName = ReadU32Static(shTable, shPos, bigEndian); + uint shName; + ulong secOffset, secSize; + if (is64Bit) + { + var shdr = ReadStruct(shTable, shPos); + if (bigEndian) shdr.SwapEndian(); + shName = shdr.sh_name; + secOffset = shdr.sh_offset; + secSize = shdr.sh_size; + } + else + { + var shdr = ReadStruct(shTable, shPos); + if (bigEndian) shdr.SwapEndian(); + shName = shdr.sh_name; + secOffset = shdr.sh_offset; + secSize = shdr.sh_size; + } if (shName >= shstrtab.Length) { @@ -407,8 +416,6 @@ internal static string ReadDebugLink(string filePath) } // Found .gnu_debuglink — read its contents. - ulong secOffset, secSize; - ReadSectionOffsetAndSize(shTable, shPos, is64Bit, bigEndian, out secOffset, out secSize); // The section must contain at least a filename byte + null + 4-byte CRC. if (secSize < MinDebugLinkSectionSize || secSize > MaxDebugLinkSectionSize) @@ -453,21 +460,30 @@ internal static string ReadDebugLink(string filePath) private static readonly byte[] GnuDebugLinkName = Encoding.UTF8.GetBytes(".gnu_debuglink"); /// - /// Reads sh_offset and sh_size from a section header at the given position. - /// Layout: sh_name(4), sh_type(4), sh_flags(4/8), sh_addr(4/8), sh_offset(4/8), sh_size(4/8). + /// Reads section header fields from a byte array at the given position. /// - private static void ReadSectionOffsetAndSize(byte[] shTable, int shPos, bool is64Bit, bool bigEndian, - out ulong offset, out ulong size) + private static void ReadSectionHeader(byte[] shTable, int shPos, bool is64Bit, bool bigEndian, + out uint shType, out ulong offset, out ulong size, out uint link, out ulong entsize) { if (is64Bit) { - offset = ReadU64Static(shTable, shPos + Shdr64_OffsetField, bigEndian); - size = ReadU64Static(shTable, shPos + Shdr64_SizeField, bigEndian); + var shdr = ReadStruct(shTable, shPos); + if (bigEndian) shdr.SwapEndian(); + shType = shdr.sh_type; + offset = shdr.sh_offset; + size = shdr.sh_size; + link = shdr.sh_link; + entsize = shdr.sh_entsize; } else { - offset = ReadU32Static(shTable, shPos + Shdr32_OffsetField, bigEndian); - size = ReadU32Static(shTable, shPos + Shdr32_SizeField, bigEndian); + var shdr = ReadStruct(shTable, shPos); + if (bigEndian) shdr.SwapEndian(); + shType = shdr.sh_type; + offset = shdr.sh_offset; + size = shdr.sh_size; + link = shdr.sh_link; + entsize = shdr.sh_entsize; } } @@ -510,47 +526,11 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected // ELF data encoding values. private const byte ElfDataMsb = 2; // Big-endian. - // ELF header sizes. - private const int Elf32EhdrSize = 52; - private const int Elf64EhdrSize = 64; - - // Byte offset of e_entry in the ELF header (first pointer-sized field). - // After: e_ident(16) + e_type(2) + e_machine(2) + e_version(4) = 24. - private const int Ehdr_Entry = EI_NIDENT + 2 + 2 + 4; - - // Size of the fixed-width fields between e_shoff and e_shentsize in the ELF header: - // e_flags(4) + e_ehsize(2) + e_phentsize(2) + e_phnum(2) = 10 bytes. - private const int Ehdr_FlagsToPhnum = 4 + 2 + 2 + 2; - - // Program header sizes. - private const int Elf32PhdrSize = 32; - private const int Elf64PhdrSize = 56; - // Maximum program header count to accept from ELF headers. private const int MaxProgramHeaderCount = 4096; - // Elf64_Phdr field offsets: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8). - private const int Phdr64_Offset = 8; // byte offset of p_offset - private const int Phdr64_Filesz = 32; // byte offset of p_filesz - - // Elf32_Phdr field offsets: p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4). - private const int Phdr32_Offset = 4; // byte offset of p_offset - private const int Phdr32_Filesz = 16; // byte offset of p_filesz - - // Section header entry sizes. - private const int Elf32ShdrSize = 40; - private const int Elf64ShdrSize = 64; + // Maximum section header entry size and section count. private const int MaxShentsize = 256; - - // Byte offsets of sh_offset and sh_size within the section header structure. - // 64-bit: sh_name(4) + sh_type(4) + sh_flags(8) + sh_addr(8) = 24 to sh_offset, 32 to sh_size. - private const int Shdr64_OffsetField = 24; - private const int Shdr64_SizeField = 32; - // 32-bit: sh_name(4) + sh_type(4) + sh_flags(4) + sh_addr(4) = 16 to sh_offset, 20 to sh_size. - private const int Shdr32_OffsetField = 16; - private const int Shdr32_SizeField = 20; - - // Maximum section count to accept from ELF headers. private const uint MaxSectionCount = 65535; // Section header types. @@ -563,8 +543,6 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected // Note types. private const uint NT_GNU_BUILD_ID = 3; // GNU build-id note type. - // Note header: namesz(4) + descsz(4) + type(4) = 12 bytes. - private const int NoteHeaderSize = 12; // Expected namesz for GNU notes ("GNU\0"). private const uint GnuNoteNameSize = 4; @@ -581,19 +559,271 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected private const byte STT_FUNC = 2; // Symbol type: function. private const byte STT_MASK = 0xf; // Mask to extract symbol type from st_info. - // Elf64_Sym field offsets: st_name(4), st_info(1), st_other(1), st_shndx(2), st_value(8), st_size(8). - private const int Sym64_Name = 0; - private const int Sym64_Info = 4; - private const int Sym64_Value = 8; - private const int Sym64_Size = 16; + private const int StrtabSegmentSize = 65536; // 64KB segments to avoid LOH. - // Elf32_Sym field offsets: st_name(4), st_value(4), st_size(4), st_info(1), st_other(1), st_shndx(2). - private const int Sym32_Name = 0; - private const int Sym32_Value = 4; - private const int Sym32_Size = 8; - private const int Sym32_Info = 12; + #region ELF binary format structs - private const int StrtabSegmentSize = 65536; // 64KB segments to avoid LOH. + // These structs match the ELF specification layouts exactly. Fields use ELF naming conventions + // (e_phoff, sh_type, st_value, etc.) for easy cross-reference with the spec. + // MemoryMarshal.Read is used to read them from byte arrays — the same pattern as PEFile.cs. + // For big-endian ELF files, SwapEndian() reverses each multi-byte field after reading. + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf64_Ehdr + { + // e_ident[16] + public byte ei_mag0, ei_mag1, ei_mag2, ei_mag3; + public byte ei_class, ei_data, ei_version, ei_osabi; + public byte ei_abiversion, ei_pad1, ei_pad2, ei_pad3, ei_pad4, ei_pad5, ei_pad6, ei_pad7; + // Header fields + public ushort e_type; + public ushort e_machine; + public uint e_version; + public ulong e_entry; + public ulong e_phoff; + public ulong e_shoff; + public uint e_flags; + public ushort e_ehsize; + public ushort e_phentsize; + public ushort e_phnum; + public ushort e_shentsize; + public ushort e_shnum; + public ushort e_shstrndx; + + public void SwapEndian() + { + e_type = BinaryPrimitives.ReverseEndianness(e_type); + e_machine = BinaryPrimitives.ReverseEndianness(e_machine); + e_version = BinaryPrimitives.ReverseEndianness(e_version); + e_entry = BinaryPrimitives.ReverseEndianness(e_entry); + e_phoff = BinaryPrimitives.ReverseEndianness(e_phoff); + e_shoff = BinaryPrimitives.ReverseEndianness(e_shoff); + e_flags = BinaryPrimitives.ReverseEndianness(e_flags); + e_ehsize = BinaryPrimitives.ReverseEndianness(e_ehsize); + e_phentsize = BinaryPrimitives.ReverseEndianness(e_phentsize); + e_phnum = BinaryPrimitives.ReverseEndianness(e_phnum); + e_shentsize = BinaryPrimitives.ReverseEndianness(e_shentsize); + e_shnum = BinaryPrimitives.ReverseEndianness(e_shnum); + e_shstrndx = BinaryPrimitives.ReverseEndianness(e_shstrndx); + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf32_Ehdr + { + // e_ident[16] + public byte ei_mag0, ei_mag1, ei_mag2, ei_mag3; + public byte ei_class, ei_data, ei_version, ei_osabi; + public byte ei_abiversion, ei_pad1, ei_pad2, ei_pad3, ei_pad4, ei_pad5, ei_pad6, ei_pad7; + // Header fields + public ushort e_type; + public ushort e_machine; + public uint e_version; + public uint e_entry; + public uint e_phoff; + public uint e_shoff; + public uint e_flags; + public ushort e_ehsize; + public ushort e_phentsize; + public ushort e_phnum; + public ushort e_shentsize; + public ushort e_shnum; + public ushort e_shstrndx; + + public void SwapEndian() + { + e_type = BinaryPrimitives.ReverseEndianness(e_type); + e_machine = BinaryPrimitives.ReverseEndianness(e_machine); + e_version = BinaryPrimitives.ReverseEndianness(e_version); + e_entry = BinaryPrimitives.ReverseEndianness(e_entry); + e_phoff = BinaryPrimitives.ReverseEndianness(e_phoff); + e_shoff = BinaryPrimitives.ReverseEndianness(e_shoff); + e_flags = BinaryPrimitives.ReverseEndianness(e_flags); + e_ehsize = BinaryPrimitives.ReverseEndianness(e_ehsize); + e_phentsize = BinaryPrimitives.ReverseEndianness(e_phentsize); + e_phnum = BinaryPrimitives.ReverseEndianness(e_phnum); + e_shentsize = BinaryPrimitives.ReverseEndianness(e_shentsize); + e_shnum = BinaryPrimitives.ReverseEndianness(e_shnum); + e_shstrndx = BinaryPrimitives.ReverseEndianness(e_shstrndx); + } + } + + // 64-bit: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8), p_memsz(8), p_align(8) + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf64_Phdr + { + public uint p_type; + public uint p_flags; + public ulong p_offset; + public ulong p_vaddr; + public ulong p_paddr; + public ulong p_filesz; + public ulong p_memsz; + public ulong p_align; + + public void SwapEndian() + { + p_type = BinaryPrimitives.ReverseEndianness(p_type); + p_flags = BinaryPrimitives.ReverseEndianness(p_flags); + p_offset = BinaryPrimitives.ReverseEndianness(p_offset); + p_vaddr = BinaryPrimitives.ReverseEndianness(p_vaddr); + p_paddr = BinaryPrimitives.ReverseEndianness(p_paddr); + p_filesz = BinaryPrimitives.ReverseEndianness(p_filesz); + p_memsz = BinaryPrimitives.ReverseEndianness(p_memsz); + p_align = BinaryPrimitives.ReverseEndianness(p_align); + } + } + + // 32-bit: p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4), p_memsz(4), p_flags(4), p_align(4) + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf32_Phdr + { + public uint p_type; + public uint p_offset; + public uint p_vaddr; + public uint p_paddr; + public uint p_filesz; + public uint p_memsz; + public uint p_flags; + public uint p_align; + + public void SwapEndian() + { + p_type = BinaryPrimitives.ReverseEndianness(p_type); + p_offset = BinaryPrimitives.ReverseEndianness(p_offset); + p_vaddr = BinaryPrimitives.ReverseEndianness(p_vaddr); + p_paddr = BinaryPrimitives.ReverseEndianness(p_paddr); + p_filesz = BinaryPrimitives.ReverseEndianness(p_filesz); + p_memsz = BinaryPrimitives.ReverseEndianness(p_memsz); + p_flags = BinaryPrimitives.ReverseEndianness(p_flags); + p_align = BinaryPrimitives.ReverseEndianness(p_align); + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf64_Shdr + { + public uint sh_name; + public uint sh_type; + public ulong sh_flags; + public ulong sh_addr; + public ulong sh_offset; + public ulong sh_size; + public uint sh_link; + public uint sh_info; + public ulong sh_addralign; + public ulong sh_entsize; + + public void SwapEndian() + { + sh_name = BinaryPrimitives.ReverseEndianness(sh_name); + sh_type = BinaryPrimitives.ReverseEndianness(sh_type); + sh_flags = BinaryPrimitives.ReverseEndianness(sh_flags); + sh_addr = BinaryPrimitives.ReverseEndianness(sh_addr); + sh_offset = BinaryPrimitives.ReverseEndianness(sh_offset); + sh_size = BinaryPrimitives.ReverseEndianness(sh_size); + sh_link = BinaryPrimitives.ReverseEndianness(sh_link); + sh_info = BinaryPrimitives.ReverseEndianness(sh_info); + sh_addralign = BinaryPrimitives.ReverseEndianness(sh_addralign); + sh_entsize = BinaryPrimitives.ReverseEndianness(sh_entsize); + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf32_Shdr + { + public uint sh_name; + public uint sh_type; + public uint sh_flags; + public uint sh_addr; + public uint sh_offset; + public uint sh_size; + public uint sh_link; + public uint sh_info; + public uint sh_addralign; + public uint sh_entsize; + + public void SwapEndian() + { + sh_name = BinaryPrimitives.ReverseEndianness(sh_name); + sh_type = BinaryPrimitives.ReverseEndianness(sh_type); + sh_flags = BinaryPrimitives.ReverseEndianness(sh_flags); + sh_addr = BinaryPrimitives.ReverseEndianness(sh_addr); + sh_offset = BinaryPrimitives.ReverseEndianness(sh_offset); + sh_size = BinaryPrimitives.ReverseEndianness(sh_size); + sh_link = BinaryPrimitives.ReverseEndianness(sh_link); + sh_info = BinaryPrimitives.ReverseEndianness(sh_info); + sh_addralign = BinaryPrimitives.ReverseEndianness(sh_addralign); + sh_entsize = BinaryPrimitives.ReverseEndianness(sh_entsize); + } + } + + // 64-bit: st_name(4), st_info(1), st_other(1), st_shndx(2), st_value(8), st_size(8) = 24 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf64_Sym + { + public uint st_name; + public byte st_info; + public byte st_other; + public ushort st_shndx; + public ulong st_value; + public ulong st_size; + + public void SwapEndian() + { + st_name = BinaryPrimitives.ReverseEndianness(st_name); + st_shndx = BinaryPrimitives.ReverseEndianness(st_shndx); + st_value = BinaryPrimitives.ReverseEndianness(st_value); + st_size = BinaryPrimitives.ReverseEndianness(st_size); + } + } + + // 32-bit: st_name(4), st_value(4), st_size(4), st_info(1), st_other(1), st_shndx(2) = 16 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf32_Sym + { + public uint st_name; + public uint st_value; + public uint st_size; + public byte st_info; + public byte st_other; + public ushort st_shndx; + + public void SwapEndian() + { + st_name = BinaryPrimitives.ReverseEndianness(st_name); + st_value = BinaryPrimitives.ReverseEndianness(st_value); + st_size = BinaryPrimitives.ReverseEndianness(st_size); + st_shndx = BinaryPrimitives.ReverseEndianness(st_shndx); + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Elf_Nhdr + { + public uint n_namesz; + public uint n_descsz; + public uint n_type; + + public void SwapEndian() + { + n_namesz = BinaryPrimitives.ReverseEndianness(n_namesz); + n_descsz = BinaryPrimitives.ReverseEndianness(n_descsz); + n_type = BinaryPrimitives.ReverseEndianness(n_type); + } + } + + /// + /// Reads an ELF struct from a byte array at the given offset. + /// For little-endian ELF files the struct is ready to use. For big-endian, the caller + /// must call SwapEndian() on the returned value. + /// + private static T ReadStruct(byte[] data, int offset) where T : struct + { + return MemoryMarshal.Read(data.AsSpan(offset)); + } + + #endregion private readonly List m_symbols; private readonly ulong m_pVaddr; @@ -630,8 +860,8 @@ private struct ElfSymbolEntry : IComparable /// private void ParseElf(Stream stream) { - // Read the ELF header (max 64 bytes for 64-bit). - byte[] header = new byte[Elf64EhdrSize]; + // Read the ELF header. + byte[] header = new byte[Unsafe.SizeOf()]; int headerRead = ReadFully(stream, header, 0, header.Length); if (headerRead < EI_NIDENT) { @@ -658,31 +888,32 @@ private void ParseElf(Stream stream) return; } - int ehSize = m_is64Bit ? Elf64EhdrSize : Elf32EhdrSize; + int ehSize = m_is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); if (headerRead < ehSize) { return; } - // Parse ELF header fields. - int pos = Ehdr_Entry; // skip e_ident(16), e_type(2), e_machine(2), e_version(4) - + // Extract section header fields from the typed struct. ulong eShoff; + ushort eShentsize, eShnum; if (m_is64Bit) { - pos += 8 + 8; // e_entry, e_phoff - eShoff = ReadU64(header, pos); pos += 8; + var ehdr = ReadStruct(header, 0); + if (m_bigEndian) ehdr.SwapEndian(); + eShoff = ehdr.e_shoff; + eShentsize = ehdr.e_shentsize; + eShnum = ehdr.e_shnum; } else { - pos += 4 + 4; - eShoff = ReadU32(header, pos); pos += 4; + var ehdr = ReadStruct(header, 0); + if (m_bigEndian) ehdr.SwapEndian(); + eShoff = ehdr.e_shoff; + eShentsize = ehdr.e_shentsize; + eShnum = ehdr.e_shnum; } - pos += Ehdr_FlagsToPhnum; // e_flags, e_ehsize, e_phentsize, e_phnum - ushort eShentsize = ReadU16(header, pos); pos += 2; - ushort eShnum = ReadU16(header, pos); - if (eShoff == 0 || eShentsize == 0) { Debug.WriteLine("ElfSymbolModule: No section headers found."); @@ -692,7 +923,7 @@ private void ParseElf(Stream stream) // Valid ELF section header sizes are 40 (32-bit) or 64 (64-bit). // Reject values below the minimum struct size (would cause out-of-bounds reads) // and cap at 256 to guard against overflow in sectionCount * eShentsize. - int minShentsize = m_is64Bit ? Elf64ShdrSize : Elf32ShdrSize; + int minShentsize = m_is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); if (eShentsize < minShentsize || eShentsize > MaxShentsize) { Debug.WriteLine("ElfSymbolModule: Invalid section header entry size: " + eShentsize); @@ -711,11 +942,15 @@ private void ParseElf(Stream stream) } if (m_is64Bit) { - sectionCount = (uint)ReadU64(firstSh, Shdr64_SizeField); + var firstShdr = ReadStruct(firstSh, 0); + if (m_bigEndian) firstShdr.SwapEndian(); + sectionCount = (uint)firstShdr.sh_size; } else { - sectionCount = ReadU32(firstSh, Shdr32_SizeField); + var firstShdr = ReadStruct(firstSh, 0); + if (m_bigEndian) firstShdr.SwapEndian(); + sectionCount = firstShdr.sh_size; } } @@ -746,9 +981,8 @@ private void ParseElf(Stream stream) for (uint i = 0; i < sectionCount; i++) { int shPos = (int)i * eShentsize; - uint shType; - long shOffset, shSize, shLink, shEntsize; - ReadSectionHeader(shTable, shPos, out shType, out shOffset, out shSize, out shLink, out shEntsize); + ReadSectionHeader(shTable, shPos, m_is64Bit, m_bigEndian, + out uint shType, out _, out ulong shSize, out uint shLink, out ulong shEntsize); if (shType != SHT_SYMTAB && shType != SHT_DYNSYM) { @@ -757,7 +991,7 @@ private void ParseElf(Stream stream) if (shEntsize > 0) { - totalSymbolCount += shSize / shEntsize; + totalSymbolCount += (long)(shSize / shEntsize); } // Get the linked string table size. @@ -767,10 +1001,9 @@ private void ParseElf(Stream stream) } int strtabShPos = (int)shLink * eShentsize; - uint strtabType; - long strtabOffset, strtabSize, strtabLink, strtabEntsize; - ReadSectionHeader(shTable, strtabShPos, out strtabType, out strtabOffset, out strtabSize, out strtabLink, out strtabEntsize); - totalStrtabSize += strtabSize; + ReadSectionHeader(shTable, strtabShPos, m_is64Bit, m_bigEndian, + out _, out _, out ulong strtabSize, out _, out _); + totalStrtabSize += (long)strtabSize; } // Pre-allocate with known sizes. @@ -782,9 +1015,8 @@ private void ParseElf(Stream stream) for (uint i = 0; i < sectionCount; i++) { int shPos = (int)i * eShentsize; - uint shType; - long shOffset, shSize, shLink, shEntsize; - ReadSectionHeader(shTable, shPos, out shType, out shOffset, out shSize, out shLink, out shEntsize); + ReadSectionHeader(shTable, shPos, m_is64Bit, m_bigEndian, + out uint shType, out ulong shOffset, out ulong shSize, out uint shLink, out ulong shEntsize); if (shType != SHT_SYMTAB && shType != SHT_DYNSYM) { @@ -798,18 +1030,17 @@ private void ParseElf(Stream stream) } int strtabShPos = (int)shLink * eShentsize; - uint strtabType; - long strtabOffset, strtabSize, strtabLink, strtabEntsize; - ReadSectionHeader(shTable, strtabShPos, out strtabType, out strtabOffset, out strtabSize, out strtabLink, out strtabEntsize); + ReadSectionHeader(shTable, strtabShPos, m_is64Bit, m_bigEndian, + out _, out ulong strtabOffset, out ulong strtabSize, out _, out _); - if (strtabSize <= 0) + if (strtabSize == 0) { continue; } // Read strtab in chunks and append to SegmentedList. - stream.Seek(strtabOffset, SeekOrigin.Begin); - long remaining = strtabSize; + stream.Seek((long)strtabOffset, SeekOrigin.Begin); + long remaining = (long)strtabSize; byte[] readBuf = new byte[Math.Min(remaining, StrtabSegmentSize)]; while (remaining > 0) { @@ -824,16 +1055,16 @@ private void ParseElf(Stream stream) } // Read the symbol table section. - byte[] symData = new byte[shSize]; - stream.Seek(shOffset, SeekOrigin.Begin); - if (ReadFully(stream, symData, 0, (int)shSize) < shSize) + byte[] symData = new byte[(long)shSize]; + stream.Seek((long)shOffset, SeekOrigin.Begin); + if (ReadFully(stream, symData, 0, (int)shSize) < (long)shSize) { - strtabBaseOffset += strtabSize; + strtabBaseOffset += (long)strtabSize; continue; } - ReadSymbolTable(symData, shSize, shEntsize, strtabBaseOffset); - strtabBaseOffset += strtabSize; + ReadSymbolTable(symData, (long)shSize, (long)shEntsize, strtabBaseOffset); + strtabBaseOffset += (long)strtabSize; } // Sort symbols by start address for binary search. @@ -859,35 +1090,6 @@ private static int ReadFully(Stream stream, byte[] buffer, int offset, int count return totalRead; } - /// - /// Reads section header fields from a byte array at the given position. - /// - private void ReadSectionHeader(byte[] data, int pos, out uint shType, out long shOffset, - out long shSize, out long shLink, out long shEntsize) - { - pos += 4; // skip sh_name - shType = ReadU32(data, pos); pos += 4; - - if (m_is64Bit) - { - pos += 8 + 8; // sh_flags, sh_addr - shOffset = (long)ReadU64(data, pos); pos += 8; - shSize = (long)ReadU64(data, pos); pos += 8; - shLink = ReadU32(data, pos); pos += 4; - pos += 4 + 8; // sh_info, sh_addralign - shEntsize = (long)ReadU64(data, pos); - } - else - { - pos += 4 + 4; // sh_flags, sh_addr - shOffset = ReadU32(data, pos); pos += 4; - shSize = ReadU32(data, pos); pos += 4; - shLink = ReadU32(data, pos); pos += 4; - pos += 4 + 4; // sh_info, sh_addralign - shEntsize = ReadU32(data, pos); - } - } - /// /// Reads all symbol entries from a pre-loaded symbol table byte array. /// Stores strtab offsets for lazy name resolution instead of decoding strings. @@ -910,34 +1112,23 @@ private void ReadSymbolTable(byte[] symData, long size, long entsize, long strta ulong stValue; ulong stSize; - if (m_is64Bit && !m_bigEndian) - { - // Fast path for 64-bit little-endian (the common case). - stName = BitConverter.ToUInt32(symData, pos + Sym64_Name); - stInfo = symData[pos + Sym64_Info]; - stValue = BitConverter.ToUInt64(symData, pos + Sym64_Value); - stSize = BitConverter.ToUInt64(symData, pos + Sym64_Size); - } - else if (m_is64Bit) - { - stName = ReadU32(symData, pos + Sym64_Name); - stInfo = symData[pos + Sym64_Info]; - stValue = ReadU64(symData, pos + Sym64_Value); - stSize = ReadU64(symData, pos + Sym64_Size); - } - else if (!m_bigEndian) + if (m_is64Bit) { - stName = BitConverter.ToUInt32(symData, pos + Sym32_Name); - stValue = BitConverter.ToUInt32(symData, pos + Sym32_Value); - stSize = BitConverter.ToUInt32(symData, pos + Sym32_Size); - stInfo = symData[pos + Sym32_Info]; + var sym = ReadStruct(symData, pos); + if (m_bigEndian) sym.SwapEndian(); + stName = sym.st_name; + stInfo = sym.st_info; + stValue = sym.st_value; + stSize = sym.st_size; } else { - stName = ReadU32(symData, pos + Sym32_Name); - stValue = ReadU32(symData, pos + Sym32_Value); - stSize = ReadU32(symData, pos + Sym32_Size); - stInfo = symData[pos + Sym32_Info]; + var sym = ReadStruct(symData, pos); + if (m_bigEndian) sym.SwapEndian(); + stName = sym.st_name; + stInfo = sym.st_info; + stValue = sym.st_value; + stSize = sym.st_size; } // Filter to STT_FUNC symbols with non-zero value and size. @@ -984,13 +1175,16 @@ private static string ExtractBuildId(byte[] noteData, bool bigEndian) { int pos = 0; int length = noteData.Length; + int nhdrSize = Unsafe.SizeOf(); - while (pos + NoteHeaderSize <= length) + while (pos + nhdrSize <= length) { - uint namesz = ReadU32Static(noteData, pos, bigEndian); - uint descsz = ReadU32Static(noteData, pos + 4, bigEndian); - uint type = ReadU32Static(noteData, pos + 8, bigEndian); - pos += NoteHeaderSize; + var nhdr = ReadStruct(noteData, pos); + if (bigEndian) nhdr.SwapEndian(); + uint namesz = nhdr.n_namesz; + uint descsz = nhdr.n_descsz; + uint type = nhdr.n_type; + pos += nhdrSize; // Guard against uint overflow in alignment arithmetic: (x + 3) wraps // when x >= 0xFFFFFFFD, producing a small aligned value and an infinite loop. @@ -1119,73 +1313,6 @@ private static string ReadNullTerminatedString(SegmentedList data, uint of return Encoding.UTF8.GetString(bytes.ToArray(), 0, bytes.Count); } - #region Endianness helpers - - /// Little-endian uint16 read from byte array. - private static ushort ReadU16LE(byte[] data, int offset) - { - return (ushort)(data[offset] | data[offset + 1] << 8); - } - - /// Big-endian uint16 read from byte array. - private static ushort ReadU16BE(byte[] data, int offset) - { - return (ushort)(data[offset] << 8 | data[offset + 1]); - } - - private ushort ReadU16(byte[] data, int offset) - { - return m_bigEndian ? ReadU16BE(data, offset) : ReadU16LE(data, offset); - } - - private uint ReadU32(byte[] data, int offset) - { - if (m_bigEndian) - { - return (uint)(data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]); - } - return BitConverter.ToUInt32(data, offset); - } - - private ulong ReadU64(byte[] data, int offset) - { - if (m_bigEndian) - { - return ((ulong)ReadU32(data, offset) << 32) | ReadU32(data, offset + 4); - } - return BitConverter.ToUInt64(data, offset); - } - - // Static overloads for use in ReadBuildId (which has no instance state). - - /// Static uint16 read with explicit endianness. - private static ushort ReadU16Static(byte[] data, int offset, bool bigEndian) - { - return bigEndian ? ReadU16BE(data, offset) : ReadU16LE(data, offset); - } - - /// Static uint32 read with explicit endianness. - private static uint ReadU32Static(byte[] data, int offset, bool bigEndian) - { - if (bigEndian) - { - return (uint)(data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]); - } - return BitConverter.ToUInt32(data, offset); - } - - /// Static uint64 read with explicit endianness. - private static ulong ReadU64Static(byte[] data, int offset, bool bigEndian) - { - if (bigEndian) - { - return ((ulong)ReadU32Static(data, offset, bigEndian) << 32) | ReadU32Static(data, offset + 4, bigEndian); - } - return BitConverter.ToUInt64(data, offset); - } - - #endregion - #endregion } } From c9f6ab9c4e924c1b787dc3158ff43de05808fc85 Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Fri, 27 Mar 2026 10:46:36 -0700 Subject: [PATCH 07/10] Refactor ElfSymbolModule: hoist SwapEndian into ReadStruct and add helpers - Add IElfStruct interface implemented by all 9 ELF structs - ReadStruct now accepts bigEndian param and calls SwapEndian() internally, eliminating ~15 caller-side endian checks - Add TryReadElfHeader helper consolidating ELF header validation and field extraction duplicated across ReadBuildId, ReadDebugLink, and ParseElf - Add ReadProgramHeader and ReadSymbolEntry helpers parallel to existing ReadSectionHeader - Extend ReadSectionHeader with sh_name output; replace inline Shdr extraction in ReadDebugLink and ParseElf extended section count - Net reduction of ~40 lines with significantly less code duplication Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TraceEvent/Symbols/ElfSymbolModule.cs | 496 ++++++++++------------ 1 file changed, 223 insertions(+), 273 deletions(-) diff --git a/src/TraceEvent/Symbols/ElfSymbolModule.cs b/src/TraceEvent/Symbols/ElfSymbolModule.cs index 0c78a9a2f..c75a79895 100644 --- a/src/TraceEvent/Symbols/ElfSymbolModule.cs +++ b/src/TraceEvent/Symbols/ElfSymbolModule.cs @@ -116,82 +116,40 @@ internal static string ReadBuildId(string filePath) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { - // Read the ELF header. + // Read and validate the ELF header. byte[] header = new byte[Unsafe.SizeOf()]; int headerRead = ReadFully(stream, header, 0, header.Length); - if (headerRead < EI_NIDENT) + if (!TryReadElfHeader(header, headerRead, out var hdr, "ReadBuildId")) { - Debug.WriteLine("ReadBuildId: File too small."); return null; } - if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) - { - Debug.WriteLine("ReadBuildId: Invalid ELF magic."); - return null; - } - - byte eiClass = header[EI_CLASS]; - byte eiData = header[EI_DATA]; - bool is64Bit = (eiClass == ElfClass64); - bool bigEndian = (eiData == ElfDataMsb); - - if (eiClass != ElfClass32 && eiClass != ElfClass64) - { - Debug.WriteLine("ReadBuildId: Unknown ELF class " + eiClass + "."); - return null; - } + bool is64Bit = hdr.Is64Bit; + bool bigEndian = hdr.BigEndian; - int ehSize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); - if (headerRead < ehSize) - { - Debug.WriteLine("ReadBuildId: Header too small."); - return null; - } - - // Extract program header fields from the typed struct. - ulong ePhoff; - ushort ePhentsize, ePhnum; - if (is64Bit) - { - var ehdr = ReadStruct(header, 0); - if (bigEndian) ehdr.SwapEndian(); - ePhoff = ehdr.e_phoff; - ePhentsize = ehdr.e_phentsize; - ePhnum = ehdr.e_phnum; - } - else - { - var ehdr = ReadStruct(header, 0); - if (bigEndian) ehdr.SwapEndian(); - ePhoff = ehdr.e_phoff; - ePhentsize = ehdr.e_phentsize; - ePhnum = ehdr.e_phnum; - } - - if (ePhoff == 0 || ePhentsize == 0 || ePhnum == 0) + if (hdr.PhOffset == 0 || hdr.PhEntrySize == 0 || hdr.PhCount == 0) { Debug.WriteLine("ReadBuildId: No program headers found."); return null; } int minPhentsize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); - if (ePhentsize < minPhentsize) + if (hdr.PhEntrySize < minPhentsize) { - Debug.WriteLine("ReadBuildId: ePhentsize too small: " + ePhentsize); + Debug.WriteLine("ReadBuildId: ePhentsize too small: " + hdr.PhEntrySize); return null; } - if (ePhnum > MaxProgramHeaderCount) + if (hdr.PhCount > MaxProgramHeaderCount) { - Debug.WriteLine("ReadBuildId: Program header count too large: " + ePhnum); + Debug.WriteLine("ReadBuildId: Program header count too large: " + hdr.PhCount); return null; } // Read all program headers in one bulk read. - int phTableSize = ePhnum * ePhentsize; + int phTableSize = hdr.PhCount * hdr.PhEntrySize; byte[] phTable = new byte[phTableSize]; - stream.Seek((long)ePhoff, SeekOrigin.Begin); + stream.Seek((long)hdr.PhOffset, SeekOrigin.Begin); if (ReadFully(stream, phTable, 0, phTableSize) < phTableSize) { Debug.WriteLine("ReadBuildId: Could not read program headers."); @@ -199,28 +157,10 @@ internal static string ReadBuildId(string filePath) } // Iterate program headers looking for PT_NOTE segments. - for (int i = 0; i < ePhnum; i++) + for (int i = 0; i < hdr.PhCount; i++) { - int phPos = i * ePhentsize; - - uint pType; - ulong pOffset, pFilesz; - if (is64Bit) - { - var phdr = ReadStruct(phTable, phPos); - if (bigEndian) phdr.SwapEndian(); - pType = phdr.p_type; - pOffset = phdr.p_offset; - pFilesz = phdr.p_filesz; - } - else - { - var phdr = ReadStruct(phTable, phPos); - if (bigEndian) phdr.SwapEndian(); - pType = phdr.p_type; - pOffset = phdr.p_offset; - pFilesz = phdr.p_filesz; - } + int phPos = i * hdr.PhEntrySize; + ReadProgramHeader(phTable, phPos, is64Bit, bigEndian, out uint pType, out ulong pOffset, out ulong pFilesz, out _); if (pType != PT_NOTE) { @@ -273,90 +213,46 @@ internal static string ReadDebugLink(string filePath) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { - // Read the ELF header. + // Read and validate the ELF header. byte[] header = new byte[Unsafe.SizeOf()]; int headerRead = ReadFully(stream, header, 0, header.Length); - if (headerRead < EI_NIDENT) - { - Debug.WriteLine("ReadDebugLink: File too small."); - return null; - } - - if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) + if (!TryReadElfHeader(header, headerRead, out var hdr, "ReadDebugLink")) { - Debug.WriteLine("ReadDebugLink: Invalid ELF magic."); return null; } - byte eiClass = header[EI_CLASS]; - byte eiData = header[EI_DATA]; - bool is64Bit = (eiClass == ElfClass64); - bool bigEndian = (eiData == ElfDataMsb); + bool is64Bit = hdr.Is64Bit; + bool bigEndian = hdr.BigEndian; - if (eiClass != ElfClass32 && eiClass != ElfClass64) - { - Debug.WriteLine("ReadDebugLink: Unknown ELF class " + eiClass + "."); - return null; - } - - int ehSize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); - if (headerRead < ehSize) - { - Debug.WriteLine("ReadDebugLink: Header too small."); - return null; - } - - // Extract section header fields from the typed struct. - ulong eShoff; - ushort eShentsize, eShnum, eShstrndx; - if (is64Bit) - { - var ehdr = ReadStruct(header, 0); - if (bigEndian) ehdr.SwapEndian(); - eShoff = ehdr.e_shoff; - eShentsize = ehdr.e_shentsize; - eShnum = ehdr.e_shnum; - eShstrndx = ehdr.e_shstrndx; - } - else - { - var ehdr = ReadStruct(header, 0); - if (bigEndian) ehdr.SwapEndian(); - eShoff = ehdr.e_shoff; - eShentsize = ehdr.e_shentsize; - eShnum = ehdr.e_shnum; - eShstrndx = ehdr.e_shstrndx; - } - - if (eShoff == 0 || eShentsize == 0 || eShnum == 0) + if (hdr.ShOffset == 0 || hdr.ShEntrySize == 0 || hdr.ShCount == 0) { Debug.WriteLine("ReadDebugLink: No section headers found."); return null; } int minShentsize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); - if (eShentsize < minShentsize || eShentsize > MaxShentsize) + if (hdr.ShEntrySize < minShentsize || hdr.ShEntrySize > MaxShentsize) { - Debug.WriteLine("ReadDebugLink: Invalid section header entry size: " + eShentsize); + Debug.WriteLine("ReadDebugLink: Invalid section header entry size: " + hdr.ShEntrySize); return null; } - if (eShnum > MaxSectionCount) + if (hdr.ShCount > MaxSectionCount) { - Debug.WriteLine("ReadDebugLink: Section count too large: " + eShnum); + Debug.WriteLine("ReadDebugLink: Section count too large: " + hdr.ShCount); return null; } - if (eShstrndx >= eShnum) + if (hdr.ShStrIndex >= hdr.ShCount) { - Debug.WriteLine("ReadDebugLink: Invalid shstrndx: " + eShstrndx); + Debug.WriteLine("ReadDebugLink: Invalid shstrndx: " + hdr.ShStrIndex); return null; } // Read all section headers in one bulk read. - int shTableSize = eShnum * eShentsize; + int shTableSize = hdr.ShCount * hdr.ShEntrySize; byte[] shTable = new byte[shTableSize]; - stream.Seek((long)eShoff, SeekOrigin.Begin); + stream.Seek((long)hdr.ShOffset, SeekOrigin.Begin); if (ReadFully(stream, shTable, 0, shTableSize) < shTableSize) { Debug.WriteLine("ReadDebugLink: Could not read section headers."); @@ -364,8 +260,8 @@ internal static string ReadDebugLink(string filePath) } // Read the section name string table (shstrtab). - int shstrPos = eShstrndx * eShentsize; - ReadSectionHeader(shTable, shstrPos, is64Bit, bigEndian, out _, out ulong shstrOffset, out ulong shstrSize, out _, out _); + int shstrPos = hdr.ShStrIndex * hdr.ShEntrySize; + ReadSectionHeader(shTable, shstrPos, is64Bit, bigEndian, out _, out _, out ulong shstrOffset, out ulong shstrSize, out _, out _); if (shstrSize == 0 || shstrSize > MaxShstrtabSize) { @@ -382,27 +278,10 @@ internal static string ReadDebugLink(string filePath) } // Iterate sections looking for .gnu_debuglink by name. - for (int i = 0; i < eShnum; i++) + for (int i = 0; i < hdr.ShCount; i++) { - int shPos = i * eShentsize; - uint shName; - ulong secOffset, secSize; - if (is64Bit) - { - var shdr = ReadStruct(shTable, shPos); - if (bigEndian) shdr.SwapEndian(); - shName = shdr.sh_name; - secOffset = shdr.sh_offset; - secSize = shdr.sh_size; - } - else - { - var shdr = ReadStruct(shTable, shPos); - if (bigEndian) shdr.SwapEndian(); - shName = shdr.sh_name; - secOffset = shdr.sh_offset; - secSize = shdr.sh_size; - } + int shPos = i * hdr.ShEntrySize; + ReadSectionHeader(shTable, shPos, is64Bit, bigEndian, out uint shName, out _, out ulong secOffset, out ulong secSize, out _, out _); if (shName >= shstrtab.Length) { @@ -459,16 +338,101 @@ internal static string ReadDebugLink(string filePath) // Name of the .gnu_debuglink section (UTF-8 bytes for fast comparison). private static readonly byte[] GnuDebugLinkName = Encoding.UTF8.GetBytes(".gnu_debuglink"); + /// + /// Common fields extracted from an ELF header (Ehdr) after validation. + /// + private struct ElfHeaderInfo + { + public bool Is64Bit; + public bool BigEndian; + // Program header table. + public ulong PhOffset; + public ushort PhEntrySize; + public ushort PhCount; + // Section header table. + public ulong ShOffset; + public ushort ShEntrySize; + public ushort ShCount; + public ushort ShStrIndex; + } + + /// + /// Validates an ELF header buffer and extracts common fields into . + /// The caller must have already read at least Unsafe.SizeOf<Elf64_Ehdr>() bytes + /// into . + /// + /// True if the header is valid; false otherwise (with a Debug.WriteLine message). + private static bool TryReadElfHeader(byte[] header, int headerRead, out ElfHeaderInfo info, string callerName) + { + info = default; + + if (headerRead < EI_NIDENT) + { + Debug.WriteLine(callerName + ": File too small."); + return false; + } + + if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) + { + Debug.WriteLine(callerName + ": Invalid ELF magic."); + return false; + } + + byte eiClass = header[EI_CLASS]; + byte eiData = header[EI_DATA]; + info.Is64Bit = (eiClass == ElfClass64); + info.BigEndian = (eiData == ElfDataMsb); + + if (eiClass != ElfClass32 && eiClass != ElfClass64) + { + Debug.WriteLine(callerName + ": Unknown ELF class " + eiClass + "."); + return false; + } + + int ehSize = info.Is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); + if (headerRead < ehSize) + { + Debug.WriteLine(callerName + ": Header too small."); + return false; + } + + // Extract all commonly needed fields from the typed header. + if (info.Is64Bit) + { + var ehdr = ReadStruct(header, 0, info.BigEndian); + info.PhOffset = ehdr.e_phoff; + info.PhEntrySize = ehdr.e_phentsize; + info.PhCount = ehdr.e_phnum; + info.ShOffset = ehdr.e_shoff; + info.ShEntrySize = ehdr.e_shentsize; + info.ShCount = ehdr.e_shnum; + info.ShStrIndex = ehdr.e_shstrndx; + } + else + { + var ehdr = ReadStruct(header, 0, info.BigEndian); + info.PhOffset = ehdr.e_phoff; + info.PhEntrySize = ehdr.e_phentsize; + info.PhCount = ehdr.e_phnum; + info.ShOffset = ehdr.e_shoff; + info.ShEntrySize = ehdr.e_shentsize; + info.ShCount = ehdr.e_shnum; + info.ShStrIndex = ehdr.e_shstrndx; + } + + return true; + } + /// /// Reads section header fields from a byte array at the given position. /// private static void ReadSectionHeader(byte[] shTable, int shPos, bool is64Bit, bool bigEndian, - out uint shType, out ulong offset, out ulong size, out uint link, out ulong entsize) + out uint name, out uint shType, out ulong offset, out ulong size, out uint link, out ulong entsize) { if (is64Bit) { - var shdr = ReadStruct(shTable, shPos); - if (bigEndian) shdr.SwapEndian(); + var shdr = ReadStruct(shTable, shPos, bigEndian); + name = shdr.sh_name; shType = shdr.sh_type; offset = shdr.sh_offset; size = shdr.sh_size; @@ -477,8 +441,8 @@ private static void ReadSectionHeader(byte[] shTable, int shPos, bool is64Bit, b } else { - var shdr = ReadStruct(shTable, shPos); - if (bigEndian) shdr.SwapEndian(); + var shdr = ReadStruct(shTable, shPos, bigEndian); + name = shdr.sh_name; shType = shdr.sh_type; offset = shdr.sh_offset; size = shdr.sh_size; @@ -487,6 +451,54 @@ private static void ReadSectionHeader(byte[] shTable, int shPos, bool is64Bit, b } } + /// + /// Reads program header fields from a byte array at the given position. + /// + private static void ReadProgramHeader(byte[] phTable, int phPos, bool is64Bit, bool bigEndian, + out uint pType, out ulong pOffset, out ulong pFilesz, out ulong pVaddr) + { + if (is64Bit) + { + var phdr = ReadStruct(phTable, phPos, bigEndian); + pType = phdr.p_type; + pOffset = phdr.p_offset; + pFilesz = phdr.p_filesz; + pVaddr = phdr.p_vaddr; + } + else + { + var phdr = ReadStruct(phTable, phPos, bigEndian); + pType = phdr.p_type; + pOffset = phdr.p_offset; + pFilesz = phdr.p_filesz; + pVaddr = phdr.p_vaddr; + } + } + + /// + /// Reads symbol table entry fields from a byte array at the given position. + /// + private static void ReadSymbolEntry(byte[] symData, int pos, bool is64Bit, bool bigEndian, + out uint stName, out byte stInfo, out ulong stValue, out ulong stSize) + { + if (is64Bit) + { + var sym = ReadStruct(symData, pos, bigEndian); + stName = sym.st_name; + stInfo = sym.st_info; + stValue = sym.st_value; + stSize = sym.st_size; + } + else + { + var sym = ReadStruct(symData, pos, bigEndian); + stName = sym.st_name; + stInfo = sym.st_info; + stValue = sym.st_value; + stSize = sym.st_size; + } + } + /// /// Compares a null-terminated string in a byte array against an expected byte sequence. /// @@ -567,9 +579,19 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected // (e_phoff, sh_type, st_value, etc.) for easy cross-reference with the spec. // MemoryMarshal.Read is used to read them from byte arrays — the same pattern as PEFile.cs. // For big-endian ELF files, SwapEndian() reverses each multi-byte field after reading. + // All structs implement IElfStruct so ReadStruct can handle endian swapping automatically. + + /// + /// Implemented by all ELF binary structs so that can + /// perform endian conversion in one place. + /// + private interface IElfStruct + { + void SwapEndian(); + } [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf64_Ehdr + private struct Elf64_Ehdr : IElfStruct { // e_ident[16] public byte ei_mag0, ei_mag1, ei_mag2, ei_mag3; @@ -609,7 +631,7 @@ public void SwapEndian() } [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf32_Ehdr + private struct Elf32_Ehdr : IElfStruct { // e_ident[16] public byte ei_mag0, ei_mag1, ei_mag2, ei_mag3; @@ -650,7 +672,7 @@ public void SwapEndian() // 64-bit: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8), p_memsz(8), p_align(8) [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf64_Phdr + private struct Elf64_Phdr : IElfStruct { public uint p_type; public uint p_flags; @@ -676,7 +698,7 @@ public void SwapEndian() // 32-bit: p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4), p_memsz(4), p_flags(4), p_align(4) [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf32_Phdr + private struct Elf32_Phdr : IElfStruct { public uint p_type; public uint p_offset; @@ -701,7 +723,7 @@ public void SwapEndian() } [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf64_Shdr + private struct Elf64_Shdr : IElfStruct { public uint sh_name; public uint sh_type; @@ -730,7 +752,7 @@ public void SwapEndian() } [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf32_Shdr + private struct Elf32_Shdr : IElfStruct { public uint sh_name; public uint sh_type; @@ -760,7 +782,7 @@ public void SwapEndian() // 64-bit: st_name(4), st_info(1), st_other(1), st_shndx(2), st_value(8), st_size(8) = 24 bytes [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf64_Sym + private struct Elf64_Sym : IElfStruct { public uint st_name; public byte st_info; @@ -780,7 +802,7 @@ public void SwapEndian() // 32-bit: st_name(4), st_value(4), st_size(4), st_info(1), st_other(1), st_shndx(2) = 16 bytes [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf32_Sym + private struct Elf32_Sym : IElfStruct { public uint st_name; public uint st_value; @@ -799,7 +821,7 @@ public void SwapEndian() } [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct Elf_Nhdr + private struct Elf_Nhdr : IElfStruct { public uint n_namesz; public uint n_descsz; @@ -815,12 +837,17 @@ public void SwapEndian() /// /// Reads an ELF struct from a byte array at the given offset. - /// For little-endian ELF files the struct is ready to use. For big-endian, the caller - /// must call SwapEndian() on the returned value. + /// When is true, + /// is called automatically before returning. /// - private static T ReadStruct(byte[] data, int offset) where T : struct + private static T ReadStruct(byte[] data, int offset, bool bigEndian) where T : struct, IElfStruct { - return MemoryMarshal.Read(data.AsSpan(offset)); + T value = MemoryMarshal.Read(data.AsSpan(offset)); + if (bigEndian) + { + value.SwapEndian(); + } + return value; } #endregion @@ -860,61 +887,18 @@ private struct ElfSymbolEntry : IComparable /// private void ParseElf(Stream stream) { - // Read the ELF header. + // Read and validate the ELF header. byte[] header = new byte[Unsafe.SizeOf()]; int headerRead = ReadFully(stream, header, 0, header.Length); - if (headerRead < EI_NIDENT) - { - Debug.WriteLine("ElfSymbolModule: File too small."); - return; - } - - // Verify ELF magic bytes: 0x7f 'E' 'L' 'F'. - if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3) + if (!TryReadElfHeader(header, headerRead, out var hdr, "ElfSymbolModule")) { - Debug.WriteLine("ElfSymbolModule: Invalid ELF magic."); return; } - byte eiClass = header[EI_CLASS]; - byte eiData = header[EI_DATA]; - - m_is64Bit = (eiClass == ElfClass64); - m_bigEndian = (eiData == ElfDataMsb); - - if (eiClass != ElfClass32 && eiClass != ElfClass64) - { - Debug.WriteLine("ElfSymbolModule: Unknown ELF class " + eiClass + "."); - return; - } - - int ehSize = m_is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); - if (headerRead < ehSize) - { - return; - } + m_is64Bit = hdr.Is64Bit; + m_bigEndian = hdr.BigEndian; - // Extract section header fields from the typed struct. - ulong eShoff; - ushort eShentsize, eShnum; - if (m_is64Bit) - { - var ehdr = ReadStruct(header, 0); - if (m_bigEndian) ehdr.SwapEndian(); - eShoff = ehdr.e_shoff; - eShentsize = ehdr.e_shentsize; - eShnum = ehdr.e_shnum; - } - else - { - var ehdr = ReadStruct(header, 0); - if (m_bigEndian) ehdr.SwapEndian(); - eShoff = ehdr.e_shoff; - eShentsize = ehdr.e_shentsize; - eShnum = ehdr.e_shnum; - } - - if (eShoff == 0 || eShentsize == 0) + if (hdr.ShOffset == 0 || hdr.ShEntrySize == 0) { Debug.WriteLine("ElfSymbolModule: No section headers found."); return; @@ -924,34 +908,24 @@ private void ParseElf(Stream stream) // Reject values below the minimum struct size (would cause out-of-bounds reads) // and cap at 256 to guard against overflow in sectionCount * eShentsize. int minShentsize = m_is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf(); - if (eShentsize < minShentsize || eShentsize > MaxShentsize) + if (hdr.ShEntrySize < minShentsize || hdr.ShEntrySize > MaxShentsize) { - Debug.WriteLine("ElfSymbolModule: Invalid section header entry size: " + eShentsize); + Debug.WriteLine("ElfSymbolModule: Invalid section header entry size: " + hdr.ShEntrySize); return; } // Handle extended section count. - uint sectionCount = eShnum; - if (eShnum == 0) + uint sectionCount = hdr.ShCount; + if (hdr.ShCount == 0) { - byte[] firstSh = new byte[eShentsize]; - stream.Seek((long)eShoff, SeekOrigin.Begin); + byte[] firstSh = new byte[hdr.ShEntrySize]; + stream.Seek((long)hdr.ShOffset, SeekOrigin.Begin); if (ReadFully(stream, firstSh, 0, firstSh.Length) < firstSh.Length) { return; } - if (m_is64Bit) - { - var firstShdr = ReadStruct(firstSh, 0); - if (m_bigEndian) firstShdr.SwapEndian(); - sectionCount = (uint)firstShdr.sh_size; - } - else - { - var firstShdr = ReadStruct(firstSh, 0); - if (m_bigEndian) firstShdr.SwapEndian(); - sectionCount = firstShdr.sh_size; - } + ReadSectionHeader(firstSh, 0, m_is64Bit, m_bigEndian, out _, out _, out _, out ulong extSize, out _, out _); + sectionCount = (uint)extSize; } if (sectionCount == 0) @@ -967,9 +941,9 @@ private void ParseElf(Stream stream) } // Read all section headers in one bulk read. - int shTableSize = (int)sectionCount * eShentsize; + int shTableSize = (int)sectionCount * hdr.ShEntrySize; byte[] shTable = new byte[shTableSize]; - stream.Seek((long)eShoff, SeekOrigin.Begin); + stream.Seek((long)hdr.ShOffset, SeekOrigin.Begin); if (ReadFully(stream, shTable, 0, shTableSize) < shTableSize) { return; @@ -980,9 +954,9 @@ private void ParseElf(Stream stream) long totalSymbolCount = 0; for (uint i = 0; i < sectionCount; i++) { - int shPos = (int)i * eShentsize; + int shPos = (int)i * hdr.ShEntrySize; ReadSectionHeader(shTable, shPos, m_is64Bit, m_bigEndian, - out uint shType, out _, out ulong shSize, out uint shLink, out ulong shEntsize); + out _, out uint shType, out _, out ulong shSize, out uint shLink, out ulong shEntsize); if (shType != SHT_SYMTAB && shType != SHT_DYNSYM) { @@ -1000,9 +974,9 @@ private void ParseElf(Stream stream) continue; } - int strtabShPos = (int)shLink * eShentsize; + int strtabShPos = (int)shLink * hdr.ShEntrySize; ReadSectionHeader(shTable, strtabShPos, m_is64Bit, m_bigEndian, - out _, out _, out ulong strtabSize, out _, out _); + out _, out _, out _, out ulong strtabSize, out _, out _); totalStrtabSize += (long)strtabSize; } @@ -1014,9 +988,9 @@ private void ParseElf(Stream stream) long strtabBaseOffset = 0; for (uint i = 0; i < sectionCount; i++) { - int shPos = (int)i * eShentsize; + int shPos = (int)i * hdr.ShEntrySize; ReadSectionHeader(shTable, shPos, m_is64Bit, m_bigEndian, - out uint shType, out ulong shOffset, out ulong shSize, out uint shLink, out ulong shEntsize); + out _, out uint shType, out ulong shOffset, out ulong shSize, out uint shLink, out ulong shEntsize); if (shType != SHT_SYMTAB && shType != SHT_DYNSYM) { @@ -1029,9 +1003,9 @@ private void ParseElf(Stream stream) continue; } - int strtabShPos = (int)shLink * eShentsize; + int strtabShPos = (int)shLink * hdr.ShEntrySize; ReadSectionHeader(shTable, strtabShPos, m_is64Bit, m_bigEndian, - out _, out ulong strtabOffset, out ulong strtabSize, out _, out _); + out _, out _, out ulong strtabOffset, out ulong strtabSize, out _, out _); if (strtabSize == 0) { @@ -1106,30 +1080,7 @@ private void ReadSymbolTable(byte[] symData, long size, long entsize, long strta for (long i = 0; i < count; i++) { int pos = (int)(i * entsize); - - uint stName; - byte stInfo; - ulong stValue; - ulong stSize; - - if (m_is64Bit) - { - var sym = ReadStruct(symData, pos); - if (m_bigEndian) sym.SwapEndian(); - stName = sym.st_name; - stInfo = sym.st_info; - stValue = sym.st_value; - stSize = sym.st_size; - } - else - { - var sym = ReadStruct(symData, pos); - if (m_bigEndian) sym.SwapEndian(); - stName = sym.st_name; - stInfo = sym.st_info; - stValue = sym.st_value; - stSize = sym.st_size; - } + ReadSymbolEntry(symData, pos, m_is64Bit, m_bigEndian, out uint stName, out byte stInfo, out ulong stValue, out ulong stSize); // Filter to STT_FUNC symbols with non-zero value and size. if ((stInfo & STT_MASK) != STT_FUNC || stValue == 0 || stSize == 0) @@ -1179,8 +1130,7 @@ private static string ExtractBuildId(byte[] noteData, bool bigEndian) while (pos + nhdrSize <= length) { - var nhdr = ReadStruct(noteData, pos); - if (bigEndian) nhdr.SwapEndian(); + var nhdr = ReadStruct(noteData, pos, bigEndian); uint namesz = nhdr.n_namesz; uint descsz = nhdr.n_descsz; uint type = nhdr.n_type; From 072fd9b929d55285d8832e4d92157b2884f10b4e Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Fri, 27 Mar 2026 16:33:46 -0700 Subject: [PATCH 08/10] Address PR review comments: overflow check, null guard, build-id match, HashCode.Combine - TraceLog.cs: Wrap RVA computation in checked context for uint overflow - SymbolReader.cs: Add null guard for buildId parameter - SymbolReader.cs: Return false when no expected build-id (safer default) - SymbolReader.cs: Use HashCode.Combine for ElfBuildIdSignature/ElfModuleSignature - Add Microsoft.Bcl.HashCode NuGet package with PerfView DLL embedding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Directory.Packages.props | 1 + src/PerfView/PerfView.csproj | 8 ++++++++ src/TraceEvent/Symbols/SymbolReader.cs | 13 +++++++++---- src/TraceEvent/TraceEvent.csproj | 1 + src/TraceEvent/TraceLog.cs | 2 +- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 7cac5d8f0..5a4660d11 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -16,6 +16,7 @@ + diff --git a/src/PerfView/PerfView.csproj b/src/PerfView/PerfView.csproj index aa2492bb7..e4dacf3b4 100644 --- a/src/PerfView/PerfView.csproj +++ b/src/PerfView/PerfView.csproj @@ -119,6 +119,7 @@ HeapDump dependencies are pulled from the HeapDump output directory because HeapDump runs out of process and can have a different set of dependencies. --> + @@ -411,6 +412,13 @@ Microsoft.Diagnostics.FastSerialization.dll False + + Non-Resx + false + .\Microsoft.Bcl.HashCode.dll + Microsoft.Bcl.HashCode.dll + False + Non-Resx false diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index cdfec3b9f..9c6f7cbbe 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -413,6 +413,11 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig /// The local file path to the downloaded symbol file, or null if not found. public string FindElfSymbolFilePath(string fileName, string buildId, string elfFilePath = null) { + if (buildId == null) + { + throw new ArgumentNullException(nameof(buildId)); + } + m_log.WriteLine("FindElfSymbolFilePath: *{{ Searching for {0} with BuildId {1}", fileName, buildId); string simpleFileName = Path.GetFileName(fileName); @@ -1238,8 +1243,8 @@ private bool ElfBuildIdMatches(string filePath, string expectedBuildId, bool che if (string.IsNullOrEmpty(expectedBuildId)) { - m_log.WriteLine("FindElfSymbolFilePath: No expected build-id provided, assuming unsafe match for {0}", filePath); - return true; + m_log.WriteLine("FindElfSymbolFilePath: No expected build-id provided, cannot verify match for {0}", filePath); + return false; } string actualBuildId = ElfSymbolModule.ReadBuildId(filePath); @@ -1973,7 +1978,7 @@ private struct R2RPerfMapSignature : IEquatable // Used as the key to the m_elfPathCache. private struct ElfBuildIdSignature : IEquatable { - public override int GetHashCode() { return FileName.GetHashCode() + BuildId.GetHashCode(); } + public override int GetHashCode() { return HashCode.Combine(FileName, BuildId); } public bool Equals(ElfBuildIdSignature other) { return FileName == other.FileName && BuildId == other.BuildId; } public string FileName; public string BuildId; @@ -1981,7 +1986,7 @@ private struct ElfBuildIdSignature : IEquatable private struct ElfModuleSignature : IEquatable { - public override int GetHashCode() { return FilePath.GetHashCode() ^ VAddr.GetHashCode() ^ Offset.GetHashCode(); } + public override int GetHashCode() { return HashCode.Combine(FilePath, VAddr, Offset); } public bool Equals(ElfModuleSignature other) { return FilePath == other.FilePath && VAddr == other.VAddr && Offset == other.Offset; } public string FilePath; public ulong VAddr; diff --git a/src/TraceEvent/TraceEvent.csproj b/src/TraceEvent/TraceEvent.csproj index fbb141a7b..c8abab7f6 100644 --- a/src/TraceEvent/TraceEvent.csproj +++ b/src/TraceEvent/TraceEvent.csproj @@ -68,6 +68,7 @@ + diff --git a/src/TraceEvent/TraceLog.cs b/src/TraceEvent/TraceLog.cs index dc7eb6d01..9bf1902ca 100644 --- a/src/TraceEvent/TraceLog.cs +++ b/src/TraceEvent/TraceLog.cs @@ -8942,7 +8942,7 @@ private void LookupSymbolsForModule(SymbolReader reader, TraceModuleFile moduleF // ELF RVA = (address - ImageBase) + FileOffset, matching ElfSymbolModule's // (st_value - pVaddr) + pOffset formula. ulong fileOffset = moduleFile.ElfInfo.FileOffset; - computeRva = (address) => (uint)(address - moduleFile.ImageBase) + (uint)fileOffset; + computeRva = (address) => checked((uint)(address - moduleFile.ImageBase) + (uint)fileOffset); } } break; From 3169b42f8a4db25778d0d2eafe702ae573da10ef Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Fri, 27 Mar 2026 16:36:20 -0700 Subject: [PATCH 09/10] Update Microsoft.Bcl.HashCode to version 6.0.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 5a4660d11..41b8f41e7 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -16,7 +16,7 @@ - + From 1cdfc0209d4034acd6db326a3e4517ee6fd6de0c Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Fri, 27 Mar 2026 16:37:42 -0700 Subject: [PATCH 10/10] Add null check for fileName in FindElfSymbolFilePath Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TraceEvent/Symbols/SymbolReader.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index 9c6f7cbbe..b7ea16039 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -413,6 +413,11 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig /// The local file path to the downloaded symbol file, or null if not found. public string FindElfSymbolFilePath(string fileName, string buildId, string elfFilePath = null) { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + if (buildId == null) { throw new ArgumentNullException(nameof(buildId));