diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index dcddd881a..c785a2927 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -417,8 +417,9 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig /// 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 GNU build-id as 8-20 bytes represented by hexadecimal characters. /// The local file path to the downloaded symbol file, or null if not found. + /// is not a supported hexadecimal GNU build-id. public string FindElfSymbolFilePath(string fileName, string buildId, string elfFilePath = null) { if (fileName == null) @@ -431,15 +432,11 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF throw new ArgumentNullException(nameof(buildId)); } - m_log.WriteLine("FindElfSymbolFilePath: *{{ Searching for {0} with BuildId {1}", fileName, buildId); + string normalizedBuildId = NormalizeElfBuildId(buildId); + m_log.WriteLine("FindElfSymbolFilePath: *{{ Searching for {0} with BuildId {1}", fileName, normalizedBuildId); 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)) { @@ -532,14 +529,16 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF } // Try debug symbols first (preferred — has .symtab with full symbols). - resultPath = GetFileFromServer(element.Target, debugIndexPath, Path.Combine(cache, debugIndexPath)); + string debugCachePath = GetContainedElfCachePath(cache, debugIndexPath); + resultPath = GetFileFromServer(element.Target, debugIndexPath, debugCachePath); if (resultPath != null) { break; } // Fall back to the binary (may only have .dynsym). - resultPath = GetFileFromServer(element.Target, binaryIndexPath, Path.Combine(cache, binaryIndexPath)); + string binaryCachePath = GetContainedElfCachePath(cache, binaryIndexPath); + resultPath = GetFileFromServer(element.Target, binaryIndexPath, binaryCachePath); if (resultPath != null) { break; @@ -1225,6 +1224,87 @@ public void Dispose() } #region private + /// + /// Validates and normalizes an ELF build-id for use in SSQP keys. + /// + private static string NormalizeElfBuildId(string buildId) + { + if (!TryNormalizeElfBuildId(buildId, out string normalizedBuildId)) + { + throw new ArgumentException( + $"ELF build-id '{buildId}' must contain an even number of hexadecimal characters representing 8 to 20 bytes.", + nameof(buildId)); + } + + return normalizedBuildId; + } + + /// + /// Converts a supported ELF build-id to the canonical 20-byte lowercase SSQP representation. + /// + private static bool TryNormalizeElfBuildId(string buildId, out string normalizedBuildId) + { + normalizedBuildId = null; + if (buildId == null || + buildId.Length < MinElfBuildIdHexLength || + buildId.Length > MaxElfBuildIdHexLength || + (buildId.Length & 1) != 0) + { + return false; + } + + for (int i = 0; i < buildId.Length; i++) + { + char c = buildId[i]; + if (!((c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'))) + { + return false; + } + } + + normalizedBuildId = buildId.ToLowerInvariant().PadRight(MaxElfBuildIdHexLength, '0'); + return true; + } + + /// + /// Resolves an ELF symbol-server cache path and verifies that it remains under the configured cache root. + /// + private static string GetContainedElfCachePath(string cacheDirectory, string relativePath) + { + if (string.IsNullOrEmpty(cacheDirectory)) + { + throw new ArgumentException("ELF symbol cache directory must not be null or empty.", nameof(cacheDirectory)); + } + + if (string.IsNullOrEmpty(relativePath) || Path.IsPathRooted(relativePath)) + { + throw new ArgumentException("ELF symbol cache path must be relative.", nameof(relativePath)); + } + + string fullCacheDirectory = Path.GetFullPath(cacheDirectory); + string fullCachePath = Path.GetFullPath(Path.Combine(fullCacheDirectory, relativePath)); + string cachePrefix = fullCacheDirectory; + char lastCachePrefixChar = cachePrefix[cachePrefix.Length - 1]; + if (lastCachePrefixChar != Path.DirectorySeparatorChar && + lastCachePrefixChar != Path.AltDirectorySeparatorChar) + { + cachePrefix += Path.DirectorySeparatorChar; + } + + StringComparison comparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + if (!fullCachePath.StartsWith(cachePrefix, comparison)) + { + throw new InvalidOperationException( + $"ELF symbol cache path '{fullCachePath}' is outside the configured cache directory '{fullCacheDirectory}'."); + } + + return fullCachePath; + } + /// /// Returns true if 'filePath' exists and is a PDB that has pdbGuid and pdbAge. /// if pdbGuid == Guid.Empty, then the pdbGuid and pdbAge checks are skipped. @@ -1337,7 +1417,8 @@ private bool ElfBuildIdMatches(string filePath, string expectedBuildId, bool che } string actualBuildId = ElfSymbolModule.ReadBuildId(filePath); - if (actualBuildId != null && string.Equals(actualBuildId, expectedBuildId, StringComparison.OrdinalIgnoreCase)) + if (TryNormalizeElfBuildId(actualBuildId, out string normalizedActualBuildId) && + string.Equals(normalizedActualBuildId, expectedBuildId, StringComparison.Ordinal)) { return true; } @@ -2091,6 +2172,8 @@ private struct ElfModuleSignature : IEquatable private Cache m_elfPathCache; private Cache m_elfModuleCache; private string m_symbolPath; + private const int MinElfBuildIdHexLength = 16; + private const int MaxElfBuildIdHexLength = 40; #endregion } @@ -2841,4 +2924,3 @@ private enum LineEnding #endregion } } - diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs index 64b6165ba..4f62c9865 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs @@ -694,8 +694,8 @@ public void FindElfSymbolFilePath_DebugSymbolsFoundLocally() string tempDir = Path.Combine(OutputDir, "elf-local-debug"); try { - string buildId = "abc123"; - string normalizedBuildId = buildId.ToLowerInvariant(); + string buildId = "abc1230000000000"; + string normalizedBuildId = CanonicalizeBuildId(buildId); // Create SSQP debug symbol directory structure with valid ELF build-id. string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); @@ -722,8 +722,8 @@ public void FindElfSymbolFilePath_BinaryFallbackLocally() string tempDir = Path.Combine(OutputDir, "elf-local-binary"); try { - string buildId = "def456"; - string normalizedBuildId = buildId.ToLowerInvariant(); + string buildId = "def4560000000000"; + string normalizedBuildId = CanonicalizeBuildId(buildId); // Create only the binary directory structure (no debug symbols). string binaryDir = Path.Combine(tempDir, "libcoreclr.so", "elf-buildid-" + normalizedBuildId); @@ -750,8 +750,8 @@ public void FindElfSymbolFilePath_DebugPreferredOverBinary() string tempDir = Path.Combine(OutputDir, "elf-local-prefer-debug"); try { - string buildId = "aabbcc"; - string normalizedBuildId = buildId.ToLowerInvariant(); + string buildId = "aabbcc0000000000"; + string normalizedBuildId = CanonicalizeBuildId(buildId); // Create both debug and binary directory structures with valid ELF build-ids. string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); @@ -786,7 +786,7 @@ public void FindElfSymbolFilePath_NotFoundLocally() Directory.CreateDirectory(tempDir); _symbolReader.SymbolPath = tempDir; - string result = _symbolReader.FindElfSymbolFilePath("libmissing.so", "deadbeef"); + string result = _symbolReader.FindElfSymbolFilePath("libmissing.so", "deadbeef00000000"); Assert.Null(result); } @@ -798,8 +798,8 @@ public void FindElfSymbolFilePath_NotFoundLocally() } [Theory] - [InlineData("abcd", "abcd")] - [InlineData("ABC123", "abc123")] + [InlineData("0123456789abcdef", "0123456789abcdef000000000000000000000000")] + [InlineData("ABCDEF0123456789ABCDEF0123456789", "abcdef0123456789abcdef012345678900000000")] [InlineData("aabbccdd00112233445566778899aabbccddeeff", "aabbccdd00112233445566778899aabbccddeeff")] public void FindElfSymbolFilePath_BuildIdNormalization(string inputBuildId, string expectedNormalized) { @@ -810,7 +810,7 @@ public void FindElfSymbolFilePath_BuildIdNormalization(string inputBuildId, stri string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + expectedNormalized); Directory.CreateDirectory(debugDir); string debugFile = Path.Combine(debugDir, "_.debug"); - File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(expectedNormalized)); + File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(inputBuildId.ToLowerInvariant())); _symbolReader.SymbolPath = tempDir; string result = _symbolReader.FindElfSymbolFilePath("libnorm.so", inputBuildId); @@ -831,8 +831,8 @@ public void FindElfSymbolFilePath_AbsolutePathExtractsFilename() string tempDir = Path.Combine(OutputDir, "elf-abspath"); try { - string buildId = "1122334455"; - string normalizedBuildId = buildId.ToLowerInvariant(); + string buildId = "1122334455000000"; + string normalizedBuildId = CanonicalizeBuildId(buildId); // Create binary directory structure using just the simple filename. string binaryDir = Path.Combine(tempDir, "libc.so.6", "elf-buildid-" + normalizedBuildId); @@ -861,7 +861,7 @@ public void FindElfSymbolFilePath_CacheOnlySkipsRemotePaths() _symbolReader.SymbolPath = @"\\nonexistent-server\symbols"; _symbolReader.Options = SymbolReaderOptions.CacheOnly; - string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", "aabbccdd"); + string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", "aabbccdd00000000"); Assert.Null(result); } @@ -872,8 +872,8 @@ public void FindElfSymbolFilePath_CacheHitSkipsSearch() string tempDir = Path.Combine(OutputDir, "elf-cache-hit"); try { - string buildId = "cacced1d12"; - string normalizedBuildId = buildId.ToLowerInvariant(); + string buildId = "cacced1d12000000"; + string normalizedBuildId = CanonicalizeBuildId(buildId); string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); @@ -910,16 +910,17 @@ public void FindElfSymbolFilePath_NegativeCacheReturnsNull() _symbolReader.SymbolPath = tempDir; // First call: nothing found, null is cached. - string result1 = _symbolReader.FindElfSymbolFilePath("libnocache.so", "ffffffff"); + string buildId = "ffffffff00000000"; + string result1 = _symbolReader.FindElfSymbolFilePath("libnocache.so", buildId); Assert.Null(result1); // Now create the file — but the negative cache should still return null. - string normalizedBuildId = "ffffffff"; + string normalizedBuildId = CanonicalizeBuildId(buildId); 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"); + string result2 = _symbolReader.FindElfSymbolFilePath("libnocache.so", buildId); Assert.Null(result2); } finally @@ -935,10 +936,9 @@ 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; + string buildId1 = "aaaaaaaaaaaaaaaa"; + string buildId2 = "bbbbbbbbbbbbbbbb"; + string norm2 = CanonicalizeBuildId(buildId2); // Only create debug symbols for the second build ID. string debugDir2 = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + norm2); @@ -968,7 +968,7 @@ public void FindElfSymbolFilePath_DebugLinkDiscovery() string tempDir = Path.Combine(OutputDir, "elf-debuglink"); try { - string buildId = "aabb0011"; + string buildId = "aabb001100000000"; // Build an ELF binary with .gnu_debuglink pointing to "libtest.so.dbg". var binaryBuilder = new ElfBuilder() @@ -1014,7 +1014,7 @@ public void FindElfSymbolFilePath_DebugLinkInSubdir() string tempDir = Path.Combine(OutputDir, "elf-debuglink-subdir"); try { - string buildId = "ccdd0022"; + string buildId = "ccdd002200000000"; // Build an ELF binary with .gnu_debuglink pointing to "libfoo.debug". var binaryBuilder = new ElfBuilder() @@ -1054,6 +1054,113 @@ public void FindElfSymbolFilePath_DebugLinkInSubdir() } } + [Fact] + public void FindElfSymbolFilePath_NullBuildIdThrows() + { + ArgumentNullException exception = Assert.Throws( + () => _symbolReader.FindElfSymbolFilePath("libtest.so", null)); + + Assert.Equal("buildId", exception.ParamName); + Assert.Empty(_handler.Requests); + } + + [Theory] + [InlineData("")] + [InlineData("0123456789abcd")] + [InlineData("0123456789abcdef0")] + [InlineData("0123456789abcdef0123456789abcdef0123456789")] + [InlineData("0123456789abcdeg")] + [InlineData("..0123456789abcd")] + [InlineData("01234567/89abcde")] + [InlineData(@"01234567\89abcde")] + [InlineData("/0123456789abcdef")] + [InlineData(@"C:\0123456789abcdef")] + [InlineData(@"\\server\share\0123456789abcdef")] + public void FindElfSymbolFilePath_InvalidBuildIdThrowsWithoutCacheAccess(string buildId) + { + string cacheDir = Path.Combine(OutputDir, "elf-invalid-build-id-" + Guid.NewGuid().ToString("N")); + try + { + Directory.CreateDirectory(cacheDir); + _symbolReader.SymbolPath = $"SRV*{cacheDir}*https://symbols.example.test"; + + ArgumentException exception = Assert.Throws( + () => _symbolReader.FindElfSymbolFilePath("libtest.so", buildId)); + + Assert.Equal("buildId", exception.ParamName); + Assert.Empty(_handler.Requests); + Assert.Empty(Directory.GetFiles(cacheDir, "*", SearchOption.AllDirectories)); + } + finally + { + if (Directory.Exists(cacheDir)) + Directory.Delete(cacheDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_SymbolServerCachePathIsCanonicalAndContained() + { + string tempDir = Path.Combine(OutputDir, "elf-contained-cache"); + try + { + string cacheDir = Path.Combine(tempDir, "cache"); + string outsideDir = Path.Combine(tempDir, "cache-escape"); + Directory.CreateDirectory(cacheDir); + Directory.CreateDirectory(outsideDir); + + string buildId = "ABCDEF0123456789"; + string canonicalBuildId = CanonicalizeBuildId(buildId); + string indexPath = $"_.debug/elf-buildid-sym-{canonicalBuildId}/_.debug"; + var expectedUri = new Uri("https://symbols.example.test/" + indexPath); + _handler.AddIntercept(expectedUri, HttpMethod.Get, HttpStatusCode.OK, + () => new ByteArrayContent(CreateMinimalElfWithBuildId(buildId.ToLowerInvariant()))); + _symbolReader.SymbolPath = $"SRV*{cacheDir}*https://symbols.example.test"; + + string result = _symbolReader.FindElfSymbolFilePath("libtest.so", buildId); + + string expectedPath = Path.Combine(cacheDir, "_.debug", "elf-buildid-sym-" + canonicalBuildId, "_.debug"); + Assert.Equal(Path.GetFullPath(expectedPath), Path.GetFullPath(result), ignoreCase: true); + Assert.StartsWith( + Path.GetFullPath(cacheDir) + Path.DirectorySeparatorChar, + Path.GetFullPath(result), + StringComparison.OrdinalIgnoreCase); + Assert.Contains(expectedUri, _handler.Requests); + Assert.Empty(Directory.GetFiles(outsideDir, "*", SearchOption.AllDirectories)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void FindElfSymbolFilePath_TraversalFileNameCannotEscapeSymbolCache() + { + string tempDir = Path.Combine(OutputDir, "elf-file-name-containment"); + try + { + string cacheDir = Path.Combine(tempDir, "cache"); + string outsideDir = Path.Combine(tempDir, "outside"); + Directory.CreateDirectory(cacheDir); + Directory.CreateDirectory(outsideDir); + + _symbolReader.SymbolPath = $"SRV*{cacheDir}*https://symbols.example.test"; + _symbolReader.Options = SymbolReaderOptions.CacheOnly; + + Assert.Throws( + () => _symbolReader.FindElfSymbolFilePath("..", "0123456789abcdef")); + Assert.Empty(_handler.Requests); + Assert.Empty(Directory.GetFiles(outsideDir, "*", SearchOption.AllDirectories)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + #endregion #region FindR2RPerfMapSymbolFilePath Tests @@ -1377,8 +1484,8 @@ public void ElfCache_ClearedWhenSymbolPathChanges() // Set up: first path has nothing, second path has the file. Directory.CreateDirectory(tempDir1); - string buildId = "cace0e0010"; - string normalizedBuildId = buildId; + string buildId = "cace0e0010000000"; + string normalizedBuildId = CanonicalizeBuildId(buildId); string debugDir = Path.Combine(tempDir2, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), CreateMinimalElfWithBuildId(normalizedBuildId)); @@ -1432,8 +1539,8 @@ public void ElfCache_ClearedWhenOptionsChange() string tempDir = Path.Combine(OutputDir, "elf-cache-opt"); try { - string buildId = "00ee0010"; - string normalizedBuildId = buildId; + string buildId = "00ee001000000000"; + string normalizedBuildId = CanonicalizeBuildId(buildId); string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId); Directory.CreateDirectory(debugDir); File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), CreateMinimalElfWithBuildId(normalizedBuildId)); @@ -1531,11 +1638,16 @@ public void OpenElfSymbolFile_CacheClearedOnSymbolPathChange() #endregion + private static string CanonicalizeBuildId(string buildId) + { + return buildId.ToLowerInvariant().PadRight(40, '0'); + } + /// /// 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). + /// Lowercase hex string (e.g., "0123456789abcdef" represents 8 bytes). private static byte[] CreateMinimalElfWithBuildId(string buildIdHex) { // Convert hex string to bytes.