Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
593c3f6
feat: Accept Android assembly store v4 versions and read content_id h…
jamescrosswell Sep 14, 2026
a804f36
feat: Decompress Zstandard-compressed Android assemblies
jamescrosswell Sep 14, 2026
c4670a0
Accept API verifier changes
getsentry-bot Sep 14, 2026
d054bf3
Accept API verifier changes
getsentry-bot Sep 14, 2026
aeda4bb
feat: Derive Android assembly store index entry size from the header
jamescrosswell Sep 14, 2026
18585cd
chore: Refresh Android assembly reader upstream attribution
jamescrosswell Sep 14, 2026
945b0bb
Merge branch 'feat/assemblystore-v4' into feat/assemblystore-zstd
jamescrosswell Sep 14, 2026
8a9967d
Merge branch 'feat/assemblystore-zstd' into feat/assemblystore-index-…
jamescrosswell Sep 14, 2026
85ff32e
Merge remote-tracking branch 'origin/version7' into feat/assemblystor…
jamescrosswell Sep 14, 2026
da10760
Merge remote-tracking branch 'origin/feat/assemblystore-zstd' into fe…
jamescrosswell Sep 14, 2026
bb04253
Merge branch 'feat/assemblystore-zstd' into feat/assemblystore-index-…
jamescrosswell Sep 14, 2026
1642213
Merge branch 'feat/assemblystore-v4' into feat/assemblystore-zstd
jamescrosswell Sep 14, 2026
9bf8c49
Merge remote-tracking branch 'origin/feat/assemblystore-v4' into feat…
jamescrosswell Sep 14, 2026
949dd52
Merge branch 'feat/assemblystore-zstd' into feat/assemblystore-index-…
jamescrosswell Sep 14, 2026
e7db72a
test: Don't compile ArchiveUtilsTests for Android
jamescrosswell Sep 14, 2026
4ee11d6
chore: Trim comments
jamescrosswell Sep 15, 2026
f1830d4
fix: Resolve debug images for Android assemblies without a file location
jamescrosswell Sep 15, 2026
a5b732a
Merge branch 'feat/assemblystore-v4' into feat/assemblystore-zstd
jamescrosswell Sep 15, 2026
082cf3e
chore: Trim comments
jamescrosswell Sep 15, 2026
66967da
Merge branch 'feat/assemblystore-zstd' into feat/assemblystore-index-…
jamescrosswell Sep 15, 2026
203a8e0
feat: Decompress Zstandard-compressed Android assemblies
jamescrosswell Sep 14, 2026
672ae6b
test: Don't compile ArchiveUtilsTests for Android
jamescrosswell Sep 14, 2026
027018e
chore: Trim comments
jamescrosswell Sep 15, 2026
61a6234
Merge remote-tracking branch 'origin/version7' into feat/assemblystor…
jamescrosswell Sep 16, 2026
f867f4e
Merge remote-tracking branch 'origin/feat/assemblystore-zstd' into fe…
jamescrosswell Sep 16, 2026
69f6718
refactor: Address review feedback on Zstandard decompression
jamescrosswell Sep 16, 2026
8877577
Merge branch 'feat/assemblystore-zstd' into feat/assemblystore-index-…
jamescrosswell Sep 16, 2026
e41eb31
Merge remote-tracking branch 'origin/version7' into feat/assemblystor…
jamescrosswell Sep 23, 2026
0ffeb40
refactor: Address review feedback on .NET 11 assembly store support
jamescrosswell Sep 23, 2026
79b8f74
ref: Record that assembly store v4 never shipped in .NET 11 (#5609)
jamescrosswell Sep 23, 2026
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
20 changes: 10 additions & 10 deletions src/Sentry.Android.AssemblyReader/ArchiveUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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");
Expand Down
10 changes: 10 additions & 0 deletions src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/

Expand Down Expand Up @@ -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;
Expand Down
45 changes: 29 additions & 16 deletions src/Sentry.Android.AssemblyReader/V2/StoreReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/

Expand All @@ -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;
Expand Down Expand Up @@ -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<IndexEntry>();
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));
}

Expand Down Expand Up @@ -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;
}
}
20 changes: 11 additions & 9 deletions src/Sentry/Internal/DebugStackTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -518,29 +518,31 @@ 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))
{
var assembly = options.FileSystem.OpenFileForReading(assemblyName);
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -78,15 +65,15 @@ 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
using var sut = GetSut(isAot: false, isAssemblyStore: false, isCompressed: true);
switch (TargetFramework)
{
case "net11.0":
Assert.IsType<AndroidAssemblyDirectoryReader>(sut);
// CoreCLR ignores AndroidUseAssemblyStore=false: https://github.com/dotnet/android/pull/12033
Assert.IsType<AndroidAssemblyStoreReader>(sut);
break;
case "net10.0":
Assert.IsType<AndroidAssemblyDirectoryReader>(sut);
Expand All @@ -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"));
}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ private static MemoryStream WithHeader(uint magic, ReadOnlySpan<byte> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@
<_TestAPK Include="3" Properties="_Aot=False;_Store=True;_Compressed=False" />
<_TestAPK Include="4" Properties="_Aot=False;_Store=True;_Compressed=True" />
<!-- Mono AOT variants are net10-only: CoreCLR is the only supported runtime for MAUI mobile
apps from .NET 11 on. Every reader test also skips on assembly store v4 there (#5454),
so there would be nothing to read even if an AOT APK could be produced. -->
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'" />
Expand Down
Loading
Loading