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
24 changes: 19 additions & 5 deletions documentation/symbols/SSQP_Key_Conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,26 @@ Example:

### WASM (WebAssembly) Modules

WebAssembly symbols, which can be used by browser developer tools to provide source-level debugging experiences, are based on the DWARF format. These are indexed by their DWARF Build ID
(built with `-Wl,--build-id` arguments) and the name of the module being debugged via the
[buildId property](https://chromedevtools.github.io/devtools-protocol/tot/Debugger/#event-scriptParsed), and the symbol file itself is suffixed with `.s` to disambiguate from the WASM file.
WebAssembly symbols, which can be used by browser developer tools to provide source-level debugging experiences, are based on the DWARF format. These are indexed by the Build ID stored in the `build_id` custom section of the Wasm binary. The Build ID is a byte sequence typically produced by the linker (e.g., with `-Wl,--build-id` arguments). Both the module and its corresponding symbol file contain the same `build_id` section, allowing them to be matched.

The key uses the actual filename of the file being indexed. For split symbol files, toolchains such as Emscripten produce a separate file (e.g., `foo.debug.wasm`) that contains the DWARF debug sections (`.debug_info`, `.debug_line`, etc.) stripped from the original module.

The final key is formatted as follows:

`<file_name>/<build_id_byte_sequence>/<file_name>`

Example (module):

**File name:** `main.wasm`

**Build ID of file:** `e3b0c44298fc1c149afbf4c8996fb92427ae41e4`
**Build ID bytes:** `0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4`

**Lookup key:** `main.wasm/e3b0c44298fc1c149afbf4c8996fb92427ae41e4/main.wasm`

Example (split symbol file):

**File name:** `main.debug.wasm`

**Build ID bytes:** `(same as module)`

**Lookup key:**: `main.wasm.s/e3b0c44298fc1c149afbf4c8996fb92427ae41e4/main.wasm.s`
**Lookup key:** `main.debug.wasm/e3b0c44298fc1c149afbf4c8996fb92427ae41e4/main.debug.wasm`
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ private IEnumerable<KeyGenerator> 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);
}
}
}
Expand Down
248 changes: 248 additions & 0 deletions src/Microsoft.SymbolStore/KeyGenerators/WasmFileKeyGenerator.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Wasm binary magic number: '\0asm'
/// </summary>
private static readonly byte[] s_wasmMagic = new byte[] { 0x00, 0x61, 0x73, 0x6D };

/// <summary>
/// Wasm binary format version 1
/// </summary>
private static readonly byte[] s_wasmVersion = new byte[] { 0x01, 0x00, 0x00, 0x00 };

/// <summary>
/// Custom section ID in Wasm binary format
/// </summary>
private const byte CustomSectionId = 0;

/// <summary>
/// The name of the custom section containing the build ID
/// </summary>
private const string BuildIdSectionName = "build_id";

/// <summary>
/// Maximum reasonable build ID length (256 bytes). Protects against
/// malformed input causing large allocations.
/// </summary>
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()
Comment thread
paolosevMSFT marked this conversation as resolved.
{
return HasIndexableWasmBuildId();
}

public bool HasIndexableWasmBuildId()
{
ParseWasmFile();
return _isValid;
}

public override IEnumerable<SymbolStoreKey> GetKeys(KeyTypeFlags flags)
{
if (IsValid())
{
if ((flags & KeyTypeFlags.IdentityKey) != 0)
{
yield return GetKey(_file.FileName, _buildId);
}
}
}

/// <summary>
/// Create a symbol store key for a Wasm file with a build ID.
/// </summary>
/// <param name="path">file name and path</param>
/// <param name="buildId">build ID bytes from the build_id custom section</param>
/// <returns>symbol store key</returns>
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);
}

/// <summary>
/// Parses the Wasm file to validate the header and find the buildId custom section.
/// </summary>
private void ParseWasmFile()
{
if (_parsed)
{
return;
}
_parsed = true;
Comment thread
noahfalk marked this conversation as resolved.
_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);
Comment thread
paolosevMSFT marked this conversation as resolved.
if (buildIdLength > 0 && buildIdLength <= MaxBuildIdLength)
{
_buildId = new byte[buildIdLength];
Comment thread
paolosevMSFT marked this conversation as resolved.
if (stream.Read(_buildId, 0, buildIdLength) == buildIdLength)
{
_isValid = true;
return;
}
}
}
}

stream.Position = sectionEnd;
Comment thread
paolosevMSFT marked this conversation as resolved.
}
}
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;
}
}

/// <summary>
/// Reads an unsigned LEB128-encoded integer from the stream.
/// </summary>
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;
}

/// <summary>
/// Maximum section name length we'll read. Names longer than this are
/// skipped since they cannot match the sections we're looking for.
/// </summary>
private const int MaxSectionNameLength = 64;

/// <summary>
/// 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.
/// </summary>
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);
}
}
}
73 changes: 73 additions & 0 deletions src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public void FileKeyGenerator()
PEFileKeyGeneratorInternal(fileGenerator: true);
PortablePDBFileKeyGeneratorInternal(fileGenerator: true);
PerfMapFileKeyGeneratorInternal(fileGenerator: true);
WasmFileKeyGeneratorInternal(fileGenerator: true);
}


Expand Down Expand Up @@ -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<SymbolStoreKey> identityKey = generator.GetKeys(KeyTypeFlags.IdentityKey);
Assert.True(identityKey.Count() == 1);
Assert.True(identityKey.First().Index == "test_module.wasm/deadbeef0123456789abcdeffedcba98/test_module.wasm");

IEnumerable<SymbolStoreKey> symbolKey = generator.GetKeys(KeyTypeFlags.SymbolKey);
Assert.True(!symbolKey.Any());

IEnumerable<SymbolStoreKey> 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<SymbolStoreKey> 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<SymbolStoreKey> 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<SymbolStoreKey> 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<SymbolStoreKey> 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");
}
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading