diff --git a/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs b/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs index fd6c477813..9bc5a0e028 100644 --- a/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs +++ b/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs @@ -42,6 +42,12 @@ internal static MemoryStream Extract(this ZipArchiveEntry zipEntry) inputStream.Position = 0; return null; } +#if !NET11_0_OR_GREATER + if (magic == ZstandardMagic) + { + throw new NotSupportedException($"Assembly {assemblyName} is Zstandard compressed, which requires .NET 11 or later"); + } +#endif reader.ReadUInt32(); // ignore descriptor index, we don't need it var decompressedLength = reader.ReadInt32(); Debug.Assert(inputStream.Position == payloadOffset); @@ -58,20 +64,14 @@ internal static MemoryStream Extract(this ZipArchiveEntry zipEntry) var inputBuffer = inputStream is MemorySlice slice ? slice.FullBuffer : inputStream.GetBuffer(); var offset = inputStream is MemorySlice memorySlice ? memorySlice.Offset + payloadOffset : payloadOffset; - int decoded; - if (magic == Lz4Magic) - { - decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength); - } - else - { #if NET11_0_OR_GREATER - decoded = ZstandardDecoder.TryDecompress(inputBuffer.AsSpan(offset, inputLength), + var decoded = magic == Lz4Magic + ? LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength) + : ZstandardDecoder.TryDecompress(inputBuffer.AsSpan(offset, inputLength), outputBuffer.AsSpan(0, decompressedLength), out var bytesWritten) ? bytesWritten : -1; #else - throw new NotSupportedException($"Assembly {assemblyName} is Zstandard compressed, which requires .NET 11 or later"); + var decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength); #endif - } if (decoded != decompressedLength) { throw new Exception($"Failed to decompress {format} data of assembly {assemblyName} - decoded {decoded} instead of expected {decompressedLength} bytes"); diff --git a/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt index e782059c7d..18700c9250 100644 --- a/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt +++ b/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt @@ -1,6 +1,16 @@ Parts of the code in this subdirectory have been adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/assembly-store-reader.csproj +and subsequently updated from: +- https://github.com/dotnet/android/tree/64018e13e53cec7246e54866b520d3284de344e0/tools/assembly-store-reader-mk2 + (assembly store v3, .NET 10) +- https://github.com/dotnet/android/tree/f1aecf9e6ae80fe3f3992ec1f52ef953dac7c06b/.github/skills/read-assembly-store + (assembly store v4 and index entry sizing, .NET 11 previews) +- https://github.com/dotnet/android/commit/8f7c4d4fa53c6682f2c4f2d2caf08e9fb4d8cd60 + (v4 reverted to v3 before .NET 11 GA) + +Individual files note which of these they were updated from. + The original license is as follows: The MIT License (MIT) diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReader.Classes.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReader.Classes.cs index 36394c5cae..472bf8f324 100644 --- a/src/Sentry.Android.AssemblyReader/V2/StoreReader.Classes.cs +++ b/src/Sentry.Android.AssemblyReader/V2/StoreReader.Classes.cs @@ -3,7 +3,9 @@ * Updated from https://github.com/dotnet/android/blob/64018e13e53cec7246e54866b520d3284de344e0/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs * - Adding support for AssemblyStore v3 format that shipped in .NET 10 (https://github.com/dotnet/android/pull/10249) * Updated from https://github.com/dotnet/android/blob/f1aecf9e6ae80fe3f3992ec1f52ef953dac7c06b/.github/skills/read-assembly-store/src/AssemblyStore/StoreReader_V2.Classes.cs - * - Adding support for AssemblyStore v4 format (CoreCLR) that ships in .NET 11 + * - Adding support for AssemblyStore v4 format (CoreCLR), which only ever shipped in .NET 11 previews + * Reviewed against https://github.com/dotnet/android/commit/8f7c4d4fa53c6682f2c4f2d2caf08e9fb4d8cd60 + * - v4 was reverted before .NET 11 GA (dotnet/android#12780); CoreCLR emits v3 again * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) */ @@ -38,6 +40,9 @@ public Header(uint magic, uint version, uint entry_count, uint index_entry_count internal sealed class IndexEntry { + public const uint NativeSize32 = 2 * sizeof(uint) + sizeof(byte); + public const uint NativeSize64 = sizeof(ulong) + sizeof(uint) + sizeof(byte); + public readonly ulong name_hash; public readonly uint descriptor_index; public readonly bool ignore; diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReader.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReader.cs index 7e56ddb0d3..f5464affdd 100644 --- a/src/Sentry.Android.AssemblyReader/V2/StoreReader.cs +++ b/src/Sentry.Android.AssemblyReader/V2/StoreReader.cs @@ -3,7 +3,10 @@ * Updated from https://github.com/dotnet/android/blob/64018e13e53cec7246e54866b520d3284de344e0/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs * - Adding support for AssemblyStore v3 format that shipped in .NET 10 (https://github.com/dotnet/android/pull/10249) * Updated from https://github.com/dotnet/android/blob/f1aecf9e6ae80fe3f3992ec1f52ef953dac7c06b/.github/skills/read-assembly-store/src/AssemblyStore/StoreReader_V2.cs - * - Adding support for AssemblyStore v4 format (CoreCLR) that ships in .NET 11 + * - Adding support for AssemblyStore v4 format (CoreCLR), which only ever shipped in .NET 11 previews + * - Deriving the index entry size from the header rather than the ABI + * Reviewed against https://github.com/dotnet/android/commit/8f7c4d4fa53c6682f2c4f2d2caf08e9fb4d8cd60 + * - v4 was reverted before .NET 11 GA (dotnet/android#12780); CoreCLR emits v3 again * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) */ @@ -14,10 +17,11 @@ internal partial class StoreReader : AssemblyStoreReader // Bit 31 is set for 64-bit platforms, cleared for the 32-bit ones private const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT_V3 = 0x80000003; private const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT_V3 = 0x00000003; - private const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT_V4 = 0x80000004; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant + // v4 was only emitted by .NET 11 previews; it was reverted to v3 before GA by dotnet/android#12780 + private const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_64BIT_V4 = 0x80000004; private const uint ASSEMBLY_STORE_FORMAT_VERSION_CORECLR_32BIT_V4 = 0x00000004; private const uint ASSEMBLY_STORE_FORMAT_VERSION_MASK = 0xF0000000; - private const uint ASSEMBLY_STORE_FORMAT_NUMBER_MASK = 0x0000FFFF; + internal const uint ASSEMBLY_STORE_FORMAT_NUMBER_MASK = 0x0000FFFF; private const uint ASSEMBLY_STORE_ABI_AARCH64 = 0x00010000; private const uint ASSEMBLY_STORE_ABI_ARM = 0x00020000; private const uint ASSEMBLY_STORE_ABI_X64 = 0x00030000; @@ -192,25 +196,18 @@ protected override void Prepare() StoreStream.Seek((long)elfOffset + header.NativeSize, SeekOrigin.Begin); using var reader = CreateReader(); + var indexEntrySize = GetIndexEntrySize(header); var index = new List(); for (uint i = 0; i < header.index_entry_count; i++) { - ulong name_hash; - if (Is64Bit) + var name_hash = indexEntrySize switch { - name_hash = reader.ReadUInt64(); - } - else - { - name_hash = (ulong)reader.ReadUInt32(); - } - + IndexEntry.NativeSize64 => reader.ReadUInt64(), + IndexEntry.NativeSize32 => reader.ReadUInt32(), + _ => throw new InvalidOperationException($"Assembly store '{StorePath}' index entry size {indexEntrySize} is not supported.") + }; uint descriptor_index = reader.ReadUInt32(); -#if NET10_0_OR_GREATER bool ignore = reader.ReadByte() != 0; -#else - bool ignore = false; -#endif index.Add(new IndexEntry(name_hash, descriptor_index, ignore)); } @@ -272,4 +269,20 @@ protected override void Prepare() Assemblies = storeItems.AsReadOnly(); } } + + // Name hash width depends on the runtime, not the ABI: CoreCLR stores use 32-bit CRC32 hashes on every ABI + private uint GetIndexEntrySize(Header header) + { + if (header.index_entry_count == 0) + { + return 0; + } + + if (header.index_size % header.index_entry_count != 0) + { + throw new InvalidOperationException($"Assembly store '{StorePath}' index is corrupted: index size {header.index_size} is not evenly divisible by entry count {header.index_entry_count}."); + } + + return header.index_size / header.index_entry_count; + } } diff --git a/src/Sentry/Internal/DebugStackTrace.cs b/src/Sentry/Internal/DebugStackTrace.cs index b4f358a463..a94b8e3e89 100644 --- a/src/Sentry/Internal/DebugStackTrace.cs +++ b/src/Sentry/Internal/DebugStackTrace.cs @@ -518,19 +518,21 @@ private static void DemangleLambdaReturnType(SentryStackFrame frame) { try { - assemblyName = module.FullyQualifiedName; - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (assemblyName is null or ModuleExtensions.UnknownLocation) + var location = module.FullyQualifiedName is { } name and not ModuleExtensions.UnknownLocation ? name : null; + if (options.AssemblyReader is { } reader) + { + // CoreCLR on Android loads assemblies from the APK, so they have no location + assemblyName = location ?? module.ScopeName; + return reader.Invoke(assemblyName); + } + if (location is null) { // When publishing as a single file or compiling AOT FullyQualifiedName will be null. This logic // compensates for the UnconditionalSuppressMessage attribute applied to this method. assemblyName = null; return null; } - if (options.AssemblyReader is { } reader) - { - return reader.Invoke(assemblyName); - } + assemblyName = location; if (options.FileSystem.FileExists(assemblyName)) { @@ -538,9 +540,9 @@ private static void DemangleLambdaReturnType(SentryStackFrame frame) return new PEReader(assembly); } } - catch + catch (Exception e) { - // Swallow and return null below + options.LogDebug("Failed to read assembly for module '{0}': {1}", module.GetNameOrScopeName(), e.Message); } assemblyName = null; return null; diff --git a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs index 9a48637529..65cdbe7ab5 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs +++ b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs @@ -15,18 +15,6 @@ public class AndroidAssemblyReaderTests #error "Target Framework not yet supported for AndroidAssemblyReader" #endif - // .NET 11 Android moved to CoreCLR and emits v4 assembly stores, which our vendored - // reader does not understand yet - it also changes ELF payload discovery, so even the - // non-store APKs fail. Tracked by https://github.com/getsentry/sentry-dotnet/issues/5454; - // re-enable these once that port lands. - private const string StoreV4SkipReason = - "Android assembly store v4 (.NET 11) is not supported yet - see getsentry/sentry-dotnet#5454"; -#if NET11_0_OR_GREATER - private const bool StoreV4Unsupported = true; -#else - private const bool StoreV4Unsupported = false; -#endif - public AndroidAssemblyReaderTests(ITestOutputHelper output) { _output = output; @@ -57,7 +45,6 @@ private IAndroidAssemblyReader GetSut(bool isAot, bool isAssemblyStore, bool isC [SkippableFact] public void CreatesCorrectStoreReader() { - Skip.If(StoreV4Unsupported, StoreV4SkipReason); #if ANDROID Skip.If(true, "It's unknown whether the current Android app APK is an assembly store or not."); #endif @@ -78,7 +65,6 @@ public void CreatesCorrectStoreReader() [SkippableFact] public void CreatesCorrectArchiveReader() { - Skip.If(StoreV4Unsupported, StoreV4SkipReason); #if ANDROID Skip.If(true, "It's unknown whether the current Android app APK is an assembly store or not."); #endif @@ -86,7 +72,8 @@ public void CreatesCorrectArchiveReader() switch (TargetFramework) { case "net11.0": - Assert.IsType(sut); + // CoreCLR ignores AndroidUseAssemblyStore=false: https://github.com/dotnet/android/pull/12033 + Assert.IsType(sut); break; case "net10.0": Assert.IsType(sut); @@ -101,7 +88,6 @@ public void CreatesCorrectArchiveReader() [InlineData(true)] public void ReturnsNullIfAssemblyDoesntExist(bool isAssemblyStore) { - Skip.If(StoreV4Unsupported, StoreV4SkipReason); using var sut = GetSut(isAot: false, isAssemblyStore, isCompressed: true); Assert.Null(sut.TryReadAssembly("NonExistent.dll")); } @@ -117,7 +103,6 @@ public void ReturnsNullIfAssemblyDoesntExist(bool isAssemblyStore) [MemberData(nameof(ReadsAssemblyPermutations))] public void ReadsAssembly(bool isAot, bool isAssemblyStore, bool isCompressed, string assemblyName) { - Skip.If(StoreV4Unsupported, StoreV4SkipReason); #if ANDROID // No need to run all combinations - we only test the current APK which is likely JIT compressed assembly store. Skip.If(isAot); diff --git a/test/Sentry.Android.AssemblyReader.Tests/ArchiveUtilsTests.cs b/test/Sentry.Android.AssemblyReader.Tests/ArchiveUtilsTests.cs index b7a21578ee..b22c8adf24 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/ArchiveUtilsTests.cs +++ b/test/Sentry.Android.AssemblyReader.Tests/ArchiveUtilsTests.cs @@ -82,8 +82,9 @@ private static MemoryStream WithHeader(uint magic, ReadOnlySpan payload) var stream = new MemoryStream(); using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) { + const uint descriptorIndex = 0; writer.Write(magic); - writer.Write(0u); // descriptor index + writer.Write(descriptorIndex); writer.Write(Assembly.Length); writer.Write(payload); } diff --git a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj index 169fd703f1..164efe10b0 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj +++ b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj @@ -43,8 +43,7 @@ <_TestAPK Include="3" Properties="_Aot=False;_Store=True;_Compressed=False" /> <_TestAPK Include="4" Properties="_Aot=False;_Store=True;_Compressed=True" /> + apps from .NET 11 on. --> <_TestAPK Include="5" Properties="_Aot=True;_Store=False;_Compressed=False" Condition="'$(TargetFramework)' == 'net10.0-android'" /> <_TestAPK Include="6" Properties="_Aot=True;_Store=False;_Compressed=True" Condition="'$(TargetFramework)' == 'net10.0-android'" /> <_TestAPK Include="7" Properties="_Aot=True;_Store=True;_Compressed=False" Condition="'$(TargetFramework)' == 'net10.0-android'" /> diff --git a/test/Sentry.Android.AssemblyReader.Tests/StoreReaderTests.cs b/test/Sentry.Android.AssemblyReader.Tests/StoreReaderTests.cs index d0f8bcd569..159cae2661 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/StoreReaderTests.cs +++ b/test/Sentry.Android.AssemblyReader.Tests/StoreReaderTests.cs @@ -5,14 +5,16 @@ namespace Sentry.Android.AssemblyReader.Tests; public class StoreReaderTests { [Theory] - [InlineData(0x80000003u | 0x00010000u, true)] // v3, 64-bit, arm64 - [InlineData(0x00000003u | 0x00020000u, false)] // v3, 32-bit, arm - [InlineData(0x80000004u | 0x00030000u, true)] // v4, 64-bit, x86_64 - [InlineData(0x00000004u | 0x00040000u, false)] // v4, 32-bit, x86 - public void Create_SupportedVersion_ReadsStore(uint version, bool is64Bit) + [InlineData(0x80000003u | 0x00010000u, true, sizeof(ulong))] // v3, 64-bit, arm64 (MonoVM) + [InlineData(0x80000003u | 0x00010000u, true, sizeof(uint))] // v3, 64-bit, arm64 (CoreCLR) + [InlineData(0x00000003u | 0x00020000u, false, sizeof(uint))] // v3, 32-bit, arm + [InlineData(0x80000004u | 0x00030000u, true, sizeof(ulong))] // v4, 64-bit, x86_64 (MonoVM, .NET 11 previews) + [InlineData(0x80000004u | 0x00030000u, true, sizeof(uint))] // v4, 64-bit, x86_64 (CoreCLR, .NET 11 previews) + [InlineData(0x00000004u | 0x00040000u, false, sizeof(uint))] // v4, 32-bit, x86 (.NET 11 previews) + public void Create_SupportedVersion_ReadsStore(uint version, bool is64Bit, int nameHashSize) { // Arrange - using var stream = CreateStore(version, is64Bit, "testAssembly.dll"); + using var stream = CreateStore(version, nameHashSize, "testAssembly.dll"); // Act var reader = AssemblyStoreReader.Create(stream, "testStore", null); @@ -29,7 +31,7 @@ public void Create_SupportedVersion_ReadsStore(uint version, bool is64Bit) public void Create_UnsupportedVersion_ReturnsNull(uint version) { // Arrange - using var stream = CreateStore(version, is64Bit: true, "testAssembly.dll"); + using var stream = CreateStore(version, sizeof(ulong), "testAssembly.dll"); // Act var reader = AssemblyStoreReader.Create(stream, "testStore", null); @@ -38,43 +40,57 @@ public void Create_UnsupportedVersion_ReturnsNull(uint version) reader.Should().BeNull(); } - private static MemoryStream CreateStore(uint version, bool is64Bit, string assemblyName) + [Theory] + [InlineData(12u, 1u)] // v2 layout: 64-bit hash, no ignore flag + [InlineData(17u, 2u)] // not evenly divisible + public void Create_InvalidIndexSize_Throws(uint indexSize, uint indexEntryCount) + { + // Arrange + using var stream = new MemoryStream(); + using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) + { + WriteHeader(writer, 0x80000004u | 0x00010000u, indexEntryCount, indexSize); + writer.Write(new byte[indexSize]); + } + stream.Position = 0; + + // Act + var act = () => AssemblyStoreReader.Create(stream, "testStore", null); + + // Assert + act.Should().Throw().WithMessage("*testStore*index*"); + } + + private static MemoryStream CreateStore(uint version, int nameHashSize, string assemblyName) { var stream = new MemoryStream(); using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) { - var indexEntrySize = (is64Bit ? sizeof(ulong) : sizeof(uint)) + sizeof(uint) + sizeof(byte); - - // Header - writer.Write(Utils.AssemblyStoreMagic); - writer.Write(version); - writer.Write(1u); // entry_count - writer.Write(1u); // index_entry_count - writer.Write((uint)indexEntrySize); // index_size - if ((version & 0xFFFF) >= 4) - { - writer.Write(0x0123456789ABCDEFul); // content_id - } + var indexEntrySize = nameHashSize + sizeof(uint) + sizeof(byte); + WriteHeader(writer, version, indexEntryCount: 1, (uint)indexEntrySize); + + const ulong nameHash64 = 0xDEADBEEFDEADBEEF; + const uint nameHash32 = 0xDEADBEEF; + const uint descriptorIndex = 0; + const byte ignore = 0; + const int descriptorFieldCount = 7; - // Index - if (is64Bit) + if (nameHashSize == sizeof(ulong)) { - writer.Write(0xDEADBEEFDEADBEEFul); // name_hash + writer.Write(nameHash64); } else { - writer.Write(0xDEADBEEFu); // name_hash + writer.Write(nameHash32); } - writer.Write(0u); // descriptor_index - writer.Write((byte)0); // ignore + writer.Write(descriptorIndex); + writer.Write(ignore); - // Descriptor: mapping_index, data offset/size, debug offset/size, config offset/size - for (var i = 0; i < 7; i++) + for (var i = 0; i < descriptorFieldCount; i++) { writer.Write(0u); } - // Names var nameBytes = Encoding.UTF8.GetBytes(assemblyName); writer.Write((uint)nameBytes.Length); writer.Write(nameBytes); @@ -84,6 +100,22 @@ private static MemoryStream CreateStore(uint version, bool is64Bit, string assem return stream; } + private static void WriteHeader(BinaryWriter writer, uint version, uint indexEntryCount, uint indexSize) + { + const uint entryCount = 1; + const ulong contentId = 0x0123456789ABCDEF; + + writer.Write(Utils.AssemblyStoreMagic); + writer.Write(version); + writer.Write(entryCount); + writer.Write(indexEntryCount); + writer.Write(indexSize); + if ((version & StoreReader.ASSEMBLY_STORE_FORMAT_NUMBER_MASK) >= 4) + { + writer.Write(contentId); + } + } + [Fact] public void IsSupported_Concurrent_IsThreadSafe() { diff --git a/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs index 087cf6500f..3c054cedd2 100644 --- a/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs +++ b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs @@ -112,6 +112,23 @@ public void DemangleAsyncFunctionName_NullModule_ContinuesNull() Assert.Null(stackFrame.Module); } + [Theory] + [InlineData("", "Foo.dll")] + [InlineData("/data/app/Foo.dll", "/data/app/Foo.dll")] + public void GetManagedModuleDebugImage_AssemblyReader_ReadsByLocationOrScopeName(string fullyQualifiedName, string expectedName) + { + string? requestedName = null; + _fixture.SentryOptions.AssemblyReader = name => + { + requestedName = name; + return null; + }; + + DebugStackTrace.GetManagedModuleDebugImage(new StubModule(fullyQualifiedName, "Foo.dll"), _fixture.SentryOptions); + + requestedName.Should().Be(expectedName); + } + [Fact] public void MergeDebugImages_Empty() { @@ -264,6 +281,14 @@ public void Inject(int identifier) }); } } + private class StubModule(string fullyQualifiedName, string scopeName) : Module + { + public override string FullyQualifiedName => fullyQualifiedName; + public override string Name => fullyQualifiedName; + public override string ScopeName => scopeName; + public override Guid ModuleVersionId { get; } = Guid.NewGuid(); + } + internal class StubNativeAOTStackFrame : IStackFrame { internal string? Function;