diff --git a/src/Microsoft.SymbolStore/KeyGenerators/FileKeyGenerator.cs b/src/Microsoft.SymbolStore/KeyGenerators/FileKeyGenerator.cs index ebab6cdfa1..52d1cb31a5 100644 --- a/src/Microsoft.SymbolStore/KeyGenerators/FileKeyGenerator.cs +++ b/src/Microsoft.SymbolStore/KeyGenerators/FileKeyGenerator.cs @@ -58,6 +58,7 @@ private IEnumerable GetGenerators() yield return new PDBFileKeyGenerator(Tracer, _file); yield return new PortablePDBFileKeyGenerator(Tracer, _file); yield return new PerfMapFileKeyGenerator(Tracer, _file); + yield return new WasmFileKeyGenerator(Tracer, _file); } } } diff --git a/src/Microsoft.SymbolStore/KeyGenerators/WasmFileKeyGenerator.cs b/src/Microsoft.SymbolStore/KeyGenerators/WasmFileKeyGenerator.cs new file mode 100644 index 0000000000..a90b58de52 --- /dev/null +++ b/src/Microsoft.SymbolStore/KeyGenerators/WasmFileKeyGenerator.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Microsoft.SymbolStore.KeyGenerators +{ + public class WasmFileKeyGenerator : KeyGenerator + { + /// + /// Wasm binary magic number: '\0asm' + /// + private static readonly byte[] s_wasmMagic = new byte[] { 0x00, 0x61, 0x73, 0x6D }; + + /// + /// Wasm binary format version 1 + /// + private static readonly byte[] s_wasmVersion = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + + /// + /// Custom section ID in Wasm binary format + /// + private const byte CustomSectionId = 0; + + /// + /// The name of the custom section containing the build ID + /// + private const string BuildIdSectionName = "build_id"; + + /// + /// Maximum reasonable build ID length (256 bytes). Protects against + /// malformed input causing large allocations. + /// + private const int MaxBuildIdLength = 256; + + private readonly SymbolStoreFile _file; + private byte[] _buildId; + private bool _parsed; + private bool _isValid; + + public WasmFileKeyGenerator(ITracer tracer, SymbolStoreFile file) + : base(tracer) + { + _file = file ?? throw new ArgumentNullException(nameof(file)); + } + + public override bool IsValid() + { + return HasIndexableWasmBuildId(); + } + + public bool HasIndexableWasmBuildId() + { + ParseWasmFile(); + return _isValid; + } + + public override IEnumerable GetKeys(KeyTypeFlags flags) + { + if (IsValid()) + { + if ((flags & KeyTypeFlags.IdentityKey) != 0) + { + yield return GetKey(_file.FileName, _buildId); + } + } + } + + /// + /// Create a symbol store key for a Wasm file with a build ID. + /// + /// file name and path + /// build ID bytes from the build_id custom section + /// symbol store key + public static SymbolStoreKey GetKey(string path, byte[] buildId) + { + Debug.Assert(path != null); + Debug.Assert(buildId != null && buildId.Length > 0); + string file = GetFileName(path).ToLowerInvariant(); + return BuildKey(path, prefix: null, buildId, file); + } + + /// + /// Parses the Wasm file to validate the header and find the buildId custom section. + /// + private void ParseWasmFile() + { + if (_parsed) + { + return; + } + _parsed = true; + _isValid = false; + + Stream stream = _file.Stream; + long prevPosition = stream.Position; + try + { + stream.Position = 0; + + // Validate magic number + byte[] magic = new byte[4]; + if (stream.Read(magic, 0, 4) != 4) + { + return; + } + for (int i = 0; i < 4; i++) + { + if (magic[i] != s_wasmMagic[i]) + { + return; + } + } + + // Validate version + byte[] version = new byte[4]; + if (stream.Read(version, 0, 4) != 4) + { + return; + } + for (int i = 0; i < 4; i++) + { + if (version[i] != s_wasmVersion[i]) + { + return; + } + } + + // Scan sections for the build_id custom section + while (stream.Position < stream.Length) + { + int sectionId = stream.ReadByte(); + if (sectionId == -1) + { + break; + } + + uint sectionSize = ReadLEB128Unsigned(stream); + long sectionEnd = stream.Position + sectionSize; + + // Validate that the section doesn't extend beyond the stream + if (sectionEnd > stream.Length) + { + break; + } + + if (sectionId == CustomSectionId) + { + string name = ReadWasmString(stream, sectionEnd); + if (name == BuildIdSectionName) + { + // The remainder of the section payload is the build ID + int buildIdLength = (int)(sectionEnd - stream.Position); + if (buildIdLength > 0 && buildIdLength <= MaxBuildIdLength) + { + _buildId = new byte[buildIdLength]; + if (stream.Read(_buildId, 0, buildIdLength) == buildIdLength) + { + _isValid = true; + return; + } + } + } + } + + stream.Position = sectionEnd; + } + } + catch (Exception ex) when (ex is IOException || ex is OverflowException || ex is ArgumentOutOfRangeException) + { + Tracer.Verbose("Error parsing Wasm file {0}: {1}", _file.FileName, ex.Message); + } + finally + { + stream.Position = prevPosition; + } + } + + /// + /// Reads an unsigned LEB128-encoded integer from the stream. + /// + private static uint ReadLEB128Unsigned(Stream stream) + { + uint result = 0; + int shift = 0; + + while (true) + { + int b = stream.ReadByte(); + if (b == -1) + { + throw new IOException("Unexpected end of stream reading LEB128 value."); + } + + result |= (uint)(b & 0x7F) << shift; + if ((b & 0x80) == 0) + { + break; + } + + shift += 7; + if (shift >= 35) + { + throw new OverflowException("LEB128 value too large for uint32."); + } + } + + return result; + } + + /// + /// Maximum section name length we'll read. Names longer than this are + /// skipped since they cannot match the sections we're looking for. + /// + private const int MaxSectionNameLength = 64; + + /// + /// Reads a Wasm string (LEB128 length prefix followed by UTF-8 bytes). + /// Returns null if the string is too long or extends past the section boundary. + /// + private static string ReadWasmString(Stream stream, long sectionEnd) + { + uint length = ReadLEB128Unsigned(stream); + if (length == 0) + { + return string.Empty; + } + if (length > MaxSectionNameLength || stream.Position + length > sectionEnd) + { + return null; + } + + int stringLength = (int)length; + byte[] bytes = new byte[stringLength]; + int bytesRead = stream.Read(bytes, 0, stringLength); + if (bytesRead != stringLength) + { + return null; + } + + return Encoding.UTF8.GetString(bytes); + } + } +} diff --git a/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs b/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs index 85509b5b96..428893488d 100644 --- a/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs +++ b/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs @@ -33,6 +33,7 @@ public void FileKeyGenerator() PEFileKeyGeneratorInternal(fileGenerator: true); PortablePDBFileKeyGeneratorInternal(fileGenerator: true); PerfMapFileKeyGeneratorInternal(fileGenerator: true); + WasmFileKeyGeneratorInternal(fileGenerator: true); } @@ -531,5 +532,77 @@ public void SourceFileKeyGenerator() Assert.True(clrKeys.Count() == 0); } } + [Fact] + public void WasmFileKeyGenerator() + { + WasmFileKeyGeneratorInternal(fileGenerator: false); + } + + private void WasmFileKeyGeneratorInternal(bool fileGenerator) + { + // Test 1: Plain Wasm module with build_id (not a symbol file) + const string WasmModulePath = "TestBinaries/test_module.wasm"; + using (Stream stream = File.OpenRead(WasmModulePath)) + { + var file = new SymbolStoreFile(stream, WasmModulePath); + KeyGenerator generator = fileGenerator ? (KeyGenerator)new FileKeyGenerator(_tracer, file) : new WasmFileKeyGenerator(_tracer, file); + + Assert.True(generator.IsValid()); + + IEnumerable identityKey = generator.GetKeys(KeyTypeFlags.IdentityKey); + Assert.True(identityKey.Count() == 1); + Assert.True(identityKey.First().Index == "test_module.wasm/deadbeef0123456789abcdeffedcba98/test_module.wasm"); + + IEnumerable symbolKey = generator.GetKeys(KeyTypeFlags.SymbolKey); + Assert.True(!symbolKey.Any()); + + IEnumerable clrKeys = generator.GetKeys(KeyTypeFlags.ClrKeys); + Assert.True(!clrKeys.Any()); + } + + // Test 2: Wasm symbol file with build_id and .debug_info section + const string WasmSymbolPath = "TestBinaries/test_module_symbols.wasm"; + using (Stream stream = File.OpenRead(WasmSymbolPath)) + { + var file = new SymbolStoreFile(stream, WasmSymbolPath); + KeyGenerator generator = fileGenerator ? (KeyGenerator)new FileKeyGenerator(_tracer, file) : new WasmFileKeyGenerator(_tracer, file); + + Assert.True(generator.IsValid()); + + IEnumerable identityKey = generator.GetKeys(KeyTypeFlags.IdentityKey); + Assert.True(identityKey.Count() == 1); + Assert.True(identityKey.First().Index == "test_module_symbols.wasm/deadbeef0123456789abcdeffedcba98/test_module_symbols.wasm"); + + IEnumerable symbolKey = generator.GetKeys(KeyTypeFlags.SymbolKey); + Assert.True(!symbolKey.Any()); + } + + // Test 3: Wasm file without build_id should be invalid + const string WasmNoBuildIdPath = "TestBinaries/test_module_no_buildid.wasm"; + using (Stream stream = File.OpenRead(WasmNoBuildIdPath)) + { + var file = new SymbolStoreFile(stream, WasmNoBuildIdPath); + var generator = new WasmFileKeyGenerator(_tracer, file); + + Assert.False(generator.IsValid()); + + IEnumerable identityKey = generator.GetKeys(KeyTypeFlags.IdentityKey); + Assert.True(!identityKey.Any()); + } + + // Test 4: Wasm file with a custom section name longer than 64 chars before build_id + const string WasmLongNamePath = "TestBinaries/test_module_long_section_name.wasm"; + using (Stream stream = File.OpenRead(WasmLongNamePath)) + { + var file = new SymbolStoreFile(stream, WasmLongNamePath); + var generator = new WasmFileKeyGenerator(_tracer, file); + + Assert.True(generator.IsValid()); + + IEnumerable identityKey = generator.GetKeys(KeyTypeFlags.IdentityKey); + Assert.True(identityKey.Count() == 1); + Assert.True(identityKey.First().Index == "test_module_long_section_name.wasm/deadbeef0123456789abcdeffedcba98/test_module_long_section_name.wasm"); + } + } } } diff --git a/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module.wasm b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module.wasm new file mode 100644 index 0000000000..d6726a9784 Binary files /dev/null and b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module.wasm differ diff --git a/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_long_section_name.wasm b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_long_section_name.wasm new file mode 100644 index 0000000000..3ccb4188e4 Binary files /dev/null and b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_long_section_name.wasm differ diff --git a/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_no_buildid.wasm b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_no_buildid.wasm new file mode 100644 index 0000000000..02402cb034 Binary files /dev/null and b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_no_buildid.wasm differ diff --git a/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_symbols.wasm b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_symbols.wasm new file mode 100644 index 0000000000..cf5e2aef9c Binary files /dev/null and b/src/tests/Microsoft.SymbolStore.UnitTests/TestBinaries/test_module_symbols.wasm differ