diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72b2e92f51..277ff99842 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,7 +117,13 @@ jobs: dotnet build src/Sentry/Sentry.csproj -t:InstallAndroidDependencies -f:net8.0-android34.0 -p:AcceptAndroidSDKLicenses=True -p:AndroidSdkPath="/usr/local/lib/android/sdk/" - name: Build - run: dotnet build Sentry-CI-Build-${{ runner.os }}.slnf -c Release --no-restore --nologo -v:minimal -flp:logfile=build.log -p:CopyLocalLockFileAssemblies=true + run: dotnet build Sentry-CI-Build-${{ runner.os }}.slnf -c Release --no-restore --nologo -v:minimal -flp:logfile=build.log -p:CopyLocalLockFileAssemblies=true -bl:build.binlog + + - name: Upload build logs + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-build-logs + path: build.binlog - name: Test run: dotnet test Sentry-CI-Build-${{ runner.os }}.slnf -c Release --no-build --nologo -l GitHubActions -l "trx;LogFilePrefix=testresults_${{ runner.os }}" --collect "XPlat Code Coverage" diff --git a/.github/workflows/device-tests-android.yml b/.github/workflows/device-tests-android.yml index 64749e1b8f..9a00195412 100644 --- a/.github/workflows/device-tests-android.yml +++ b/.github/workflows/device-tests-android.yml @@ -11,7 +11,12 @@ on: jobs: build: + name: Build (${{ matrix.tfm }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + tfm: [net8.0, net9.0] env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_NOLOGO: 1 @@ -32,18 +37,27 @@ jobs: uses: ./.github/actions/buildnative - name: Build Android Test App - run: pwsh ./scripts/device-test.ps1 android -Build + run: pwsh ./scripts/device-test.ps1 android -Build -Tfm ${{ matrix.tfm }} + + - name: Upload Android Test App (net8.0) + if: matrix.tfm == 'net8.0' + uses: actions/upload-artifact@v4 + with: + name: device-test-android-net8.0 + if-no-files-found: error + path: test/Sentry.Maui.Device.TestApp/bin/Release/net8.0-android/android-x64/io.sentry.dotnet.maui.device.testapp-Signed.apk - - name: Upload Android Test App + - name: Upload Android Test App (net9.0) + if: matrix.tfm == 'net9.0' uses: actions/upload-artifact@v4 with: - name: device-test-android + name: device-test-android-net9.0 if-no-files-found: error - path: test/Sentry.Maui.Device.TestApp/bin/Release/net8.0-android34.0/android-x64/io.sentry.dotnet.maui.device.testapp-Signed.apk + path: test/Sentry.Maui.Device.TestApp/bin/Release/net9.0-android/android-x64/io.sentry.dotnet.maui.device.testapp-Signed.apk android: needs: [build] - name: Run Android API-${{ matrix.api-level }} Test + name: Run Android API-${{ matrix.api-level }} Test (${{ matrix.tfm }}) # Requires a "larger runner", for nested virtualization support runs-on: ubuntu-latest-4-cores @@ -51,6 +65,7 @@ jobs: strategy: fail-fast: false matrix: + tfm: [net8.0, net9.0] # We run against both an older and a newer API api-level: [27, 33] env: @@ -70,7 +85,7 @@ jobs: - name: Download test app artifact uses: actions/download-artifact@v4 with: - name: device-test-android + name: device-test-android-${{ matrix.tfm }} path: bin - name: Setup Gradle @@ -78,7 +93,6 @@ jobs: # Cached AVD setup per https://github.com/ReactiveCircus/android-emulator-runner/blob/main/README.md - - name: Run Tests timeout-minutes: 40 uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # Tag: v2.33.0 @@ -92,11 +106,11 @@ jobs: disk-size: 4096M emulator-options: -no-snapshot-save -no-window -accel on -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: false - script: pwsh scripts/device-test.ps1 android -Run + script: pwsh scripts/device-test.ps1 android -Run -Tfm ${{ matrix.tfm }} - name: Upload results if: success() || failure() uses: actions/upload-artifact@v4 with: - name: device-test-android-${{ matrix.api-level }}-results + name: device-test-android-${{ matrix.api-level }}-${{ matrix.tfm }}-results path: test_output diff --git a/CHANGELOG.md b/CHANGELOG.md index efc1251bc8..0573ad4806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,18 @@ ## Unreleased +### Features + +- Exception.HResult is now included in the mechanism data for all exceptions ([#4029](https://github.com/getsentry/sentry-dotnet/pull/4029)) + ### Fixes +- Fixed symbolication and source context for net9.0-android ([#4033](https://github.com/getsentry/sentry-dotnet/pull/4033)) - Single quotes added to the release name when using MS Build to create Sentry releases on Windows ([#4015](https://github.com/getsentry/sentry-dotnet/pull/4015)) - Target `net9.0` on Sentry.Google.Cloud.Functions to avoid conflict with Sentry.AspNetCore ([#4039](https://github.com/getsentry/sentry-dotnet/pull/4039)) - Changed default value for `SentryOptions.EnableAppHangTrackingV2` to `false` ([#4042](https://github.com/getsentry/sentry-dotnet/pull/4042)) - Missing MAUI `Shell` navigation breadcrumbs on iOS ([#4006](https://github.com/getsentry/sentry-dotnet/pull/4006)) -### Features - -- Exception.HResult is now included in the mechanism data for all exceptions ([#4029](https://github.com/getsentry/sentry-dotnet/pull/4029)) - ### Dependencies - Bump CLI from v2.42.2 to v2.43.0 ([#4036](https://github.com/getsentry/sentry-dotnet/pull/4036), [#4049](https://github.com/getsentry/sentry-dotnet/pull/4049), [#4060](https://github.com/getsentry/sentry-dotnet/pull/4060), [#4062](https://github.com/getsentry/sentry-dotnet/pull/4062)) diff --git a/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj index 6cf009ec38..bdb8ed918a 100644 --- a/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj +++ b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/scripts/device-test.ps1 b/scripts/device-test.ps1 index 6812c42dff..c9e03ffdd8 100644 --- a/scripts/device-test.ps1 +++ b/scripts/device-test.ps1 @@ -5,7 +5,8 @@ param( [String] $Platform, [Switch] $Build, - [Switch] $Run + [Switch] $Run, + [String] $Tfm ) Set-StrictMode -Version latest @@ -21,13 +22,16 @@ $CI = Test-Path env:CI Push-Location $PSScriptRoot/.. try { - $tfm = 'net8.0-' + if (!$Tfm) + { + $Tfm = 'net8.0' + } $arch = (!$IsWindows -and $(uname -m) -eq 'arm64') ? 'arm64' : 'x64' if ($Platform -eq 'android') { - $tfm += 'android34.0' + $Tfm += '-android' $group = 'android' - $buildDir = $CI ? 'bin' : "test/Sentry.Maui.Device.TestApp/bin/Release/$tfm/android-$arch" + $buildDir = $CI ? 'bin' : "test/Sentry.Maui.Device.TestApp/bin/Release/$Tfm/android-$arch" $arguments = @( '--app', "$buildDir/io.sentry.dotnet.maui.device.testapp-Signed.apk", '--package-name', 'io.sentry.dotnet.maui.device.testapp', @@ -43,11 +47,11 @@ try } elseif ($Platform -eq 'ios') { - $tfm += 'ios17.0' + $Tfm += '-ios' $group = 'apple' # Always use x64 on iOS, since arm64 doesn't support JIT, which is required for tests using NSubstitute $arch = 'x64' - $buildDir = "test/Sentry.Maui.Device.TestApp/bin/Release/$tfm/iossimulator-$arch" + $buildDir = "test/Sentry.Maui.Device.TestApp/bin/Release/$Tfm/iossimulator-$arch" $envValue = $CI ? 'true' : 'false' $arguments = @( '--app', "$buildDir/Sentry.Maui.Device.TestApp.app", @@ -60,7 +64,7 @@ try if ($Build) { # We disable AOT for device tests: https://github.com/nsubstitute/NSubstitute/issues/834 - dotnet build -f $tfm -c Release -p:EnableAot=false -p:NoSymbolStrip=true test/Sentry.Maui.Device.TestApp + dotnet build -f $Tfm -c Release -p:EnableAot=false -p:NoSymbolStrip=true test/Sentry.Maui.Device.TestApp if ($LASTEXITCODE -ne 0) { throw 'Failed to build Sentry.Maui.Device.TestApp' diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs index 746b11cbe2..addd6633ff 100644 --- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs +++ b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReader.cs @@ -17,69 +17,4 @@ public void Dispose() { ZipArchive.Dispose(); } - - protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream) - { - var decompressedStream = TryDecompressLZ4(assemblyName, inputStream); - - // Use the decompressed stream, or if null, i.e. it wasn't compressed, use the original. - return new PEReader(decompressedStream ?? inputStream); - } - - /// - /// The DLL may be LZ4 compressed, see https://github.com/xamarin/xamarin-android/pull/4686 - /// The format is: - /// [ 4 byte magic header ] (XALZ) - /// [ 4 byte header index ] - /// [ 4 byte uncompressed payload length ] - /// [rest: lz4 compressed payload] - /// - /// - private Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream) - { - const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian - const int payloadOffset = 12; - var reader = new BinaryReader(inputStream); - if (reader.ReadUInt32() != compressedDataMagic) - { - // Restore the input stream to the beginning if we're not decompressing. - inputStream.Position = 0; - return null; - } - reader.ReadUInt32(); // ignore descriptor index, we don't need it - var decompressedLength = reader.ReadInt32(); - Debug.Assert(inputStream.Position == payloadOffset); - var inputLength = (int)(inputStream.Length - payloadOffset); - - Logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength); - - var outputStream = new MemoryStream(decompressedLength); - - // We're writing to the underlying array manually, so we need to set the length. - outputStream.SetLength(decompressedLength); - var outputBuffer = outputStream.GetBuffer(); - - var inputBuffer = inputStream is MemorySlice slice ? slice.FullBuffer : inputStream.GetBuffer(); - var offset = inputStream is MemorySlice memorySlice ? memorySlice.Offset + payloadOffset : payloadOffset; - var decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength); - if (decoded != decompressedLength) - { - throw new Exception($"Failed to decompress LZ4 data of assembly {assemblyName} - decoded {decoded} instead of expected {decompressedLength} bytes"); - } - return outputStream; - } - - // Allows consumer to access the underlying buffer even if the MemoryStream is created as a slice over another. - // Plain MemoryStream would throw "MemoryStream's internal buffer cannot be accessed." - protected class MemorySlice : MemoryStream - { - public readonly int Offset; - public readonly byte[] FullBuffer; - - public MemorySlice(MemoryStream other, int offset, int size) : base(other.GetBuffer(), offset, size, writable: false) - { - Offset = offset; - FullBuffer = other.GetBuffer(); - } - } } diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs index 399f8622e3..62670899a9 100644 --- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs +++ b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs @@ -1,3 +1,6 @@ +using Sentry.Android.AssemblyReader.V1; +using Sentry.Android.AssemblyReader.V2; + namespace Sentry.Android.AssemblyReader; /// @@ -15,15 +18,29 @@ public static class AndroidAssemblyReaderFactory public static IAndroidAssemblyReader Open(string apkPath, IList supportedAbis, DebugLogger? logger = null) { logger?.Invoke("Opening APK: {0}", apkPath); - var zipArchive = ZipFile.Open(apkPath, ZipArchiveMode.Read); +#if NET9_0 + logger?.Invoke("Reading files using V2 APK layout."); + if (AndroidAssemblyStoreReaderV2.TryReadStore(apkPath, supportedAbis, logger, out var readerV2)) + { + logger?.Invoke("APK uses AssemblyStore V2"); + return readerV2; + } + + logger?.Invoke("APK doesn't use AssemblyStore"); + return new AndroidAssemblyDirectoryReaderV2(apkPath, supportedAbis, logger); +#else + logger?.Invoke("Reading files using V1 APK layout."); + + var zipArchive = ZipFile.OpenRead(apkPath); if (zipArchive.GetEntry("assemblies/assemblies.manifest") is not null) { - logger?.Invoke("APK uses AssemblyStore"); - return new AndroidAssemblyStoreReader(zipArchive, supportedAbis, logger); + logger?.Invoke("APK uses AssemblyStore V1"); + return new AndroidAssemblyStoreReaderV1(zipArchive, supportedAbis, logger); } logger?.Invoke("APK doesn't use AssemblyStore"); - return new AndroidAssemblyDirectoryReader(zipArchive, supportedAbis, logger); + return new AndroidAssemblyDirectoryReaderV1(zipArchive, supportedAbis, logger); +#endif } } diff --git a/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs b/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs new file mode 100644 index 0000000000..0613eed1f9 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ArchiveUtils.cs @@ -0,0 +1,72 @@ +namespace Sentry.Android.AssemblyReader; + +internal static class ArchiveUtils +{ + internal static PEReader CreatePEReader(string assemblyName, MemoryStream inputStream, DebugLogger? logger) + { + var decompressedStream = TryDecompressLZ4(assemblyName, inputStream, logger); // Returns null if not compressed + return new PEReader(decompressedStream ?? inputStream); + } + + internal static MemoryStream Extract(this ZipArchiveEntry zipEntry) + { + var memStream = new MemoryStream((int)zipEntry.Length); + using var zipStream = zipEntry.Open(); + zipStream.CopyTo(memStream); + memStream.Position = 0; + return memStream; + } + + /// + /// The DLL may be LZ4 compressed, see https://github.com/xamarin/xamarin-android/pull/4686 + /// In particular: https://github.com/dotnet/android/blob/44c5c30d3da692c54ca27d4a41571ef20b73670f/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyCompression.cs#L96-L104 + /// The format is: + /// [ 4 byte magic header ] (XALZ) + /// [ 4 byte descriptor header index ] + /// [ 4 byte uncompressed payload length ] + /// [rest: lz4 compressed payload] + /// + /// + private static Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream, DebugLogger? logger) + { + const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian + const int payloadOffset = 12; + var reader = new BinaryReader(inputStream); + if (reader.ReadUInt32() != compressedDataMagic) + { + // Restore the input stream to the beginning if we're not decompressing. + inputStream.Position = 0; + return null; + } + reader.ReadUInt32(); // ignore descriptor index, we don't need it + var decompressedLength = reader.ReadInt32(); + Debug.Assert(inputStream.Position == payloadOffset); + var inputLength = (int)(inputStream.Length - payloadOffset); + + logger?.Invoke("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength); + + var outputStream = new MemoryStream(decompressedLength); + + // We're writing to the underlying array manually, so we need to set the length. + outputStream.SetLength(decompressedLength); + var outputBuffer = outputStream.GetBuffer(); + + var inputBuffer = inputStream is MemorySlice slice ? slice.FullBuffer : inputStream.GetBuffer(); + var offset = inputStream is MemorySlice memorySlice ? memorySlice.Offset + payloadOffset : payloadOffset; + var decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength); + if (decoded != decompressedLength) + { + throw new Exception($"Failed to decompress LZ4 data of assembly {assemblyName} - decoded {decoded} instead of expected {decompressedLength} bytes"); + } + return outputStream; + } + + // Allows consumer to access the underlying buffer even if the MemoryStream is created as a slice over another. + // Plain MemoryStream would throw "MemoryStream's internal buffer cannot be accessed." + internal class MemorySlice(MemoryStream other, int offset, int size) + : MemoryStream(other.GetBuffer(), offset, size, writable: false) + { + public readonly int Offset = offset; + public readonly byte[] FullBuffer = other.GetBuffer(); + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/ELFSharp/ATTRIBUTION.txt new file mode 100644 index 0000000000..1c47bdece1 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ATTRIBUTION.txt @@ -0,0 +1,76 @@ +The code in this subdirectory was adapted from: +https://github.com/konrad-kruczynski/elfsharp/tree/0c859b7b4b8c73bd1194021672681586c1b7139e + +The only changes to the code are: +- Public members have been made internal +- Nullability warnings have been disabled + +The original license is as follows: + +Copyright (c) 2011 Konrad KruczyƄski and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This software uses ELF machine constants from the LLVM projects, whose license +is provided below: + +============================================================================== +LLVM Release License +============================================================================== +University of Illinois/NCSA +Open Source License + +Copyright (c) 2003-2010 University of Illinois at Urbana-Champaign. +All rights reserved. + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Class.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Class.cs new file mode 100644 index 0000000000..ca60eb04f5 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Class.cs @@ -0,0 +1,9 @@ +namespace ELFSharp.ELF +{ + internal enum Class + { + Bit32, + Bit64, + NotELF + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Consts.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Consts.cs new file mode 100644 index 0000000000..83c44d49c1 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Consts.cs @@ -0,0 +1,11 @@ +namespace ELFSharp.ELF +{ + internal static class Consts + { + public const string ObjectsStringTableName = ".strtab"; + public const string DynamicStringTableName = ".dynstr"; + public const int SymbolEntrySize32 = 16; + public const int SymbolEntrySize64 = 24; + public const int MinimalELFSize = 16; + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELF.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELF.cs new file mode 100644 index 0000000000..19eb3dfbda --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELF.cs @@ -0,0 +1,411 @@ +using ELFSharp.ELF.Sections; +using ELFSharp.ELF.Segments; +using ELFSharp.Utilities; +using SectionHeader = ELFSharp.ELF.Sections.SectionHeader; + +#nullable disable + +namespace ELFSharp.ELF +{ + internal sealed class ELF : IELF where T : struct + { + private const int SectionNameNotUniqueMarker = -1; + private readonly bool ownsStream; + + private readonly SimpleEndianessAwareReader reader; + private Stage currentStage; + private StringTable dynamicStringTable; + private StringTable objectsStringTable; + private uint sectionHeaderEntryCount; + private ushort sectionHeaderEntrySize; + private long sectionHeaderOffset; + private List sectionHeaders; + private Dictionary sectionIndicesByName; + private List> sections; + private ushort segmentHeaderEntryCount; + private ushort segmentHeaderEntrySize; + private long segmentHeaderOffset; + private List> segments; + private uint stringTableIndex; + + internal ELF(Stream stream, bool ownsStream) + { + this.ownsStream = ownsStream; + reader = ObtainEndianessAwareReader(stream); + ReadFields(); + ReadStringTable(); + ReadSections(); + ReadSegmentHeaders(); + } + + public T EntryPoint { get; private set; } + + public T MachineFlags { get; private set; } + + public IReadOnlyList> Segments => segments.AsReadOnly(); + + public IReadOnlyList> Sections => sections.AsReadOnly(); + + public Endianess Endianess { get; private set; } + + public Class Class { get; private set; } + + public FileType Type { get; private set; } + + public Machine Machine { get; private set; } + + public bool HasSegmentHeader => segmentHeaderOffset != 0; + + public bool HasSectionHeader => sectionHeaderOffset != 0; + + public bool HasSectionsStringTable => stringTableIndex != 0; + + IReadOnlyList IELF.Segments => Segments; + + public IStringTable SectionsStringTable { get; private set; } + + IEnumerable IELF.GetSections() + { + return Sections.Where(x => x is TSectionType).Cast(); + } + + IReadOnlyList IELF.Sections => Sections; + + bool IELF.TryGetSection(string name, out ISection section) + { + var result = TryGetSection(name, out var concreteSection); + section = concreteSection; + return result; + } + + ISection IELF.GetSection(string name) + { + return GetSection(name); + } + + bool IELF.TryGetSection(int index, out ISection section) + { + var result = TryGetSection(index, out var sectionConcrete); + section = sectionConcrete; + return result; + } + + ISection IELF.GetSection(int index) + { + return GetSection(index); + } + + public void Dispose() + { + if (ownsStream) + reader.BaseStream.Dispose(); + } + + public IEnumerable GetSections() where TSection : Section + { + return Sections.Where(x => x is TSection).Cast(); + } + + public bool TryGetSection(string name, out Section section) + { + return TryGetSectionInner(name, out section) == GetSectionResult.Success; + } + + public Section GetSection(string name) + { + var result = TryGetSectionInner(name, out var section); + + switch (result) + { + case GetSectionResult.Success: + return section; + case GetSectionResult.SectionNameNotUnique: + throw new InvalidOperationException("Given section name is not unique, order is ambigous."); + case GetSectionResult.NoSectionsStringTable: + throw new InvalidOperationException( + "Given ELF does not contain section header string table, therefore names of sections cannot be obtained."); + case GetSectionResult.NoSuchSection: + throw new KeyNotFoundException(string.Format("Given section {0} could not be found in the file.", + name)); + default: + throw new InvalidOperationException("Unhandled error."); + } + } + + public Section GetSection(int index) + { + var result = TryGetSectionInner(index, out var section); + switch (result) + { + case GetSectionResult.Success: + return section; + case GetSectionResult.NoSuchSection: + throw new IndexOutOfRangeException(string.Format("Given section index {0} is out of range.", + index)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override string ToString() + { + return string.Format("[ELF: Endianess={0}, Class={1}, Type={2}, Machine={3}, EntryPoint=0x{4:X}, " + + "NumberOfSections={5}, NumberOfSegments={6}]", Endianess, Class, Type, Machine, + EntryPoint, sections.Count, segments.Count); + } + + private bool TryGetSection(int index, out Section section) + { + return TryGetSectionInner(index, out section) == GetSectionResult.Success; + } + + private Section GetSectionFromSectionHeader(SectionHeader header) + { + Section returned; + switch (header.Type) + { + case SectionType.Null: + goto default; + case SectionType.ProgBits: + returned = new ProgBitsSection(header, reader); + break; + case SectionType.SymbolTable: + returned = new SymbolTable(header, reader, objectsStringTable, this); + break; + case SectionType.StringTable: + returned = new StringTable(header, reader); + break; + case SectionType.RelocationAddends: + goto default; + case SectionType.HashTable: + goto default; + case SectionType.Dynamic: + returned = new DynamicSection(header, reader, this); + break; + case SectionType.Note: + returned = new NoteSection(header, reader); + break; + case SectionType.NoBits: + goto default; + case SectionType.Relocation: + goto default; + case SectionType.Shlib: + goto default; + case SectionType.DynamicSymbolTable: + returned = new SymbolTable(header, reader, dynamicStringTable, this); + break; + default: + returned = new Section(header, reader); + break; + } + + return returned; + } + + private void ReadSegmentHeaders() + { + segments = new List>(segmentHeaderEntryCount); + + for (var i = 0u; i < segmentHeaderEntryCount; i++) + { + var seekTo = segmentHeaderOffset + i * segmentHeaderEntrySize; + reader.BaseStream.Seek(seekTo, SeekOrigin.Begin); + var segmentType = Segment.ProbeType(reader); + + Segment segment; + if (segmentType == SegmentType.Note) + segment = new NoteSegment(segmentHeaderOffset + i * segmentHeaderEntrySize, Class, reader); + else + segment = new Segment(segmentHeaderOffset + i * segmentHeaderEntrySize, Class, reader); + + segments.Add(segment); + } + } + + private void ReadSections() + { + sectionHeaders = new List(); + if (HasSectionsStringTable) + sectionIndicesByName = new Dictionary(); + + for (var i = 0; i < sectionHeaderEntryCount; i++) + { + var header = ReadSectionHeader(i); + sectionHeaders.Add(header); + if (HasSectionsStringTable) + { + var name = header.Name; + if (!sectionIndicesByName.ContainsKey(name)) + sectionIndicesByName.Add(name, i); + else + sectionIndicesByName[name] = SectionNameNotUniqueMarker; + } + } + + sections = new List>(Enumerable.Repeat>( + null, + sectionHeaders.Count + )); + FindStringTables(); + for (var i = 0; i < sectionHeaders.Count; i++) + TouchSection(i); + sectionHeaders = null; + currentStage = Stage.AfterSectionsAreRead; + } + + private void TouchSection(int index) + { + if (currentStage != Stage.Initalizing) + throw new InvalidOperationException("TouchSection invoked in improper state."); + if (sections[index] != null) + return; + var section = GetSectionFromSectionHeader(sectionHeaders[index]); + sections[index] = section; + } + + private void FindStringTables() + { + TryGetSection(Consts.ObjectsStringTableName, out var section); + objectsStringTable = (StringTable)section; + TryGetSection(Consts.DynamicStringTableName, out section); + + // It might happen that the section is not really available, represented as a NoBits one. + dynamicStringTable = section as StringTable; + } + + private void ReadStringTable() + { + if (!HasSectionHeader || !HasSectionsStringTable) + return; + + var header = ReadSectionHeader(checked((int)stringTableIndex)); + if (header.Type != SectionType.StringTable) + throw new InvalidOperationException( + "Given index of section header does not point at string table which was expected."); + + SectionsStringTable = new StringTable(header, reader); + } + + private SectionHeader ReadSectionHeader(int index, bool ignoreUpperLimit = false) + { + if (index < 0 || (!ignoreUpperLimit && index >= sectionHeaderEntryCount)) + throw new ArgumentOutOfRangeException(nameof(index)); + + reader.BaseStream.Seek( + sectionHeaderOffset + index * sectionHeaderEntrySize, + SeekOrigin.Begin + ); + + return new SectionHeader(reader, Class, SectionsStringTable); + } + + private SimpleEndianessAwareReader ObtainEndianessAwareReader(Stream stream) + { + var reader = new BinaryReader(stream); + reader.ReadBytes(4); // ELF magic + var classByte = reader.ReadByte(); + + Class = classByte switch + { + 1 => Class.Bit32, + 2 => Class.Bit64, + _ => throw new ArgumentException($"Given ELF file is of unknown class {classByte}.") + }; + + var endianessByte = reader.ReadByte(); + + Endianess = endianessByte switch + { + 1 => Endianess.LittleEndian, + 2 => Endianess.BigEndian, + _ => throw new ArgumentException($"Given ELF file uses unknown endianess {endianessByte}.") + }; + + reader.ReadBytes(10); // padding bytes of section e_ident + return new SimpleEndianessAwareReader(stream, Endianess); + } + + private void ReadFields() + { + Type = (FileType)reader.ReadUInt16(); + Machine = (Machine)reader.ReadUInt16(); + var version = reader.ReadUInt32(); + if (version != 1) + throw new ArgumentException(string.Format( + "Given ELF file is of unknown version {0}.", + version + )); + EntryPoint = (Class == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To(); + // TODO: assertions for (u)longs + segmentHeaderOffset = Class == Class.Bit32 ? reader.ReadUInt32() : reader.ReadInt64(); + sectionHeaderOffset = Class == Class.Bit32 ? reader.ReadUInt32() : reader.ReadInt64(); + MachineFlags = reader.ReadUInt32().To(); // TODO: always 32bit? + reader.ReadUInt16(); // elf header size + segmentHeaderEntrySize = reader.ReadUInt16(); + segmentHeaderEntryCount = reader.ReadUInt16(); + sectionHeaderEntrySize = reader.ReadUInt16(); + sectionHeaderEntryCount = reader.ReadUInt16(); + stringTableIndex = reader.ReadUInt16(); + + // If the number of sections is greater than or equal to SHN_LORESERVE (0xff00), this member has the + // value zero and the actual number of section header table entries is contained in the sh_size field + // of the section header at index 0. (Otherwise, the sh_size member of the initial entry contains 0.) + if (sectionHeaderEntryCount == 0) + { + var firstSectionHeader = ReadSectionHeader(0, true); + sectionHeaderEntryCount = checked((uint)firstSectionHeader.Size); + + // If the index of the string table is larger than or equal to SHN_LORESERVE (0xff00), this member holds SHN_XINDEX (0xffff) + // and the real index of the section name string table section is held in the sh_link member of the initial entry in section + // header table. Otherwise, the sh_link member of the initial entry in section header table contains the value zero. + if (stringTableIndex == 0xffff) + stringTableIndex = checked(firstSectionHeader.Link); + } + } + + private GetSectionResult TryGetSectionInner(string name, out Section section) + { + section = default; + if (!HasSectionsStringTable) + return GetSectionResult.NoSectionsStringTable; + if (!sectionIndicesByName.TryGetValue(name, out var index)) + return GetSectionResult.NoSuchSection; + if (index == SectionNameNotUniqueMarker) + return GetSectionResult.SectionNameNotUnique; + return TryGetSectionInner(index, out section); + } + + private GetSectionResult TryGetSectionInner(int index, out Section section) + { + section = default; + if (index >= sections.Count) + return GetSectionResult.NoSuchSection; + if (sections[index] != null) + { + section = sections[index]; + return GetSectionResult.Success; + } + + if (currentStage != Stage.Initalizing) + throw new InvalidOperationException( + "Assert not met: null section by proper index in not initializing stage."); + TouchSection(index); + section = sections[index]; + return GetSectionResult.Success; + } + + private enum Stage + { + Initalizing, + AfterSectionsAreRead + } + + private enum GetSectionResult + { + Success, + SectionNameNotUnique, + NoSectionsStringTable, + NoSuchSection + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELFReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELFReader.cs new file mode 100644 index 0000000000..13521bb3ae --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/ELFReader.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Text; + +#nullable disable + +namespace ELFSharp.ELF +{ + internal static class ELFReader + { + private const string NotELFMessage = "Given stream is not a proper ELF file."; + + private static readonly byte[] Magic = + { + 0x7F, + 0x45, + 0x4C, + 0x46 + }; // 0x7F 'E' 'L' 'F' + + public static IELF Load(Stream stream, bool shouldOwnStream) + { + if (!TryLoad(stream, shouldOwnStream, out var elf)) + throw new ArgumentException(NotELFMessage); + + return elf; + } + + public static IELF Load(string fileName) + { + return Load(File.OpenRead(fileName), true); + } + + public static bool TryLoad(Stream stream, bool shouldOwnStream, out IELF elf) + { + switch (CheckELFType(stream)) + { + case Class.Bit32: + elf = new ELF(stream, shouldOwnStream); + return true; + case Class.Bit64: + elf = new ELF(stream, shouldOwnStream); + return true; + default: + elf = null; + stream.Close(); + return false; + } + } + + public static bool TryLoad(string fileName, out IELF elf) + { + return TryLoad(File.OpenRead(fileName), true, out elf); + } + + public static Class CheckELFType(Stream stream) + { + var currentStreamPosition = stream.Position; + + if (stream.Length < Consts.MinimalELFSize) + return Class.NotELF; + + using (var reader = new BinaryReader(stream, Encoding.UTF8, true)) + { + var magic = reader.ReadBytes(4); + for (var i = 0; i < 4; i++) + if (magic[i] != Magic[i]) + return Class.NotELF; + + var value = reader.ReadByte(); + stream.Position = currentStreamPosition; + return value == 1 ? Class.Bit32 : Class.Bit64; + } + } + + public static Class CheckELFType(string fileName) + { + using (var stream = File.OpenRead(fileName)) + { + return CheckELFType(stream); + } + } + + public static ELF Load(Stream stream, bool shouldOwnStream) where T : struct + { + if (CheckELFType(stream) == Class.NotELF) + throw new ArgumentException(NotELFMessage); + + return new ELF(stream, shouldOwnStream); + } + + public static ELF Load(string fileName) where T : struct + { + return Load(File.OpenRead(fileName), true); + } + + public static bool TryLoad(Stream stream, bool shouldOwnStream, out ELF elf) where T : struct + { + switch (CheckELFType(stream)) + { + case Class.Bit32: + case Class.Bit64: + elf = new ELF(stream, shouldOwnStream); + return true; + default: + elf = null; + return false; + } + } + + public static bool TryLoad(string fileName, out ELF elf) where T : struct + { + return TryLoad(File.OpenRead(fileName), true, out elf); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/FileType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/FileType.cs new file mode 100644 index 0000000000..9e04bf8ab9 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/FileType.cs @@ -0,0 +1,11 @@ +namespace ELFSharp.ELF +{ + internal enum FileType : ushort + { + None = 0, + Relocatable, + Executable, + SharedObject, + Core + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/IELF.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/IELF.cs new file mode 100644 index 0000000000..9310425cdd --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/IELF.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using ELFSharp.ELF.Sections; +using ELFSharp.ELF.Segments; + +namespace ELFSharp.ELF +{ + internal interface IELF : IDisposable + { + public Endianess Endianess { get; } + public Class Class { get; } + public FileType Type { get; } + public Machine Machine { get; } + public bool HasSegmentHeader { get; } + public bool HasSectionHeader { get; } + public bool HasSectionsStringTable { get; } + public IReadOnlyList Segments { get; } + public IStringTable SectionsStringTable { get; } + public IReadOnlyList Sections { get; } + public IEnumerable GetSections() where T : ISection; + public bool TryGetSection(string name, out ISection section); + public ISection GetSection(string name); + public bool TryGetSection(int index, out ISection section); + public ISection GetSection(int index); + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Machine.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Machine.cs new file mode 100644 index 0000000000..42e823288a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Machine.cs @@ -0,0 +1,169 @@ +/* + * This file is based on LLVM's elf.h file. You can find its license + * in the LICENSE file. + * + */ + +namespace ELFSharp.ELF +{ + internal enum Machine : ushort + { + None = 0, // No machine + M32 = 1, // AT&T WE 32100 + SPARC = 2, // SPARC + Intel386 = 3, // Intel 386 + M68K = 4, // Motorola 68000 + M88K = 5, // Motorola 88000 + Intel486 = 6, // Intel 486 (deprecated) + Intel860 = 7, // Intel 80860 + MIPS = 8, // MIPS R3000 + S370 = 9, // IBM System/370 + MIPSRS3LE = 10, // MIPS RS3000 Little-endian + PARISC = 15, // Hewlett-Packard PA-RISC + VPP500 = 17, // Fujitsu VPP500 + SPARC32Plus = 18, // Enhanced instruction set SPARC + Intel960 = 19, // Intel 80960 + PPC = 20, // PowerPC + PPC64 = 21, // PowerPC64 + S390 = 22, // IBM System/390 + SPU = 23, // IBM SPU/SPC + V800 = 36, // NEC V800 + FR20 = 37, // Fujitsu FR20 + RH32 = 38, // TRW RH-32 + RCE = 39, // Motorola RCE + ARM = 40, // ARM + Alpha = 41, // DEC Alpha + SuperH = 42, // Hitachi SH + SPARCv9 = 43, // SPARC V9 + TriCore = 44, // Siemens TriCore + ARC = 45, // Argonaut RISC Core + H8300 = 46, // Hitachi H8/300 + H8300H = 47, // Hitachi H8/300H + H8S = 48, // Hitachi H8S + H8500 = 49, // Hitachi H8/500 + IA64 = 50, // Intel IA-64 processor architecture + MIPSX = 51, // Stanford MIPS-X + ColdFire = 52, // Motorola ColdFire + M68HC12 = 53, // Motorola M68HC12 + MMA = 54, // Fujitsu MMA Multimedia Accelerator + PCP = 55, // Siemens PCP + NCPU = 56, // Sony nCPU embedded RISC processor + NDR1 = 57, // Denso NDR1 microprocessor + StarCore = 58, // Motorola Star*Core processor + ME16 = 59, // Toyota ME16 processor + ST100 = 60, // STMicroelectronics ST100 processor + TinyJ = 61, // Advanced Logic Corp. TinyJ embedded processor family + AMD64 = 62, // AMD x86-64 architecture + PDSP = 63, // Sony DSP Processor + PDP10 = 64, // Digital Equipment Corp. PDP-10 + PDP11 = 65, // Digital Equipment Corp. PDP-11 + FX66 = 66, // Siemens FX66 microcontroller + ST9PLUS = 67, // STMicroelectronics ST9+ 8/16 bit microcontroller + ST7 = 68, // STMicroelectronics ST7 8-bit microcontroller + M68HC16 = 69, // Motorola MC68HC16 Microcontroller + M68HC11 = 70, // Motorola MC68HC11 Microcontroller + M68HC08 = 71, // Motorola MC68HC08 Microcontroller + M68HC05 = 72, // Motorola MC68HC05 Microcontroller + SVX = 73, // Silicon Graphics SVx + ST19 = 74, // STMicroelectronics ST19 8-bit microcontroller + VAX = 75, // Digital VAX + CRIS = 76, // Axis Communications 32-bit embedded processor + Javelin = 77, // Infineon Technologies 32-bit embedded processor + FirePath = 78, // Element 14 64-bit DSP Processor + ZSP = 79, // LSI Logic 16-bit DSP Processor + MMIX = 80, // Donald Knuth's educational 64-bit processor + HUANY = 81, // Harvard University machine-independent object files + PRISM = 82, // SiTera Prism + AVR = 83, // Atmel AVR 8-bit microcontroller + FR30 = 84, // Fujitsu FR30 + D10V = 85, // Mitsubishi D10V + D30V = 86, // Mitsubishi D30V + V850 = 87, // NEC v850 + M32R = 88, // Mitsubishi M32R + MN10300 = 89, // Matsushita MN10300 + MN10200 = 90, // Matsushita MN10200 + PicoJava = 91, // picoJava + OpenRISC = 92, // OpenRISC 32-bit embedded processor + ARCompact = 93, // ARC International ARCompact processo + Xtensa = 94, // Tensilica Xtensa Architecture + VideoCore = 95, // Alphamosaic VideoCore processor + TMMGPP = 96, // Thompson Multimedia General Purpose Processor + NS32K = 97, // National Semiconductor 32000 series + TPC = 98, // Tenor Network TPC processor + SNP1k = 99, // Trebia SNP 1000 processor + ST200 = 100, // STMicroelectronics (www.st.com) ST200 + IP2K = 101, // Ubicom IP2xxx microcontroller family + MAX = 102, // MAX Processor + CompactRISC = 103, // National Semiconductor CompactRISC microprocessor + F2MC16 = 104, // Fujitsu F2MC16 + MSP430 = 105, // Texas Instruments embedded microcontroller msp430 + Blackfin = 106, // Analog Devices Blackfin (DSP) processor + S1C33 = 107, // S1C33 Family of Seiko Epson processors + SEP = 108, // Sharp embedded microprocessor + ArcaRISC = 109, // Arca RISC Microprocessor + UNICORE = 110, // Microprocessor series from PKU-Unity Ltd. and MPRC of Peking University + Excess = 111, // eXcess: 16/32/64-bit configurable embedded CPU + DXP = 112, // Icera Semiconductor Inc. Deep Execution Processor + AlteraNios2 = 113, // Altera Nios II soft-core processor + CRX = 114, // National Semiconductor CompactRISC CRX + XGATE = 115, // Motorola XGATE embedded processor + C166 = 116, // Infineon C16x/XC16x processor + M16C = 117, // Renesas M16C series microprocessors + DSPIC30F = 118, // Microchip Technology dsPIC30F Digital Signal + + // Controller + EngineRISC = 119, // Freescale Communication Engine RISC core + M32C = 120, // Renesas M32C series microprocessors + TSK3000 = 131, // Altium TSK3000 core + RS08 = 132, // Freescale RS08 embedded processor + SHARC = 133, // Analog Devices SHARC family of 32-bit DSP processors + ECOG2 = 134, // Cyan Technology eCOG2 microprocessor + Score7 = 135, // Sunplus S+core7 RISC processor + DSP24 = 136, // New Japan Radio (NJR) 24-bit DSP Processor + VideoCore3 = 137, // Broadcom VideoCore III processor + LatticeMico32 = 138, // RISC processor for Lattice FPGA architecture + SeikoEpsonC17 = 139, // Seiko Epson C17 family + TIC6000 = 140, // The Texas Instruments TMS320C6000 DSP family + TIC2000 = 141, // The Texas Instruments TMS320C2000 DSP family + TIC5500 = 142, // The Texas Instruments TMS320C55x DSP family + MMDSPPlus = 160, // STMicroelectronics 64bit VLIW Data Signal Processor + CypressM8C = 161, // Cypress M8C microprocessor + R32C = 162, // Renesas R32C series microprocessors + TriMedia = 163, // NXP Semiconductors TriMedia architecture family + Hexagon = 164, // Qualcomm Hexagon processor + Intel8051 = 165, // Intel 8051 and variants + STxP7x = 166, // STMicroelectronics STxP7x family of configurable and extensible RISC processors + NDS32 = 167, // Andes Technology compact code size embedded RISC processor family + ECOG1 = 168, // Cyan Technology eCOG1X family + ECOG1X = 168, // Cyan Technology eCOG1X family + MAXQ30 = 169, // Dallas Semiconductor MAXQ30 Core Micro-controllers + XIMO16 = 170, // New Japan Radio (NJR) 16-bit DSP Processor + MANIK = 171, // M2000 Reconfigurable RISC Microprocessor + CrayNV2 = 172, // Cray Inc. NV2 vector architecture + RX = 173, // Renesas RX family + METAG = 174, // Imagination Technologies META processor architecture + MCSTElbrus = 175, // MCST Elbrus general purpose hardware architecture + ECOG16 = 176, // Cyan Technology eCOG16 family + CR16 = 177, // National Semiconductor CompactRISC CR16 16-bit microprocessor + ETPU = 178, // Freescale Extended Time Processing Unit + SLE9X = 179, // Infineon Technologies SLE9X core + L10M = 180, // Intel L10M + K10M = 181, // Intel K10M + AArch64 = 183, // ARM AArch64 + AVR32 = 185, // Atmel Corporation 32-bit microprocessor family + STM8 = 186, // STMicroeletronics STM8 8-bit microcontroller + TILE64 = 187, // Tilera TILE64 multicore architecture family + TILEPro = 188, // Tilera TILEPro multicore architecture family + CUDA = 190, // NVIDIA CUDA architecture + TILEGx = 191, // Tilera TILE-Gx multicore architecture family + CloudShield = 192, // CloudShield architecture family + CoreA1st = 193, // KIPO-KAIST Core-A 1st generation processor family + CoreA2nd = 194, // KIPO-KAIST Core-A 2nd generation processor family + ARCompact2 = 195, // Synopsys ARCompact V2 + Open8 = 196, // Open8 8-bit RISC soft processor core + RL78 = 197, // Renesas RL78 family + VideoCore5 = 198, // Broadcom VideoCore V processor + R78KOR = 199, // Renesas 78KOR family + F56800EX = 200 // Freescale 56800EX Digital Signal Controller (DSC) + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicEntry.cs new file mode 100644 index 0000000000..64d2c50b6a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicEntry.cs @@ -0,0 +1,27 @@ +#nullable disable + +namespace ELFSharp.ELF.Sections +{ + /// + /// Dynamic table entries are made up of a 32 bit or 64 bit "tag" + /// and a 32 bit or 64 bit union (val/pointer in 64 bit, val/pointer/offset in 32 bit). + /// See LLVM elf.h file for the C/C++ version. + /// + internal class DynamicEntry : IDynamicEntry + { + public DynamicEntry(T tagValue, T value) + { + Tag = (DynamicTag)tagValue.To(); + Value = value; + } + + public T Value { get; } + + public DynamicTag Tag { get; } + + public override string ToString() + { + return string.Format("{0} \t 0x{1:X}", Tag, Value); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicSection.cs new file mode 100644 index 0000000000..00c6ad3424 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicSection.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using ELFSharp.Utilities; + +#nullable disable + +namespace ELFSharp.ELF.Sections +{ + internal sealed class DynamicSection : Section, IDynamicSection where T : struct + { + private readonly ELF elf; + + private List> entries; + + internal DynamicSection(SectionHeader header, SimpleEndianessAwareReader reader, ELF elf) : base(header, + reader) + { + this.elf = elf; + ReadEntries(); + } + + public IEnumerable> Entries => new ReadOnlyCollection>(entries); + + IEnumerable IDynamicSection.Entries => entries; + + public override string ToString() + { + return string.Format("{0}: {2}, load @0x{4:X}, {5} entries", Name, NameIndex, Type, RawFlags, LoadAddress, + Entries.Count()); + } + + private void ReadEntries() + { + // "Kind-of" Bug: + // So, this winds up with "extra" DT_NULL entries for some executables. The issue + // is basically that sometimes the .dynamic section's size (and # of entries) per the + // header is higher than the actual # of entries. The extra space gets filled with null + // entries in all of the ELF files I tested, so we shouldn't end up with any 'incorrect' entries + // here unless someone is messing with the ELF structure. + + SeekToSectionBeginning(); + var entryCount = elf.Class == Class.Bit32 ? Header.Size / 8 : Header.Size / 16; + entries = new List>(); + + for (ulong i = 0; i < entryCount; i++) + if (elf.Class == Class.Bit32) + entries.Add(new DynamicEntry(Reader.ReadUInt32().To(), Reader.ReadUInt32().To())); + else if (elf.Class == Class.Bit64) + entries.Add(new DynamicEntry(Reader.ReadUInt64().To(), Reader.ReadUInt64().To())); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicTag.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicTag.cs new file mode 100644 index 0000000000..716874df4c --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/DynamicTag.cs @@ -0,0 +1,64 @@ +namespace ELFSharp.ELF.Sections +{ + /// + /// This enum holds some of the possible values for the DynamicTag value (dropping platform + /// specific contents, such as MIPS flags.) + /// Values are coming from LLVM's elf.h headers. + /// File can be found in LLVM 3.8.1 source at: + /// ../include/llvm/support/elf.h + /// License of the original C code is LLVM license. + /// + internal enum DynamicTag : ulong + { + Null = 0, // Marks end of dynamic array. + Needed = 1, // String table offset of needed library. + PLTRelSz = 2, // Size of relocation entries in PLT. + PLTGOT = 3, // Address associated with linkage table. + Hash = 4, // Address of symbolic hash table. + StrTab = 5, // Address of dynamic string table. + SymTab = 6, // Address of dynamic symbol table. + RelA = 7, // Address of relocation table (Rela entries). + RelASz = 8, // Size of Rela relocation table. + RelAEnt = 9, // Size of a Rela relocation entry. + StrSz = 10, // Total size of the string table. + SymEnt = 11, // Size of a symbol table entry. + Init = 12, // Address of initialization function. + Fini = 13, // Address of termination function. + SoName = 14, // String table offset of a shared objects name. + RPath = 15, // String table offset of library search path. + Symbolic = 16, // Changes symbol resolution algorithm. + Rel = 17, // Address of relocation table (Rel entries). + RelSz = 18, // Size of Rel relocation table. + RelEnt = 19, // Size of a Rel relocation entry. + PLTRel = 20, // Type of relocation entry used for linking. + Debug = 21, // Reserved for debugger. + TextRel = 22, // Relocations exist for non-writable segments. + JmpRel = 23, // Address of relocations associated with PLT. + BindNow = 24, // Process all relocations before execution. + InitArray = 25, // Pointer to array of initialization functions. + FiniArray = 26, // Pointer to array of termination functions. + InitArraySz = 27, // Size of DT_INIT_ARRAY. + FiniArraySz = 28, // Size of DT_FINI_ARRAY. + RunPath = 29, // String table offset of lib search path. + Flags = 30, // Flags. + Encoding = 32, // Values from here to DT_LOOS follow the rules for the interpretation of the d_un union. + + PreInitArray = 32, // Pointer to array of preinit functions. + PreInitArraySz = 33, // Size of the DT_PREINIT_ARRAY array. + LoOS = 0x60000000, // Start of environment specific tags. + HiOS = 0x6FFFFFFF, // End of environment specific tags. + LoProc = 0x70000000, // Start of processor specific tags. + HiProc = 0x7FFFFFFF, // End of processor specific tags. + GNUHash = 0x6FFFFEF5, // Reference to the GNU hash table. + TLSDescPLT = 0x6FFFFEF6, // Location of PLT entry for TLS descriptor resolver calls. + TLSDescGOT = 0x6FFFFEF7, // Location of GOT entry used by TLS descriptor resolver PLT entry. + RelACount = 0x6FFFFFF9, // ELF32_Rela count. + RelCount = 0x6FFFFFFA, // ELF32_Rel count. + Flags1 = 0X6FFFFFFB, // Flags_1. + VerSym = 0x6FFFFFF0, // The address of .gnu.version section. + VerDef = 0X6FFFFFFC, // The address of the version definition table. + VerDefNum = 0X6FFFFFFD, // The number of entries in DT_VERDEF. + VerNeed = 0X6FFFFFFE, // The address of the version Dependency table. + VerNeedNum = 0X6FFFFFFF // The number of entries in DT_VERNEED. + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicEntry.cs new file mode 100644 index 0000000000..40fd1d65c3 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicEntry.cs @@ -0,0 +1,12 @@ +namespace ELFSharp.ELF.Sections +{ + /// + /// Represents an entry in the dynamic table. + /// Interface--because this is a union type in C, if we want more detail at some point on the values in the Union type, + /// we can have separate classes. + /// + internal interface IDynamicEntry + { + public DynamicTag Tag { get; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicSection.cs new file mode 100644 index 0000000000..1b88de7dc9 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IDynamicSection.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace ELFSharp.ELF.Sections +{ + internal interface IDynamicSection : ISection + { + public IEnumerable Entries { get; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/INoteSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/INoteSection.cs new file mode 100644 index 0000000000..82516c4653 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/INoteSection.cs @@ -0,0 +1,8 @@ +namespace ELFSharp.ELF.Sections +{ + internal interface INoteSection : ISection + { + public string NoteName { get; } + public byte[] Description { get; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IProgBitsSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IProgBitsSection.cs new file mode 100644 index 0000000000..e98415ceaf --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IProgBitsSection.cs @@ -0,0 +1,7 @@ +namespace ELFSharp.ELF.Sections +{ + internal interface IProgBitsSection : ISection + { + public void WriteContents(byte[] destination, int offset, int length = 0); + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISection.cs new file mode 100644 index 0000000000..23f812c7e7 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISection.cs @@ -0,0 +1,11 @@ +namespace ELFSharp.ELF.Sections +{ + internal interface ISection + { + public string Name { get; } + public uint NameIndex { get; } + public SectionType Type { get; } + public SectionFlags Flags { get; } + public byte[] GetContents(); + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IStringTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IStringTable.cs new file mode 100644 index 0000000000..f529c273c5 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/IStringTable.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace ELFSharp.ELF.Sections +{ + internal interface IStringTable : ISection + { + public string this[long index] { get; } + public IEnumerable Strings { get; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolEntry.cs new file mode 100644 index 0000000000..ac1ced6a9b --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolEntry.cs @@ -0,0 +1,13 @@ +namespace ELFSharp.ELF.Sections +{ + internal interface ISymbolEntry + { + public string Name { get; } + public SymbolBinding Binding { get; } + public SymbolType Type { get; } + public SymbolVisibility Visibility { get; } + public bool IsPointedIndexSpecial { get; } + public ISection PointedSection { get; } + public ushort PointedSectionIndex { get; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolTable.cs new file mode 100644 index 0000000000..da9763727d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ISymbolTable.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace ELFSharp.ELF.Sections +{ + internal interface ISymbolTable : ISection + { + public IEnumerable Entries { get; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteData.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteData.cs new file mode 100644 index 0000000000..e26b4f4ee3 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteData.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Text; +using ELFSharp.ELF.Segments; +using ELFSharp.Utilities; + +#nullable disable + +namespace ELFSharp.ELF.Sections +{ + internal class NoteData : INoteData + { + public const ulong NoteDataHeaderSize = 12; // name size + description size + field + + private readonly SimpleEndianessAwareReader reader; + + internal NoteData(ulong sectionOffset, ulong sectionSize, SimpleEndianessAwareReader reader) + { + this.reader = reader; + var sectionEnd = (long)(sectionOffset + sectionSize); + reader.BaseStream.Seek((long)sectionOffset, SeekOrigin.Begin); + var nameSize = ReadSize(); + var descriptionSize = ReadSize(); + Type = ReadField(); + int remainder; + var fields = Math.DivRem(nameSize, FieldSize, out remainder); + var alignedNameSize = FieldSize * (remainder > 0 ? fields + 1 : fields); + + fields = Math.DivRem(descriptionSize, FieldSize, out remainder); + var alignedDescriptionSize = FieldSize * (remainder > 0 ? fields + 1 : fields); + + // We encountered binaries where nameSize and descriptionSize are + // invalid (i.e. significantly larger than the size of the binary itself). + // To avoid throwing on such binaries, we only read in name and description + // if the sizes are within range of the containing section. + if (reader.BaseStream.Position + alignedNameSize <= sectionEnd) + { + var name = reader.ReadBytes(alignedNameSize); + if (nameSize > 0) + Name = Encoding.UTF8.GetString(name, 0, nameSize - 1); // minus one to omit terminating NUL + if (reader.BaseStream.Position + descriptionSize <= sectionEnd) + DescriptionBytes = descriptionSize > 0 ? reader.ReadBytes(descriptionSize) : new byte[0]; + } + + // If there are multiple notes inside one segment, keep track of the end position so we can read them + // all when parsing the segment + NoteOffset = sectionOffset; + NoteFileSize = (ulong)alignedNameSize + (ulong)alignedDescriptionSize + NoteDataHeaderSize; + } + + internal byte[] DescriptionBytes { get; } + + internal ulong NoteOffset { get; } + internal ulong NoteFileSize { get; } + internal ulong NoteFileEnd => NoteOffset + NoteFileSize; + + private int FieldSize => 4; + + public string Name { get; } + + public ReadOnlyCollection Description => new ReadOnlyCollection(DescriptionBytes); + + public ulong Type { get; } + + public Stream ToStream() + { + return new MemoryStream(DescriptionBytes); + } + + public override string ToString() + { + return $"Name={Name} DataSize=0x{DescriptionBytes.Length.ToString("x8")}"; + } + + private int ReadSize() + { + /* + * According to some versions of ELF64 specfication, in 64-bit ELF files words, of which + * such section consists, should have 8 byte length. However, this is not the case in + * some other specifications (some of theme contradicts with themselves like the 64bit MIPS + * one). In real life scenarios I also observed that note sections are identical in both + * ELF classes. There is also only one structure (i.e. Elf_External_Note) in existing and + * well tested GNU tools. + * + * Nevertheless I leave here the whole machinery as it is already written and may be useful + * some day. + */ + return reader.ReadInt32(); + } + + private ulong ReadField() + { + // see comment above + return reader.ReadUInt32(); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteSection.cs new file mode 100644 index 0000000000..145c21daaa --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/NoteSection.cs @@ -0,0 +1,25 @@ +using ELFSharp.Utilities; + +namespace ELFSharp.ELF.Sections +{ + internal sealed class NoteSection : Section, INoteSection where T : struct + { + private readonly NoteData data; + + internal NoteSection(SectionHeader header, SimpleEndianessAwareReader reader) : base(header, reader) + { + data = new NoteData(header.Offset, header.Size, reader); + } + + public T NoteType => data.Type.To(); + + public string NoteName => data.Name; + + public byte[] Description => data.DescriptionBytes; + + public override string ToString() + { + return string.Format("{0}: {2}, Type={1}", Name, NoteType, Type); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ProgBitsSection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ProgBitsSection.cs new file mode 100644 index 0000000000..4d1f39ea2c --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/ProgBitsSection.cs @@ -0,0 +1,29 @@ +using System; +using ELFSharp.Utilities; + +namespace ELFSharp.ELF.Sections +{ + internal sealed class ProgBitsSection : Section, IProgBitsSection where T : struct + { + private const int BufferSize = 10 * 1024; + + internal ProgBitsSection(SectionHeader header, SimpleEndianessAwareReader reader) : base(header, reader) + { + } + + + public void WriteContents(byte[] destination, int offset, int length = 0) + { + SeekToSectionBeginning(); + if (length == 0 || (ulong)length > Header.Size) + length = Convert.ToInt32(Header.Size); + var remaining = length; + while (remaining > 0) + { + var buffer = Reader.ReadBytes(Math.Min(BufferSize, remaining)); + buffer.CopyTo(destination, offset + (length - remaining)); + remaining -= buffer.Length; + } + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/Section.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/Section.cs new file mode 100644 index 0000000000..5c88fa1a1d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/Section.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.ELF.Sections +{ + internal class Section : ISection where T : struct + { + protected readonly SimpleEndianessAwareReader Reader; + + internal Section(SectionHeader header, SimpleEndianessAwareReader reader) + { + Header = header; + Reader = reader; + } + + public T RawFlags => Header.RawFlags.To(); + + public T LoadAddress => Header.LoadAddress.To(); + + public T Alignment => Header.Alignment.To(); + + public T EntrySize => Header.EntrySize.To(); + + public T Size => Header.Size.To(); + + public T Offset => Header.Offset.To(); + + internal SectionHeader Header { get; } + + public virtual byte[] GetContents() + { + if (Type == SectionType.NoBits) + return Array.Empty(); + + Reader.BaseStream.Seek((long)Header.Offset, SeekOrigin.Begin); + return Reader.ReadBytes(Convert.ToInt32(Header.Size)); + } + + public string Name => Header.Name; + + public uint NameIndex => Header.NameIndex; + + public SectionType Type => Header.Type; + + public SectionFlags Flags => Header.Flags; + + public override string ToString() + { + return Header.ToString(); + } + + protected void SeekToSectionBeginning() + { + Reader.BaseStream.Seek((long)Header.Offset, SeekOrigin.Begin); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionFlags.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionFlags.cs new file mode 100644 index 0000000000..7abd030186 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace ELFSharp.ELF.Sections +{ + [Flags] + internal enum SectionFlags + { + Writable = 1, + Allocatable = 2, + Executable = 4 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionHeader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionHeader.cs new file mode 100644 index 0000000000..54ea890727 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionHeader.cs @@ -0,0 +1,70 @@ +using ELFSharp.Utilities; + +#nullable disable + +namespace ELFSharp.ELF.Sections +{ + internal sealed class SectionHeader + { + private readonly Class elfClass; + + private readonly SimpleEndianessAwareReader reader; + + private readonly IStringTable table; + + // TODO: make elf consts file with things like SHT_LOUSER + internal SectionHeader(SimpleEndianessAwareReader reader, Class elfClass, IStringTable table = null) + { + this.reader = reader; + this.table = table; + this.elfClass = elfClass; + ReadSectionHeader(); + } + + internal string Name { get; private set; } + internal uint NameIndex { get; private set; } + internal SectionType Type { get; private set; } + internal SectionFlags Flags { get; private set; } + internal ulong RawFlags { get; private set; } + internal ulong LoadAddress { get; private set; } + internal ulong Alignment { get; private set; } + internal ulong EntrySize { get; private set; } + internal ulong Size { get; private set; } + internal ulong Offset { get; private set; } + internal uint Link { get; private set; } + internal uint Info { get; private set; } + + public override string ToString() + { + return string.Format("{0}: {2}, load @0x{4:X}, {5} bytes long", Name, NameIndex, Type, RawFlags, + LoadAddress, Size); + } + + private void ReadSectionHeader() + { + NameIndex = reader.ReadUInt32(); + if (table != null) + Name = table[NameIndex]; + Type = (SectionType)reader.ReadUInt32(); + RawFlags = ReadAddress(); + Flags = unchecked((SectionFlags)RawFlags); + LoadAddress = ReadAddress(); + Offset = ReadOffset(); + Size = ReadOffset(); + Link = reader.ReadUInt32(); + Info = reader.ReadUInt32(); + Alignment = ReadAddress(); + EntrySize = ReadAddress(); + } + + private ulong ReadAddress() + { + return elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64(); + } + + private ulong ReadOffset() + { + return elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64(); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionType.cs new file mode 100644 index 0000000000..c1b3b006a1 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SectionType.cs @@ -0,0 +1,18 @@ +namespace ELFSharp.ELF.Sections +{ + internal enum SectionType : uint + { + Null = 0, + ProgBits, + SymbolTable, + StringTable, + RelocationAddends, + HashTable, + Dynamic, + Note, + NoBits, + Relocation, + Shlib, + DynamicSymbolTable + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionIndex.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionIndex.cs new file mode 100644 index 0000000000..ca51e9e648 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionIndex.cs @@ -0,0 +1,9 @@ +namespace ELFSharp.ELF.Sections +{ + internal enum SpecialSectionIndex : ushort + { + Absolute = 0, + Common = 0xFFF1, + Undefined = 0xFFF2 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionType.cs new file mode 100644 index 0000000000..710773e6cc --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SpecialSectionType.cs @@ -0,0 +1,12 @@ +namespace ELFSharp.ELF.Sections +{ + internal enum SpecialSectionType + { + Null, + ProgBits, + NoBits, + Shlib, + ProcessorSpecific, + UserSpecific + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/StringTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/StringTable.cs new file mode 100644 index 0000000000..c1d710e708 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/StringTable.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using ELFSharp.Utilities; + +namespace ELFSharp.ELF.Sections +{ + internal sealed class StringTable : Section, IStringTable where T : struct + { + private readonly byte[] stringBlob; + + private readonly Dictionary stringCache; + private bool cachePopulated; + + internal StringTable(SectionHeader header, SimpleEndianessAwareReader reader) : base(header, reader) + { + stringCache = new Dictionary + { + { 0, string.Empty } + }; + stringBlob = ReadStringData(); + } + + public IEnumerable Strings + { + get + { + if (!cachePopulated) + PrepopulateCache(); + return stringCache.Values; + } + } + + public string this[long index] + { + get + { + if (stringCache.TryGetValue(index, out var result)) + return result; + return HandleUnexpectedIndex(index); + } + } + + private string HandleUnexpectedIndex(long index) + { + var stringStart = (int)index; + for (var i = stringStart; i < stringBlob.Length; ++i) + if (stringBlob[i] == 0) + { + var str = Encoding.UTF8.GetString(stringBlob, stringStart, i - stringStart); + stringCache.Add(stringStart, str); + return str; + } + + throw new IndexOutOfRangeException(); + } + + private void PrepopulateCache() + { + cachePopulated = true; + + var stringStart = 1; + for (var i = 1; i < stringBlob.Length; ++i) + if (stringBlob[i] == 0) + { + if (!stringCache.ContainsKey(stringStart)) + stringCache.Add(stringStart, Encoding.UTF8.GetString(stringBlob, stringStart, i - stringStart)); + stringStart = i + 1; + } + } + + private byte[] ReadStringData() + { + SeekToSectionBeginning(); + var blob = Reader.ReadBytes((int)Header.Size); + Debug.Assert(blob.Length == 0 || (blob[0] == 0 && blob[blob.Length - 1] == 0), + "First and last bytes must be the null character (except for empty string tables)"); + return blob; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolBinding.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolBinding.cs new file mode 100644 index 0000000000..3f447e97b1 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolBinding.cs @@ -0,0 +1,10 @@ +namespace ELFSharp.ELF.Sections +{ + internal enum SymbolBinding + { + Local, + Global, + Weak, + ProcessorSpecific + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolEntry.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolEntry.cs new file mode 100644 index 0000000000..445b9a445a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolEntry.cs @@ -0,0 +1,60 @@ +using System; + +#nullable disable + +namespace ELFSharp.ELF.Sections +{ + internal class SymbolEntry : ISymbolEntry where T : struct + { + private readonly ELF elf; + + public SymbolEntry(string name, T value, T size, SymbolVisibility visibility, + SymbolBinding binding, SymbolType type, ELF elf, ushort sectionIdx) + { + Name = name; + Value = value; + Size = size; + Binding = binding; + Type = type; + Visibility = visibility; + this.elf = elf; + PointedSectionIndex = sectionIdx; + } + + public T Value { get; } + + public T Size { get; } + + public Section PointedSection => IsPointedIndexSpecial ? null : elf.GetSection(PointedSectionIndex); + + public SpecialSectionIndex SpecialPointedSectionIndex + { + get + { + if (IsPointedIndexSpecial) + return (SpecialSectionIndex)PointedSectionIndex; + throw new InvalidOperationException("Given pointed section index does not have special meaning."); + } + } + + public string Name { get; } + + public SymbolBinding Binding { get; } + + public SymbolType Type { get; } + + public SymbolVisibility Visibility { get; } + + public bool IsPointedIndexSpecial => Enum.IsDefined(typeof(SpecialSectionIndex), PointedSectionIndex); + + ISection ISymbolEntry.PointedSection => PointedSection; + + public ushort PointedSectionIndex { get; } + + public override string ToString() + { + return string.Format("[{3} {4} {0}: 0x{1:X}, size: {2}, section idx: {5}]", + Name, Value, Size, Binding, Type, (SpecialSectionIndex)PointedSectionIndex); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolTable.cs new file mode 100644 index 0000000000..86b2cab0b2 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolTable.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using ELFSharp.Utilities; + +#nullable disable + +namespace ELFSharp.ELF.Sections +{ + internal sealed class SymbolTable : Section, ISymbolTable where T : struct + { + private readonly ELF elf; + private readonly IStringTable table; + + private List> entries; + + internal SymbolTable(SectionHeader header, SimpleEndianessAwareReader Reader, IStringTable table, ELF elf) : + base(header, Reader) + { + this.table = table; + this.elf = elf; + ReadSymbols(); + } + + public IEnumerable> Entries => new ReadOnlyCollection>(entries); + + IEnumerable ISymbolTable.Entries => Entries; + + private void ReadSymbols() + { + SeekToSectionBeginning(); + entries = new List>(); + var adder = (ulong)(elf.Class == Class.Bit32 ? Consts.SymbolEntrySize32 : Consts.SymbolEntrySize64); + for (var i = 0UL; i < Header.Size; i += adder) + { + var value = 0UL; + var size = 0UL; + var nameIdx = Reader.ReadUInt32(); + + if (elf.Class == Class.Bit32) + { + value = Reader.ReadUInt32(); + size = Reader.ReadUInt32(); + } + + var info = Reader.ReadByte(); + var other = Reader.ReadByte(); + var visibility = (SymbolVisibility)(other & 3); // Only three lowest bits are meaningful. + var sectionIdx = Reader.ReadUInt16(); + + if (elf.Class == Class.Bit64) + { + value = Reader.ReadUInt64(); + size = Reader.ReadUInt64(); + } + + var name = table == null ? "" : table[nameIdx]; + var binding = (SymbolBinding)(info >> 4); + var type = (SymbolType)(info & 0x0F); + entries.Add(new SymbolEntry(name, value.To(), size.To(), visibility, binding, type, elf, + sectionIdx)); + } + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolType.cs new file mode 100644 index 0000000000..ce2fa60b6f --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolType.cs @@ -0,0 +1,12 @@ +namespace ELFSharp.ELF.Sections +{ + internal enum SymbolType + { + NotSpecified, + Object, + Function, + Section, + File, + ProcessorSpecific + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolVisibility.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolVisibility.cs new file mode 100644 index 0000000000..535762ece8 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Sections/SymbolVisibility.cs @@ -0,0 +1,10 @@ +namespace ELFSharp.ELF.Sections +{ + internal enum SymbolVisibility : byte + { + Default = 0, + Internal = 1, + Hidden = 2, + Protected = 3 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteData.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteData.cs new file mode 100644 index 0000000000..c8e560581a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteData.cs @@ -0,0 +1,31 @@ +using System.Collections.ObjectModel; +using System.IO; + +namespace ELFSharp.ELF.Segments +{ + internal interface INoteData + { + /// + /// Owner of the note. + /// + public string Name { get; } + + /// + /// Data contents of the note. The format of this depends on the combination of the Name and Type properties and often + /// corresponds to a struct. + /// For example, see elf.h in the Linux kernel source tree. + /// + public ReadOnlyCollection Description { get; } + + /// + /// Data type + /// + public ulong Type { get; } + + /// + /// Returns the Description byte[] as a Stream + /// + /// + public Stream ToStream(); + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteSegment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteSegment.cs new file mode 100644 index 0000000000..fba41c72cc --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/INoteSegment.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace ELFSharp.ELF.Segments +{ + internal interface INoteSegment : ISegment + { + public string NoteName { get; } + public ulong NoteType { get; } + public byte[] NoteDescription { get; } + + /// + /// Returns all notes within the segment + /// + public IReadOnlyList Notes { get; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/ISegment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/ISegment.cs new file mode 100644 index 0000000000..2d9b00b608 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/ISegment.cs @@ -0,0 +1,11 @@ +namespace ELFSharp.ELF.Segments +{ + internal interface ISegment + { + public SegmentType Type { get; } + public SegmentFlags Flags { get; } + public byte[] GetRawHeader(); + public byte[] GetFileContents(); + public byte[] GetMemoryContents(); + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/NoteSegment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/NoteSegment.cs new file mode 100644 index 0000000000..2085294e52 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/NoteSegment.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using ELFSharp.ELF.Sections; +using ELFSharp.Utilities; + +namespace ELFSharp.ELF.Segments +{ + internal sealed class NoteSegment : Segment, INoteSegment + { + private readonly NoteData data; + + private readonly List notes = new List(); + + internal NoteSegment(long headerOffset, Class elfClass, SimpleEndianessAwareReader reader) + : base(headerOffset, elfClass, reader) + { + var offset = (ulong)Offset; + var fileSize = (ulong)FileSize; + var remainingSize = fileSize; + + // Keep the first NoteData as a property for backwards compatibility + data = new NoteData(offset, remainingSize, reader); + notes.Add(data); + + offset += data.NoteFileSize; + + // Read all additional notes within the segment + // Multiple notes are common in ELF core files + if (data.NoteFileSize < remainingSize) + { + remainingSize -= data.NoteFileSize; + + while (remainingSize > NoteData.NoteDataHeaderSize) + { + var note = new NoteData(offset, remainingSize, reader); + notes.Add(note); + offset += note.NoteFileSize; + if (note.NoteFileSize <= remainingSize) + remainingSize -= note.NoteFileSize; + else + // File is damaged + throw new IndexOutOfRangeException("NoteSegment internal note-data is out of bounds"); + } + } + } + + public string NoteName => data.Name; + + public ulong NoteType => data.Type; + + public byte[] NoteDescription => data.DescriptionBytes; + public IReadOnlyList Notes => notes.AsReadOnly(); + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/Segment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/Segment.cs new file mode 100644 index 0000000000..91d28c38d4 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/Segment.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using ELFSharp.Utilities; + +#nullable disable + +namespace ELFSharp.ELF.Segments +{ + internal class Segment : ISegment + { + private readonly Class elfClass; + + private readonly long headerOffset; + private readonly SimpleEndianessAwareReader reader; + + internal Segment(long headerOffset, Class elfClass, SimpleEndianessAwareReader reader) + { + this.reader = reader; + this.headerOffset = headerOffset; + this.elfClass = elfClass; + ReadHeader(); + } + + public T Address { get; private set; } + + public T PhysicalAddress { get; private set; } + + public T Size { get; private set; } + + public T Alignment { get; private set; } + + public long FileSize { get; private set; } + + public long Offset { get; private set; } + + public SegmentType Type { get; private set; } + + public SegmentFlags Flags { get; private set; } + + /// + /// Returns content of the section as it is given in the file. + /// Note that it may be an array of length 0. + /// + /// Segment contents as byte array. + public byte[] GetFileContents() + { + if (FileSize == 0) + return new byte[0]; + + SeekTo(Offset); + var result = new byte[checked((int)FileSize)]; + var fileImage = reader.ReadBytes(result.Length); + fileImage.CopyTo(result, 0); + return result; + } + + /// + /// Returns content of the section, possibly padded or truncated to the memory size. + /// Note that it may be an array of length 0. + /// + /// Segment image as a byte array. + public byte[] GetMemoryContents() + { + var sizeAsInt = Size.To(); + if (sizeAsInt == 0) + return new byte[0]; + + SeekTo(Offset); + var result = new byte[sizeAsInt]; + var fileImage = reader.ReadBytes(Math.Min(result.Length, checked((int)FileSize))); + fileImage.CopyTo(result, 0); + return result; + } + + public byte[] GetRawHeader() + { + SeekTo(headerOffset); + return reader.ReadBytes(elfClass == Class.Bit32 ? 32 : 56); + } + + public static SegmentType ProbeType(SimpleEndianessAwareReader reader) + { + return (SegmentType)reader.ReadUInt32(); + } + + public override string ToString() + { + return string.Format("{2}: size {3}, @ 0x{0:X}", Address, PhysicalAddress, Type, Size); + } + + private void ReadHeader() + { + SeekTo(headerOffset); + Type = (SegmentType)reader.ReadUInt32(); + if (elfClass == Class.Bit64) + Flags = (SegmentFlags)reader.ReadUInt32(); + // TODO: some functions?s + Offset = elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadInt64(); + Address = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To(); + PhysicalAddress = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To(); + FileSize = elfClass == Class.Bit32 ? reader.ReadInt32() : reader.ReadInt64(); + Size = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To(); + if (elfClass == Class.Bit32) + Flags = (SegmentFlags)reader.ReadUInt32(); + + Alignment = (elfClass == Class.Bit32 ? reader.ReadUInt32() : reader.ReadUInt64()).To(); + } + + private void SeekTo(long givenOffset) + { + reader.BaseStream.Seek(givenOffset, SeekOrigin.Begin); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentFlags.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentFlags.cs new file mode 100644 index 0000000000..4d10a6589d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace ELFSharp.ELF.Segments +{ + [Flags] + internal enum SegmentFlags : uint + { + Execute = 1, + Write = 2, + Read = 4 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentType.cs new file mode 100644 index 0000000000..f9c681aa54 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Segments/SegmentType.cs @@ -0,0 +1,13 @@ +namespace ELFSharp.ELF.Segments +{ + internal enum SegmentType : uint + { + Null = 0, + Load, + Dynamic, + Interpreter, + Note, + SharedLibrary, + ProgramHeader + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Utilities.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Utilities.cs new file mode 100644 index 0000000000..8d84290714 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/ELF/Utilities.cs @@ -0,0 +1,12 @@ +using System; + +namespace ELFSharp.ELF +{ + internal static class Utilities + { + internal static T To(this object source) + { + return (T)Convert.ChangeType(source, typeof(T)); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Endianess.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Endianess.cs new file mode 100644 index 0000000000..a2c0aadb0d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Endianess.cs @@ -0,0 +1,8 @@ +namespace ELFSharp +{ + internal enum Endianess + { + LittleEndian, + BigEndian + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Command.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Command.cs new file mode 100644 index 0000000000..01ae9069bf --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Command.cs @@ -0,0 +1,17 @@ +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal class Command + { + protected readonly SimpleEndianessAwareReader Reader; + protected readonly Stream Stream; + + internal Command(SimpleEndianessAwareReader reader, Stream stream) + { + Stream = stream; + Reader = reader; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/CommandType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/CommandType.cs new file mode 100644 index 0000000000..1e5acb136d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/CommandType.cs @@ -0,0 +1,15 @@ +namespace ELFSharp.MachO +{ + internal enum CommandType : uint + { + Segment = 0x1, + SymbolTable = 0x2, + LoadDylib = 0xc, + IdDylib = 0xd, + LoadWeakDylib = 0x80000018u, + Segment64 = 0x19, + ReexportDylib = 0x8000001fu, + Main = 0x80000028u, + UUID = 0x1b + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Dylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Dylib.cs new file mode 100644 index 0000000000..de9e5f12b8 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Dylib.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Text; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal abstract class Dylib : Command + { + internal Dylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream) + { + var offset = reader.ReadUInt32(); + var timestamp = reader.ReadInt32(); + var currentVersion = reader.ReadUInt32(); + var compatibilityVersion = reader.ReadUInt32(); + Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime; + CurrentVersion = GetVersion(currentVersion); + CompatibilityVersion = GetVersion(compatibilityVersion); + Name = GetString(reader.ReadBytes((int)(commandSize - offset))); + } + + public string Name { get; } + public DateTime Timestamp { get; } + public Version CurrentVersion { get; } + public Version CompatibilityVersion { get; } + + private static Version GetVersion(uint version) + { + return new Version((int)(version >> 16), (int)((version >> 8) & 0xff), (int)(version & 0xff)); + } + + private static string GetString(byte[] bytes) + { + var nullTerminatorIndex = Array.FindIndex(bytes, e => e == '\0'); + return Encoding.ASCII.GetString(bytes, 0, nullTerminatorIndex >= 0 ? nullTerminatorIndex : bytes.Length); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/EntryPoint.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/EntryPoint.cs new file mode 100644 index 0000000000..0d41fc1a34 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/EntryPoint.cs @@ -0,0 +1,18 @@ +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal class EntryPoint : Command + { + public EntryPoint(SimpleEndianessAwareReader reader, Stream stream) : base(reader, stream) + { + Value = Reader.ReadInt64(); + StackSize = Reader.ReadInt64(); + } + + public long Value { get; private set; } + + public long StackSize { get; private set; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FatArchiveReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FatArchiveReader.cs new file mode 100644 index 0000000000..401382c02a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FatArchiveReader.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal static class FatArchiveReader + { + public static IEnumerable Enumerate(Stream stream, bool shouldOwnStream) + { + // Fat header is always big endian. + var reader = new SimpleEndianessAwareReader(stream, Endianess.BigEndian, !shouldOwnStream); + + // We assume that fat magic has been already read. + var machOCount = reader.ReadInt32(); + var alreadyRead = 0; + var fatEntriesBegin = stream.Position; + + while (alreadyRead < machOCount) + { + // We're only interested in offset and size. + stream.Seek(fatEntriesBegin + 20 * alreadyRead + 8, SeekOrigin.Begin); + var offset = reader.ReadInt32(); + var size = reader.ReadInt32(); + var substream = new SubStream(stream, offset, size); + yield return MachOReader.Load(substream, false); + + alreadyRead++; + } + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FileType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FileType.cs new file mode 100644 index 0000000000..515717389a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/FileType.cs @@ -0,0 +1,17 @@ +namespace ELFSharp.MachO +{ + internal enum FileType : uint + { + Object = 0x1, + Executable = 0x2, + FixedVM = 0x3, + Core = 0x4, + Preload = 0x5, + DynamicLibrary = 0x6, + DynamicLinker = 0x7, + Bundle = 0x8, + DynamicLibraryStub = 0x9, + Debug = 0xA, + Kext = 0xB + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/HeaderFlags.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/HeaderFlags.cs new file mode 100644 index 0000000000..1866e4569c --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/HeaderFlags.cs @@ -0,0 +1,76 @@ +using System; + +namespace ELFSharp.MachO +{ + [Flags] + internal enum HeaderFlags : uint + { + NoUndefs = 0x1, /* the object file has no undefined references */ + + IncrLink = 0x2, /* the object file is the output of an incremental link against a base file + * and can't be link edited again */ + + DyldLink = 0x4, /* the object file is input for the dynamic linker and can't be staticly link edited again */ + + BindAtLoad = 0x8, /* the object file's undefined references are bound by the dynamic + * linker when loaded. */ + + Prebound = 0x10, /* the file has its dynamic undefined references prebound. */ + + SplitSeg = 0x20, /* the file has its read-only and read-write segments split */ + + LazyInit = 0x40, /* the shared library init routine is to be run lazily via catching memory + * faults to its writeable segments (obsolete) */ + + TwoLevel = 0x80, /* the image is using two-level name space bindings */ + + ForceFlat = 0x100, /* the executable is forcing all images to use flat name space bindings */ + + NoMultiDefs = 0x200, /* this umbrella guarantees no multiple defintions of symbols in its + * sub-images so the two-level namespace hints can always be used. */ + + NoFixPrebinding = 0x400, /* do not have dyld notify the prebinding agent about this executable*/ + + Prebindable = 0x800, /* the binary is not prebound but can have its prebinding redone. only used + * when MH_PREBOUND is not set. */ + + AllModsBound = 0x1000, /* indicates that this binary binds to all two-level namespace modules of + * its dependent libraries.only used when MH_PREBINDABLE and MH_TWOLEVEL are both set. */ + + SubsectionsViaSymbols = 0x2000, /* safe to divide up the sections into sub-sections via symbols for dead + * code stripping*/ + + Canonical = 0x4000, /* the binary has been canonicalized via the unprebind operation */ + + WeakDefines = 0x8000, /* the final linked image contains external weak symbols*/ + + BindsToWeak = 0x10000, /* the final linked image uses weak symbols */ + + AllowStackExecution = 0x20000, /* When this bit is set, all stacks in the task will be given stack + * execution privilege. Only used in MH_EXECUTE filetypes. */ + + RootSafe = + 0x40000, /* When this bit is set, the binary declares it is safe for use in processes with uid zero */ + + SetuidSafe = + 0x80000, /* When this bit is set, the binary declares it is safe for use in processes when issetugid() is true */ + + NoReexportedDylibs = 0x100000, /* When this bit is set on a dylib, the static linker does not need to + * examine dependent dylibs to see if any are re-exported */ + + PIE = 0x200000, /* When this bit is set, the OS will load the main executable at a random + * address.Only used in MH_EXECUTE filetypes. */ + + DeadStrippableDylib = 0x400000, /* Only for use on dylibs. When linking against a dylib that has this bit + * set, the static linker will automatically not create a LC_LOAD_DYLIB + * load command to the dylib if no symbols are being referenced from the dylib. */ + + HasTLVDescriptors = 0x800000, /* Contains a section of type S_THREAD_LOCAL_VARIABLES*/ + + NoHeapExecution = 0x1000000, /* When this bit is set, the OS will run the main executable with a + * non-executable heap even on platforms (e.g.i386) that don't require + * it. Only used in MH_EXECUTE filetypes. */ + + AppExtensionSafe = 0x02000000 /* The code was linked for use in an application extension. */ + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/IdDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/IdDylib.cs new file mode 100644 index 0000000000..c57ae7b66b --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/IdDylib.cs @@ -0,0 +1,13 @@ +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal class IdDylib : Dylib + { + public IdDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream, + commandSize) + { + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadDylib.cs new file mode 100644 index 0000000000..339bc62a0e --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadDylib.cs @@ -0,0 +1,13 @@ +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal class LoadDylib : Dylib + { + public LoadDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream, + commandSize) + { + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadWeakDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadWeakDylib.cs new file mode 100644 index 0000000000..aa92ebf5d2 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/LoadWeakDylib.cs @@ -0,0 +1,13 @@ +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal class LoadWeakDylib : Dylib + { + public LoadWeakDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream, + commandSize) + { + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachO.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachO.cs new file mode 100644 index 0000000000..a900c167e0 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachO.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal sealed class MachO + { + internal const int Architecture64 = 0x1000000; + private readonly Command[] commands; + + internal MachO(Stream stream, bool is64, Endianess endianess, bool ownsStream) + { + Is64 = is64; + + using var reader = new SimpleEndianessAwareReader(stream, endianess, !ownsStream); + + Machine = (Machine)reader.ReadInt32(); + reader.ReadBytes(4); // we don't support the cpu subtype now + FileType = (FileType)reader.ReadUInt32(); + var noOfCommands = reader.ReadInt32(); + reader.ReadInt32(); // size of commands + Flags = (HeaderFlags)reader.ReadUInt32(); + if (is64) + reader.ReadBytes(4); // reserved + commands = new Command[noOfCommands]; + ReadCommands(noOfCommands, stream, reader); + } + + public Machine Machine { get; private set; } + + public FileType FileType { get; private set; } + + public HeaderFlags Flags { get; private set; } + + public bool Is64 { get; } + + public IEnumerable GetCommandsOfType() where T : Command + { + return commands.Where(x => x != null).OfType(); + } + + private void ReadCommands(int noOfCommands, Stream stream, SimpleEndianessAwareReader reader) + { + for (var i = 0; i < noOfCommands; i++) + { + var loadCommandType = reader.ReadUInt32(); + var commandSize = reader.ReadUInt32(); + switch ((CommandType)loadCommandType) + { + case CommandType.SymbolTable: + commands[i] = new SymbolTable(reader, stream, Is64, + commands.OfType().SelectMany(e => e.Sections).ToList()); + break; + case CommandType.IdDylib: + commands[i] = new IdDylib(reader, stream, commandSize); + break; + case CommandType.LoadDylib: + commands[i] = new LoadDylib(reader, stream, commandSize); + break; + case CommandType.LoadWeakDylib: + commands[i] = new LoadWeakDylib(reader, stream, commandSize); + break; + case CommandType.ReexportDylib: + commands[i] = new ReexportDylib(reader, stream, commandSize); + break; + case CommandType.Main: + commands[i] = new EntryPoint(reader, stream); + break; + case CommandType.Segment: + case CommandType.Segment64: + commands[i] = new Segment(reader, stream, this); + break; + case CommandType.UUID: + commands[i] = new UUID(reader, stream); + break; + default: + reader.ReadBytes((int)commandSize - 8); // 8 bytes is the size of the common command header + break; + } + } + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOReader.cs new file mode 100644 index 0000000000..3ce1e516ec --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOReader.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +#nullable disable + +namespace ELFSharp.MachO +{ + internal static class MachOReader + { + private const uint FatMagic = 0xBEBAFECA; + + private const string FatArchiveErrorMessage = + "Given file is a fat archive, contains more than one MachO binary. Use (Try)LoadFat to handle it."; + + private const string NotMachOErrorMessage = "Given file is not a Mach-O file."; + + private static readonly IReadOnlyDictionary MagicToMachOType = + new Dictionary + { + { 0xFEEDFACE, (false, Endianess.LittleEndian) }, + { 0xFEEDFACF, (true, Endianess.LittleEndian) }, + { 0xCEFAEDFE, (false, Endianess.BigEndian) }, + { 0xCFFAEDFE, (true, Endianess.BigEndian) } + }; + + public static MachO Load(string fileName) + { + return Load(File.OpenRead(fileName), true); + } + + public static MachO Load(Stream stream, bool shouldOwnStream) + { + return TryLoad(stream, shouldOwnStream, out var result) switch + { + MachOResult.OK => result, + MachOResult.NotMachO => throw new InvalidOperationException(NotMachOErrorMessage), + MachOResult.FatMachO => throw new InvalidOperationException(FatArchiveErrorMessage), + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static IReadOnlyList LoadFat(Stream stream, bool shouldOwnStream) + { + var result = TryLoadFat(stream, shouldOwnStream, out var machOs); + if (result == MachOResult.OK || result == MachOResult.FatMachO) + return machOs; + + throw new InvalidOperationException(NotMachOErrorMessage); + } + + public static MachOResult TryLoad(string fileName, out MachO machO) + { + return TryLoad(File.OpenRead(fileName), true, out machO); + } + + public static MachOResult TryLoad(Stream stream, bool shouldOwnStream, out MachO machO) + { + var result = TryLoadFat(stream, shouldOwnStream, out var machOs); + + if (result == MachOResult.OK) + machO = machOs.SingleOrDefault(); + else + machO = null; + + return result; + } + + public static MachOResult TryLoadFat(Stream stream, bool shouldOwnStream, out IReadOnlyList machOs) + { + machOs = null; + + using var reader = new BinaryReader(stream, Encoding.UTF8, true); + var magic = reader.ReadUInt32(); + + if (magic == FatMagic) + { + machOs = FatArchiveReader.Enumerate(stream, shouldOwnStream).ToArray(); + return MachOResult.FatMachO; + } + + if (!MagicToMachOType.TryGetValue(magic, out var machOType)) + return MachOResult.NotMachO; + + var machO = new MachO(stream, machOType.Is64Bit, machOType.Endianess, shouldOwnStream); + machOs = new[] { machO }; + return MachOResult.OK; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOResult.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOResult.cs new file mode 100644 index 0000000000..1ea75cc7a1 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/MachOResult.cs @@ -0,0 +1,9 @@ +namespace ELFSharp.MachO +{ + internal enum MachOResult + { + OK, + NotMachO, + FatMachO + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Machine.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Machine.cs new file mode 100644 index 0000000000..55545e5a28 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Machine.cs @@ -0,0 +1,20 @@ +namespace ELFSharp.MachO +{ + internal enum Machine + { + Any = -1, + Vax = 1, + M68k = 6, + X86 = 7, + X86_64 = X86 | MachO.Architecture64, + M98k = 10, + PaRisc = 11, + Arm = 12, + Arm64 = Arm | MachO.Architecture64, + M88k = 13, + Sparc = 14, + I860 = 15, + PowerPc = 18, + PowerPc64 = PowerPc | MachO.Architecture64 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Protection.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Protection.cs new file mode 100644 index 0000000000..f50d18fcdf --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Protection.cs @@ -0,0 +1,12 @@ +using System; + +namespace ELFSharp.MachO +{ + [Flags] + internal enum Protection + { + Read = 1, + Write = 2, + Execute = 4 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/ReexportDylib.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/ReexportDylib.cs new file mode 100644 index 0000000000..b187cf6b19 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/ReexportDylib.cs @@ -0,0 +1,13 @@ +using System.IO; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal class ReexportDylib : Dylib + { + public ReexportDylib(SimpleEndianessAwareReader reader, Stream stream, uint commandSize) : base(reader, stream, + commandSize) + { + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Section.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Section.cs new file mode 100644 index 0000000000..2cbd673dff --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Section.cs @@ -0,0 +1,45 @@ +using System; +using System.Diagnostics; + +namespace ELFSharp.MachO +{ + [DebuggerDisplay("Section({segment.Name,nq},{Name,nq})")] + internal sealed class Section + { + private readonly Segment segment; + + public Section(string name, string segmentName, ulong address, ulong size, uint offset, uint alignExponent, + uint relocOffset, uint numberOfReloc, uint flags, Segment segment) + { + Name = name; + SegmentName = segmentName; + Address = address; + Size = size; + Offset = offset; + AlignExponent = alignExponent; + RelocOffset = relocOffset; + RelocCount = numberOfReloc; + Flags = flags; + this.segment = segment; + } + + public string Name { get; private set; } + public string SegmentName { get; private set; } + public ulong Address { get; private set; } + public ulong Size { get; } + public uint Offset { get; } + public uint AlignExponent { get; private set; } + public uint RelocOffset { get; private set; } + public uint RelocCount { get; private set; } + public uint Flags { get; private set; } + + public byte[] GetData() + { + if (Offset < segment.FileOffset || Offset + Size > segment.FileOffset + segment.Size) + return new byte[0]; + var result = new byte[Size]; + Array.Copy(segment.GetData(), (int)(Offset - segment.FileOffset), result, 0, (int)Size); + return result; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Segment.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Segment.cs new file mode 100644 index 0000000000..fd755fc74a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Segment.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using ELFSharp.Utilities; + +#nullable disable + +namespace ELFSharp.MachO +{ + [DebuggerDisplay("{Type}({Name,nq})")] + internal sealed class Segment : Command + { + private readonly byte[] data; + + private readonly bool is64; + + public Segment(SimpleEndianessAwareReader reader, Stream stream, MachO machO) : base(reader, stream) + { + is64 = machO.Is64; + Name = ReadSectionOrSegmentName(); + Address = ReadUInt32OrUInt64(); + Size = ReadUInt32OrUInt64(); + FileOffset = ReadUInt32OrUInt64(); + var fileSize = ReadUInt32OrUInt64(); + MaximalProtection = ReadProtection(); + InitialProtection = ReadProtection(); + var numberOfSections = Reader.ReadInt32(); + Reader.ReadInt32(); // we ignore flags for now + + if (fileSize > 0) + { + var streamPosition = Stream.Position; + Stream.Seek((long)FileOffset, SeekOrigin.Begin); + data = new byte[Size]; + var buffer = stream.ReadBytesOrThrow(checked((int)fileSize)); + Array.Copy(buffer, data, buffer.Length); + Stream.Position = streamPosition; + } + + var sections = new List
(); + for (var i = 0; i < numberOfSections; i++) + { + var sectionName = ReadSectionOrSegmentName(); + var segmentName = ReadSectionOrSegmentName(); + + // An intermediate object file contains only one segment. + // This segment name is empty, its sections segment names are not empty. + if (machO.FileType != FileType.Object && segmentName != Name) + throw new InvalidOperationException("Unexpected name of the section's segment."); + + var sectionAddress = ReadUInt32OrUInt64(); + var sectionSize = ReadUInt32OrUInt64(); + var offset = Reader.ReadUInt32(); + var alignExponent = Reader.ReadUInt32(); + var relocOffset = Reader.ReadUInt32(); + var numberOfReloc = Reader.ReadUInt32(); + var flags = Reader.ReadUInt32(); + _ = Reader.ReadUInt32(); // reserved1 + _ = Reader.ReadUInt32(); // reserved2 + _ = is64 ? Reader.ReadUInt32() : 0; // reserved3 + + var section = new Section(sectionName, segmentName, sectionAddress, sectionSize, offset, alignExponent, + relocOffset, numberOfReloc, flags, this); + sections.Add(section); + } + + Sections = new ReadOnlyCollection
(sections); + } + + public string Name { get; private set; } + public ulong Address { get; private set; } + public ulong Size { get; } + public ulong FileOffset { get; } + public Protection InitialProtection { get; private set; } + public Protection MaximalProtection { get; private set; } + public ReadOnlyCollection
Sections { get; private set; } + private CommandType Type => is64 ? CommandType.Segment64 : CommandType.Segment; + + public byte[] GetData() + { + if (data == null) + return new byte[Size]; + return data.ToArray(); + } + + private ulong ReadUInt32OrUInt64() + { + return is64 ? Reader.ReadUInt64() : Reader.ReadUInt32(); + } + + private Protection ReadProtection() + { + return (Protection)Reader.ReadInt32(); + } + + private string ReadSectionOrSegmentName() + { + var nameAsBytes = Reader.ReadBytes(16).TakeWhile(x => x != 0).ToArray(); + return Encoding.UTF8.GetString(nameAsBytes); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Symbol.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Symbol.cs new file mode 100644 index 0000000000..94715b7c02 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/Symbol.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; + +namespace ELFSharp.MachO +{ + [DebuggerDisplay("Symbol({Name,nq},{Value}) in {Section}")] + internal struct Symbol + { + public Symbol(string name, long value, Section section) : this() + { + Name = name; + Value = value; + Section = section; + } + + public string Name { get; private set; } + public long Value { get; private set; } + public Section Section { get; private set; } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/SymbolTable.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/SymbolTable.cs new file mode 100644 index 0000000000..c8293f0263 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/SymbolTable.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using ELFSharp.Utilities; + +#nullable disable + +namespace ELFSharp.MachO +{ + internal class SymbolTable : Command + { + private readonly bool is64; + + private Symbol[] symbols; + + public SymbolTable(SimpleEndianessAwareReader reader, Stream stream, bool is64, IReadOnlyList
sections) + : base(reader, stream) + { + this.is64 = is64; + ReadSymbols(sections); + } + + public IEnumerable Symbols + { + get { return symbols.Select(x => x); } + } + + private void ReadSymbols(IReadOnlyList
sections) + { + var symbolTableOffset = Reader.ReadInt32(); + var numberOfSymbols = Reader.ReadInt32(); + symbols = new Symbol[numberOfSymbols]; + var stringTableOffset = Reader.ReadInt32(); + Reader.ReadInt32(); // string table size + + var streamPosition = Stream.Position; + Stream.Seek(symbolTableOffset, SeekOrigin.Begin); + + try + { + for (var i = 0; i < numberOfSymbols; i++) + { + var nameOffset = Reader.ReadInt32(); + var name = ReadStringFromOffset(stringTableOffset + nameOffset); + var type = Reader.ReadByte(); + var sect = Reader.ReadByte(); + var desc = Reader.ReadInt16(); + var value = is64 ? Reader.ReadInt64() : Reader.ReadInt32(); + var symbol = new Symbol(name, value, + sect > 0 && sect <= sections.Count ? sections[sect - 1] : null); + symbols[i] = symbol; + } + } + finally + { + Stream.Position = streamPosition; + } + } + + private string ReadStringFromOffset(int offset) + { + var streamPosition = Stream.Position; + Stream.Seek(offset, SeekOrigin.Begin); + try + { + var asBytes = new List(); + int readByte; + while ((readByte = Stream.ReadByte()) != 0) + { + if (readByte == -1) + throw new EndOfStreamException("Premature end of the stream while reading string."); + asBytes.Add((byte)readByte); + } + + return Encoding.UTF8.GetString(asBytes.ToArray()); + } + finally + { + Stream.Position = streamPosition; + } + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/UUID.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/UUID.cs new file mode 100644 index 0000000000..893cd4c325 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/MachO/UUID.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Linq; +using ELFSharp.Utilities; + +namespace ELFSharp.MachO +{ + internal class UUID : Command + { + internal UUID(SimpleEndianessAwareReader reader, Stream stream) : base(reader, stream) + { + ID = ReadUUID(); + } + + public Guid ID { get; } + + private Guid ReadUUID() + { + var rawBytes = Reader.ReadBytes(16).ToArray(); + + // Deal here with UUID endianess. Switch scheme is 4(r)-2(r)-2(r)-8(o) + // where r is reverse, o is original order. + Array.Reverse(rawBytes, 0, 4); + Array.Reverse(rawBytes, 4, 2); + Array.Reverse(rawBytes, 6, 2); + + var guid = new Guid(rawBytes); + return guid; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/Architecture.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/Architecture.cs new file mode 100644 index 0000000000..8d320d7b15 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/Architecture.cs @@ -0,0 +1,27 @@ +namespace ELFSharp.UImage +{ + internal enum Architecture : byte + { + Invalid = 0, + Alpha = 1, + ARM = 2, + Ix86 = 3, + Itanium = 4, + MIPS = 5, + MIPS64 = 6, + PowerPC = 7, + S390 = 8, + SuperH = 9, + SPARC = 10, + SPARC64 = 11, + M68k = 12, + MicroBlaze = 14, + Nios2 = 15, + Blackfin = 16, + AVR32 = 17, + ST200 = 18, + Sandbox = 19, + NDS32 = 20, + OpenRISC = 21 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/CompressionType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/CompressionType.cs new file mode 100644 index 0000000000..555c3aa9a3 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/CompressionType.cs @@ -0,0 +1,11 @@ +namespace ELFSharp.UImage +{ + internal enum CompressionType : byte + { + None = 0, + Gzip = 1, + Bzip2 = 2, + Lzma = 3, + Lzo = 4 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageDataResult.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageDataResult.cs new file mode 100644 index 0000000000..acea570b1d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageDataResult.cs @@ -0,0 +1,10 @@ +namespace ELFSharp.UImage +{ + internal enum ImageDataResult + { + OK, + BadChecksum, + UnsupportedCompressionFormat, + InvalidIndex + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageType.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageType.cs new file mode 100644 index 0000000000..f54c189990 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/ImageType.cs @@ -0,0 +1,10 @@ +namespace ELFSharp.UImage +{ + // here only supported image types are listed + internal enum ImageType : byte + { + Standalone = 1, + Kernel = 2, + MultiFileImage = 4 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/OS.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/OS.cs new file mode 100644 index 0000000000..6fa0e6118a --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/OS.cs @@ -0,0 +1,30 @@ +namespace ELFSharp.UImage +{ + internal enum OS : byte + { + Invalid = 0, + OpenBSD = 1, + NetBSD = 2, + FreeBSD = 3, + BSD44 = 4, + Linux = 5, + SVR4 = 6, + Esix = 7, + Solaris = 8, + Irix = 9, + SCO = 10, + Dell = 11, + NCR = 12, + LynxOS = 13, + VxWorks = 14, + PSOS = 15, + QNX = 16, + Firmware = 17, + RTEMS = 18, + ARTOS = 19, + UnityOS = 20, + INTEGRITY = 21, + OSE = 22, + Plan9 = 23 + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImage.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImage.cs new file mode 100644 index 0000000000..680ca7e6fb --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImage.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; + +#nullable disable + +namespace ELFSharp.UImage +{ + internal sealed class UImage + { + private const int MaximumNameLength = 32; + private readonly List imageSizes; + private readonly byte[] rawImage; + private readonly bool shouldOwnStream; + + internal UImage(Stream stream, bool multiFileImage, bool ownsStream) + { + shouldOwnStream = ownsStream; + imageSizes = new List(); + + using var reader = new BinaryReader(stream, Encoding.UTF8, !ownsStream); + + reader.ReadBytes(8); // magic and CRC, already checked + Timestamp = (new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) + + TimeSpan.FromSeconds(reader.ReadInt32BigEndian())).ToLocalTime(); + Size = reader.ReadUInt32BigEndian(); + LoadAddress = reader.ReadUInt32BigEndian(); + EntryPoint = reader.ReadUInt32BigEndian(); + CRC = reader.ReadUInt32BigEndian(); + OperatingSystem = (OS)reader.ReadByte(); + Architecture = (Architecture)reader.ReadByte(); + Type = (ImageType)reader.ReadByte(); + Compression = (CompressionType)reader.ReadByte(); + var nameAsBytes = reader.ReadBytes(32); + Name = Encoding.UTF8.GetString(nameAsBytes.Reverse().SkipWhile(x => x == 0).Reverse().ToArray()); + + if (multiFileImage) + { + var startingPosition = stream.Position; + + int nextImageSize; + do + { + nextImageSize = reader.ReadInt32BigEndian(); + imageSizes.Add(nextImageSize); + } while (nextImageSize != 0); + + // Last image size is actually a terminator. + imageSizes.RemoveAt(imageSizes.Count - 1); + ImageCount = imageSizes.Count; + stream.Position = startingPosition; + } + + rawImage = reader.ReadBytes((int)Size); + } + + public uint CRC { get; } + public bool IsChecksumOK { get; private set; } + public uint Size { get; } + public uint LoadAddress { get; private set; } + public uint EntryPoint { get; private set; } + public string Name { get; private set; } + public DateTime Timestamp { get; private set; } + public CompressionType Compression { get; } + public ImageType Type { get; private set; } + public OS OperatingSystem { get; private set; } + public Architecture Architecture { get; private set; } + public int ImageCount { get; } + + public ImageDataResult TryGetImageData(int imageIndex, out byte[] result) + { + result = null; + + if (imageIndex > ImageCount - 1 || imageIndex < 0) + return ImageDataResult.InvalidIndex; + + if (ImageCount == 1) + return TryGetImageData(out result); + + if (Compression != CompressionType.None) + // We only support multi file images without compression + return ImageDataResult.UnsupportedCompressionFormat; + + if (CRC != UImageReader.GzipCrc32(rawImage)) + return ImageDataResult.BadChecksum; + + // Images sizes * 4 + terminator (which also takes 4 bytes). + var startingOffset = 4 * (ImageCount + 1) + imageSizes.Take(imageIndex).Sum(); + result = new byte[imageSizes[imageIndex]]; + Array.Copy(rawImage, startingOffset, result, 0, result.Length); + + return ImageDataResult.OK; + } + + public ImageDataResult TryGetImageData(out byte[] result) + { + result = null; + + if (ImageCount > 1) + return TryGetImageData(0, out result); + + if (Compression != CompressionType.None && Compression != CompressionType.Gzip) + return ImageDataResult.UnsupportedCompressionFormat; + + if (CRC != UImageReader.GzipCrc32(rawImage)) + return ImageDataResult.BadChecksum; + + result = new byte[rawImage.Length]; + Array.Copy(rawImage, result, result.Length); + if (Compression == CompressionType.Gzip) + using (var stream = new GZipStream(new MemoryStream(result), CompressionMode.Decompress)) + { + using (var decompressed = new MemoryStream()) + { + stream.CopyTo(decompressed); + result = decompressed.ToArray(); + } + } + + return ImageDataResult.OK; + } + + public byte[] GetImageData(int imageIndex) + { + byte[] result; + var imageDataResult = TryGetImageData(imageIndex, out result); + return InterpretImageResult(result, imageDataResult); + } + + public byte[] GetImageData() + { + byte[] result; + var imageDataResult = TryGetImageData(out result); + return InterpretImageResult(result, imageDataResult); + } + + private byte[] InterpretImageResult(byte[] result, ImageDataResult imageDataResult) + { + return imageDataResult switch + { + ImageDataResult.OK => result, + ImageDataResult.BadChecksum => throw new InvalidOperationException( + "Bad checksum of the image, probably corrupted image."), + ImageDataResult.UnsupportedCompressionFormat => throw new InvalidOperationException( + string.Format("Unsupported compression format '{0}'.", Compression)), + ImageDataResult.InvalidIndex => throw new ArgumentException("Invalid image index."), + _ => throw new ArgumentOutOfRangeException() + }; + } + + public byte[] GetRawImageData() + { + var result = new byte[rawImage.Length]; + Array.Copy(rawImage, result, result.Length); + return result; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageReader.cs new file mode 100644 index 0000000000..b81c887415 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageReader.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Net; +using System.Text; + +#nullable disable + +namespace ELFSharp.UImage +{ + internal static class UImageReader + { + private const uint Magic = 0x27051956; + private const uint Polynomial = 0xEDB88320; + private const uint Seed = 0xFFFFFFFF; + + public static UImage Load(string fileName) + { + return Load(File.OpenRead(fileName), true); + } + + public static UImage Load(Stream stream, bool shouldOwnStream) + { + return TryLoad(stream, shouldOwnStream, out var result) switch + { + UImageResult.OK => result, + UImageResult.NotUImage => throw new InvalidOperationException("Given file is not an UBoot image."), + UImageResult.BadChecksum => throw new InvalidOperationException( + "Wrong header checksum of the given UImage file."), + UImageResult.NotSupportedImageType => throw new InvalidOperationException( + "Given image type is not supported."), + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static UImageResult TryLoad(string fileName, out UImage uImage) + { + return TryLoad(File.OpenRead(fileName), true, out uImage); + } + + public static UImageResult TryLoad(Stream stream, bool shouldOwnStream, out UImage uImage) + { + var startingStreamPosition = stream.Position; + + uImage = null; + if (stream.Length < 64) + return UImageResult.NotUImage; + + using var reader = new BinaryReader(stream, Encoding.UTF8, true); + + var headerForCrc = reader.ReadBytes(64); + // we need to zero crc part + for (var i = 4; i < 8; i++) + headerForCrc[i] = 0; + + stream.Position = startingStreamPosition; + + var magic = reader.ReadUInt32BigEndian(); + if (magic != Magic) + return UImageResult.NotUImage; + + var crc = reader.ReadUInt32BigEndian(); + if (crc != GzipCrc32(headerForCrc)) + return UImageResult.BadChecksum; + + reader.ReadBytes(22); + var imageType = (ImageType)reader.ReadByte(); + if (!Enum.IsDefined(typeof(ImageType), imageType)) + return UImageResult.NotSupportedImageType; + + var multiFileImage = imageType == ImageType.MultiFileImage; + stream.Position = startingStreamPosition; + uImage = new UImage(stream, multiFileImage, shouldOwnStream); + return UImageResult.OK; + } + + internal static uint GzipCrc32(byte[] data) + { + var remainder = Seed; + for (var i = 0; i < data.Length; i++) + { + remainder ^= data[i]; + for (var j = 0; j < 8; j++) + if ((remainder & 1) != 0) + remainder = (remainder >> 1) ^ Polynomial; + else + remainder >>= 1; + } + + return remainder ^ Seed; + } + + internal static uint ReadUInt32BigEndian(this BinaryReader reader) + { + return (uint)IPAddress.HostToNetworkOrder(reader.ReadInt32()); + } + + internal static int ReadInt32BigEndian(this BinaryReader reader) + { + return IPAddress.HostToNetworkOrder(reader.ReadInt32()); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageResult.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageResult.cs new file mode 100644 index 0000000000..c84858d27f --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/UImage/UImageResult.cs @@ -0,0 +1,10 @@ +namespace ELFSharp.UImage +{ + internal enum UImageResult + { + OK, + NotUImage, + BadChecksum, + NotSupportedImageType + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/Extensions.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/Extensions.cs new file mode 100644 index 0000000000..3ccdd3a478 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/Extensions.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace ELFSharp.Utilities +{ + internal static class Extensions + { + public static byte[] ReadBytesOrThrow(this Stream stream, int count) + { + var result = new byte[count]; + while (count > 0) + { + var readThisTurn = stream.Read(result, result.Length - count, count); + if (readThisTurn == 0) + throw new EndOfStreamException($"End of stream reached while {count} bytes more expected."); + count -= readThisTurn; + } + + return result; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SimpleEndianessAwareReader.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SimpleEndianessAwareReader.cs new file mode 100644 index 0000000000..74df4c4521 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SimpleEndianessAwareReader.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Net; + +namespace ELFSharp.Utilities +{ + internal sealed class SimpleEndianessAwareReader : IDisposable + { + private readonly bool beNonClosing; + + private readonly bool needsAdjusting; + + public SimpleEndianessAwareReader(Stream stream, Endianess endianess, bool beNonClosing = false) + { + this.beNonClosing = beNonClosing; + BaseStream = stream; + needsAdjusting = (endianess == Endianess.LittleEndian) ^ BitConverter.IsLittleEndian; + } + + public Stream BaseStream { get; } + + public void Dispose() + { + if (beNonClosing) + return; + BaseStream.Dispose(); + } + + public byte[] ReadBytes(int count) + { + return BaseStream.ReadBytesOrThrow(count); + } + + public byte ReadByte() + { + var result = BaseStream.ReadByte(); + if (result == -1) + throw new EndOfStreamException("End of stream reached while trying to read one byte."); + return (byte)result; + } + + public short ReadInt16() + { + var value = BitConverter.ToInt16(ReadBytes(2), 0); + if (needsAdjusting) + value = IPAddress.NetworkToHostOrder(value); + return value; + } + + public ushort ReadUInt16() + { + return (ushort)ReadInt16(); + } + + public int ReadInt32() + { + var value = BitConverter.ToInt32(ReadBytes(4), 0); + if (needsAdjusting) + value = IPAddress.NetworkToHostOrder(value); + return value; + } + + public uint ReadUInt32() + { + return (uint)ReadInt32(); + } + + public long ReadInt64() + { + var value = BitConverter.ToInt64(ReadBytes(8), 0); + if (needsAdjusting) + value = IPAddress.NetworkToHostOrder(value); + return value; + } + + public ulong ReadUInt64() + { + return (ulong)ReadInt64(); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SubStream.cs b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SubStream.cs new file mode 100644 index 0000000000..3bedf6d06b --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/Utilities/SubStream.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; + +namespace ELFSharp.Utilities +{ + internal sealed class SubStream : Stream + { + private const string NegativeArgumentMessage = "The argument cannot be negative."; + private const string OutsideStreamMessage = "The argument must be within the wrapped stream."; + private readonly long startingPosition; + + private readonly Stream wrappedStream; + + public SubStream(Stream wrappedStream, long startingPosition, long length) + { + if (startingPosition < 0) + throw new ArgumentException(nameof(startingPosition), NegativeArgumentMessage); + + if (startingPosition > wrappedStream.Length) + throw new ArgumentException(nameof(startingPosition), OutsideStreamMessage); + + if (length < 0) + throw new ArgumentException(nameof(length), NegativeArgumentMessage); + + if (startingPosition + length > wrappedStream.Length) + throw new ArgumentException(nameof(startingPosition), OutsideStreamMessage); + + if (!wrappedStream.CanSeek) + throw new ArgumentException(nameof(wrappedStream), "Wrapped streem has to be seekable."); + ; + this.wrappedStream = wrappedStream; + this.startingPosition = startingPosition; + Length = length; + + wrappedStream.Seek(startingPosition, SeekOrigin.Begin); + } + + public override bool CanRead => wrappedStream.CanRead; + + public override bool CanSeek => wrappedStream.CanSeek; + + public override bool CanWrite => false; + + public override long Length { get; } + + public override long Position + { + get => wrappedStream.Position - startingPosition; + + set => wrappedStream.Position = value + startingPosition; + } + + public override void Flush() + { + wrappedStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = (int)Math.Min(count, Length - Position); + return wrappedStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + // All offsets are adjusted to represent a begin-based offset in + // the original stream, so that we can simplify sanity checks. + + var adjustedOffset = origin switch + { + SeekOrigin.Begin => offset + startingPosition, + SeekOrigin.End => wrappedStream.Length - offset, + SeekOrigin.Current => wrappedStream.Position + offset, + _ => throw new InvalidOperationException("Should never reach here.") + }; + + if (adjustedOffset < startingPosition || adjustedOffset > startingPosition + Length) + throw new ArgumentException(nameof(offset), "Effective offset cannot move outside of the substream."); + + return wrappedStream.Seek(adjustedOffset, SeekOrigin.Begin) - startingPosition; + } + + public override void SetLength(long value) + { + throw new NotSupportedException($"Setting length is not available for {nameof(SubStream)}."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException($"Writing is not available for {nameof(SubStream)}."); + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/ELFSharp/make-internal.sh b/src/Sentry.Android.AssemblyReader/ELFSharp/make-internal.sh new file mode 100755 index 0000000000..4d04f6dd78 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/ELFSharp/make-internal.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public sealed class/internal sealed class/g' +find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public static class/internal static class/g' +find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public partial class/internal partial class/g' +find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public class/internal class/g' +find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public struct/internal struct/g' +find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public enum/internal enum/g' +find . -name \*.cs -print0 | xargs -0 sed -E -i '' 's/public interface/internal interface/g' diff --git a/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj index f64bf0f045..2c6dcde33e 100644 --- a/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj +++ b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj @@ -1,16 +1,12 @@ - netstandard2.0;net8.0;net9.0 + net8.0;net9.0 .NET assembly reader for Android - - - - - + @@ -30,4 +26,8 @@ + + + + diff --git a/src/Sentry.Android.AssemblyReader/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt similarity index 100% rename from src/Sentry.Android.AssemblyReader/ATTRIBUTION.txt rename to src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs similarity index 73% rename from src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs rename to src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs index b32a11112f..30314a5546 100644 --- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyDirectoryReader.cs +++ b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs @@ -1,9 +1,9 @@ -namespace Sentry.Android.AssemblyReader; +namespace Sentry.Android.AssemblyReader.V1; // The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file. -internal sealed class AndroidAssemblyDirectoryReader : AndroidAssemblyReader, IAndroidAssemblyReader +internal sealed class AndroidAssemblyDirectoryReaderV1 : AndroidAssemblyReader, IAndroidAssemblyReader { - public AndroidAssemblyDirectoryReader(ZipArchive zip, IList supportedAbis, DebugLogger? logger) + public AndroidAssemblyDirectoryReaderV1(ZipArchive zip, IList supportedAbis, DebugLogger? logger) : base(zip, supportedAbis, logger) { } public PEReader? TryReadAssembly(string name) @@ -25,13 +25,8 @@ public AndroidAssemblyDirectoryReader(ZipArchive zip, IList supportedAbi Logger?.Invoke("Resolved assembly {0} in the APK at {1}", name, zipEntry.FullName); // We need a seekable stream for the PEReader (or even to check whether the DLL is compressed), so make a copy. - var memStream = new MemoryStream((int)zipEntry.Length); - using (var zipStream = zipEntry.Open()) - { - zipStream.CopyTo(memStream); - memStream.Position = 0; - } - return CreatePEReader(name, memStream); + var memStream = zipEntry.Extract(); + return ArchiveUtils.CreatePEReader(name, memStream, Logger); } private ZipArchiveEntry? FindAssembly(string name) diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyStoreReader.cs b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs similarity index 97% rename from src/Sentry.Android.AssemblyReader/AndroidAssemblyStoreReader.cs rename to src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs index dbdb5451ba..3073cc2095 100644 --- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyStoreReader.cs +++ b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs @@ -1,11 +1,11 @@ -namespace Sentry.Android.AssemblyReader; +namespace Sentry.Android.AssemblyReader.V1; // See https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores -internal sealed class AndroidAssemblyStoreReader : AndroidAssemblyReader, IAndroidAssemblyReader +internal sealed class AndroidAssemblyStoreReaderV1 : AndroidAssemblyReader, IAndroidAssemblyReader { private readonly AssemblyStoreExplorer _explorer; - public AndroidAssemblyStoreReader(ZipArchive zip, IList supportedAbis, DebugLogger? logger) + public AndroidAssemblyStoreReaderV1(ZipArchive zip, IList supportedAbis, DebugLogger? logger) : base(zip, supportedAbis, logger) { _explorer = new(zip, supportedAbis, logger); @@ -29,7 +29,7 @@ public AndroidAssemblyStoreReader(ZipArchive zip, IList supportedAbis, D return null; } - return CreatePEReader(name, stream); + return ArchiveUtils.CreatePEReader(name, stream, Logger); } private AssemblyStoreAssembly? TryFindAssembly(string name) @@ -409,7 +409,7 @@ public AssemblyStoreReader(MemoryStream store, string? arch = null) assembly.ConfigDataOffset == 0 ? null : GetDataSlice(assembly.ConfigDataOffset, assembly.ConfigDataSize); private MemoryStream? GetDataSlice(uint offset, uint size) => - size == 0 ? null : new MemorySlice(_storeData, (int)offset, (int)size); + size == 0 ? null : new ArchiveUtils.MemorySlice(_storeData, (int)offset, (int)size); public bool HasIdenticalContent(AssemblyStoreReader other) { diff --git a/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt new file mode 100644 index 0000000000..e782059c7d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/ATTRIBUTION.txt @@ -0,0 +1,28 @@ +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 + +The original license is as follows: + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs new file mode 100644 index 0000000000..d89f81da79 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs @@ -0,0 +1,267 @@ +namespace Sentry.Android.AssemblyReader.V2; + +// The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file. +internal sealed class AndroidAssemblyDirectoryReaderV2 : IAndroidAssemblyReader +{ + private DebugLogger? Logger { get; } + private HashSet SupportedArchitectures { get; } = new(); + private readonly ArchiveAssemblyHelper _archiveAssemblyHelper; + + public AndroidAssemblyDirectoryReaderV2(string apkPath, IList supportedAbis, DebugLogger? logger) + { + Logger = logger; + foreach (var abi in supportedAbis) + { + SupportedArchitectures.Add(abi.AbiToDeviceArchitecture()); + } + _archiveAssemblyHelper = new ArchiveAssemblyHelper(apkPath, logger); + } + + public PEReader? TryReadAssembly(string name) + { + if (File.Exists(name)) + { + // The assembly is already extracted to the file system. Just read it. + var stream = File.OpenRead(name); + return new PEReader(stream); + } + + foreach (var arch in SupportedArchitectures) + { + if (_archiveAssemblyHelper.ReadEntry($"assemblies/{name}", arch) is not { } memStream) + { + continue; + } + + Logger?.Invoke("Resolved assembly {0} in the APK", name); + return ArchiveUtils.CreatePEReader(name, memStream, Logger); + } + + Logger?.Invoke("Couldn't find assembly {0} in the APK", name); + return null; + } + + public void Dispose() + { + // No-op + } + + /* + * Adapted from https://github.com/dotnet/android/blob/6394773fad5108b0d7b4e6f087dc3e6ea997401a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ArchiveAssemblyHelper.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android-tools/blob/ab2165daf27d4fcb29e88bc022e0ab0be33aff69/LICENSE) + */ + internal class ArchiveAssemblyHelper + { + private static readonly ArrayPool Buffers = ArrayPool.Shared; + + private readonly string _archivePath; + private readonly DebugLogger? _logger; + + public ArchiveAssemblyHelper(string archivePath, DebugLogger? logger) + { + if (string.IsNullOrEmpty(archivePath)) + { + throw new ArgumentException("must not be null or empty", nameof(archivePath)); + } + + _archivePath = archivePath; + _logger = logger; + } + + public MemoryStream? ReadEntry(string path, AndroidTargetArch arch = AndroidTargetArch.None, bool uncompressIfNecessary = false) + { + var ret = ReadZipEntry(path, arch); + if (ret == null) + { + return null; + } + + ret.Flush(); + ret.Seek(0, SeekOrigin.Begin); + var (elfPayloadOffset, elfPayloadSize, error) = Utils.FindELFPayloadSectionOffsetAndSize(ret); + + if (error != ELFPayloadError.None) + { + var message = error switch + { + ELFPayloadError.NotELF => $"Entry '{path}' is not a valid ELF binary", + ELFPayloadError.LoadFailed => $"Entry '{path}' could not be loaded", + ELFPayloadError.NotSharedLibrary => $"Entry '{path}' is not a shared ELF library", + ELFPayloadError.NotLittleEndian => $"Entry '{path}' is not a little-endian ELF image", + ELFPayloadError.NoPayloadSection => $"Entry '{path}' does not contain the 'payload' section", + _ => $"Unknown ELF payload section error for entry '{path}': {error}" + }; + _logger?.Invoke(message); + } + else + { + _logger?.Invoke($"Extracted content from ELF image '{path}'"); + } + + if (elfPayloadOffset == 0) + { + ret.Seek(0, SeekOrigin.Begin); + return ret; + } + + // Make a copy of JUST the payload section, so that it contains only the data the tests expect and support + var payload = new MemoryStream(); + var data = Buffers.Rent(16384); + var toRead = data.Length; + var nRead = 0; + var remaining = elfPayloadSize; + + ret.Seek((long)elfPayloadOffset, SeekOrigin.Begin); + while (remaining > 0 && (nRead = ret.Read(data, 0, toRead)) > 0) + { + payload.Write(data, 0, nRead); + remaining -= (ulong)nRead; + + if (remaining < (ulong)data.Length) + { + // Make sure the last chunk doesn't gobble in more than we need + toRead = (int)remaining; + } + } + Buffers.Return(data); + + payload.Flush(); + ret.Dispose(); + + payload.Seek(0, SeekOrigin.Begin); + return payload; + } + + private MemoryStream? ReadZipEntry(string path, AndroidTargetArch arch) + { + var potentialEntries = TransformArchiveAssemblyPath(path, arch); + if (potentialEntries == null || potentialEntries.Count == 0) + { + return null; + } + + using var zip = ZipFile.OpenRead(_archivePath); + foreach (var assemblyPath in potentialEntries) + { + if (zip.GetEntry(assemblyPath) is not { } entry) + { + continue; + } + + var ret = entry.Extract(); + ret.Flush(); + return ret; + } + + return null; + } + + /// + /// Takes "old style" `assemblies/assembly.dll` path and returns (if possible) a set of paths that reflect the new + /// location of `lib/{ARCH}/assembly.dll.so`. A list is returned because, if `arch` is `None`, we'll return all + /// the possible architectural paths. + /// An exception is thrown if we cannot transform the path for some reason. It should **not** be handled. + /// + private static List? TransformArchiveAssemblyPath(string path, AndroidTargetArch arch) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException(nameof(path), "must not be null or empty"); + } + + if (!path.StartsWith("assemblies/", StringComparison.Ordinal)) + { + return [path]; + } + + var parts = path.Split('/'); + if (parts.Length < 2) + { + throw new InvalidOperationException($"Path '{path}' must consist of at least two segments separated by `/`"); + } + + // We accept: + // assemblies/assembly.dll + // assemblies/{CULTURE}/assembly.dll + // assemblies/{ABI}/assembly.dll + // assemblies/{ABI}/{CULTURE}/assembly.dll + if (parts.Length > 4) + { + throw new InvalidOperationException($"Path '{path}' must not consist of more than 4 segments separated by `/`"); + } + + string? fileName = null; + string? culture = null; + string? abi = null; + + switch (parts.Length) + { + // Full satellite assembly path, with abi + case 4: + abi = parts[1]; + culture = parts[2]; + fileName = parts[3]; + break; + + // Assembly path with abi or culture + case 3: + // If the middle part isn't a valid abi, we treat it as a culture name + if (MonoAndroidHelper.IsValidAbi(parts[1])) + { + abi = parts[1]; + } + else + { + culture = parts[1]; + } + fileName = parts[2]; + break; + + // Assembly path without abi or culture + case 2: + fileName = parts[1]; + break; + } + + var fileTypeMarker = MonoAndroidHelper.MANGLED_ASSEMBLY_REGULAR_ASSEMBLY_MARKER; + var abis = new List(); + if (!string.IsNullOrEmpty(abi)) + { + abis.Add(abi); + } + else if (arch == AndroidTargetArch.None) + { + foreach (AndroidTargetArch targetArch in MonoAndroidHelper.SupportedTargetArchitectures) + { + abis.Add(MonoAndroidHelper.ArchToAbi(targetArch)); + } + } + else + { + abis.Add(MonoAndroidHelper.ArchToAbi(arch)); + } + + if (!string.IsNullOrEmpty(culture)) + { + // Android doesn't allow us to put satellite assemblies in lib/{CULTURE}/assembly.dll.so, we must instead + // mangle the name. + fileTypeMarker = MonoAndroidHelper.MANGLED_ASSEMBLY_SATELLITE_ASSEMBLY_MARKER; + fileName = $"{culture}{MonoAndroidHelper.SATELLITE_CULTURE_END_MARKER_CHAR}{fileName}"; + } + + var ret = new List(); + var newParts = new List { + string.Empty, // ABI placeholder + $"{fileTypeMarker}{fileName}.so", + }; + + foreach (var a in abis) + { + newParts[0] = a; + ret.Add(MonoAndroidHelper.MakeZipArchivePath("lib", newParts)); + } + + return ret; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs new file mode 100644 index 0000000000..73b170a0c5 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs @@ -0,0 +1,117 @@ +namespace Sentry.Android.AssemblyReader.V2; + +internal class AndroidAssemblyStoreReaderV2 : IAndroidAssemblyReader +{ + private readonly IList _explorers; + private readonly DebugLogger? _logger; + + private AndroidAssemblyStoreReaderV2(IList explorers, DebugLogger? logger) + { + _explorers = explorers; + _logger = logger; + } + + public static bool TryReadStore(string inputFile, IList supportedAbis, DebugLogger? logger, [NotNullWhen(true)] out AndroidAssemblyStoreReaderV2? reader) + { + var (explorers, errorMessage) = AssemblyStoreExplorer.Open(inputFile, logger); + if (errorMessage != null) + { + logger?.Invoke(errorMessage); + reader = null; + return false; + } + + List supportedExplorers = []; + if (explorers is not null) + { + foreach (var explorer in explorers) + { + if (explorer.TargetArch is null) + { + continue; + } + + foreach (var supportedAbi in supportedAbis) + { + if (supportedAbi.AbiToDeviceArchitecture() == explorer.TargetArch) + { + supportedExplorers.Add(explorer); + } + } + } + } + + if (supportedExplorers.Count == 0) + { + logger?.Invoke("Could not find V2 AssemblyStoreExplorer for the supported ABIs: {0}", string.Join(", ", supportedAbis)); + reader = null; + return false; + } + + reader = new AndroidAssemblyStoreReaderV2(supportedExplorers, logger); + return true; + } + + public PEReader? TryReadAssembly(string name) + { + var explorerAssembly = TryFindAssembly(name); + if (explorerAssembly is null) + { + _logger?.Invoke("Couldn't find assembly {0} in the APK AssemblyStore", name); + return null; + } + + var (explorer, storeItem) = explorerAssembly; + _logger?.Invoke("Resolved assembly {0} in the APK {1} AssemblyStore", name, storeItem.TargetArch); + + var stream = explorer.ReadImageData(storeItem, false); + if (stream is null) + { + _logger?.Invoke("Couldn't access assembly {0} image stream", name); + return null; + } + + return ArchiveUtils.CreatePEReader(name, stream, _logger); + } + + private ExplorerStoreItem? TryFindAssembly(string name) + { + if (FindBestAssembly(name, out var assembly)) + { + return assembly; + } + + if ((IsFileType(".dll") || IsFileType(".exe")) && FindBestAssembly(name[..^4], out assembly)) + { + return assembly; + } + + return null; + + bool IsFileType(string extension) + { + return name.EndsWith(extension, ignoreCase: true, CultureInfo.InvariantCulture); + } + } + + private bool FindBestAssembly(string name, out ExplorerStoreItem? explorerAssembly) + { + foreach (var explorer in _explorers) + { + if (explorer.AssembliesByName?.TryGetValue(name, out var assembly) is true) + { + explorerAssembly = new(explorer, assembly); + return true; + } + } + explorerAssembly = null; + return false; + } + + private record ExplorerStoreItem(AssemblyStoreExplorer Explorer, AssemblyStoreItem StoreItem); + + public void Dispose() + { + // No-op + } +} diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidTargetArch.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidTargetArch.cs new file mode 100644 index 0000000000..8564e04e6f --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidTargetArch.cs @@ -0,0 +1,18 @@ +/* + * Adapted from https://github.com/dotnet/android-tools/blob/ab2165daf27d4fcb29e88bc022e0ab0be33aff69/src/Xamarin.Android.Tools.AndroidSdk/AndroidTargetArch.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android-tools/blob/ab2165daf27d4fcb29e88bc022e0ab0be33aff69/LICENSE) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +[Flags] +internal enum AndroidTargetArch +{ + None = 0, + Arm = 1, + X86 = 2, + Mips = 4, + Arm64 = 8, + X86_64 = 16, + Other = 0x10000 // hope it's not too optimistic +} diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs new file mode 100644 index 0000000000..de2148ddeb --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs @@ -0,0 +1,132 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreExplorer.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal class AssemblyStoreExplorer +{ + private readonly AssemblyStoreReader _reader; + + public AndroidTargetArch? TargetArch { get; } + public IList? Assemblies { get; } + public IDictionary? AssembliesByName { get; } + public bool Is64Bit { get; } + + private AssemblyStoreExplorer(Stream storeStream, string path, DebugLogger? logger) + { + var storeReader = AssemblyStoreReader.Create(storeStream, path, logger); + if (storeReader == null) + { + storeStream.Dispose(); + throw new NotSupportedException($"Format of assembly store '{path}' is unsupported"); + } + + _reader = storeReader; + TargetArch = _reader.TargetArch; + Assemblies = _reader.Assemblies; + Is64Bit = _reader.Is64Bit; + + var dict = new Dictionary(StringComparer.Ordinal); + if (Assemblies is not null) + { + foreach (var item in Assemblies) + { + dict.Add(item.Name, item); + } + } + AssembliesByName = dict.AsReadOnly(); + } + + private AssemblyStoreExplorer(FileInfo storeInfo, DebugLogger? logger) + : this(storeInfo.OpenRead(), storeInfo.FullName, logger) + { } + + public static (IList? explorers, string? errorMessage) Open(string inputFile, DebugLogger? logger) + { + var (format, info) = Utils.DetectFileFormat(inputFile); + if (info == null) + { + return (null, $"File '{inputFile}' does not exist."); + } + + switch (format) + { + case FileFormat.Unknown: + return (null, $"File '{inputFile}' has an unknown format."); + + case FileFormat.Zip: + return (null, $"File '{inputFile}' is a ZIP archive, but not an Android one."); + + case FileFormat.AssemblyStore: + case FileFormat.ELF: + return (new List { new AssemblyStoreExplorer(info, logger) }, null); + + case FileFormat.Aab: + return OpenAab(info, logger); + + case FileFormat.AabBase: + return OpenAabBase(info, logger); + + case FileFormat.Apk: + return OpenApk(info, logger); + + default: + return (null, $"File '{inputFile}' has an unsupported format '{format}'"); + } + } + + private static (IList? explorers, string? errorMessage) OpenAab(FileInfo fi, DebugLogger? logger) + => OpenCommon(fi, [StoreReaderV2.AabPaths, StoreReader_V1.AabPaths], logger); + + private static (IList? explorers, string? errorMessage) OpenAabBase(FileInfo fi, DebugLogger? logger) + => OpenCommon(fi, [StoreReaderV2.AabBasePaths, StoreReader_V1.AabBasePaths], logger); + + private static (IList? explorers, string? errorMessage) OpenApk(FileInfo fi, DebugLogger? logger) + => OpenCommon(fi, [StoreReaderV2.ApkPaths, StoreReader_V1.ApkPaths], logger); + + private static (IList? explorers, string? errorMessage) OpenCommon(FileInfo fi, List> pathLists, DebugLogger? logger) + { + using var zip = ZipFile.OpenRead(fi.FullName); + + foreach (var paths in pathLists) + { + var (explorers, errorMessage, pathsFound) = TryLoad(fi, zip, paths, logger); + if (pathsFound) + { + return (explorers, errorMessage); + } + } + + return (null, "Unable to find any blob entries"); + } + + private static (IList? explorers, string? errorMessage, bool pathsFound) TryLoad(FileInfo fi, ZipArchive zip, IList paths, DebugLogger? logger) + { + var ret = new List(); + + foreach (var path in paths) + { + if (zip.GetEntry(path) is not { } entry) + { + continue; + } + + var stream = entry.Extract(); + ret.Add(new AssemblyStoreExplorer(stream, $"{fi.FullName}!{path}", logger)); + } + + if (ret.Count == 0) + { + return (null, null, false); + } + + return (ret, null, true); + } + + public MemoryStream? ReadImageData(AssemblyStoreItem item, bool uncompressIfNeeded = false) + { + return _reader.ReadEntryImageData(item, uncompressIfNeeded); + } +} diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs new file mode 100644 index 0000000000..a132f51ead --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs @@ -0,0 +1,27 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/86260ed36dfe1a90c8ed6a2bb1cd0607d637f403/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal abstract class AssemblyStoreItem +{ + public string Name { get; } + public IList Hashes { get; } + public bool Is64Bit { get; } + public uint DataOffset { get; protected set; } + public uint DataSize { get; protected set; } + public uint DebugOffset { get; protected set; } + public uint DebugSize { get; protected set; } + public uint ConfigOffset { get; protected set; } + public uint ConfigSize { get; protected set; } + public AndroidTargetArch TargetArch { get; protected set; } + + protected AssemblyStoreItem(string name, bool is64Bit, List hashes) + { + Name = name; + Hashes = hashes.AsReadOnly(); + Is64Bit = is64Bit; + } +} diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs new file mode 100644 index 0000000000..834cb2c36d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs @@ -0,0 +1,93 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreReader.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal abstract class AssemblyStoreReader +{ + protected DebugLogger? Logger { get; } + + private static readonly UTF8Encoding ReaderEncoding = new UTF8Encoding(false); + + protected Stream StoreStream { get; } + + public abstract string Description { get; } + public abstract bool NeedsExtensionInName { get; } + public string StorePath { get; } + + public AndroidTargetArch TargetArch { get; protected set; } = AndroidTargetArch.Arm; + public uint AssemblyCount { get; protected set; } + public uint IndexEntryCount { get; protected set; } + public IList? Assemblies { get; protected set; } + public bool Is64Bit { get; protected set; } + + protected AssemblyStoreReader(Stream store, string path, DebugLogger? logger) + { + StoreStream = store; + StorePath = path; + Logger = logger; + } + + public static AssemblyStoreReader? Create(Stream store, string path, DebugLogger? logger) + { + var reader = MakeReaderReady(new StoreReader_V1(store, path, logger)); + if (reader != null) + { + return reader; + } + + reader = MakeReaderReady(new StoreReaderV2(store, path, logger)); + if (reader != null) + { + return reader; + } + + return null; + } + + private static AssemblyStoreReader? MakeReaderReady(AssemblyStoreReader reader) + { + if (!reader.IsSupported()) + { + return null; + } + + reader.Prepare(); + return reader; + } + + protected BinaryReader CreateReader() => new BinaryReader(StoreStream, ReaderEncoding, leaveOpen: true); + + protected abstract bool IsSupported(); + protected abstract void Prepare(); + protected abstract ulong GetStoreStartDataOffset(); + + public MemoryStream ReadEntryImageData(AssemblyStoreItem entry, bool uncompressIfNeeded = false) + { + ulong startOffset = GetStoreStartDataOffset(); + StoreStream.Seek((uint)startOffset + entry.DataOffset, SeekOrigin.Begin); + var stream = new MemoryStream(); + + if (uncompressIfNeeded) + { + throw new NotImplementedException(); + } + + const long BufferSize = 65535; + byte[] buffer = Utils.BytePool.Rent((int)BufferSize); + long remainingToRead = entry.DataSize; + + while (remainingToRead > 0) + { + int nread = StoreStream.Read(buffer, 0, (int)Math.Min(BufferSize, remainingToRead)); + stream.Write(buffer, 0, nread); + remainingToRead -= (long)nread; + } + stream.Flush(); + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } +} diff --git a/src/Sentry.Android.AssemblyReader/V2/DeviceArchitectureExtensions.cs b/src/Sentry.Android.AssemblyReader/V2/DeviceArchitectureExtensions.cs new file mode 100644 index 0000000000..692789686f --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/DeviceArchitectureExtensions.cs @@ -0,0 +1,15 @@ +namespace Sentry.Android.AssemblyReader.V2; + +internal static class DeviceArchitectureExtensions +{ + public static AndroidTargetArch AbiToDeviceArchitecture(this string abi) => + abi switch + { + "armeabi-v7a" => AndroidTargetArch.Arm, + "arm64-v8a" => AndroidTargetArch.Arm64, + "x86" => AndroidTargetArch.X86, + "x86_64" => AndroidTargetArch.X86_64, + "mips" => AndroidTargetArch.Mips, + _ => AndroidTargetArch.Other, + }; +} diff --git a/src/Sentry.Android.AssemblyReader/V2/ELFPayloadError.cs b/src/Sentry.Android.AssemblyReader/V2/ELFPayloadError.cs new file mode 100644 index 0000000000..b7644d68af --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/ELFPayloadError.cs @@ -0,0 +1,16 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/ELFPayloadError.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal enum ELFPayloadError +{ + None, + NotELF, + LoadFailed, + NotSharedLibrary, + NotLittleEndian, + NoPayloadSection, +} diff --git a/src/Sentry.Android.AssemblyReader/V2/FileFormat.cs b/src/Sentry.Android.AssemblyReader/V2/FileFormat.cs new file mode 100644 index 0000000000..d1cef2ad05 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/FileFormat.cs @@ -0,0 +1,17 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/FileFormat.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal enum FileFormat +{ + Aab, + AabBase, + Apk, + AssemblyStore, + ELF, + Zip, + Unknown, +} diff --git a/src/Sentry.Android.AssemblyReader/V2/MonoAndroidHelper.Basic.cs b/src/Sentry.Android.AssemblyReader/V2/MonoAndroidHelper.Basic.cs new file mode 100644 index 0000000000..fda2ff8ad1 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/MonoAndroidHelper.Basic.cs @@ -0,0 +1,92 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/3822f2b1ee7061813b1d456af22e043e66e2f698/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.Basic.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal static class MonoAndroidHelper +{ + private static class AndroidAbi + { + public const string Arm32 = "armeabi-v7a"; + public const string Arm64 = "arm64-v8a"; + public const string X86 = "x86"; + public const string X64 = "x86_64"; + } + + private static class RuntimeIdentifier + { + public const string Arm32 = "android-arm"; + public const string Arm64 = "android-arm64"; + public const string X86 = "android-x86"; + public const string X64 = "android-x64"; + } + + public static readonly HashSet SupportedTargetArchitectures = + [ + AndroidTargetArch.Arm, + AndroidTargetArch.Arm64, + AndroidTargetArch.X86, + AndroidTargetArch.X86_64 + ]; + private static readonly char[] ZipPathTrimmedChars = { '/', '\\' }; + private static readonly Dictionary AbiToRidMap = new(StringComparer.OrdinalIgnoreCase) { + { AndroidAbi.Arm32, RuntimeIdentifier.Arm32 }, + { AndroidAbi.Arm64, RuntimeIdentifier.Arm64 }, + { AndroidAbi.X86, RuntimeIdentifier.X86 }, + { AndroidAbi.X64, RuntimeIdentifier.X64 }, + }; + private static readonly Dictionary ArchToAbiMap = new Dictionary { + { AndroidTargetArch.Arm, AndroidAbi.Arm32 }, + { AndroidTargetArch.Arm64, AndroidAbi.Arm64 }, + { AndroidTargetArch.X86, AndroidAbi.X86 }, + { AndroidTargetArch.X86_64, AndroidAbi.X64 }, + }; + + public static string ArchToAbi(AndroidTargetArch arch) + { + if (!ArchToAbiMap.TryGetValue(arch, out var abi)) + { + throw new InvalidOperationException($"Internal error: unsupported architecture '{arch}'"); + } + + return abi; + } + + public static bool IsValidAbi(string abi) => AbiToRidMap.ContainsKey(abi); + + public static string MakeZipArchivePath(string part1, ICollection? pathParts) + { + var parts = new List(); + if (!string.IsNullOrEmpty(part1)) + { + parts.Add(part1.TrimEnd(ZipPathTrimmedChars)); + } + + if (pathParts != null && pathParts.Count > 0) + { + foreach (string p in pathParts) + { + if (string.IsNullOrEmpty(p)) + { + continue; + } + parts.Add(p.TrimEnd(ZipPathTrimmedChars)); + } + } + + if (parts.Count == 0) + { + return string.Empty; + } + + return string.Join("/", parts); + } + + // These 3 MUST be the same as the like-named constants in src/monodroid/jni/shared-constants.hh + public const string MANGLED_ASSEMBLY_NAME_EXT = ".so"; + public const string MANGLED_ASSEMBLY_REGULAR_ASSEMBLY_MARKER = "lib_"; + public const string MANGLED_ASSEMBLY_SATELLITE_ASSEMBLY_MARKER = "lib-"; + public const string SATELLITE_CULTURE_END_MARKER_CHAR = "_"; +} diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs new file mode 100644 index 0000000000..c1a3fe3c2d --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs @@ -0,0 +1,38 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V1.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal class StoreReader_V1 : AssemblyStoreReader +{ + public override string Description => "Assembly store v1"; + public override bool NeedsExtensionInName => false; + + public static IList ApkPaths { get; } + public static IList AabPaths { get; } + public static IList AabBasePaths { get; } + + static StoreReader_V1() + { + ApkPaths = new List().AsReadOnly(); + AabPaths = new List().AsReadOnly(); + AabBasePaths = new List().AsReadOnly(); + } + + public StoreReader_V1(Stream store, string path, DebugLogger? logger) + : base(store, path, logger) + { } + + protected override bool IsSupported() + { + return false; + } + + protected override void Prepare() + { + } + + protected override ulong GetStoreStartDataOffset() => 0; +} diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs new file mode 100644 index 0000000000..85aa91ba89 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs @@ -0,0 +1,96 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal partial class StoreReaderV2 +{ + private sealed class Header + { + public const uint NativeSize = 5 * sizeof(uint); + + public readonly uint magic; + public readonly uint version; + public readonly uint entry_count; + public readonly uint index_entry_count; + + // Index size in bytes + public readonly uint index_size; + + public Header(uint magic, uint version, uint entry_count, uint index_entry_count, uint index_size) + { + this.magic = magic; + this.version = version; + this.entry_count = entry_count; + this.index_entry_count = index_entry_count; + this.index_size = index_size; + } + } + + private sealed class IndexEntry + { + public readonly ulong name_hash; + public readonly uint descriptor_index; + + public IndexEntry(ulong name_hash, uint descriptor_index) + { + this.name_hash = name_hash; + this.descriptor_index = descriptor_index; + } + } + + private sealed class EntryDescriptor + { + public uint mapping_index; + + public uint data_offset; + public uint data_size; + + public uint debug_data_offset; + public uint debug_data_size; + + public uint config_data_offset; + public uint config_data_size; + } + + private sealed class StoreItemV2 : AssemblyStoreItem + { + public StoreItemV2(AndroidTargetArch targetArch, string name, bool is64Bit, List indexEntries, EntryDescriptor descriptor) + : base(name, is64Bit, IndexToHashes(indexEntries)) + { + DataOffset = descriptor.data_offset; + DataSize = descriptor.data_size; + DebugOffset = descriptor.debug_data_offset; + DebugSize = descriptor.debug_data_size; + ConfigOffset = descriptor.config_data_offset; + ConfigSize = descriptor.config_data_size; + TargetArch = targetArch; + } + + private static List IndexToHashes(List indexEntries) + { + var ret = new List(); + foreach (var ie in indexEntries) + { + ret.Add(ie.name_hash); + } + + return ret; + } + } + + private sealed class TemporaryItem + { + public readonly string Name; + public readonly List IndexEntries = new List(); + public readonly EntryDescriptor Descriptor; + + public TemporaryItem(string name, EntryDescriptor descriptor) + { + Name = name; + Descriptor = descriptor; + } + } +} diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs new file mode 100644 index 0000000000..6a40e05bc0 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs @@ -0,0 +1,241 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ + +namespace Sentry.Android.AssemblyReader.V2; + +internal partial class StoreReaderV2 : AssemblyStoreReader +{ + // Bit 31 is set for 64-bit platforms, cleared for the 32-bit ones + private const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000002; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant + private const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000002; + private const uint ASSEMBLY_STORE_FORMAT_VERSION_MASK = 0xF0000000; + private const uint ASSEMBLY_STORE_ABI_AARCH64 = 0x00010000; + private const uint ASSEMBLY_STORE_ABI_ARM = 0x00020000; + private const uint ASSEMBLY_STORE_ABI_X64 = 0x00030000; + private const uint ASSEMBLY_STORE_ABI_X86 = 0x00040000; + private const uint ASSEMBLY_STORE_ABI_MASK = 0x00FF0000; + + public override string Description => "Assembly store v2"; + public override bool NeedsExtensionInName => true; + + public static IList ApkPaths { get; } + public static IList AabPaths { get; } + public static IList AabBasePaths { get; } + + private readonly HashSet supportedVersions; + private Header? header; + private ulong elfOffset = 0; + + static StoreReaderV2() + { + var paths = new List { + GetArchPath (AndroidTargetArch.Arm64), + GetArchPath (AndroidTargetArch.Arm), + GetArchPath (AndroidTargetArch.X86_64), + GetArchPath (AndroidTargetArch.X86), + }; + ApkPaths = paths.AsReadOnly(); + AabBasePaths = ApkPaths; + + const string AabBaseDir = "base"; + paths = new List { + GetArchPath (AndroidTargetArch.Arm64, AabBaseDir), + GetArchPath (AndroidTargetArch.Arm, AabBaseDir), + GetArchPath (AndroidTargetArch.X86_64, AabBaseDir), + GetArchPath (AndroidTargetArch.X86, AabBaseDir), + }; + AabPaths = paths.AsReadOnly(); + + string GetArchPath(AndroidTargetArch arch, string? root = null) + { + const string LibDirName = "lib"; + + string abi = MonoAndroidHelper.ArchToAbi(arch); + var parts = new List(); + if (!string.IsNullOrEmpty(root)) + { + parts.Add(LibDirName); + } + else + { + root = LibDirName; + } + parts.Add(abi); + parts.Add(GetBlobName(abi)); + + return MonoAndroidHelper.MakeZipArchivePath(root, parts); + } + } + + public StoreReaderV2(Stream store, string path, DebugLogger? logger) + : base(store, path, logger) + { + supportedVersions = new HashSet { + ASSEMBLY_STORE_FORMAT_VERSION_64BIT | ASSEMBLY_STORE_ABI_AARCH64, + ASSEMBLY_STORE_FORMAT_VERSION_64BIT | ASSEMBLY_STORE_ABI_X64, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT | ASSEMBLY_STORE_ABI_ARM, + ASSEMBLY_STORE_FORMAT_VERSION_32BIT | ASSEMBLY_STORE_ABI_X86, + }; + } + + private static string GetBlobName(string abi) => $"libassemblies.{abi}.blob.so"; + + protected override ulong GetStoreStartDataOffset() => elfOffset; + + protected override bool IsSupported() + { + StoreStream.Seek(0, SeekOrigin.Begin); + using var reader = CreateReader(); + + uint magic = reader.ReadUInt32(); + if (magic == Utils.ELFMagic) + { + ELFPayloadError error; + (elfOffset, _, error) = Utils.FindELFPayloadSectionOffsetAndSize(StoreStream); + + if (error != ELFPayloadError.None) + { + string message = error switch + { + ELFPayloadError.NotELF => $"Store '{StorePath}' is not a valid ELF binary", + ELFPayloadError.LoadFailed => $"Store '{StorePath}' could not be loaded", + ELFPayloadError.NotSharedLibrary => $"Store '{StorePath}' is not a shared ELF library", + ELFPayloadError.NotLittleEndian => $"Store '{StorePath}' is not a little-endian ELF image", + ELFPayloadError.NoPayloadSection => $"Store '{StorePath}' does not contain the 'payload' section", + _ => $"Unknown ELF payload section error for store '{StorePath}': {error}" + }; + Logger?.Invoke(message); + // Was originally: + // ``` + // } else if (elfOffset >= 0) { + // ``` + // However since elfOffset is an ulong, it will never be less than 0 + } + else + { + StoreStream.Seek((long)elfOffset, SeekOrigin.Begin); + magic = reader.ReadUInt32(); + } + } + + if (magic != Utils.AssemblyStoreMagic) + { + Logger?.Invoke("Store '{0}' has invalid header magic number.", StorePath); + return false; + } + + uint version = reader.ReadUInt32(); + if (!supportedVersions.Contains(version)) + { + Logger?.Invoke("Store '{0}' has unsupported version 0x{1:x}", StorePath, version); + return false; + } + + uint entry_count = reader.ReadUInt32(); + uint index_entry_count = reader.ReadUInt32(); + uint index_size = reader.ReadUInt32(); + + header = new Header(magic, version, entry_count, index_entry_count, index_size); + return true; + } + + protected override void Prepare() + { + if (header == null) + { + throw new InvalidOperationException("Internal error: header not set, was IsSupported() called?"); + } + + TargetArch = (header.version & ASSEMBLY_STORE_ABI_MASK) switch + { + ASSEMBLY_STORE_ABI_AARCH64 => AndroidTargetArch.Arm64, + ASSEMBLY_STORE_ABI_ARM => AndroidTargetArch.Arm, + ASSEMBLY_STORE_ABI_X64 => AndroidTargetArch.X86_64, + ASSEMBLY_STORE_ABI_X86 => AndroidTargetArch.X86, + _ => throw new NotSupportedException($"Unsupported ABI in store version: 0x{header.version:x}") + }; + + Is64Bit = (header.version & ASSEMBLY_STORE_FORMAT_VERSION_MASK) != 0; + AssemblyCount = header.entry_count; + IndexEntryCount = header.index_entry_count; + + StoreStream.Seek((long)elfOffset + Header.NativeSize, SeekOrigin.Begin); + using var reader = CreateReader(); + + var index = new List(); + for (uint i = 0; i < header.index_entry_count; i++) + { + ulong name_hash; + if (Is64Bit) + { + name_hash = reader.ReadUInt64(); + } + else + { + name_hash = (ulong)reader.ReadUInt32(); + } + + uint descriptor_index = reader.ReadUInt32(); + index.Add(new IndexEntry(name_hash, descriptor_index)); + } + + var descriptors = new List(); + for (uint i = 0; i < header.entry_count; i++) + { + uint mapping_index = reader.ReadUInt32(); + uint data_offset = reader.ReadUInt32(); + uint data_size = reader.ReadUInt32(); + uint debug_data_offset = reader.ReadUInt32(); + uint debug_data_size = reader.ReadUInt32(); + uint config_data_offset = reader.ReadUInt32(); + uint config_data_size = reader.ReadUInt32(); + + var desc = new EntryDescriptor + { + mapping_index = mapping_index, + data_offset = data_offset, + data_size = data_size, + debug_data_offset = debug_data_offset, + debug_data_size = debug_data_size, + config_data_offset = config_data_offset, + config_data_size = config_data_size, + }; + descriptors.Add(desc); + } + + var names = new List(); + for (uint i = 0; i < header.entry_count; i++) + { + uint name_length = reader.ReadUInt32(); + byte[] name_bytes = reader.ReadBytes((int)name_length); + names.Add(Encoding.UTF8.GetString(name_bytes)); + } + + var tempItems = new Dictionary(); + foreach (IndexEntry ie in index) + { + if (!tempItems.TryGetValue(ie.descriptor_index, out TemporaryItem? item)) + { + item = new TemporaryItem(names[(int)ie.descriptor_index], descriptors[(int)ie.descriptor_index]); + tempItems.Add(ie.descriptor_index, item); + } + item.IndexEntries.Add(ie); + } + + if (tempItems.Count != descriptors.Count) + { + throw new InvalidOperationException($"Assembly store '{StorePath}' index is corrupted."); + } + + var storeItems = new List(); + foreach (var kvp in tempItems) + { + TemporaryItem ti = kvp.Value; + var item = new StoreItemV2(TargetArch, ti.Name, Is64Bit, ti.IndexEntries, ti.Descriptor); + storeItems.Add(item); + } + Assemblies = storeItems.AsReadOnly(); + } +} diff --git a/src/Sentry.Android.AssemblyReader/V2/Utils.cs b/src/Sentry.Android.AssemblyReader/V2/Utils.cs new file mode 100644 index 0000000000..750f0eea62 --- /dev/null +++ b/src/Sentry.Android.AssemblyReader/V2/Utils.cs @@ -0,0 +1,169 @@ +/* + * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/Utils.cs + * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) + */ +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; +using Machine = ELFSharp.ELF.Machine; + +namespace Sentry.Android.AssemblyReader.V2; + +internal static class Utils +{ + private static readonly string[] AabZipEntries = { + "base/manifest/AndroidManifest.xml", + "BundleConfig.pb", + }; + private static readonly string[] AabBaseZipEntries = { + "manifest/AndroidManifest.xml", + }; + private static readonly string[] ApkZipEntries = { + "AndroidManifest.xml", + }; + + public const uint ZipMagic = 0x4034b50; + public const uint AssemblyStoreMagic = 0x41424158; + public const uint ELFMagic = 0x464c457f; + + public static readonly ArrayPool BytePool = ArrayPool.Shared; + + public static (ulong offset, ulong size, ELFPayloadError error) FindELFPayloadSectionOffsetAndSize(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + var elfClass = ELFReader.CheckELFType(stream); + if (elfClass == Class.NotELF) + { + return ReturnError(null, ELFPayloadError.NotELF); + } + + if (!ELFReader.TryLoad(stream, shouldOwnStream: false, out IELF? elf)) + { + return ReturnError(elf, ELFPayloadError.LoadFailed); + } + + if (elf.Type != FileType.SharedObject) + { + return ReturnError(elf, ELFPayloadError.NotSharedLibrary); + } + + if (elf.Endianess != ELFSharp.Endianess.LittleEndian) + { + return ReturnError(elf, ELFPayloadError.NotLittleEndian); + } + + if (!elf.TryGetSection("payload", out ISection? payloadSection)) + { + return ReturnError(elf, ELFPayloadError.NoPayloadSection); + } + + var is64 = elf.Machine switch + { + Machine.ARM => false, + Machine.Intel386 => false, + + Machine.AArch64 => true, + Machine.AMD64 => true, + + _ => throw new NotSupportedException($"Unsupported ELF architecture '{elf.Machine}'") + }; + + ulong offset; + ulong size; + + if (is64) + { + (offset, size) = GetOffsetAndSize64((Section)payloadSection); + } + else + { + (offset, size) = GetOffsetAndSize32((Section)payloadSection); + } + + elf.Dispose(); + return (offset, size, ELFPayloadError.None); + + (ulong offset, ulong size) GetOffsetAndSize64(Section payload) + { + return (payload.Offset, payload.Size); + } + + (ulong offset, ulong size) GetOffsetAndSize32(Section payload) + { + return ((ulong)payload.Offset, (ulong)payload.Size); + } + + (ulong offset, ulong size, ELFPayloadError error) ReturnError(IELF? elf, ELFPayloadError error) + { + elf?.Dispose(); + + return (0, 0, error); + } + } + + public static (FileFormat format, FileInfo? info) DetectFileFormat(string path) + { + if (string.IsNullOrEmpty(path)) + { + return (FileFormat.Unknown, null); + } + + var info = new FileInfo(path); + if (!info.Exists) + { + return (FileFormat.Unknown, null); + } + + using var reader = new BinaryReader(info.OpenRead()); + + // ATM, all formats we recognize have 4-byte magic at the start + var format = reader.ReadUInt32() switch + { + ZipMagic => FileFormat.Zip, + ELFMagic => FileFormat.ELF, + AssemblyStoreMagic => FileFormat.AssemblyStore, + _ => FileFormat.Unknown + }; + + if (format == FileFormat.Unknown || format != FileFormat.Zip) + { + return (format, info); + } + + return (DetectAndroidArchive(info, format), info); + } + + private static FileFormat DetectAndroidArchive(FileInfo info, FileFormat defaultFormat) + { + using var zip = ZipFile.OpenRead(info.FullName); + + if (HasAllEntries(zip, AabZipEntries)) + { + return FileFormat.Aab; + } + + if (HasAllEntries(zip, ApkZipEntries)) + { + return FileFormat.Apk; + } + + if (HasAllEntries(zip, AabBaseZipEntries)) + { + return FileFormat.AabBase; + } + + return defaultFormat; + + static bool HasAllEntries(ZipArchive zip, string[] entries) + { + foreach (var entry in entries) + { + if (zip.GetEntry(entry) is null) + { + return false; + } + } + + return true; + } + } +} diff --git a/test/AndroidTestApp/AndroidTestApp.csproj b/test/AndroidTestApp/AndroidTestApp.csproj index 15dedbe4c9..3d8a1981e1 100644 --- a/test/AndroidTestApp/AndroidTestApp.csproj +++ b/test/AndroidTestApp/AndroidTestApp.csproj @@ -1,6 +1,7 @@ - net8.0-android34.0 + net8.0-android;net9.0-android + false 21 Exe enable diff --git a/test/Directory.Build.props b/test/Directory.Build.props index d6cc562cef..4cc80884d9 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -46,7 +46,7 @@ - + diff --git a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs index 4b98a43257..4a9985f827 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs +++ b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs @@ -1,15 +1,27 @@ +using Sentry.Android.AssemblyReader.V1; +using Sentry.Android.AssemblyReader.V2; + namespace Sentry.Android.AssemblyReader.Tests; public class AndroidAssemblyReaderTests { private readonly ITestOutputHelper _output; +#if NET9_0 + private static string TargetFramework => "net9.0"; +#elif NET8_0 + private static string TargetFramework => "net8.0"; +#else + // Adding a new TFM to the project? Include it above +#error "Target Framework not yet supported for AndroidAssemblyReader" +#endif + public AndroidAssemblyReaderTests(ITestOutputHelper output) { _output = output; } - private IAndroidAssemblyReader GetSut(bool isAssemblyStore, bool isCompressed) + private IAndroidAssemblyReader GetSut(bool isAot, bool isAssemblyStore, bool isCompressed) { #if ANDROID var logger = new TestOutputDiagnosticLogger(_output); @@ -19,33 +31,55 @@ private IAndroidAssemblyReader GetSut(bool isAssemblyStore, bool isCompressed) Path.GetFullPath(Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "..", "..", "..", "TestAPKs", - $"android-Store={isAssemblyStore}-Compressed={isCompressed}.apk")); + $"{TargetFramework}-android-A={isAot}-S={isAssemblyStore}-C={isCompressed}.apk")); _output.WriteLine($"Checking if APK exists: {apkPath}"); File.Exists(apkPath).Should().BeTrue(); + // Note: This needs to match the RID used when publishing the test APK string[] supportedAbis = { "x86_64" }; return AndroidAssemblyReaderFactory.Open(apkPath, supportedAbis, logger: (message, args) => _output.WriteLine(message, args)); #endif } - [SkippableTheory] - [InlineData(false)] - [InlineData(true)] - public void CreatesCorrectReader(bool isAssemblyStore) + [SkippableFact] + public void CreatesCorrectStoreReader() { #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(isAssemblyStore, isCompressed: true); - if (isAssemblyStore) + using var sut = GetSut(isAot: false, isAssemblyStore: true, isCompressed: true); + switch (TargetFramework) { - Assert.IsType(sut); + case "net9.0": + Assert.IsType(sut); + break; + case "net8.0": + Assert.IsType(sut); + break; + default: + throw new NotSupportedException($"Unsupported target framework: {TargetFramework}"); } - else + } + + [SkippableFact] + public void CreatesCorrectArchiveReader() + { +#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) { - Assert.IsType(sut); + case "net9.0": + Assert.IsType(sut); + break; + case "net8.0": + Assert.IsType(sut); + break; + default: + throw new NotSupportedException($"Unsupported target framework: {TargetFramework}"); } } @@ -54,27 +88,32 @@ public void CreatesCorrectReader(bool isAssemblyStore) [InlineData(true)] public void ReturnsNullIfAssemblyDoesntExist(bool isAssemblyStore) { - using var sut = GetSut(isAssemblyStore, isCompressed: true); + using var sut = GetSut(isAot: false, isAssemblyStore, isCompressed: true); Assert.Null(sut.TryReadAssembly("NonExistent.dll")); } + public static IEnumerable ReadsAssemblyPermutations => +#if NET8_0 + from isAot in new[] { false } +#else + from isAot in new[] { true, false } +#endif + from isStore in new[] { true, false } + from isCompressed in new[] { true, false } + from assemblyName in new[] { "Mono.Android.dll", "System.Private.CoreLib.dll" } + select new object[] { isAot, isStore, isCompressed, assemblyName }; + [SkippableTheory] - [InlineData(false, true, "Mono.Android.dll")] - [InlineData(false, false, "Mono.Android.dll")] - [InlineData(false, true, "System.Runtime.dll")] - [InlineData(false, false, "System.Runtime.dll")] - [InlineData(true, true, "Mono.Android.dll")] - [InlineData(true, false, "Mono.Android.dll")] - [InlineData(true, true, "System.Runtime.dll")] - [InlineData(true, false, "System.Runtime.dll")] - public void ReadsAssembly(bool isAssemblyStore, bool isCompressed, string assemblyName) + [MemberData(nameof(ReadsAssemblyPermutations))] + public void ReadsAssembly(bool isAot, bool isAssemblyStore, bool isCompressed, string assemblyName) { #if ANDROID - // No need to run all combinations - we only test the current APK which is (likely) compressed assembly store. + // No need to run all combinations - we only test the current APK which is likely JIT compressed assembly store. + Skip.If(isAot); Skip.If(!isAssemblyStore); Skip.If(!isCompressed); #endif - using var sut = GetSut(isAssemblyStore, isCompressed); + using var sut = GetSut(isAot, isAssemblyStore, isCompressed); var peReader = sut.TryReadAssembly(assemblyName); Assert.NotNull(peReader); 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 d6dd9fc202..ee88950c58 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj +++ b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj @@ -2,7 +2,8 @@ net9.0;net8.0 - $(TargetFrameworks);net8.0-android34.0 + + $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 enable @@ -11,23 +12,43 @@ + + + + + + - - - - - + + + + + + + + + - <_ConfigString>Store=$(_Store)-Compressed=$(_Compressed) + <_ConfigString>A=$(_Aot)-S=$(_Store)-C=$(_Compressed) + ..\AndroidTestApp\bin\$(TargetFramework)\$(_ConfigString)\com.companyname.AndroidTestApp-Signed.apk + TestAPKs\$(TargetFramework)-$(_ConfigString).apk + + + + + + + + True + False - - + diff --git a/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj b/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj index 947d10b537..7b0d03edad 100644 --- a/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj +++ b/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj @@ -2,9 +2,9 @@ net9.0;net8.0;net48 - $(TargetFrameworks);net8.0-android34.0 - $(TargetFrameworks);net8.0-ios17.0 - $(TargetFrameworks);net8.0-maccatalyst17.0 + $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 + $(TargetFrameworks);net8.0-ios17.0;net9.0-ios18.0 + $(TargetFrameworks);net8.0-maccatalyst17.0;net9.0-maccatalyst18.0 diff --git a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj index 5953b65d70..f08b24c8c4 100644 --- a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj +++ b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj @@ -2,8 +2,8 @@ - $(TargetFrameworks);net8.0-android34.0 - $(TargetFrameworks);net8.0-ios17.0 + $(TargetFrameworks);net8.0-android;net9.0-android + $(TargetFrameworks);net8.0-ios;net9.0-ios true