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.