Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/PerfView.Tests/Memory/PathUtilitiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,30 @@ public void SanitizeFileName_PreservesValidNames(string input, string expected)
{
Assert.Equal(expected, PathUtilities.SanitizeFileName(input));
}

[Theory]
[InlineData("foo.debug")]
[InlineData("My Provider")]
[InlineData("provider.with.dots")]
public void IsSafeFileName_AcceptsNamesThatNeedNoSanitization(string input)
{
Assert.True(PathUtilities.IsSafeFileName(input));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(".")]
[InlineData("..")]
[InlineData("../outside")]
[InlineData(@"..\outside")]
[InlineData("with:stream")]
[InlineData("with?wildcard")]
[InlineData("Trailing.")]
[InlineData("NUL.debug")]
public void IsSafeFileName_RejectsNamesThatWouldBeSanitized(string input)
{
Assert.False(PathUtilities.IsSafeFileName(input));
}
}
}

84 changes: 79 additions & 5 deletions src/TraceEvent/Symbols/ElfSymbolModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Diagnostics.Utilities;

namespace Microsoft.Diagnostics.Symbols
{
Expand Down Expand Up @@ -251,6 +252,12 @@ internal static string ReadDebugLink(string filePath)

// Read all section headers in one bulk read.
int shTableSize = hdr.ShCount * hdr.ShEntrySize;
if (!IsValidFileRange(stream, hdr.ShOffset, (ulong)shTableSize))
{
Debug.WriteLine("ReadDebugLink: Section headers are outside the file.");
return null;
}

byte[] shTable = new byte[shTableSize];
stream.Seek((long)hdr.ShOffset, SeekOrigin.Begin);
if (ReadFully(stream, shTable, 0, shTableSize) < shTableSize)
Expand All @@ -269,6 +276,12 @@ internal static string ReadDebugLink(string filePath)
return null;
}

if (!IsValidFileRange(stream, shstrOffset, shstrSize))
{
Debug.WriteLine("ReadDebugLink: shstrtab is outside the file.");
return null;
}

byte[] shstrtab = new byte[(int)shstrSize];
stream.Seek((long)shstrOffset, SeekOrigin.Begin);
if (ReadFully(stream, shstrtab, 0, shstrtab.Length) < shstrtab.Length)
Expand Down Expand Up @@ -303,6 +316,12 @@ internal static string ReadDebugLink(string filePath)
return null;
}

if (!IsValidFileRange(stream, secOffset, secSize))
{
Debug.WriteLine("ReadDebugLink: .gnu_debuglink section is outside the file.");
return null;
}

byte[] sectionData = new byte[(int)secSize];
stream.Seek((long)secOffset, SeekOrigin.Begin);
if (ReadFully(stream, sectionData, 0, sectionData.Length) < sectionData.Length)
Expand All @@ -311,15 +330,13 @@ internal static string ReadDebugLink(string filePath)
return null;
}

// Extract the null-terminated filename.
int nullPos = Array.IndexOf(sectionData, (byte)0);
if (nullPos <= 0)
if (!TryParseDebugLinkSection(sectionData, out string debugLink))
{
Debug.WriteLine("ReadDebugLink: Empty or missing filename in .gnu_debuglink.");
Debug.WriteLine("ReadDebugLink: Invalid .gnu_debuglink section data.");
return null;
}

return Encoding.UTF8.GetString(sectionData, 0, nullPos);
return debugLink;
}

Debug.WriteLine("ReadDebugLink: No .gnu_debuglink section found.");
Expand All @@ -337,6 +354,62 @@ 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");
private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);

private static bool IsValidFileRange(Stream stream, ulong offset, ulong size)
{
ulong streamLength = (ulong)stream.Length;
return offset <= streamLength && size <= streamLength - offset;
}

private static bool TryParseDebugLinkSection(byte[] sectionData, out string debugLink)
{
debugLink = null;

// Reserve the final four bytes for the CRC so zero bytes in the CRC cannot be
// mistaken for the filename's required null terminator.
int crcOffset = sectionData.Length - DebugLinkCrcSize;
int nullPos = Array.IndexOf(sectionData, (byte)0, 0, crcOffset);
if (nullPos <= 0)
{
return false;
}

// The CRC starts at the next 4-byte boundary after the null-terminated filename.
int alignedCrcOffset = (nullPos + 1 + 3) & ~3;
if (alignedCrcOffset != crcOffset)
{
return false;
}

// GNU requires any bytes between the filename terminator and aligned CRC to be zero.
for (int i = nullPos + 1; i < crcOffset; i++)
{
if (sectionData[i] != 0)
{
return false;
}
}

// Decode without replacement characters so malformed section bytes fail explicitly.
try
{
debugLink = StrictUtf8.GetString(sectionData, 0, nullPos);
}
catch (DecoderFallbackException)
{
return false;
}

// The section value is a filename, not a path. Reject it before any path construction.
if (!PathUtilities.IsSafeFileName(debugLink))
{
debugLink = null;
return false;
}

return true;
}

/// <summary>
/// Common fields extracted from an ELF header (Ehdr) after validation.
Expand Down Expand Up @@ -566,6 +639,7 @@ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected
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;
private const int DebugLinkCrcSize = sizeof(uint);

// Symbol table constants.
private const byte STT_FUNC = 2; // Symbol type: function.
Expand Down
86 changes: 70 additions & 16 deletions src/TraceEvent/Symbols/SymbolReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,29 +452,41 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF
string binaryIndexPath = $"{simpleFileName}/elf-buildid-{normalizedBuildId}/{simpleFileName}";

string resultPath = null;
string localElfFilePath = null;
if (elfFilePath != null)
{
if (!PathUtilities.TryGetSafeLocalFilePath(elfFilePath, out localElfFilePath))
{
m_log.WriteLine(
"FindElfSymbolFilePath: Ignoring unsafe ELF file path {0}.",
elfFilePath);
}
}

// 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)
if (localElfFilePath != null)
{
string elfDir = Path.GetDirectoryName(elfFilePath);
string elfDir = Path.GetDirectoryName(localElfFilePath);
if (!string.IsNullOrEmpty(elfDir))
{
m_log.WriteLine("FindElfSymbolFilePath: Checking relative to ELF binary path {0}", elfFilePath);
string basePath = elfFilePath;
m_log.WriteLine("FindElfSymbolFilePath: Checking relative to ELF binary path {0}", localElfFilePath);
string basePath = localElfFilePath;
string executableFileName = Path.GetFileName(basePath);

// Try {path}.debug
string candidate = basePath + ".debug";
if (ElfBuildIdMatches(candidate, normalizedBuildId))
string candidate;
if (TryGetDebugLinkCandidate(elfDir, executableFileName + ".debug", out candidate) &&
ElfBuildIdMatches(candidate, normalizedBuildId))
{
resultPath = candidate;
}

// Try {path}.dbg
if (resultPath == null)
{
candidate = basePath + ".dbg";
if (ElfBuildIdMatches(candidate, normalizedBuildId))
if (TryGetDebugLinkCandidate(elfDir, executableFileName + ".dbg", out candidate) &&
ElfBuildIdMatches(candidate, normalizedBuildId))
{
resultPath = candidate;
}
Expand All @@ -497,17 +509,18 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF
if (debugLink != null)
{
// Try {bindir}/{debuglink}
candidate = Path.Combine(elfDir, debugLink);
if (ElfBuildIdMatches(candidate, normalizedBuildId))
if (TryGetDebugLinkCandidate(elfDir, debugLink, out candidate) &&
ElfBuildIdMatches(candidate, normalizedBuildId))
{
resultPath = candidate;
}

// Try {bindir}/.debug/{debuglink}
if (resultPath == null)
{
candidate = Path.Combine(elfDir, ".debug", debugLink);
if (ElfBuildIdMatches(candidate, normalizedBuildId))
string debugDirectory = Path.Combine(elfDir, ".debug");
if (TryGetDebugLinkCandidate(debugDirectory, debugLink, out candidate) &&
ElfBuildIdMatches(candidate, normalizedBuildId))
{
resultPath = candidate;
}
Expand Down Expand Up @@ -579,11 +592,11 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF
// 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 (resultPath == null && localElfFilePath != null)
{
if (ElfBuildIdMatches(elfFilePath, normalizedBuildId))
if (ElfBuildIdMatches(localElfFilePath, normalizedBuildId))
{
resultPath = elfFilePath;
resultPath = localElfFilePath;
}
}

Expand All @@ -606,6 +619,48 @@ public string FindElfSymbolFilePath(string fileName, string buildId, string elfF
return resultPath;
}

/// <summary>
/// Constructs a debug-link candidate that is a direct child of the intended search directory.
/// </summary>
internal static bool TryGetDebugLinkCandidate(string searchDirectory, string debugLink, out string candidate)
{
candidate = null;
if (string.IsNullOrEmpty(searchDirectory) ||
!PathUtilities.IsSafeFileName(debugLink))
{
return false;
}

try
{
string canonicalSearchDirectory = Path.GetFullPath(searchDirectory);
string canonicalCandidate = Path.GetFullPath(Path.Combine(canonicalSearchDirectory, debugLink));
if (!PathUtilities.IsPathWithinDirectory(canonicalCandidate, canonicalSearchDirectory))
{
return false;
}

candidate = canonicalCandidate;
return true;
}
catch (ArgumentException)
{
return false;
}
catch (NotSupportedException)
{
return false;
}
catch (PathTooLongException)
{
return false;
}
catch (System.Security.SecurityException)
{
return false;
}
}

// Find an executable file path (not a PDB) based on information about the file image.
/// <summary>
/// This API looks up an executable file, by its build-timestamp and size (on a symbol server), 'fileName' should be
Expand Down Expand Up @@ -2850,4 +2905,3 @@ private enum LineEnding
#endregion
}
}

31 changes: 28 additions & 3 deletions src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ internal class ElfBuilder
private ulong m_pOffset = 0;
private byte[] m_buildId = null;
private string m_debugLink = null;
private byte[] m_debugLinkSectionData = null;
private ulong? m_debugLinkSectionOffset = null;
private ulong? m_debugLinkSectionSize = null;
private readonly List<SymbolDef> m_symtabSymbols = new List<SymbolDef>();
private readonly List<SymbolDef> m_dynsymSymbols = new List<SymbolDef>();

Expand Down Expand Up @@ -83,6 +86,27 @@ public ElfBuilder SetBuildId(byte[] buildId)
public ElfBuilder SetDebugLink(string filename)
{
m_debugLink = filename;
m_debugLinkSectionData = null;
return this;
}

/// <summary>
/// Sets the raw .gnu_debuglink section data for malformed-section tests.
/// </summary>
public ElfBuilder SetDebugLinkSectionData(byte[] sectionData)
{
m_debugLink = null;
m_debugLinkSectionData = sectionData;
return this;
}

/// <summary>
/// Overrides the .gnu_debuglink section header bounds for malformed-section tests.
/// </summary>
public ElfBuilder SetDebugLinkSectionBounds(ulong offset, ulong size)
{
m_debugLinkSectionOffset = offset;
m_debugLinkSectionSize = size;
return this;
}

Expand Down Expand Up @@ -150,7 +174,7 @@ public byte[] Build()
// [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;
bool hasDebugLink = m_debugLink != null || m_debugLinkSectionData != null;
int sectionCount = hasDynsym ? 5 : 3;
if (hasDebugLink)
{
Expand Down Expand Up @@ -193,7 +217,7 @@ public byte[] Build()
int shstrtabSectionIndex = 0;
if (hasDebugLink)
{
debugLinkData = BuildDebugLinkSection(m_debugLink);
debugLinkData = m_debugLinkSectionData ?? BuildDebugLinkSection(m_debugLink);
debugLinkSectionIndex = hasDynsym ? 5 : 3;
shstrtabSectionIndex = debugLinkSectionIndex + 1;

Expand Down Expand Up @@ -310,7 +334,8 @@ public byte[] Build()
{
// .gnu_debuglink (SHT_PROGBITS)
WriteSectionHeader(writer, (uint)debugLinkShName, SHT_PROGBITS,
(ulong)debugLinkOffset, (ulong)debugLinkData.Length, 0, 0);
m_debugLinkSectionOffset ?? (ulong)debugLinkOffset,
m_debugLinkSectionSize ?? (ulong)debugLinkData.Length, 0, 0);

// .shstrtab (SHT_STRTAB)
WriteSectionHeader(writer, (uint)shstrtabShName, SHT_STRTAB,
Expand Down
Loading
Loading