diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/AssemblyRebuilderTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/AssemblyRebuilderTests.cs new file mode 100644 index 00000000000..de8eb1cccaf --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/AssemblyRebuilderTests.cs @@ -0,0 +1,567 @@ +using System; +using System.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text; +using NUnit.Framework; +using Xamarin.Android.Tasks.JniRemapping; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + [Parallelizable (ParallelScope.Children)] + public class AssemblyRebuilderTests : BaseTest + { + [Test] + public void EmptyPlanPreservesMappedFieldDataAndResources () + { + var fixture = new JniFixtureBuilder (); + byte [] fieldData = { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04 }; + byte [] resourceData = { 1, 2, 3, 4, 5, 6, 7 }; + fixture.AddEmbeddedResource ("Fixture.resources", resourceData); + + TypeDefinitionHandle enclosing = fixture.EnsurePrivateImplementationDetails (); + TypeDefinitionHandle dataType = fixture.AddType (null, "__StaticArrayInitTypeSize=8", fixture.NextFieldRid, fixture.NextMethodRid, + TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + fixture.ValueTypeReference); + fixture.Metadata.AddTypeLayout (dataType, packingSize: 1, size: (uint) fieldData.Length); + fixture.Metadata.AddNestedType (dataType, enclosing); + + var signature = new BlobBuilder (); + new BlobEncoder (signature).FieldSignature ().Type (dataType, isValueType: true); + int rva = fixture.MappedFieldData.Count; + fixture.MappedFieldData.WriteBytes (fieldData); + FieldDefinitionHandle dataField = fixture.Metadata.AddFieldDefinition ( + FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA, + fixture.Metadata.GetOrAddString ("ArrayData"), fixture.Metadata.GetOrAddBlob (signature)); + fixture.Metadata.AddFieldRelativeVirtualAddress (dataField, rva); + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader before = sourcePe.GetMetadataReader (); + FieldRvaTable sourceFieldRvas = FieldRvaTable.Read (sourcePe, before); + + var result = new AssemblyRebuilder (sourcePe, before, new JniRewritePlan (), sourceFieldRvas).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + MetadataReader after = rebuiltPe.GetMetadataReader (); + FieldRvaTable rebuiltFieldRvas = FieldRvaTable.Read (rebuiltPe, after); + FieldRvaEntry rebuiltField = rebuiltFieldRvas.Get (dataField); + + Assert.IsFalse (result.StrongNameSignatureCleared); + Assert.IsNotNull (rebuiltField); + CollectionAssert.AreEqual (fieldData, rebuiltField.Data); + CollectionAssert.AreEqual (resourceData, ReadResource (rebuiltPe, after, "Fixture.resources")); + Assert.AreEqual (before.GetGuid (before.GetModuleDefinition ().Mvid), after.GetGuid (after.GetModuleDefinition ().Mvid)); + + for (int i = 0; i < MetadataTokens.TableCount; i++) { + var table = (TableIndex) i; + Assert.AreEqual (before.GetTableRowCount (table), after.GetTableRowCount (table), $"Row count of table '{table}' changed."); + } + } + + [Test] + public void RejectsFieldBackedImplMapRows () + { + var fixture = new JniFixtureBuilder (); + fixture.AddType ("Acme", "NativeData", fixture.NextFieldRid, fixture.NextMethodRid); + + var fieldSignature = new BlobBuilder (); + new BlobEncoder (fieldSignature).FieldSignature ().Int32 (); + FieldDefinitionHandle field = fixture.Metadata.AddFieldDefinition ( + FieldAttributes.Static | FieldAttributes.PinvokeImpl, + fixture.Metadata.GetOrAddString ("NativeField"), + fixture.Metadata.GetOrAddBlob (fieldSignature)); + + MethodDefinitionHandle method = fixture.AddVoidMethod ("Placeholder", fixture.EmitReturnOnlyBody ()); + ModuleReferenceHandle module = fixture.Metadata.AddModuleReference (fixture.Metadata.GetOrAddString ("native")); + fixture.Metadata.AddMethodImport ( + method, + MethodImportAttributes.CallingConventionCDecl, + fixture.Metadata.GetOrAddString ("native_field"), + module); + + byte [] source = fixture.Serialize (); + PatchImplMapMemberForwarded (source, field); + + using var peReader = new PEReader (ImmutableArray.Create (source)); + MetadataReader reader = peReader.GetMetadataReader (); + FieldRvaTable fieldRvas = FieldRvaTable.Read (peReader, reader); + + var ex = Assert.Throws (() => + new AssemblyRebuilder (peReader, reader, new JniRewritePlan (), fieldRvas).Build ()); + StringAssert.Contains ("field-backed ImplMap", ex.Message); + } + + [Test] + public void RejectsSwitchOperandWhoseSizeWouldOverflow () + { + const int switchOffset = 10; + var il = new byte [switchOffset + 5]; + il [switchOffset] = (byte) ILOpCode.Switch; + uint caseCount = (uint) ((int.MaxValue - sizeof (uint)) / sizeof (int)); + IlInstructionScanner.WriteUInt32 (il, switchOffset + 1, caseCount); + + var ex = Assert.Throws (() => + IlInstructionScanner.Walk (il, (_, _, _, _) => { })); + StringAssert.Contains ("extends past the end", ex.Message); + } + + [Test] + public void RejectsNegativeCompressedIntegerOffset () + { + var ex = Assert.Throws (() => + MetadataEncoding.ReadCompressedInteger (new byte [1], -1, out int _)); + StringAssert.Contains ("truncated compressed integer", ex.Message); + } + + [Test] + public void RejectsEmbeddedResourceOffsetOutsideInt32Range () + { + var fixture = new JniFixtureBuilder (); + fixture.AddEmbeddedResource ("Fixture.resources", new byte [] { 1 }); + byte [] source = fixture.Serialize (); + + int resourceOffset; + using (var sourcePe = new PEReader (ImmutableArray.Create (source))) { + MetadataReader reader = sourcePe.GetMetadataReader (); + resourceOffset = sourcePe.PEHeaders.MetadataStartOffset + + reader.GetTableMetadataOffset (TableIndex.ManifestResource); + } + WriteUInt32 (source, resourceOffset, uint.MaxValue); + + using var peReader = new PEReader (ImmutableArray.Create (source)); + MetadataReader metadata = peReader.GetMetadataReader (); + var ex = Assert.Throws (() => + new AssemblyRebuilder (peReader, metadata, new JniRewritePlan (), FieldRvaTable.Read (peReader, metadata)).Build ()); + StringAssert.Contains ("starts outside of the resources directory", ex.Message); + } + + [Test] + public void EmptyPlanPreservesFieldRvasFromDifferentSections () + { + var fixture = new JniFixtureBuilder (); + fixture.AddType ("Acme", "MappedData", fixture.NextFieldRid, fixture.NextMethodRid); + + var signature = new BlobBuilder (); + new BlobEncoder (signature).FieldSignature ().Int32 (); + BlobHandle signatureHandle = fixture.Metadata.GetOrAddBlob (signature); + + FieldDefinitionHandle firstField = AddMappedInt32Field (fixture, signatureHandle, "First", 0x12345678); + FieldDefinitionHandle secondField = AddMappedInt32Field (fixture, signatureHandle, "Second", 0x23456789); + + byte [] source = fixture.Serialize (); + MoveFieldRvaToAnotherSection (source, firstField, secondField); + + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader before = sourcePe.GetMetadataReader (); + FieldRvaTable sourceFieldRvas = FieldRvaTable.Read (sourcePe, before); + + var result = new AssemblyRebuilder (sourcePe, before, new JniRewritePlan (), sourceFieldRvas).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + MetadataReader after = rebuiltPe.GetMetadataReader (); + FieldRvaTable rebuiltFieldRvas = FieldRvaTable.Read (rebuiltPe, after); + + CollectionAssert.AreEqual (sourceFieldRvas.Get (firstField).Data, rebuiltFieldRvas.Get (firstField).Data); + CollectionAssert.AreEqual (sourceFieldRvas.Get (secondField).Data, rebuiltFieldRvas.Get (secondField).Data); + } + + [Test] + public void LongerUtf8FieldReplacementAddsCorrectlySizedNestedType () + { + const string replacement = "aMuchLongerReplacement"; + int replacementSize = Encoding.UTF8.GetByteCount (replacement) + 1; + var fixture = new JniFixtureBuilder (); + FieldDefinitionHandle field = fixture.AddUtf8Field ("x"); + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader before = sourcePe.GetMetadataReader (); + var plan = new JniRewritePlan (); + plan.AddUtf8FieldValue (field, replacement); + + var result = new AssemblyRebuilder (sourcePe, before, plan, FieldRvaTable.Read (sourcePe, before)).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + MetadataReader after = rebuiltPe.GetMetadataReader (); + FieldRvaEntry rebuiltField = GetRequiredFieldRva (rebuiltPe, after, field); + TypeDefinitionHandle sizedType = GetFieldValueType (after, field); + TypeDefinition type = after.GetTypeDefinition (sizedType); + TypeDefinitionHandle enclosing = type.GetDeclaringType (); + TypeLayout layout = type.GetLayout (); + + Assert.AreEqual (replacement, rebuiltField.Utf8Value); + Assert.AreEqual (before.GetTableRowCount (TableIndex.TypeDef) + 1, after.GetTableRowCount (TableIndex.TypeDef)); + Assert.AreEqual (before.GetTableRowCount (TableIndex.ClassLayout) + 1, after.GetTableRowCount (TableIndex.ClassLayout)); + Assert.AreEqual (before.GetTableRowCount (TableIndex.NestedClass) + 1, after.GetTableRowCount (TableIndex.NestedClass)); + Assert.AreEqual (FieldRvaTable.Utf8FieldNamePrefix + replacementSize, after.GetString (type.Name)); + Assert.AreEqual (JniFixtureBuilder.PrivateImplementationDetails, after.GetString (after.GetTypeDefinition (enclosing).Name)); + Assert.AreEqual (replacementSize, layout.Size); + Assert.AreEqual (1, layout.PackingSize); + + AssertTableRowCountsMatchExcept (before, after, TableIndex.TypeDef, TableIndex.ClassLayout, TableIndex.NestedClass); + } + + [Test] + public void RejectsIncompatibleExistingUtf8SizedType () + { + const string replacement = "longer"; + int replacementSize = Encoding.UTF8.GetByteCount (replacement) + 1; + var fixture = new JniFixtureBuilder (); + FieldDefinitionHandle field = fixture.AddUtf8Field ("x"); + TypeDefinitionHandle enclosing = fixture.EnsurePrivateImplementationDetails (); + TypeDefinitionHandle incompatible = fixture.AddType (null, FieldRvaTable.Utf8FieldNamePrefix + replacementSize, + fixture.NextFieldRid, fixture.NextMethodRid, + TypeAttributes.NestedAssembly | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + fixture.ValueTypeReference); + fixture.Metadata.AddTypeLayout (incompatible, packingSize: 1, size: (uint) replacementSize + 1); + fixture.Metadata.AddNestedType (incompatible, enclosing); + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader reader = sourcePe.GetMetadataReader (); + var plan = new JniRewritePlan (); + plan.AddUtf8FieldValue (field, replacement); + + var ex = Assert.Throws (() => + new AssemblyRebuilder (sourcePe, reader, plan, FieldRvaTable.Read (sourcePe, reader)).Build ()); + StringAssert.Contains ("incompatible layout", ex.Message); + } + + [Test] + public void ShorterUtf8FieldReplacementPreservesTypeAndZeroFillsSlot () + { + var fixture = new JniFixtureBuilder (); + FieldDefinitionHandle field = fixture.AddUtf8Field ("longOriginalValue"); + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader before = sourcePe.GetMetadataReader (); + TypeDefinitionHandle originalType = GetFieldValueType (before, field); + int originalSize = GetRequiredFieldRva (sourcePe, before, field).Data.Length; + var plan = new JniRewritePlan (); + plan.AddUtf8FieldValue (field, "x"); + + var result = new AssemblyRebuilder (sourcePe, before, plan, FieldRvaTable.Read (sourcePe, before)).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + MetadataReader after = rebuiltPe.GetMetadataReader (); + FieldRvaEntry rebuiltField = GetRequiredFieldRva (rebuiltPe, after, field); + + Assert.AreEqual (originalType, GetFieldValueType (after, field)); + Assert.AreEqual ("x", rebuiltField.Utf8Value); + Assert.AreEqual (originalSize, rebuiltField.Data.Length); + Assert.AreEqual ((byte) 'x', rebuiltField.Data [0]); + for (int i = 1; i < rebuiltField.Data.Length; i++) { + Assert.AreEqual (0, rebuiltField.Data [i], $"Mapped field byte {i} was not zero-filled."); + } + AssertTableRowCountsMatchExcept (before, after); + } + + [Test] + public void ShorterUtf8FieldReplacementDoesNotModifyAliasedField () + { + const string original = "longOriginalValue"; + var fixture = new JniFixtureBuilder (); + (FieldDefinitionHandle field, FieldDefinitionHandle alias) = AddAliasedUtf8Fields (fixture, original); + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader before = sourcePe.GetMetadataReader (); + TypeDefinitionHandle originalType = GetFieldValueType (before, field); + byte [] aliasData = GetRequiredFieldRva (sourcePe, before, alias).Data; + var plan = new JniRewritePlan (); + plan.AddUtf8FieldValue (field, "x"); + + var result = new AssemblyRebuilder (sourcePe, before, plan, FieldRvaTable.Read (sourcePe, before)).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + MetadataReader after = rebuiltPe.GetMetadataReader (); + FieldRvaEntry rebuiltField = GetRequiredFieldRva (rebuiltPe, after, field); + FieldRvaEntry rebuiltAlias = GetRequiredFieldRva (rebuiltPe, after, alias); + + Assert.AreEqual (originalType, GetFieldValueType (after, field)); + Assert.AreEqual ("x", rebuiltField.Utf8Value); + CollectionAssert.AreEqual (aliasData, rebuiltAlias.Data); + Assert.AreNotEqual (rebuiltField.RelativeVirtualAddress, rebuiltAlias.RelativeVirtualAddress); + AssertTableRowCountsMatchExcept (before, after); + } + + [Test] + public void SharedUserStringCanBeReplacedAtOneUseSite () + { + var fixture = new JniFixtureBuilder (); + UserStringHandle shared = fixture.String ("shared"); + MethodDefinitionHandle method = fixture.AddVoidMethod ("LoadShared", fixture.EmitLoadStringBody (shared, shared)); + fixture.AddType ("Acme", "StringUser", fixture.NextFieldRid, MetadataTokens.GetRowNumber (method)); + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader before = sourcePe.GetMetadataReader (); + var plan = new JniRewritePlan (); + plan.AddUserString (method, operandOffset: 1, "changed"); + + var result = new AssemblyRebuilder (sourcePe, before, plan, FieldRvaTable.Read (sourcePe, before)).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + MetadataReader after = rebuiltPe.GetMetadataReader (); + byte [] il = rebuiltPe.GetMethodBody (after.GetMethodDefinition (method).RelativeVirtualAddress).GetILBytes (); + var tokens = new System.Collections.Generic.List (); + var values = new System.Collections.Generic.List (); + IlInstructionScanner.Walk (il, (code, _, operandOffset, _) => { + if (code != (ushort) ILOpCode.Ldstr) { + return; + } + int token = checked ((int) IlInstructionScanner.ReadUInt32 (il, operandOffset)); + tokens.Add (token); + values.Add (after.GetUserString (MetadataTokens.UserStringHandle (token & 0x00FFFFFF))); + }); + + CollectionAssert.AreEqual (new [] { "changed", "shared" }, values); + Assert.AreEqual (2, tokens.Count); + Assert.AreNotEqual (tokens [0], tokens [1], "The two use sites should resolve through distinct #US tokens."); + } + + [Test] + public void StrongNameFlagIsClearedButSignatureSpaceIsPreserved () + { + var fixture = new JniFixtureBuilder (hasPublicKey: true) { + Flags = CorFlags.ILOnly | CorFlags.StrongNameSigned, + StrongNameSignatureSize = 128, + }; + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader reader = sourcePe.GetMetadataReader (); + + var result = new AssemblyRebuilder (sourcePe, reader, new JniRewritePlan (), FieldRvaTable.Read (sourcePe, reader)).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + CorHeader corHeader = rebuiltPe.PEHeaders.CorHeader; + Assert.IsTrue (result.StrongNameSignatureCleared); + Assert.AreEqual ((CorFlags) 0, corHeader.Flags & CorFlags.StrongNameSigned); + Assert.AreEqual (128, corHeader.StrongNameSignatureDirectory.Size); + Assert.AreNotEqual (0, corHeader.StrongNameSignatureDirectory.RelativeVirtualAddress); + } + + [Test] + public void RejectsInvalidStrongNameSignatureDirectorySize () + { + var fixture = new JniFixtureBuilder (hasPublicKey: true) { + Flags = CorFlags.ILOnly | CorFlags.StrongNameSigned, + StrongNameSignatureSize = 128, + }; + byte [] source = fixture.Serialize (); + + int strongNameSizeOffset; + using (var sourcePe = new PEReader (ImmutableArray.Create (source))) { + const int StrongNameSignatureDirectoryOffset = 32; + strongNameSizeOffset = sourcePe.PEHeaders.CorHeaderStartOffset + + StrongNameSignatureDirectoryOffset + sizeof (uint); + } + WriteUInt32 (source, strongNameSizeOffset, uint.MaxValue); + + using var peReader = new PEReader (ImmutableArray.Create (source)); + MetadataReader metadata = peReader.GetMetadataReader (); + var ex = Assert.Throws (() => + new AssemblyRebuilder (peReader, metadata, new JniRewritePlan (), FieldRvaTable.Read (peReader, metadata)).Build ()); + StringAssert.Contains ("invalid strong-name signature directory", ex.Message); + } + + [Test] + public void EmptyPlanPreservesEventAndPropertyAccessors () + { + var fixture = new JniFixtureBuilder (); + int methodStart = fixture.NextMethodRid; + MethodDefinitionHandle event1Adder = fixture.AddVoidMethod ("add_Event1", fixture.EmitReturnOnlyBody ()); + MethodDefinitionHandle event1Remover = fixture.AddVoidMethod ("remove_Event1", fixture.EmitReturnOnlyBody ()); + MethodDefinitionHandle propertyGetter = fixture.AddVoidMethod ("get_Value", fixture.EmitReturnOnlyBody ()); + MethodDefinitionHandle event2Adder = fixture.AddVoidMethod ("add_Event2", fixture.EmitReturnOnlyBody ()); + MethodDefinitionHandle event2Remover = fixture.AddVoidMethod ("remove_Event2", fixture.EmitReturnOnlyBody ()); + TypeDefinitionHandle type = fixture.AddType ("Acme", "Members", fixture.NextFieldRid, methodStart); + + EventDefinitionHandle event1 = fixture.Metadata.AddEvent ( + EventAttributes.None, fixture.Metadata.GetOrAddString ("Event1"), fixture.ExceptionReference); + EventDefinitionHandle event2 = fixture.Metadata.AddEvent ( + EventAttributes.None, fixture.Metadata.GetOrAddString ("Event2"), fixture.ExceptionReference); + var propertySignature = new BlobBuilder (); + new BlobEncoder (propertySignature).PropertySignature (isInstanceProperty: true) + .Parameters (0, out ReturnTypeEncoder returnType, out ParametersEncoder _); + returnType.Type ().Int32 (); + PropertyDefinitionHandle property = fixture.Metadata.AddProperty ( + PropertyAttributes.None, fixture.Metadata.GetOrAddString ("Value"), fixture.Metadata.GetOrAddBlob (propertySignature)); + + fixture.Metadata.AddEventMap (type, event1); + fixture.Metadata.AddPropertyMap (type, property); + fixture.Metadata.AddMethodSemantics (event1, MethodSemanticsAttributes.Adder, event1Adder); + fixture.Metadata.AddMethodSemantics (event1, MethodSemanticsAttributes.Remover, event1Remover); + fixture.Metadata.AddMethodSemantics (property, MethodSemanticsAttributes.Getter, propertyGetter); + fixture.Metadata.AddMethodSemantics (event2, MethodSemanticsAttributes.Adder, event2Adder); + fixture.Metadata.AddMethodSemantics (event2, MethodSemanticsAttributes.Remover, event2Remover); + + byte [] source = fixture.Serialize (); + using var sourcePe = new PEReader (ImmutableArray.Create (source)); + MetadataReader before = sourcePe.GetMetadataReader (); + var result = new AssemblyRebuilder ( + sourcePe, before, new JniRewritePlan (), FieldRvaTable.Read (sourcePe, before)).Build (); + + using var rebuiltPe = new PEReader (ImmutableArray.Create (result.Image)); + MetadataReader after = rebuiltPe.GetMetadataReader (); + Assert.AreEqual (before.GetEventDefinition (event1).GetAccessors ().Adder, after.GetEventDefinition (event1).GetAccessors ().Adder); + Assert.AreEqual (before.GetEventDefinition (event1).GetAccessors ().Remover, after.GetEventDefinition (event1).GetAccessors ().Remover); + Assert.AreEqual (before.GetPropertyDefinition (property).GetAccessors ().Getter, after.GetPropertyDefinition (property).GetAccessors ().Getter); + Assert.AreEqual (before.GetEventDefinition (event2).GetAccessors ().Adder, after.GetEventDefinition (event2).GetAccessors ().Adder); + Assert.AreEqual (before.GetEventDefinition (event2).GetAccessors ().Remover, after.GetEventDefinition (event2).GetAccessors ().Remover); + AssertTableRowCountsMatchExcept (before, after); + } + + static FieldRvaEntry GetRequiredFieldRva (PEReader peReader, MetadataReader reader, FieldDefinitionHandle field) + { + FieldRvaEntry? entry = FieldRvaTable.Read (peReader, reader).Get (field); + if (entry == null) { + throw new AssertionException ($"Field 0x{MetadataTokens.GetToken (field):X8} has no FieldRVA row."); + } + return entry; + } + + static TypeDefinitionHandle GetFieldValueType (MetadataReader reader, FieldDefinitionHandle field) + { + BlobReader signature = reader.GetBlobReader (reader.GetFieldDefinition (field).Signature); + Assert.AreEqual (SignatureKind.Field, signature.ReadSignatureHeader ().Kind); + Assert.AreEqual ((int) SignatureTypeKind.ValueType, signature.ReadCompressedInteger ()); + EntityHandle type = signature.ReadTypeHandle (); + Assert.AreEqual (HandleKind.TypeDefinition, type.Kind); + return (TypeDefinitionHandle) type; + } + + static void AssertTableRowCountsMatchExcept (MetadataReader expected, MetadataReader actual, params TableIndex [] except) + { + for (int i = 0; i < MetadataTokens.TableCount; i++) { + var table = (TableIndex) i; + if (Array.IndexOf (except, table) >= 0) { + continue; + } + Assert.AreEqual (expected.GetTableRowCount (table), actual.GetTableRowCount (table), $"Row count of table '{table}' changed."); + } + } + + static FieldDefinitionHandle AddMappedInt32Field (JniFixtureBuilder fixture, BlobHandle signature, string name, int value) + { + int rva = fixture.MappedFieldData.Count; + fixture.MappedFieldData.WriteInt32 (value); + FieldDefinitionHandle field = fixture.Metadata.AddFieldDefinition ( + FieldAttributes.Static | FieldAttributes.HasFieldRVA, + fixture.Metadata.GetOrAddString (name), + signature); + fixture.Metadata.AddFieldRelativeVirtualAddress (field, rva); + return field; + } + + static (FieldDefinitionHandle Field, FieldDefinitionHandle Alias) AddAliasedUtf8Fields (JniFixtureBuilder fixture, string value) + { + int size = Encoding.UTF8.GetByteCount (value) + 1; + TypeDefinitionHandle enclosing = fixture.EnsurePrivateImplementationDetails (); + TypeDefinitionHandle dataType = fixture.AddType (null, FieldRvaTable.Utf8FieldNamePrefix + size, + fixture.NextFieldRid, fixture.NextMethodRid, + TypeAttributes.NestedAssembly | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + fixture.ValueTypeReference); + fixture.Metadata.AddTypeLayout (dataType, packingSize: 1, size: (uint) size); + fixture.Metadata.AddNestedType (dataType, enclosing); + + var signature = new BlobBuilder (); + new BlobEncoder (signature).FieldSignature ().Type (dataType, isValueType: true); + BlobHandle signatureHandle = fixture.Metadata.GetOrAddBlob (signature); + int rva = fixture.MappedFieldData.Count; + fixture.MappedFieldData.WriteUTF8 (value); + fixture.MappedFieldData.WriteByte (0); + + FieldDefinitionHandle field = fixture.Metadata.AddFieldDefinition ( + FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA | FieldAttributes.InitOnly, + fixture.Metadata.GetOrAddString (FieldRvaTable.Utf8FieldNamePrefix + "0"), signatureHandle); + fixture.Metadata.AddFieldRelativeVirtualAddress (field, rva); + FieldDefinitionHandle alias = fixture.Metadata.AddFieldDefinition ( + FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA | FieldAttributes.InitOnly, + fixture.Metadata.GetOrAddString ("Alias"), signatureHandle); + fixture.Metadata.AddFieldRelativeVirtualAddress (alias, rva); + return (field, alias); + } + + static void PatchImplMapMemberForwarded (byte [] image, FieldDefinitionHandle field) + { + int memberOffset; + using (var peReader = new PEReader (ImmutableArray.Create (image))) { + MetadataReader reader = peReader.GetMetadataReader (); + Assert.AreEqual (1, reader.GetTableRowCount (TableIndex.ImplMap)); + memberOffset = peReader.PEHeaders.MetadataStartOffset + + reader.GetTableMetadataOffset (TableIndex.ImplMap) + sizeof (ushort); + } + + ushort codedIndex = checked ((ushort) (MetadataTokens.GetRowNumber (field) << 1)); + image [memberOffset] = (byte) codedIndex; + image [memberOffset + 1] = (byte) (codedIndex >> 8); + } + + static void WriteUInt32 (byte [] image, int offset, uint value) + { + image [offset] = (byte) value; + image [offset + 1] = (byte) (value >> 8); + image [offset + 2] = (byte) (value >> 16); + image [offset + 3] = (byte) (value >> 24); + } + + static void MoveFieldRvaToAnotherSection ( + byte [] image, + FieldDefinitionHandle firstField, + FieldDefinitionHandle fieldToMove) + { + int fieldRvaOffset; + int targetRva = 0; + using (var peReader = new PEReader (ImmutableArray.Create (image))) { + MetadataReader reader = peReader.GetMetadataReader (); + int firstRva = reader.GetFieldDefinition (firstField).GetRelativeVirtualAddress (); + + foreach (SectionHeader section in peReader.PEHeaders.SectionHeaders) { + int sectionSize = Math.Max (section.VirtualSize, section.SizeOfRawData); + bool containsFirstField = firstRva >= section.VirtualAddress && firstRva - section.VirtualAddress < sectionSize; + if (!containsFirstField && peReader.GetSectionData (section.VirtualAddress).Length >= sizeof (int)) { + targetRva = section.VirtualAddress; + break; + } + } + + Assert.AreNotEqual (0, targetRva, "The fixture needs a second non-empty PE section."); + int row = MetadataTokens.GetRowNumber (fieldToMove); + fieldRvaOffset = peReader.PEHeaders.MetadataStartOffset + + reader.GetTableMetadataOffset (TableIndex.FieldRva) + + (row - 1) * reader.GetTableRowSize (TableIndex.FieldRva); + } + + image [fieldRvaOffset] = (byte) targetRva; + image [fieldRvaOffset + 1] = (byte) (targetRva >> 8); + image [fieldRvaOffset + 2] = (byte) (targetRva >> 16); + image [fieldRvaOffset + 3] = (byte) (targetRva >> 24); + } + + static byte [] ReadResource (PEReader peReader, MetadataReader reader, string name) + { + foreach (ManifestResourceHandle handle in reader.ManifestResources) { + ManifestResource resource = reader.GetManifestResource (handle); + if (reader.GetString (resource.Name) != name) { + continue; + } + + DirectoryEntry directory = peReader.PEHeaders.CorHeader.ResourcesDirectory; + PEMemoryBlock block = peReader.GetSectionData (directory.RelativeVirtualAddress); + int offset = checked ((int) resource.Offset); + int size = block.GetReader (offset, sizeof (int)).ReadInt32 (); + return block.GetReader (offset + sizeof (int), size).ReadBytes (size); + } + + Assert.Fail ($"Resource '{name}' is missing."); + return []; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniFixtureBuilder.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniFixtureBuilder.cs new file mode 100644 index 00000000000..70030b6d71c --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniFixtureBuilder.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text; + +namespace Xamarin.Android.Build.Tests +{ + /// + /// Builds small but structurally faithful managed PE fixtures with System.Reflection.Metadata + /// only, so the JNI rewriter can be exercised end to end without any external assembly. + /// + class JniFixtureBuilder + { + public const string RegisterAttributeNamespace = "Android.Runtime"; + public const string RegisterAttributeName = "RegisterAttribute"; + public const string JavaInteropNamespace = "Java.Interop"; + public const string RuntimeInteropServicesNamespace = "System.Runtime.InteropServices"; + public const string PrivateImplementationDetails = ""; + + public MetadataBuilder Metadata { get; } = new MetadataBuilder (); + public BlobBuilder Il { get; } = new BlobBuilder (); + public BlobBuilder MappedFieldData { get; } = new BlobBuilder (); + public BlobBuilder ManagedResources { get; } = new BlobBuilder (); + + public Guid Mvid { get; } = Guid.NewGuid (); + public uint TimeDateStamp { get; } = 0x5A5A1234; + public CorFlags Flags { get; set; } = CorFlags.ILOnly; + public int StrongNameSignatureSize { get; set; } + public DebugDirectoryBuilder DebugDirectory { get; set; } + public ResourceSectionBuilder NativeResources { get; set; } + + public AssemblyReferenceHandle CoreLibraryReference { get; } + public TypeReferenceHandle ValueTypeReference { get; } + public TypeReferenceHandle JavaPeerProxyReference { get; } + public TypeReferenceHandle ExceptionReference { get; } + + public MethodDefinitionHandle RegisterCtor1 { get; } + public MethodDefinitionHandle RegisterCtor3 { get; } + public MethodDefinitionHandle JniTypeSignatureCtor1 { get; } + public MethodDefinitionHandle JniMethodSignatureCtor2 { get; } + public MethodDefinitionHandle JniConstructorSignatureCtor1 { get; } + public MethodDefinitionHandle JavaPeerAliasesCtor1 { get; } + public MemberReferenceHandle TypeMapCtor3 { get; } + + readonly MethodBodyStreamEncoder bodyEncoder; + TypeDefinitionHandle privateImplementationDetails; + readonly Dictionary sizedTypes = new Dictionary (); + int utf8FieldCounter; + + public JniFixtureBuilder (bool hasPublicKey = false) + { + bodyEncoder = new MethodBodyStreamEncoder (Il); + + Metadata.AddModule (0, Metadata.GetOrAddString ("Fixture.dll"), Metadata.GetOrAddGuid (Mvid), default, default); + BlobHandle publicKey = hasPublicKey ? Metadata.GetOrAddBlob (new byte [] { 1, 2, 3, 4, 5, 6, 7, 8 }) : default; + AssemblyFlags assemblyFlags = hasPublicKey ? AssemblyFlags.PublicKey : 0; + Metadata.AddAssembly (Metadata.GetOrAddString ("Fixture"), new Version (1, 0, 0, 0), default, publicKey, assemblyFlags, AssemblyHashAlgorithm.Sha1); + Metadata.AddTypeDefinition (default, default, Metadata.GetOrAddString (""), default, + MetadataTokens.FieldDefinitionHandle (1), MetadataTokens.MethodDefinitionHandle (1)); + + Version? coreLibraryVersion = typeof (object).Assembly.GetName ().Version; + if (coreLibraryVersion == null) { + throw new InvalidOperationException ("The executing runtime's core assembly has no version."); + } + CoreLibraryReference = Metadata.AddAssemblyReference ( + Metadata.GetOrAddString ("System.Runtime"), coreLibraryVersion, default, default, default, default); + ValueTypeReference = Metadata.AddTypeReference (CoreLibraryReference, + Metadata.GetOrAddString ("System"), Metadata.GetOrAddString ("ValueType")); + ExceptionReference = Metadata.AddTypeReference (CoreLibraryReference, + Metadata.GetOrAddString ("System"), Metadata.GetOrAddString ("Exception")); + JavaPeerProxyReference = Metadata.AddTypeReference (CoreLibraryReference, + Metadata.GetOrAddString (JavaInteropNamespace), Metadata.GetOrAddString ("JavaPeerProxy")); + + int fieldStart = NextFieldRid; + int methodStart = NextMethodRid; + RegisterCtor1 = AddAttributeCtor (1); + RegisterCtor3 = AddAttributeCtor (3); + AddType (RegisterAttributeNamespace, RegisterAttributeName, fieldStart, methodStart); + + fieldStart = NextFieldRid; + methodStart = NextMethodRid; + JniTypeSignatureCtor1 = AddAttributeCtor (1); + AddType (JavaInteropNamespace, "JniTypeSignatureAttribute", fieldStart, methodStart); + + fieldStart = NextFieldRid; + methodStart = NextMethodRid; + JniMethodSignatureCtor2 = AddAttributeCtor (2); + AddType (JavaInteropNamespace, "JniMethodSignatureAttribute", fieldStart, methodStart); + + fieldStart = NextFieldRid; + methodStart = NextMethodRid; + JniConstructorSignatureCtor1 = AddAttributeCtor (1); + AddType (JavaInteropNamespace, "JniConstructorSignatureAttribute", fieldStart, methodStart); + + fieldStart = NextFieldRid; + methodStart = NextMethodRid; + JavaPeerAliasesCtor1 = AddStringArrayAttributeCtor (); + AddType (JavaInteropNamespace, "JavaPeerAliasesAttribute", fieldStart, methodStart); + + TypeMapCtor3 = AddGenericStringAttributeCtor (RuntimeInteropServicesNamespace, "TypeMapAttribute`1", 3); + } + + public int NextFieldRid => Metadata.GetRowCount (TableIndex.Field) + 1; + + public int NextMethodRid => Metadata.GetRowCount (TableIndex.MethodDef) + 1; + + public TypeDefinitionHandle AddType (string ns, string name, int fieldStart, int methodStart, + TypeAttributes attributes = TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.BeforeFieldInit, + EntityHandle baseType = default) + => Metadata.AddTypeDefinition ( + attributes, + ns == null ? default : Metadata.GetOrAddString (ns), + Metadata.GetOrAddString (name), + baseType, + MetadataTokens.FieldDefinitionHandle (fieldStart), + MetadataTokens.MethodDefinitionHandle (methodStart)); + + public MethodDefinitionHandle AddVoidMethod (string name, int bodyOffset, + MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.HideBySig) + => Metadata.AddMethodDefinition ( + attributes, + MethodImplAttributes.IL, + Metadata.GetOrAddString (name), + Metadata.GetOrAddBlob (BuildVoidNoArgsSignature ()), + bodyOffset, + MetadataTokens.ParameterHandle (Metadata.GetRowCount (TableIndex.Param) + 1)); + + MethodDefinitionHandle AddAttributeCtor (int argCount) + { + var signature = new BlobBuilder (); + new BlobEncoder (signature).MethodSignature (isInstanceMethod: true) + .Parameters (argCount, out ReturnTypeEncoder returnType, out ParametersEncoder parameters); + returnType.Void (); + for (int i = 0; i < argCount; i++) { + parameters.AddParameter ().Type ().String (); + } + + return Metadata.AddMethodDefinition ( + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + MethodImplAttributes.IL, + Metadata.GetOrAddString (".ctor"), + Metadata.GetOrAddBlob (signature), + EmitReturnOnlyBody (), + MetadataTokens.ParameterHandle (Metadata.GetRowCount (TableIndex.Param) + 1)); + } + + MethodDefinitionHandle AddStringArrayAttributeCtor () + { + var signature = new BlobBuilder (); + new BlobEncoder (signature).MethodSignature (isInstanceMethod: true) + .Parameters (1, out ReturnTypeEncoder returnType, out ParametersEncoder parameters); + returnType.Void (); + parameters.AddParameter ().Type ().SZArray ().String (); + + return Metadata.AddMethodDefinition ( + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + MethodImplAttributes.IL, + Metadata.GetOrAddString (".ctor"), + Metadata.GetOrAddBlob (signature), + EmitReturnOnlyBody (), + MetadataTokens.ParameterHandle (Metadata.GetRowCount (TableIndex.Param) + 1)); + } + + MemberReferenceHandle AddGenericStringAttributeCtor (string ns, string name, int argCount) + { + TypeReferenceHandle openType = Metadata.AddTypeReference (CoreLibraryReference, + Metadata.GetOrAddString (ns), Metadata.GetOrAddString (name)); + var typeSpec = new BlobBuilder (); + typeSpec.WriteByte ((byte) SignatureTypeCode.GenericTypeInstance); + typeSpec.WriteByte ((byte) SignatureTypeKind.Class); + typeSpec.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openType)); + typeSpec.WriteCompressedInteger (1); + typeSpec.WriteByte ((byte) SignatureTypeKind.Class); + typeSpec.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (ValueTypeReference)); + TypeSpecificationHandle closedType = Metadata.AddTypeSpecification (Metadata.GetOrAddBlob (typeSpec)); + + var signature = new BlobBuilder (); + new BlobEncoder (signature).MethodSignature (isInstanceMethod: true) + .Parameters (argCount, out ReturnTypeEncoder returnType, out ParametersEncoder parameters); + returnType.Void (); + for (int i = 0; i < argCount; i++) { + parameters.AddParameter ().Type ().String (); + } + return Metadata.AddMemberReference (closedType, Metadata.GetOrAddString (".ctor"), Metadata.GetOrAddBlob (signature)); + } + + static BlobBuilder BuildVoidNoArgsSignature () + { + var signature = new BlobBuilder (); + new BlobEncoder (signature).MethodSignature (isInstanceMethod: true) + .Parameters (0, out ReturnTypeEncoder returnType, out ParametersEncoder _); + returnType.Void (); + return signature; + } + + public int EmitReturnOnlyBody () => EmitBody (encoder => encoder.OpCode (ILOpCode.Ret)); + + public int EmitLoadStringBody (params UserStringHandle [] strings) + => EmitBody (encoder => { + foreach (UserStringHandle handle in strings) { + encoder.LoadString (handle); + encoder.OpCode (ILOpCode.Pop); + } + encoder.OpCode (ILOpCode.Ret); + }); + + public int EmitBody (Action emit, StandaloneSignatureHandle localSignature = default, + ControlFlowBuilder controlFlow = null, int maxStack = 8) + { + var code = new BlobBuilder (); + var encoder = new InstructionEncoder (code, controlFlow); + emit (encoder); + return bodyEncoder.AddMethodBody (encoder, maxStack, localSignature); + } + + public UserStringHandle String (string value) => Metadata.GetOrAddUserString (value); + + public BlobHandle AttributeBlob (params string [] fixedStringArgs) + { + var blob = new BlobBuilder (); + blob.WriteUInt16 (0x0001); // Prolog + foreach (string arg in fixedStringArgs) { + blob.WriteSerializedString (arg); + } + blob.WriteUInt16 (0x0000); // NumNamed + return Metadata.GetOrAddBlob (blob); + } + + public BlobHandle StringArrayAttributeBlob (params string [] values) + { + var blob = new BlobBuilder (); + blob.WriteUInt16 (0x0001); + blob.WriteInt32 (values.Length); + foreach (string value in values) { + blob.WriteSerializedString (value); + } + blob.WriteUInt16 (0x0000); + return Metadata.GetOrAddBlob (blob); + } + + public void AddEmbeddedResource (string name, byte [] content) + { + ManagedResources.Align (8); + int offset = ManagedResources.Count; + ManagedResources.WriteInt32 (content.Length); + ManagedResources.WriteBytes (content); + Metadata.AddManifestResource (ManifestResourceAttributes.Public, Metadata.GetOrAddString (name), default, (uint) offset); + } + + /// + /// Emits a null-terminated UTF-8 JNI datum exactly the way + /// Microsoft.Android.Sdk.TrimmableTypeMap's PEAssemblyBuilder does: a static HasFieldRVA + /// field whose type is a <PrivateImplementationDetails>/__utf8_N + /// explicit-layout value type sized to the datum. + /// + public FieldDefinitionHandle AddUtf8Field (string value) + { + int size = Encoding.UTF8.GetByteCount (value) + 1; + TypeDefinitionHandle sizedType = GetOrCreateSizedType (size); + + var signature = new BlobBuilder (); + new BlobEncoder (signature).FieldSignature ().Type (sizedType, isValueType: true); + + int rva = MappedFieldData.Count; + var bytes = new byte [size]; + Encoding.UTF8.GetBytes (value, 0, value.Length, bytes, 0); + MappedFieldData.WriteBytes (bytes); + + FieldDefinitionHandle handle = Metadata.AddFieldDefinition ( + FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA | FieldAttributes.InitOnly, + Metadata.GetOrAddString ("__utf8_" + utf8FieldCounter++), + Metadata.GetOrAddBlob (signature)); + Metadata.AddFieldRelativeVirtualAddress (handle, rva); + return handle; + } + + public TypeDefinitionHandle EnsurePrivateImplementationDetails () + { + if (!privateImplementationDetails.IsNil) { + return privateImplementationDetails; + } + + privateImplementationDetails = AddType (null, PrivateImplementationDetails, NextFieldRid, NextMethodRid, + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.BeforeFieldInit); + return privateImplementationDetails; + } + + TypeDefinitionHandle GetOrCreateSizedType (int size) + { + if (sizedTypes.TryGetValue (size, out TypeDefinitionHandle existing)) { + return existing; + } + + TypeDefinitionHandle enclosing = EnsurePrivateImplementationDetails (); + TypeDefinitionHandle handle = AddType (null, "__utf8_" + size, NextFieldRid, NextMethodRid, + TypeAttributes.NestedAssembly | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + ValueTypeReference); + Metadata.AddTypeLayout (handle, packingSize: 1, size: (uint) size); + Metadata.AddNestedType (handle, enclosing); + + sizedTypes [size] = handle; + return handle; + } + + public byte [] Serialize () + { + var headerBuilder = new PEHeaderBuilder ( + imageCharacteristics: Characteristics.Dll | Characteristics.ExecutableImage); + + var peBuilder = new ManagedPEBuilder ( + headerBuilder, + new MetadataRootBuilder (Metadata), + Il, + mappedFieldData: MappedFieldData.Count > 0 ? MappedFieldData : null, + managedResources: ManagedResources.Count > 0 ? ManagedResources : null, + nativeResources: NativeResources, + debugDirectoryBuilder: DebugDirectory, + strongNameSignatureSize: StrongNameSignatureSize, + entryPoint: default, + flags: Flags, + deterministicIdProvider: _ => new BlobContentId (Mvid, TimeDateStamp)); + + var peBlob = new BlobBuilder (); + peBuilder.Serialize (peBlob); + + using var stream = new MemoryStream (); + peBlob.WriteContentTo (stream); + return stream.ToArray (); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/NativeResourceSectionCopierTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/NativeResourceSectionCopierTests.cs new file mode 100644 index 00000000000..1c9e363d726 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/NativeResourceSectionCopierTests.cs @@ -0,0 +1,237 @@ +using System.Collections.Immutable; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using NUnit.Framework; +using Xamarin.Android.Tasks.JniRemapping; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + [Parallelizable (ParallelScope.Children)] + public class NativeResourceSectionCopierTests : BaseTest + { + const int ResourceDirectoryHeaderSize = 16; + const int ResourceDirectoryEntrySize = 8; + const int ResourceDataEntrySize = 16; + + /// + /// A minimal hand-built Win32 resource directory: one root directory with a single ID + /// entry that points straight at one data entry (no named entries, no subdirectories). + /// + sealed class OneEntryResourceSectionBuilder : ResourceSectionBuilder + { + readonly byte [] data; + readonly int dataEntryRvaDelta; + readonly uint dataEntrySize; + + /// + /// Added to the section's own final RVA to produce the data entry's absolute + /// OffsetToData. Use 0 for a well-formed, in-range entry; a large positive + /// value to simulate a corrupt entry that points past the copied section. + /// + public OneEntryResourceSectionBuilder (byte [] data, int dataEntryRvaDelta, uint dataEntrySize) + { + this.data = data; + this.dataEntryRvaDelta = dataEntryRvaDelta; + this.dataEntrySize = dataEntrySize; + } + + protected override void Serialize (BlobBuilder builder, SectionLocation location) + { + int dataEntryOffset = ResourceDirectoryHeaderSize + ResourceDirectoryEntrySize; + int dataOffset = dataEntryOffset + ResourceDataEntrySize; + + // IMAGE_RESOURCE_DIRECTORY: no named entries, one ID entry. + builder.WriteUInt32 (0); // Characteristics + builder.WriteUInt32 (0); // TimeDateStamp + builder.WriteUInt16 (0); // MajorVersion + builder.WriteUInt16 (0); // MinorVersion + builder.WriteUInt16 (0); // NumberOfNamedEntries + builder.WriteUInt16 (1); // NumberOfIdEntries + + // IMAGE_RESOURCE_DIRECTORY_ENTRY: straight to the data entry (no high bit). + builder.WriteUInt32 (1); // Id + builder.WriteUInt32 ((uint) dataEntryOffset); // OffsetToData + + // IMAGE_RESOURCE_DATA_ENTRY. + uint dataRva = unchecked ((uint) (location.RelativeVirtualAddress + dataOffset + dataEntryRvaDelta)); + builder.WriteUInt32 (dataRva); // OffsetToData (an absolute RVA, not a section offset) + builder.WriteUInt32 (dataEntrySize); // Size + builder.WriteUInt32 (0); // CodePage + builder.WriteUInt32 (0); // Reserved + + builder.WriteBytes (data); + } + } + + sealed class SharedEntryResourceSectionBuilder : ResourceSectionBuilder + { + readonly byte [] data; + + public SharedEntryResourceSectionBuilder (byte [] data) + { + this.data = data; + } + + protected override void Serialize (BlobBuilder builder, SectionLocation location) + { + int dataEntryOffset = ResourceDirectoryHeaderSize + 2 * ResourceDirectoryEntrySize; + int dataOffset = dataEntryOffset + ResourceDataEntrySize; + + builder.WriteUInt32 (0); + builder.WriteUInt32 (0); + builder.WriteUInt16 (0); + builder.WriteUInt16 (0); + builder.WriteUInt16 (0); + builder.WriteUInt16 (2); + for (uint id = 1; id <= 2; id++) { + builder.WriteUInt32 (id); + builder.WriteUInt32 ((uint) dataEntryOffset); + } + builder.WriteUInt32 ((uint) (location.RelativeVirtualAddress + dataOffset)); + builder.WriteUInt32 ((uint) data.Length); + builder.WriteUInt32 (0); + builder.WriteUInt32 (0); + builder.WriteBytes (data); + } + } + + sealed class InvalidSubdirectoryOffsetResourceSectionBuilder : ResourceSectionBuilder + { + protected override void Serialize (BlobBuilder builder, SectionLocation location) + { + builder.WriteUInt32 (0); + builder.WriteUInt32 (0); + builder.WriteUInt16 (0); + builder.WriteUInt16 (0); + builder.WriteUInt16 (0); + builder.WriteUInt16 (1); + builder.WriteUInt32 (1); + builder.WriteUInt32 (uint.MaxValue); + } + } + + static byte [] BuildAndReadBack (ResourceSectionBuilder nativeResources) + { + var fixture = new JniFixtureBuilder { + NativeResources = nativeResources, + }; + return fixture.Serialize (); + } + + [Test] + public void ThrowsWhenADataEntryRvaPlusSizeExtendsPastTheCopiedSection () + { + byte [] payload = { 1, 2, 3, 4 }; + // The RVA delta pushes OffsetToData well past the end of the section this class + // copies, simulating a corrupt (or malicious) resource directory. + var builder = new OneEntryResourceSectionBuilder (payload, dataEntryRvaDelta: 4096, dataEntrySize: (uint) payload.Length); + + byte [] image = BuildAndReadBack (builder); + using var peReader = new PEReader (ImmutableArray.Create (image)); + + var ex = Assert.Throws (() => NativeResourceSectionCopier.TryCreate (peReader)); + StringAssert.Contains ("outside of the copied resource section", ex.Message); + } + + [Test] + public void ThrowsWhenADataEntrySizeAloneExtendsPastTheCopiedSection () + { + byte [] payload = { 1, 2, 3, 4 }; + // The RVA itself is in range, but the declared size overruns the section. + var builder = new OneEntryResourceSectionBuilder (payload, dataEntryRvaDelta: 0, dataEntrySize: 0x7FFFFFFF); + + byte [] image = BuildAndReadBack (builder); + using var peReader = new PEReader (ImmutableArray.Create (image)); + + var ex = Assert.Throws (() => NativeResourceSectionCopier.TryCreate (peReader)); + StringAssert.Contains ("outside of the copied resource section", ex.Message); + } + + [Test] + public void ThrowsWhenASubdirectoryOffsetOverflowsItsRange () + { + byte [] image = BuildAndReadBack (new InvalidSubdirectoryOffsetResourceSectionBuilder ()); + using var peReader = new PEReader (ImmutableArray.Create (image)); + + var ex = Assert.Throws (() => NativeResourceSectionCopier.TryCreate (peReader)); + StringAssert.Contains ("outside of the resource section", ex.Message); + } + + [TestCase (false)] + [TestCase (true)] + public void ThrowsWhenResourceDirectoryHasOnlyRvaOrSize (bool clearRva) + { + byte [] image = BuildAndReadBack (new OneEntryResourceSectionBuilder ( + new byte [] { 1 }, dataEntryRvaDelta: 0, dataEntrySize: 1)); + PatchResourceDirectory (image, clearRva); + using var peReader = new PEReader (ImmutableArray.Create (image)); + + var ex = Assert.Throws (() => NativeResourceSectionCopier.TryCreate (peReader)); + StringAssert.Contains ("invalid RVA or size", ex.Message); + } + + [Test] + public void AcceptsAndRelocatesAWellFormedDataEntry () + { + byte [] payload = { 0xAA, 0xBB, 0xCC, 0xDD }; + var builder = new OneEntryResourceSectionBuilder (payload, dataEntryRvaDelta: 0, dataEntrySize: (uint) payload.Length); + + byte [] image = BuildAndReadBack (builder); + using var peReader = new PEReader (ImmutableArray.Create (image)); + + NativeResourceSectionCopier copier = NativeResourceSectionCopier.TryCreate (peReader); + Assert.IsNotNull (copier, "A resource directory was built but TryCreate did not find it."); + + DirectoryEntry directory = peReader.PEHeaders.PEHeader.ResourceTableDirectory; + CollectionAssert.AreEqual (payload, + peReader.GetSectionData (directory.RelativeVirtualAddress) + .GetReader (directory.Size - payload.Length, payload.Length) + .ReadBytes (payload.Length), + "The payload bytes following the data entry should be exactly what was written."); + } + + [Test] + public void RelocatesASharedDataEntryOnlyOnce () + { + byte [] payload = { 0x11, 0x22, 0x33, 0x44 }; + var sourceFixture = new JniFixtureBuilder { + NativeResources = new SharedEntryResourceSectionBuilder (payload), + }; + sourceFixture.AddEmbeddedResource ("padding", new byte [8192]); + byte [] source = sourceFixture.Serialize (); + using var sourceReader = new PEReader (ImmutableArray.Create (source)); + NativeResourceSectionCopier copier = NativeResourceSectionCopier.TryCreate (sourceReader); + Assert.IsNotNull (copier); + + byte [] rewritten = BuildAndReadBack (copier); + using var rewrittenReader = new PEReader (ImmutableArray.Create (rewritten)); + DirectoryEntry directory = rewrittenReader.PEHeaders.PEHeader.ResourceTableDirectory; + int dataEntryOffset = ResourceDirectoryHeaderSize + 2 * ResourceDirectoryEntrySize; + int dataRva = rewrittenReader.GetSectionData (directory.RelativeVirtualAddress).GetReader (dataEntryOffset, sizeof (int)).ReadInt32 (); + + CollectionAssert.AreEqual (payload, + rewrittenReader.GetSectionData (dataRva).GetReader (0, payload.Length).ReadBytes (payload.Length)); + } + + static void PatchResourceDirectory (byte [] image, bool clearRva) + { + int directoryOffset; + using (var peReader = new PEReader (ImmutableArray.Create (image))) { + const int Pe32DataDirectoriesOffset = 96; + const int Pe32PlusDataDirectoriesOffset = 112; + const int ResourceDirectoryIndex = 2; + int dataDirectoriesOffset = peReader.PEHeaders.PEHeader.Magic == PEMagic.PE32Plus + ? Pe32PlusDataDirectoriesOffset + : Pe32DataDirectoriesOffset; + directoryOffset = peReader.PEHeaders.PEHeaderStartOffset + + dataDirectoriesOffset + ResourceDirectoryIndex * 2 * sizeof (uint); + } + + int valueOffset = directoryOffset + (clearRva ? 0 : sizeof (uint)); + for (int i = 0; i < sizeof (uint); i++) { + image [valueOffset + i] = 0; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/AssemblyRebuilder.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/AssemblyRebuilder.cs new file mode 100644 index 00000000000..5d2cefef25e --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/AssemblyRebuilder.cs @@ -0,0 +1,1155 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + sealed class AssemblyRebuildResult + { + public byte [] Image { get; } + + /// + /// True when the source assembly was marked StrongNameSigned. The rebuilt image + /// cannot carry a valid signature (no key is available here), so only the + /// StrongNameSigned flag is cleared - the signature directory keeps its original + /// size so the image is genuinely delay-signed (space is reserved) and can be re-signed + /// without another rewrite, rather than left with a stale flag or a truncated directory. + /// + public bool StrongNameSignatureCleared { get; } + + public AssemblyRebuildResult (byte [] image, bool strongNameSignatureCleared) + { + Image = image; + StrongNameSignatureCleared = strongNameSignatureCleared; + } + } + + /// + /// Pass two of the rewrite: reconstructs a complete managed PE from a source assembly, + /// cloning every metadata table row in its original order - so every entity token keeps its + /// value - while re-emitting the heaps, the method bodies, the managed resources, and the + /// mapped field data. The JNI edits collected by are applied + /// as the corresponding data is re-emitted, which is what lets two use sites that shared one + /// deduplicated #US/#Blob entry receive different values. + /// + sealed class AssemblyRebuilder + { + static readonly TableIndex [] UnsupportedTables = { + TableIndex.FieldPtr, TableIndex.MethodPtr, TableIndex.ParamPtr, TableIndex.EventPtr, TableIndex.PropertyPtr, + TableIndex.EncLog, TableIndex.EncMap, + TableIndex.AssemblyProcessor, TableIndex.AssemblyOS, TableIndex.AssemblyRefProcessor, TableIndex.AssemblyRefOS, + TableIndex.Document, TableIndex.MethodDebugInformation, TableIndex.LocalScope, TableIndex.LocalVariable, + TableIndex.LocalConstant, TableIndex.ImportScope, TableIndex.StateMachineMethod, TableIndex.CustomDebugInformation, + }; + + // Tables whose rows carry tokens that IL, coded indices, or the plan itself can reference; + // their row counts must survive the round trip exactly. + static readonly TableIndex [] TokenCriticalTables = { + TableIndex.TypeRef, TableIndex.TypeDef, TableIndex.Field, TableIndex.MethodDef, TableIndex.Param, + TableIndex.InterfaceImpl, TableIndex.MemberRef, TableIndex.CustomAttribute, TableIndex.DeclSecurity, + TableIndex.StandAloneSig, TableIndex.Event, TableIndex.Property, TableIndex.ModuleRef, TableIndex.TypeSpec, + TableIndex.AssemblyRef, TableIndex.File, TableIndex.ExportedType, TableIndex.ManifestResource, + TableIndex.GenericParam, TableIndex.MethodSpec, TableIndex.GenericParamConstraint, + }; + + readonly PEReader peReader; + readonly MetadataReader reader; + readonly JniRewritePlan plan; + readonly FieldRvaTable fieldRvaTable; + readonly MetadataBuilder metadata = new (); + readonly BlobBuilder ilStream = new (); + readonly BlobBuilder mappedFieldData = new (); + readonly BlobBuilder managedResources = new (); + + readonly Dictionary bodyOffsets = new (); + readonly Dictionary newFieldRvaOffsets = new (); + readonly Dictionary resizedFieldTypes = new (); + readonly Dictionary resourceOffsets = new (); + readonly List syntheticSizedTypes = new (); + + int sourceTypeDefCount; + + sealed class SyntheticSizedType + { + public TypeDefinitionHandle Handle { get; } + public TypeDefinitionHandle Template { get; } + public TypeDefinitionHandle Enclosing { get; } + public int Size { get; } + + public SyntheticSizedType (TypeDefinitionHandle handle, TypeDefinitionHandle template, TypeDefinitionHandle enclosing, int size) + { + Handle = handle; + Template = template; + Enclosing = enclosing; + Size = size; + } + } + + public AssemblyRebuilder (PEReader peReader, MetadataReader reader, JniRewritePlan plan, FieldRvaTable fieldRvaTable) + { + this.peReader = peReader; + this.reader = reader; + this.plan = plan; + this.fieldRvaTable = fieldRvaTable; + } + + public AssemblyRebuildResult Build () + { + ValidateSupported (); + sourceTypeDefCount = reader.GetTableRowCount (TableIndex.TypeDef); + + PlanMappedFieldData (); + EmitMethodBodies (); + EmitManagedResources (); + CloneTables (); + EmitSyntheticSizedTypes (); + ValidateRowCounts (); + + return Serialize (); + } + + void ValidateSupported () + { + if (reader.MetadataKind != MetadataKind.Ecma335) { + throw new JniRewriteException ($"Only ECMA-335 metadata can be rewritten; this assembly uses '{reader.MetadataKind}'."); + } + + foreach (TableIndex table in UnsupportedTables) { + int rows = reader.GetTableRowCount (table); + if (rows > 0) { + throw new JniRewriteException ($"The assembly uses the '{table}' metadata table ({rows} row(s)), which this rewriter cannot reproduce."); + } + } + + int implMapCount = reader.GetTableRowCount (TableIndex.ImplMap); + for (int rid = 1; rid <= implMapCount; rid++) { + if (MetadataRawColumns.GetImplMapMemberForwarded (reader, rid).Kind == HandleKind.FieldDefinition) { + throw new JniRewriteException ("The assembly has a field-backed ImplMap row, which this rewriter cannot reproduce."); + } + } + + CorHeader? corHeader = peReader.PEHeaders.CorHeader; + if (corHeader == null) { + throw new JniRewriteException ("The file has no CLI header and is not a managed assembly."); + } + if ((corHeader.Flags & CorFlags.NativeEntryPoint) != 0) { + throw new JniRewriteException ("The assembly declares a native entry point, which this rewriter cannot reproduce."); + } + + RequireEmptyDirectory (corHeader.ManagedNativeHeaderDirectory, "ReadyToRun/NGen native header"); + RequireEmptyDirectory (corHeader.VtableFixupsDirectory, "CLI vtable fixups (C++/CLI)"); + RequireEmptyDirectory (corHeader.ExportAddressTableJumpsDirectory, "CLI export address table jumps"); + ValidateStrongNameSignatureDirectory (corHeader.StrongNameSignatureDirectory); + + PEHeader? peHeader = peReader.PEHeaders.PEHeader; + if (peHeader == null) { + throw new JniRewriteException ("The file has no PE optional header and cannot be rebuilt."); + } + + // Authenticode covers the complete PE image and is invalidated by any metadata rewrite. + // ManagedPEBuilder intentionally omits the source certificate table; APK signing protects + // the final packaged image instead. + RequireEmptyDirectory (peHeader.ExportTableDirectory, "native export table"); + RequireEmptyDirectory (peHeader.DelayImportTableDirectory, "delay import table"); + RequireEmptyDirectory (peHeader.LoadConfigTableDirectory, "load configuration table"); + RequireEmptyDirectory (peHeader.ThreadLocalStorageTableDirectory, "thread local storage table"); + RequireEmptyDirectory (peHeader.ExceptionTableDirectory, "native exception (unwind) table"); + + int entryPointToken = corHeader.EntryPointTokenOrRelativeVirtualAddress; + if (entryPointToken != 0 && (entryPointToken & 0xFF000000) != 0x06000000) { + throw new JniRewriteException ($"The assembly's entry point token 0x{entryPointToken:X8} is not a MethodDef token; multi-module entry points are not supported."); + } + } + + void ValidateStrongNameSignatureDirectory (DirectoryEntry directory) + { + if (directory.RelativeVirtualAddress == 0 && directory.Size == 0) { + return; + } + if (directory.RelativeVirtualAddress <= 0 || directory.Size <= 0) { + throw new JniRewriteException ("The assembly has an invalid strong-name signature directory."); + } + + PEMemoryBlock block = peReader.GetSectionData (directory.RelativeVirtualAddress); + if (block.Length < directory.Size) { + throw new JniRewriteException ("The strong-name signature directory extends past the end of its PE section."); + } + } + + static void RequireEmptyDirectory (DirectoryEntry directory, string description) + { + if (directory.Size != 0 || directory.RelativeVirtualAddress != 0) { + throw new JniRewriteException ($"The assembly has a {description}, which this rewriter cannot reproduce."); + } + } + + void ValidateRowCounts () + { + foreach (TableIndex table in TokenCriticalTables) { + int expected = reader.GetTableRowCount (table); + if (table == TableIndex.TypeDef) { + expected += syntheticSizedTypes.Count; + } + + int actual = metadata.GetRowCount (table); + if (actual != expected) { + throw new JniRewriteException ($"Internal error: rebuilt '{table}' table has {actual} row(s) but {expected} were expected; metadata tokens would move."); + } + } + } + + /// + /// Re-lays out the FieldRVA data. Overlapping and adjacent source ranges are copied + /// together so aliases and their relative offsets survive, while ranges from separate PE + /// sections are emitted as independently aligned blocks. Rewritten UTF-8 JNI data is + /// written back over its own slot when it still fits. Longer replacements are appended + /// and the field is re-typed to a wider __utf8_N value type. + /// + void PlanMappedFieldData () + { + IReadOnlyList entries = fieldRvaTable.Entries; + if (entries.Count == 0) { + return; + } + + var sortedEntries = new List (entries.Count); + foreach (FieldRvaEntry entry in entries) { + sortedEntries.Add (entry); + } + sortedEntries.Sort (static (left, right) => { + int result = left.RelativeVirtualAddress.CompareTo (right.RelativeVirtualAddress); + return result != 0 ? result : MetadataTokens.GetRowNumber (left.Field).CompareTo (MetadataTokens.GetRowNumber (right.Field)); + }); + + HashSet overlappingFields = FindOverlappingFields (sortedEntries); + var appended = new List> (); + int first = 0; + while (first < sortedEntries.Count) { + int groupStartRva = sortedEntries [first].RelativeVirtualAddress; + int groupEndRva = GetFieldDataEnd (sortedEntries [first]); + int end = first + 1; + while (end < sortedEntries.Count && sortedEntries [end].RelativeVirtualAddress <= groupEndRva) { + groupEndRva = Math.Max (groupEndRva, GetFieldDataEnd (sortedEntries [end])); + end++; + } + + var data = new byte [groupEndRva - groupStartRva]; + for (int i = first; i < end; i++) { + FieldRvaEntry entry = sortedEntries [i]; + Array.Copy (entry.Data, 0, data, entry.RelativeVirtualAddress - groupStartRva, entry.Data.Length); + } + + mappedFieldData.Align (ManagedPEBuilder.MappedFieldDataAlignment); + int outputGroupOffset = mappedFieldData.Count; + + for (int i = first; i < end; i++) { + FieldRvaEntry entry = sortedEntries [i]; + int offset = entry.RelativeVirtualAddress - groupStartRva; + string? newValue = plan.GetUtf8FieldValue (entry.Field); + if (newValue == null) { + newFieldRvaOffsets [entry.Field] = outputGroupOffset + offset; + continue; + } + + byte [] replacement = EncodeNullTerminatedUtf8 (newValue); + if (replacement.Length <= entry.Data.Length && !overlappingFields.Contains (entry.Field)) { + // Shorter or equal: write the NUL-terminated bytes over the original slot + // and zero the tail. The field keeps its declared size. + Array.Clear (data, offset, entry.Data.Length); + Array.Copy (replacement, 0, data, offset, replacement.Length); + newFieldRvaOffsets [entry.Field] = outputGroupOffset + offset; + continue; + } + + if (replacement.Length <= entry.Data.Length) { + // The source slot overlaps another field, so changing it in place would + // silently change that alias as well. Preserve this field's declared size + // while moving only its replacement to independent storage. + var detached = new byte [entry.Data.Length]; + Array.Copy (replacement, detached, replacement.Length); + replacement = detached; + } else { + resizedFieldTypes [entry.Field] = GetOrCreateSizedType (entry, replacement.Length); + } + appended.Add (new KeyValuePair (entry.Field, replacement)); + } + + mappedFieldData.WriteBytes (data); + first = end; + } + + foreach (KeyValuePair extra in appended) { + mappedFieldData.Align (ManagedPEBuilder.MappedFieldDataAlignment); + newFieldRvaOffsets [extra.Key] = mappedFieldData.Count; + mappedFieldData.WriteBytes (extra.Value); + } + } + + static HashSet FindOverlappingFields (IReadOnlyList sortedEntries) + { + var overlapping = new HashSet (); + FieldDefinitionHandle furthestField = default; + int furthestEnd = 0; + + foreach (FieldRvaEntry entry in sortedEntries) { + int end = GetFieldDataEnd (entry); + if (!furthestField.IsNil && entry.RelativeVirtualAddress < furthestEnd) { + overlapping.Add (furthestField); + overlapping.Add (entry.Field); + } + if (furthestField.IsNil || end > furthestEnd) { + furthestField = entry.Field; + furthestEnd = end; + } + } + + return overlapping; + } + + static int GetFieldDataEnd (FieldRvaEntry entry) + { + if (entry.RelativeVirtualAddress < 0 || entry.Data.Length > int.MaxValue - entry.RelativeVirtualAddress) { + throw new JniRewriteException ("A FieldRVA data range cannot be represented safely."); + } + return entry.RelativeVirtualAddress + entry.Data.Length; + } + + static byte [] EncodeNullTerminatedUtf8 (string value) + { + byte [] utf8 = System.Text.Encoding.UTF8.GetBytes (value); + var result = new byte [utf8.Length + 1]; + Array.Copy (utf8, result, utf8.Length); + return result; + } + + /// + /// Finds - or schedules the creation of - the <PrivateImplementationDetails>/__utf8_N + /// explicit-layout value type of the requested size. New types are appended after every + /// cloned TypeDef row, so no existing token moves. + /// + TypeDefinitionHandle GetOrCreateSizedType (FieldRvaEntry entry, int size) + { + if (entry.Utf8SizedType.IsNil) { + throw new JniRewriteException ("Internal error: a non-UTF-8 mapped field was scheduled for replacement."); + } + + TypeDefinition template = reader.GetTypeDefinition (entry.Utf8SizedType); + string wantedName = FieldRvaTable.Utf8FieldNamePrefix + size.ToString (System.Globalization.CultureInfo.InvariantCulture); + TypeDefinitionHandle enclosing = template.GetDeclaringType (); + + foreach (TypeDefinitionHandle candidate in reader.TypeDefinitions) { + TypeDefinition typeDef = reader.GetTypeDefinition (candidate); + if (reader.GetString (typeDef.Name) == wantedName && typeDef.GetDeclaringType ().Equals (enclosing)) { + TypeLayout layout = typeDef.GetLayout (); + if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) == 0 || + layout.IsDefault || layout.Size != size) { + throw new JniRewriteException ($"The assembly already contains '{wantedName}' with an incompatible layout."); + } + return candidate; + } + } + + foreach (SyntheticSizedType existing in syntheticSizedTypes) { + if (existing.Size == size && existing.Enclosing.Equals (enclosing)) { + return existing.Handle; + } + } + + var handle = MetadataTokens.TypeDefinitionHandle (sourceTypeDefCount + syntheticSizedTypes.Count + 1); + syntheticSizedTypes.Add (new SyntheticSizedType (handle, entry.Utf8SizedType, enclosing, size)); + return handle; + } + + void EmitSyntheticSizedTypes () + { + if (syntheticSizedTypes.Count == 0) { + return; + } + + var fieldList = MetadataTokens.FieldDefinitionHandle (reader.GetTableRowCount (TableIndex.Field) + 1); + var methodList = MetadataTokens.MethodDefinitionHandle (reader.GetTableRowCount (TableIndex.MethodDef) + 1); + + foreach (SyntheticSizedType synthetic in syntheticSizedTypes) { + TypeDefinition template = reader.GetTypeDefinition (synthetic.Template); + string name = FieldRvaTable.Utf8FieldNamePrefix + synthetic.Size.ToString (System.Globalization.CultureInfo.InvariantCulture); + + TypeDefinitionHandle added = metadata.AddTypeDefinition ( + template.Attributes, + CloneString (template.Namespace), + metadata.GetOrAddString (name), + template.BaseType, + fieldList, + methodList); + + if (!added.Equals (synthetic.Handle)) { + throw new JniRewriteException ("Internal error: a synthetic sized type did not land at its reserved token."); + } + + metadata.AddTypeLayout (added, packingSize: 1, size: (uint) synthetic.Size); + + if (!synthetic.Enclosing.IsNil) { + metadata.AddNestedType (added, synthetic.Enclosing); + } + } + } + + void EmitMethodBodies () + { + var encoder = new MethodBodyStreamEncoder (ilStream); + int methodCount = reader.GetTableRowCount (TableIndex.MethodDef); + + for (int rid = 1; rid <= methodCount; rid++) { + var handle = MetadataTokens.MethodDefinitionHandle (rid); + MethodDefinition method = reader.GetMethodDefinition (handle); + if (method.RelativeVirtualAddress == 0) { + bodyOffsets [handle] = -1; + continue; + } + + MethodBodyBlock body = peReader.GetMethodBody (method.RelativeVirtualAddress); + byte [] il = RewriteIL (handle, body.GetILBytes () ?? [], out bool hasDynamicStackAllocation); + ImmutableArray regions = body.ExceptionRegions; + + MethodBodyStreamEncoder.MethodBody emitted = encoder.AddMethodBody ( + il.Length, + body.MaxStack, + regions.Length, + HasSmallExceptionRegions (regions), + body.LocalSignature, + body.LocalVariablesInitialized ? MethodBodyAttributes.InitLocals : MethodBodyAttributes.None, + hasDynamicStackAllocation); + + new BlobWriter (emitted.Instructions).WriteBytes (il); + + ExceptionRegionEncoder regionEncoder = emitted.ExceptionRegions; + foreach (ExceptionRegion region in regions) { + regionEncoder.Add (region.Kind, region.TryOffset, region.TryLength, region.HandlerOffset, region.HandlerLength, + region.Kind == ExceptionRegionKind.Catch ? region.CatchType : default, + region.Kind == ExceptionRegionKind.Filter ? region.FilterOffset : 0); + } + + bodyOffsets [handle] = emitted.Offset; + } + } + + static bool HasSmallExceptionRegions (ImmutableArray regions) + { + if (!ExceptionRegionEncoder.IsSmallRegionCount (regions.Length)) { + return false; + } + foreach (ExceptionRegion region in regions) { + if (!ExceptionRegionEncoder.IsSmallExceptionRegion (region.TryOffset, region.TryLength) || + !ExceptionRegionEncoder.IsSmallExceptionRegion (region.HandlerOffset, region.HandlerLength)) { + return false; + } + } + return true; + } + + /// + /// Copies a method body's IL, replacing every ldstr token: the #US heap is + /// rebuilt from scratch, so even unchanged strings need a fresh handle. Instruction widths + /// never change, so branch targets, exception region offsets, and PDB sequence points stay + /// valid. + /// + byte [] RewriteIL (MethodDefinitionHandle method, byte [] original, out bool hasDynamicStackAllocation) + { + var il = (byte []) original.Clone (); + Dictionary? replacements = plan.GetUserStrings (method); + bool localloc = false; + + IlInstructionScanner.Walk (il, (code, _, operandOffset, _) => { + if (code == (ushort) ILOpCode.Localloc) { + localloc = true; + return; + } + if (code != (ushort) ILOpCode.Ldstr) { + return; + } + + uint token = IlInstructionScanner.ReadUInt32 (il, operandOffset); + if ((token & 0xFF000000) != 0x70000000) { + throw new JniRewriteException ($"Malformed IL: ldstr operand 0x{token:X8} is not a #US token."); + } + + string? replacement = null; + replacements?.TryGetValue (operandOffset, out replacement); + string value = replacement ?? reader.GetUserString (MetadataTokens.UserStringHandle ((int) (token & 0x00FFFFFF))); + + UserStringHandle newHandle = metadata.GetOrAddUserString (value); + IlInstructionScanner.WriteUInt32 (il, operandOffset, (uint) MetadataTokens.GetToken (newHandle)); + }); + + hasDynamicStackAllocation = localloc; + return il; + } + + void EmitManagedResources () + { + int resourceCount = reader.GetTableRowCount (TableIndex.ManifestResource); + if (resourceCount == 0) { + return; + } + + CorHeader corHeader = GetCorHeader (); + DirectoryEntry directory = corHeader.ResourcesDirectory; + + for (int rid = 1; rid <= resourceCount; rid++) { + ManifestResource resource = reader.GetManifestResource (MetadataTokens.ManifestResourceHandle (rid)); + if (!resource.Implementation.IsNil) { + continue; // Lives in another file; the offset is meaningful there, not here. + } + + if (directory.RelativeVirtualAddress == 0) { + throw new JniRewriteException ("The assembly declares embedded resources but has no resources directory."); + } + + PEMemoryBlock block = peReader.GetSectionData (directory.RelativeVirtualAddress); + if (block.Length < directory.Size) { + throw new JniRewriteException ("The resources directory extends past the end of its PE section."); + } + + // Use long arithmetic throughout: offset and size both come from the file (an + // attacker- or corruption-controlled uint32), and offset + sizeof(int) + size can + // overflow a 32-bit sum and wrap around to a small or negative value, which would + // defeat the bounds check below instead of catching it. + long offset = resource.Offset; + long headerEnd = offset + sizeof (int); + if (offset < 0 || headerEnd > directory.Size) { + throw new JniRewriteException ($"Embedded resource '{reader.GetString (resource.Name)}' starts outside of the resources directory."); + } + + int resourceOffset = (int) offset; + int size = block.GetReader (resourceOffset, sizeof (int)).ReadInt32 (); + if (size < 0 || headerEnd + (long) size > directory.Size) { + throw new JniRewriteException ($"Embedded resource '{reader.GetString (resource.Name)}' extends past the end of the resources directory."); + } + + managedResources.Align (ManagedPEBuilder.ManagedResourcesDataAlignment); + resourceOffsets [MetadataTokens.ManifestResourceHandle (rid)] = managedResources.Count; + managedResources.WriteInt32 (size); + managedResources.WriteBytes (block.GetReader (resourceOffset + sizeof (int), size).ReadBytes (size)); + } + } + + void CloneTables () + { + CloneModule (); + CloneTypeReferences (); + CloneTypeDefinitions (); + CloneFields (); + CloneMethods (); + CloneParameters (); + CloneInterfaceImplementations (); + CloneMemberReferences (); + CloneConstants (); + CloneCustomAttributes (); + CloneFieldMarshals (); + CloneDeclarativeSecurity (); + CloneClassLayouts (); + CloneFieldLayouts (); + CloneStandaloneSignatures (); + CloneEventsAndProperties (); + CloneMethodImplementations (); + CloneModuleReferences (); + CloneTypeSpecifications (); + CloneImplMaps (); + CloneFieldRvas (); + CloneAssembly (); + CloneAssemblyReferences (); + CloneFiles (); + CloneExportedTypes (); + CloneManifestResources (); + CloneNestedClasses (); + CloneGenericParameters (); + CloneMethodSpecifications (); + CloneGenericParameterConstraints (); + } + + void CloneModule () + { + ModuleDefinition module = reader.GetModuleDefinition (); + metadata.AddModule ( + module.Generation, + CloneString (module.Name), + CloneGuid (module.Mvid), + CloneGuid (module.GenerationId), + CloneGuid (module.BaseGenerationId)); + } + + void CloneTypeReferences () + { + int count = reader.GetTableRowCount (TableIndex.TypeRef); + for (int rid = 1; rid <= count; rid++) { + TypeReference typeRef = reader.GetTypeReference (MetadataTokens.TypeReferenceHandle (rid)); + metadata.AddTypeReference (typeRef.ResolutionScope, CloneString (typeRef.Namespace), CloneString (typeRef.Name)); + } + } + + void CloneTypeDefinitions () + { + int count = sourceTypeDefCount; + var fieldLists = new FieldDefinitionHandle [count + 1]; + var methodLists = new MethodDefinitionHandle [count + 1]; + + var nextField = MetadataTokens.FieldDefinitionHandle (reader.GetTableRowCount (TableIndex.Field) + 1); + var nextMethod = MetadataTokens.MethodDefinitionHandle (reader.GetTableRowCount (TableIndex.MethodDef) + 1); + + // An empty member list is encoded as "the next type's first member", so the lists have + // to be resolved from the back. + for (int rid = count; rid >= 1; rid--) { + TypeDefinition typeDef = reader.GetTypeDefinition (MetadataTokens.TypeDefinitionHandle (rid)); + + foreach (FieldDefinitionHandle field in typeDef.GetFields ()) { + nextField = field; + break; + } + foreach (MethodDefinitionHandle method in typeDef.GetMethods ()) { + nextMethod = method; + break; + } + + fieldLists [rid] = nextField; + methodLists [rid] = nextMethod; + } + + for (int rid = 1; rid <= count; rid++) { + TypeDefinition typeDef = reader.GetTypeDefinition (MetadataTokens.TypeDefinitionHandle (rid)); + metadata.AddTypeDefinition ( + typeDef.Attributes, + CloneString (typeDef.Namespace), + CloneString (typeDef.Name), + typeDef.BaseType, + fieldLists [rid], + methodLists [rid]); + } + } + + void CloneFields () + { + int count = reader.GetTableRowCount (TableIndex.Field); + for (int rid = 1; rid <= count; rid++) { + var handle = MetadataTokens.FieldDefinitionHandle (rid); + FieldDefinition field = reader.GetFieldDefinition (handle); + metadata.AddFieldDefinition (field.Attributes, CloneString (field.Name), CloneFieldSignature (handle, field)); + } + } + + BlobHandle CloneFieldSignature (FieldDefinitionHandle handle, FieldDefinition field) + { + if (!resizedFieldTypes.TryGetValue (handle, out TypeDefinitionHandle sizedType)) { + return CloneBlob (field.Signature); + } + + var blob = new BlobBuilder (); + blob.WriteByte ((byte) SignatureKind.Field); + blob.WriteByte ((byte) SignatureTypeKind.ValueType); + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (sizedType)); + return metadata.GetOrAddBlob (blob); + } + + void CloneMethods () + { + int count = reader.GetTableRowCount (TableIndex.MethodDef); + var parameterLists = new ParameterHandle [count + 1]; + var nextParameter = MetadataTokens.ParameterHandle (reader.GetTableRowCount (TableIndex.Param) + 1); + + for (int rid = count; rid >= 1; rid--) { + MethodDefinition method = reader.GetMethodDefinition (MetadataTokens.MethodDefinitionHandle (rid)); + foreach (ParameterHandle parameter in method.GetParameters ()) { + nextParameter = parameter; + break; + } + parameterLists [rid] = nextParameter; + } + + for (int rid = 1; rid <= count; rid++) { + var handle = MetadataTokens.MethodDefinitionHandle (rid); + MethodDefinition method = reader.GetMethodDefinition (handle); + metadata.AddMethodDefinition ( + method.Attributes, + method.ImplAttributes, + CloneString (method.Name), + CloneBlob (method.Signature), + bodyOffsets [handle], + parameterLists [rid]); + } + } + + void CloneParameters () + { + int count = reader.GetTableRowCount (TableIndex.Param); + for (int rid = 1; rid <= count; rid++) { + Parameter parameter = reader.GetParameter (MetadataTokens.ParameterHandle (rid)); + metadata.AddParameter (parameter.Attributes, CloneString (parameter.Name), parameter.SequenceNumber); + } + } + + void CloneInterfaceImplementations () + { + // The table is sorted by its (implicit) Class column, so walking the types in row + // order reproduces the original row order exactly. + foreach (TypeDefinitionHandle typeHandle in reader.TypeDefinitions) { + TypeDefinition typeDef = reader.GetTypeDefinition (typeHandle); + foreach (InterfaceImplementationHandle implHandle in typeDef.GetInterfaceImplementations ()) { + InterfaceImplementation impl = reader.GetInterfaceImplementation (implHandle); + metadata.AddInterfaceImplementation (typeHandle, impl.Interface); + } + } + } + + void CloneMemberReferences () + { + int count = reader.GetTableRowCount (TableIndex.MemberRef); + for (int rid = 1; rid <= count; rid++) { + MemberReference memberRef = reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (rid)); + metadata.AddMemberReference (memberRef.Parent, CloneString (memberRef.Name), CloneBlob (memberRef.Signature)); + } + } + + void CloneConstants () + { + int count = reader.GetTableRowCount (TableIndex.Constant); + for (int rid = 1; rid <= count; rid++) { + Constant constant = reader.GetConstant (MetadataTokens.ConstantHandle (rid)); + BlobReader value = reader.GetBlobReader (constant.Value); + metadata.AddConstant (constant.Parent, value.ReadConstant (constant.TypeCode)); + } + } + + void CloneCustomAttributes () + { + int count = reader.GetTableRowCount (TableIndex.CustomAttribute); + for (int rid = 1; rid <= count; rid++) { + var handle = MetadataTokens.CustomAttributeHandle (rid); + CustomAttribute attribute = reader.GetCustomAttribute (handle); + byte []? replacement = plan.GetCustomAttributeBlob (handle); + BlobHandle value = replacement != null ? metadata.GetOrAddBlob (replacement) : CloneBlob (attribute.Value); + metadata.AddCustomAttribute (attribute.Parent, attribute.Constructor, value); + } + } + + void CloneFieldMarshals () + { + foreach (FieldDefinitionHandle handle in reader.FieldDefinitions) { + BlobHandle descriptor = reader.GetFieldDefinition (handle).GetMarshallingDescriptor (); + if (!descriptor.IsNil) { + metadata.AddMarshallingDescriptor (handle, CloneBlob (descriptor)); + } + } + + int parameterCount = reader.GetTableRowCount (TableIndex.Param); + for (int rid = 1; rid <= parameterCount; rid++) { + var handle = MetadataTokens.ParameterHandle (rid); + BlobHandle descriptor = reader.GetParameter (handle).GetMarshallingDescriptor (); + if (!descriptor.IsNil) { + metadata.AddMarshallingDescriptor (handle, CloneBlob (descriptor)); + } + } + } + + void CloneDeclarativeSecurity () + { + int count = reader.GetTableRowCount (TableIndex.DeclSecurity); + for (int rid = 1; rid <= count; rid++) { + DeclarativeSecurityAttribute security = reader.GetDeclarativeSecurityAttribute (MetadataTokens.DeclarativeSecurityAttributeHandle (rid)); + metadata.AddDeclarativeSecurityAttribute (security.Parent, security.Action, CloneBlob (security.PermissionSet)); + } + } + + void CloneClassLayouts () + { + foreach (TypeDefinitionHandle handle in reader.TypeDefinitions) { + TypeLayout layout = reader.GetTypeDefinition (handle).GetLayout (); + if (!layout.IsDefault) { + metadata.AddTypeLayout (handle, checked ((ushort) layout.PackingSize), checked ((uint) layout.Size)); + } + } + } + + void CloneFieldLayouts () + { + foreach (FieldDefinitionHandle handle in reader.FieldDefinitions) { + int offset = reader.GetFieldDefinition (handle).GetOffset (); + if (offset >= 0) { + metadata.AddFieldLayout (handle, offset); + } + } + } + + void CloneStandaloneSignatures () + { + int count = reader.GetTableRowCount (TableIndex.StandAloneSig); + for (int rid = 1; rid <= count; rid++) { + StandaloneSignature signature = reader.GetStandaloneSignature (MetadataTokens.StandaloneSignatureHandle (rid)); + metadata.AddStandaloneSignature (CloneBlob (signature.Signature)); + } + } + + void CloneEventsAndProperties () + { + int eventCount = reader.GetTableRowCount (TableIndex.Event); + for (int rid = 1; rid <= eventCount; rid++) { + EventDefinition eventDef = reader.GetEventDefinition (MetadataTokens.EventDefinitionHandle (rid)); + metadata.AddEvent (eventDef.Attributes, CloneString (eventDef.Name), eventDef.Type); + } + + int propertyCount = reader.GetTableRowCount (TableIndex.Property); + for (int rid = 1; rid <= propertyCount; rid++) { + PropertyDefinition property = reader.GetPropertyDefinition (MetadataTokens.PropertyDefinitionHandle (rid)); + metadata.AddProperty (property.Attributes, CloneString (property.Name), CloneBlob (property.Signature)); + } + + foreach (TypeDefinitionHandle typeHandle in reader.TypeDefinitions) { + TypeDefinition typeDef = reader.GetTypeDefinition (typeHandle); + + foreach (EventDefinitionHandle eventHandle in typeDef.GetEvents ()) { + metadata.AddEventMap (typeHandle, eventHandle); + break; + } + foreach (PropertyDefinitionHandle propertyHandle in typeDef.GetProperties ()) { + metadata.AddPropertyMap (typeHandle, propertyHandle); + break; + } + } + + int associationCount = Math.Max (eventCount, propertyCount); + for (int rid = 1; rid <= associationCount; rid++) { + // HasSemantics uses Event tag 0 and Property tag 1, so associations are sorted + // Event 1, Property 1, Event 2, Property 2, and so on. + if (rid <= eventCount) { + var handle = MetadataTokens.EventDefinitionHandle (rid); + EventAccessors accessors = reader.GetEventDefinition (handle).GetAccessors (); + AddSemantics (handle, MethodSemanticsAttributes.Adder, accessors.Adder); + AddSemantics (handle, MethodSemanticsAttributes.Remover, accessors.Remover); + AddSemantics (handle, MethodSemanticsAttributes.Raiser, accessors.Raiser); + foreach (MethodDefinitionHandle other in accessors.Others) { + AddSemantics (handle, MethodSemanticsAttributes.Other, other); + } + } + + if (rid <= propertyCount) { + var handle = MetadataTokens.PropertyDefinitionHandle (rid); + PropertyAccessors accessors = reader.GetPropertyDefinition (handle).GetAccessors (); + AddSemantics (handle, MethodSemanticsAttributes.Getter, accessors.Getter); + AddSemantics (handle, MethodSemanticsAttributes.Setter, accessors.Setter); + foreach (MethodDefinitionHandle other in accessors.Others) { + AddSemantics (handle, MethodSemanticsAttributes.Other, other); + } + } + } + } + + void AddSemantics (EntityHandle association, MethodSemanticsAttributes semantics, MethodDefinitionHandle method) + { + if (!method.IsNil) { + metadata.AddMethodSemantics (association, semantics, method); + } + } + + void CloneMethodImplementations () + { + int count = reader.GetTableRowCount (TableIndex.MethodImpl); + for (int rid = 1; rid <= count; rid++) { + MethodImplementation impl = reader.GetMethodImplementation (MetadataTokens.MethodImplementationHandle (rid)); + metadata.AddMethodImplementation ((TypeDefinitionHandle) impl.Type, impl.MethodBody, impl.MethodDeclaration); + } + } + + void CloneModuleReferences () + { + int count = reader.GetTableRowCount (TableIndex.ModuleRef); + for (int rid = 1; rid <= count; rid++) { + ModuleReference moduleRef = reader.GetModuleReference (MetadataTokens.ModuleReferenceHandle (rid)); + metadata.AddModuleReference (CloneString (moduleRef.Name)); + } + } + + void CloneTypeSpecifications () + { + int count = reader.GetTableRowCount (TableIndex.TypeSpec); + for (int rid = 1; rid <= count; rid++) { + TypeSpecification typeSpec = reader.GetTypeSpecification (MetadataTokens.TypeSpecificationHandle (rid)); + metadata.AddTypeSpecification (CloneBlob (typeSpec.Signature)); + } + } + + void CloneImplMaps () + { + int count = reader.GetTableRowCount (TableIndex.MethodDef); + for (int rid = 1; rid <= count; rid++) { + var handle = MetadataTokens.MethodDefinitionHandle (rid); + MethodImport import = reader.GetMethodDefinition (handle).GetImport (); + if (!import.Module.IsNil) { + metadata.AddMethodImport (handle, import.Attributes, CloneString (import.Name), import.Module); + } + } + } + + void CloneFieldRvas () + { + foreach (FieldRvaEntry entry in fieldRvaTable.Entries) { + if (!newFieldRvaOffsets.TryGetValue (entry.Field, out int offset)) { + throw new JniRewriteException ("Internal error: a FieldRVA row was not laid out."); + } + metadata.AddFieldRelativeVirtualAddress (entry.Field, offset); + } + } + + void CloneAssembly () + { + if (!reader.IsAssembly) { + return; + } + + AssemblyDefinition assembly = reader.GetAssemblyDefinition (); + metadata.AddAssembly ( + CloneString (assembly.Name), + assembly.Version, + CloneString (assembly.Culture), + CloneBlob (assembly.PublicKey), + assembly.Flags, + assembly.HashAlgorithm); + } + + void CloneAssemblyReferences () + { + int count = reader.GetTableRowCount (TableIndex.AssemblyRef); + for (int rid = 1; rid <= count; rid++) { + AssemblyReference assemblyRef = reader.GetAssemblyReference (MetadataTokens.AssemblyReferenceHandle (rid)); + metadata.AddAssemblyReference ( + CloneString (assemblyRef.Name), + assemblyRef.Version, + CloneString (assemblyRef.Culture), + CloneBlob (assemblyRef.PublicKeyOrToken), + assemblyRef.Flags, + CloneBlob (assemblyRef.HashValue)); + } + } + + void CloneFiles () + { + int count = reader.GetTableRowCount (TableIndex.File); + for (int rid = 1; rid <= count; rid++) { + AssemblyFile file = reader.GetAssemblyFile (MetadataTokens.AssemblyFileHandle (rid)); + metadata.AddAssemblyFile (CloneString (file.Name), CloneBlob (file.HashValue), file.ContainsMetadata); + } + } + + void CloneExportedTypes () + { + int count = reader.GetTableRowCount (TableIndex.ExportedType); + for (int rid = 1; rid <= count; rid++) { + ExportedType exported = reader.GetExportedType (MetadataTokens.ExportedTypeHandle (rid)); + metadata.AddExportedType ( + exported.Attributes, + CloneString (exported.Namespace), + CloneString (exported.Name), + exported.Implementation, + MetadataRawColumns.GetExportedTypeDefinitionId (reader, rid)); + } + } + + void CloneManifestResources () + { + int count = reader.GetTableRowCount (TableIndex.ManifestResource); + for (int rid = 1; rid <= count; rid++) { + var handle = MetadataTokens.ManifestResourceHandle (rid); + ManifestResource resource = reader.GetManifestResource (handle); + uint offset = resourceOffsets.TryGetValue (handle, out int newOffset) + ? (uint) newOffset + : checked ((uint) resource.Offset); + metadata.AddManifestResource (resource.Attributes, CloneString (resource.Name), resource.Implementation, offset); + } + } + + void CloneNestedClasses () + { + foreach (TypeDefinitionHandle handle in reader.TypeDefinitions) { + TypeDefinitionHandle enclosing = reader.GetTypeDefinition (handle).GetDeclaringType (); + if (!enclosing.IsNil) { + metadata.AddNestedType (handle, enclosing); + } + } + } + + void CloneGenericParameters () + { + int count = reader.GetTableRowCount (TableIndex.GenericParam); + for (int rid = 1; rid <= count; rid++) { + GenericParameter parameter = reader.GetGenericParameter (MetadataTokens.GenericParameterHandle (rid)); + metadata.AddGenericParameter (parameter.Parent, parameter.Attributes, CloneString (parameter.Name), parameter.Index); + } + } + + void CloneGenericParameterConstraints () + { + int count = reader.GetTableRowCount (TableIndex.GenericParamConstraint); + for (int rid = 1; rid <= count; rid++) { + GenericParameterConstraint constraint = reader.GetGenericParameterConstraint (MetadataTokens.GenericParameterConstraintHandle (rid)); + metadata.AddGenericParameterConstraint (constraint.Parameter, constraint.Type); + } + } + + void CloneMethodSpecifications () + { + int count = reader.GetTableRowCount (TableIndex.MethodSpec); + for (int rid = 1; rid <= count; rid++) { + MethodSpecification methodSpec = reader.GetMethodSpecification (MetadataTokens.MethodSpecificationHandle (rid)); + metadata.AddMethodSpecification (methodSpec.Method, CloneBlob (methodSpec.Signature)); + } + } + + StringHandle CloneString (StringHandle handle) => handle.IsNil ? default : metadata.GetOrAddString (reader.GetString (handle)); + + BlobHandle CloneBlob (BlobHandle handle) => handle.IsNil ? default : metadata.GetOrAddBlob (reader.GetBlobBytes (handle)); + + GuidHandle CloneGuid (GuidHandle handle) => handle.IsNil ? default : metadata.GetOrAddGuid (reader.GetGuid (handle)); + + CorHeader GetCorHeader () + { + CorHeader? corHeader = peReader.PEHeaders.CorHeader; + if (corHeader == null) { + throw new JniRewriteException ("The file has no CLI header and is not a managed assembly."); + } + return corHeader; + } + + AssemblyRebuildResult Serialize () + { + CorHeader corHeader = GetCorHeader (); + CoffHeader coffHeader = peReader.PEHeaders.CoffHeader; + PEHeader? peHeader = peReader.PEHeaders.PEHeader; + if (peHeader == null) { + throw new JniRewriteException ("The file has no PE optional header and cannot be rebuilt."); + } + + bool wasStrongNameSigned = (corHeader.Flags & CorFlags.StrongNameSigned) != 0; + CorFlags flags = wasStrongNameSigned ? corHeader.Flags & ~CorFlags.StrongNameSigned : corHeader.Flags; + + // Reserve the original signature directory's size even though the flag is cleared: + // the rebuilt image is delay-signed (the space for a signature is preserved) rather + // than left with no room to re-sign it later. + int strongNameSignatureSize = corHeader.StrongNameSignatureDirectory.Size; + + var headerBuilder = new PEHeaderBuilder ( + machine: coffHeader.Machine, + sectionAlignment: peHeader.SectionAlignment, + fileAlignment: peHeader.FileAlignment, + imageBase: peHeader.ImageBase, + majorLinkerVersion: peHeader.MajorLinkerVersion, + minorLinkerVersion: peHeader.MinorLinkerVersion, + majorOperatingSystemVersion: peHeader.MajorOperatingSystemVersion, + minorOperatingSystemVersion: peHeader.MinorOperatingSystemVersion, + majorImageVersion: peHeader.MajorImageVersion, + minorImageVersion: peHeader.MinorImageVersion, + majorSubsystemVersion: peHeader.MajorSubsystemVersion, + minorSubsystemVersion: peHeader.MinorSubsystemVersion, + subsystem: peHeader.Subsystem, + dllCharacteristics: peHeader.DllCharacteristics, + imageCharacteristics: coffHeader.Characteristics, + sizeOfStackReserve: peHeader.SizeOfStackReserve, + sizeOfStackCommit: peHeader.SizeOfStackCommit, + sizeOfHeapReserve: peHeader.SizeOfHeapReserve, + sizeOfHeapCommit: peHeader.SizeOfHeapCommit); + + int entryPointToken = corHeader.EntryPointTokenOrRelativeVirtualAddress; + MethodDefinitionHandle entryPoint = entryPointToken == 0 + ? default + : MetadataTokens.MethodDefinitionHandle (entryPointToken & 0x00FFFFFF); + + Guid contentIdGuid = GetModuleVersionId (); + uint timeDateStamp = unchecked ((uint) coffHeader.TimeDateStamp); + + var peBuilder = new ManagedPEBuilder ( + headerBuilder, + new MetadataRootBuilder (metadata, reader.MetadataVersion), + ilStream, + mappedFieldData: mappedFieldData.Count > 0 ? mappedFieldData : null, + managedResources: managedResources.Count > 0 ? managedResources : null, + nativeResources: NativeResourceSectionCopier.TryCreate (peReader), + debugDirectoryBuilder: CloneDebugDirectory (), + strongNameSignatureSize: strongNameSignatureSize, + entryPoint: entryPoint, + flags: flags, + deterministicIdProvider: _ => new BlobContentId (contentIdGuid, timeDateStamp)); + + var peBlob = new BlobBuilder (); + try { + peBuilder.Serialize (peBlob); + } catch (InvalidOperationException e) { + throw new JniRewriteException ($"The rebuilt metadata was rejected during serialization: {e.Message}", e); + } catch (ArgumentException e) { + throw new JniRewriteException ($"The rebuilt metadata was rejected during serialization: {e.Message}", e); + } + + using var stream = new MemoryStream (); + peBlob.WriteContentTo (stream); + return new AssemblyRebuildResult (stream.ToArray (), wasStrongNameSigned); + } + + Guid GetModuleVersionId () + { + GuidHandle mvid = reader.GetModuleDefinition ().Mvid; + return mvid.IsNil ? Guid.Empty : reader.GetGuid (mvid); + } + + /// + /// Reproduces the source debug directory so an existing portable PDB keeps matching: + /// method tokens and IL offsets are preserved by construction, and the CodeView identity + /// (GUID, age, path) is copied verbatim. + /// + DebugDirectoryBuilder CloneDebugDirectory () + { + var builder = new DebugDirectoryBuilder (); + + foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory ()) { + switch (entry.Type) { + case DebugDirectoryEntryType.CodeView: { + CodeViewDebugDirectoryData data = peReader.ReadCodeViewDebugDirectoryData (entry); + ushort portablePdbVersion = entry.IsPortableCodeView ? entry.MajorVersion : (ushort) 0; + builder.AddCodeViewEntry (data.Path, new BlobContentId (data.Guid, entry.Stamp), portablePdbVersion, data.Age); + break; + } + case DebugDirectoryEntryType.PdbChecksum: { + PdbChecksumDebugDirectoryData data = peReader.ReadPdbChecksumDebugDirectoryData (entry); + builder.AddPdbChecksumEntry (data.AlgorithmName, data.Checksum); + break; + } + case DebugDirectoryEntryType.Reproducible: + builder.AddReproducibleEntry (); + break; + default: + AddRawDebugDirectoryEntry (builder, entry); + break; + } + } + + return builder; + } + + void AddRawDebugDirectoryEntry (DebugDirectoryBuilder builder, DebugDirectoryEntry entry) + { + uint version = (uint) entry.MajorVersion | ((uint) entry.MinorVersion << 16); + if (entry.DataSize == 0) { + builder.AddEntry (entry.Type, version, entry.Stamp); + return; + } + + PEMemoryBlock block = peReader.GetSectionData (entry.DataRelativeVirtualAddress); + if (block.Length < entry.DataSize) { + throw new JniRewriteException ($"Debug directory entry '{entry.Type}' points at data outside of any PE section."); + } + + byte [] data = block.GetReader (0, entry.DataSize).ReadBytes (entry.DataSize); + builder.AddEntry (entry.Type, version, entry.Stamp, data, static (blob, bytes) => blob.WriteBytes (bytes)); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/FieldRvaTable.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/FieldRvaTable.cs new file mode 100644 index 00000000000..d7400ffe698 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/FieldRvaTable.cs @@ -0,0 +1,224 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// A single FieldRVA row together with the size and contents of the mapped data it + /// points at, plus - when the field is structurally one of the trimmable typemap generator's + /// null-terminated UTF-8 JNI data fields - its decoded string value. + /// + sealed class FieldRvaEntry + { + public FieldDefinitionHandle Field { get; } + public int RelativeVirtualAddress { get; } + public byte [] Data { get; } + + /// + /// Non-nil when the field's type is a <PrivateImplementationDetails>/__utf8_N + /// explicit-layout value type, i.e. the field holds a null-terminated UTF-8 JNI name or + /// signature emitted by Microsoft.Android.Sdk.TrimmableTypeMap. + /// + public TypeDefinitionHandle Utf8SizedType { get; } + + public string? Utf8Value { get; } + + public bool IsUtf8Datum => Utf8Value != null; + + public FieldRvaEntry (FieldDefinitionHandle field, int rva, byte [] data, TypeDefinitionHandle utf8SizedType, string? utf8Value) + { + Field = field; + RelativeVirtualAddress = rva; + Data = data; + Utf8SizedType = utf8SizedType; + Utf8Value = utf8Value; + } + } + + /// + /// Reads every FieldRVA row of an assembly, resolving the byte length of each mapped + /// data block from the field's signature so the block can be copied - or replaced - when the + /// assembly is rebuilt. + /// + sealed class FieldRvaTable + { + public const string PrivateImplementationDetailsTypeName = ""; + public const string Utf8FieldNamePrefix = "__utf8_"; + + readonly List entries = new (); + readonly Dictionary byField = new (); + + public IReadOnlyList Entries => entries; + + /// + /// Returns the mapped-data entry for , or null when the field has + /// no FieldRVA row. + /// + public FieldRvaEntry? Get (FieldDefinitionHandle field) => byField.TryGetValue (field, out FieldRvaEntry? entry) ? entry : null; + + /// + /// Reads every mapped field, in Field row order - which is the order the + /// FieldRVA table is required to be sorted in. + /// + public static FieldRvaTable Read (PEReader peReader, MetadataReader reader) + { + var table = new FieldRvaTable (); + + foreach (FieldDefinitionHandle field in reader.FieldDefinitions) { + FieldDefinition fieldDef = reader.GetFieldDefinition (field); + int rva = fieldDef.GetRelativeVirtualAddress (); + if (rva == 0) { + continue; + } + + int size = ComputeFieldDataSize (reader, fieldDef, out TypeDefinitionHandle valueType); + byte [] data = ReadMappedData (peReader, rva, size, reader.GetString (fieldDef.Name)); + + string? utf8Value = TryDecodeUtf8Datum (reader, fieldDef, valueType, data); + var entry = new FieldRvaEntry (field, rva, data, utf8Value != null ? valueType : default, utf8Value); + table.entries.Add (entry); + table.byField [field] = entry; + } + + if (table.entries.Count != reader.GetTableRowCount (TableIndex.FieldRva)) { + throw new JniRewriteException ("The FieldRVA table has rows whose fields do not declare an RVA; the assembly is malformed."); + } + + return table; + } + + static byte [] ReadMappedData (PEReader peReader, int rva, int size, string fieldName) + { + PEMemoryBlock block = peReader.GetSectionData (rva); + if (block.Length < size) { + throw new JniRewriteException ($"FieldRVA data for field '{fieldName}' (RVA 0x{rva:X}, {size} byte(s)) extends past the end of its PE section."); + } + return block.GetReader (0, size).ReadBytes (size); + } + + static string? TryDecodeUtf8Datum (MetadataReader reader, FieldDefinition fieldDef, TypeDefinitionHandle valueType, byte [] data) + { + const FieldAttributes required = FieldAttributes.Static | FieldAttributes.HasFieldRVA; + if ((fieldDef.Attributes & required) != required) { + return null; + } + if (valueType.IsNil || !reader.GetString (fieldDef.Name).StartsWith (Utf8FieldNamePrefix, StringComparison.Ordinal)) { + return null; + } + + // The datum's type must be the generator's `/__utf8_N` + // explicit-layout value type whose size is exactly the field's byte count. + TypeDefinition sized = reader.GetTypeDefinition (valueType); + if ((sized.Attributes & TypeAttributes.ExplicitLayout) == 0) { + return null; + } + if (reader.GetString (sized.Name) != Utf8FieldNamePrefix + data.Length.ToString (System.Globalization.CultureInfo.InvariantCulture)) { + return null; + } + + TypeDefinitionHandle enclosing = sized.GetDeclaringType (); + if (enclosing.IsNil || reader.GetString (reader.GetTypeDefinition (enclosing).Name) != PrivateImplementationDetailsTypeName) { + return null; + } + + // Rewriting a JNI value to a shorter string preserves the original field type and + // zero-fills its remaining bytes so metadata tokens do not move. Accept that padding, + // but reject non-zero data after the first C-string terminator. + int terminator = Array.IndexOf (data, (byte) 0); + if (terminator < 0) { + return null; + } + for (int i = terminator + 1; i < data.Length; i++) { + if (data [i] != 0) { + return null; + } + } + + try { + return new UTF8Encoding (encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true) + .GetString (data, 0, terminator); + } catch (ArgumentException) { + return null; + } + } + + /// + /// Computes the number of bytes a mapped field occupies, from its field signature. + /// + static int ComputeFieldDataSize (MetadataReader reader, FieldDefinition fieldDef, out TypeDefinitionHandle valueType) + { + valueType = default; + + BlobReader blob = reader.GetBlobReader (fieldDef.Signature); + SignatureHeader header = blob.ReadSignatureHeader (); + if (header.Kind != SignatureKind.Field) { + throw new JniRewriteException ($"Field '{reader.GetString (fieldDef.Name)}' has a FieldRVA row but its signature is not a field signature."); + } + + int code = ReadTypeCodeSkippingModifiers (ref blob); + if (code == (int) SignatureTypeKind.ValueType) { + EntityHandle handle = blob.ReadTypeHandle (); + if (handle.Kind != HandleKind.TypeDefinition) { + throw new JniRewriteException ($"Field '{reader.GetString (fieldDef.Name)}' maps data whose value type lives in another assembly; its size cannot be determined."); + } + valueType = (TypeDefinitionHandle) handle; + return ComputeTypeSize (reader, valueType, reader.GetString (fieldDef.Name)); + } + + int primitiveSize = GetPrimitiveSize (code); + if (primitiveSize > 0) { + return primitiveSize; + } + + throw new JniRewriteException ($"Field '{reader.GetString (fieldDef.Name)}' maps data of an unsupported type (signature element type 0x{code:X2})."); + } + + static int ReadTypeCodeSkippingModifiers (ref BlobReader blob) + { + const int CModReqd = 0x1F; + const int CModOpt = 0x20; + + int code = blob.ReadCompressedInteger (); + while (code == CModReqd || code == CModOpt) { + blob.ReadTypeHandle (); + code = blob.ReadCompressedInteger (); + } + return code; + } + + static int GetPrimitiveSize (int code) + => code switch { + (int) SignatureTypeCode.Boolean or (int) SignatureTypeCode.SByte or (int) SignatureTypeCode.Byte => 1, + (int) SignatureTypeCode.Char or (int) SignatureTypeCode.Int16 or (int) SignatureTypeCode.UInt16 => 2, + (int) SignatureTypeCode.Int32 or (int) SignatureTypeCode.UInt32 or (int) SignatureTypeCode.Single => 4, + (int) SignatureTypeCode.Int64 or (int) SignatureTypeCode.UInt64 or (int) SignatureTypeCode.Double => 8, + _ => 0, + }; + + /// + /// Determines the byte size of a value type used as FieldRVA-mapped data, from its + /// explicit ClassLayout row alone. Summing up instance field sizes is deliberately + /// not attempted as a fallback: without an explicit layout the CLR is free to reorder, + /// pad, or otherwise size the type differently than a naive sum would predict, and + /// silently under- or over-sizing the mapped data block risks reading truncated or + /// out-of-bounds bytes. A type with no explicit layout size is therefore a hard error. + /// + static int ComputeTypeSize (MetadataReader reader, TypeDefinitionHandle handle, string fieldName) + { + TypeDefinition type = reader.GetTypeDefinition (handle); + TypeLayout layout = type.GetLayout (); + if (layout.IsDefault || layout.Size <= 0) { + throw new JniRewriteException ($"Field '{fieldName}' maps data whose value type has no explicit ClassLayout size; its size cannot be determined safely."); + } + + return layout.Size; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/IlOpcodeTable.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/IlOpcodeTable.cs new file mode 100644 index 00000000000..5cca7732788 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/IlOpcodeTable.cs @@ -0,0 +1,101 @@ +#nullable enable + +using System.Collections.Generic; +using System.Reflection.Metadata; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// A minimal ECMA-335 (III.3-4) IL instruction operand-size table, used to walk a method + /// body's IL bytes (looking for ldstr instructions) without any IL-parsing library. + /// Every legal opcode has a fixed operand size, except switch whose operand length + /// depends on the embedded case count, which is handled specially by the caller. + /// + static class IlOpcodeTable + { + public static readonly Dictionary OperandSizes = BuildTable (); + + static Dictionary BuildTable () + { + var table = new Dictionary (); + + // No operand. + Add (table, 0, ILOpCode.Nop, ILOpCode.Break, + ILOpCode.Ldarg_0, ILOpCode.Ldarg_1, ILOpCode.Ldarg_2, ILOpCode.Ldarg_3, + ILOpCode.Ldloc_0, ILOpCode.Ldloc_1, ILOpCode.Ldloc_2, ILOpCode.Ldloc_3, + ILOpCode.Stloc_0, ILOpCode.Stloc_1, ILOpCode.Stloc_2, ILOpCode.Stloc_3, + ILOpCode.Ldnull, + ILOpCode.Ldc_i4_m1, ILOpCode.Ldc_i4_0, ILOpCode.Ldc_i4_1, ILOpCode.Ldc_i4_2, ILOpCode.Ldc_i4_3, + ILOpCode.Ldc_i4_4, ILOpCode.Ldc_i4_5, ILOpCode.Ldc_i4_6, ILOpCode.Ldc_i4_7, ILOpCode.Ldc_i4_8, + ILOpCode.Dup, ILOpCode.Pop, ILOpCode.Ret, + ILOpCode.Ldind_i1, ILOpCode.Ldind_u1, ILOpCode.Ldind_i2, ILOpCode.Ldind_u2, + ILOpCode.Ldind_i4, ILOpCode.Ldind_u4, ILOpCode.Ldind_i8, ILOpCode.Ldind_i, + ILOpCode.Ldind_r4, ILOpCode.Ldind_r8, ILOpCode.Ldind_ref, + ILOpCode.Stind_ref, ILOpCode.Stind_i1, ILOpCode.Stind_i2, ILOpCode.Stind_i4, + ILOpCode.Stind_i8, ILOpCode.Stind_r4, ILOpCode.Stind_r8, ILOpCode.Stind_i, + ILOpCode.Add, ILOpCode.Sub, ILOpCode.Mul, ILOpCode.Div, ILOpCode.Div_un, + ILOpCode.Rem, ILOpCode.Rem_un, ILOpCode.And, ILOpCode.Or, ILOpCode.Xor, + ILOpCode.Shl, ILOpCode.Shr, ILOpCode.Shr_un, ILOpCode.Neg, ILOpCode.Not, + ILOpCode.Conv_i1, ILOpCode.Conv_i2, ILOpCode.Conv_i4, ILOpCode.Conv_i8, + ILOpCode.Conv_r4, ILOpCode.Conv_r8, ILOpCode.Conv_u4, ILOpCode.Conv_u8, ILOpCode.Conv_r_un, + ILOpCode.Throw, + ILOpCode.Ldlen, + ILOpCode.Ldelem_i1, ILOpCode.Ldelem_u1, ILOpCode.Ldelem_i2, ILOpCode.Ldelem_u2, + ILOpCode.Ldelem_i4, ILOpCode.Ldelem_u4, ILOpCode.Ldelem_i8, ILOpCode.Ldelem_i, + ILOpCode.Ldelem_r4, ILOpCode.Ldelem_r8, ILOpCode.Ldelem_ref, + ILOpCode.Stelem_i, ILOpCode.Stelem_i1, ILOpCode.Stelem_i2, ILOpCode.Stelem_i4, + ILOpCode.Stelem_i8, ILOpCode.Stelem_r4, ILOpCode.Stelem_r8, ILOpCode.Stelem_ref, + ILOpCode.Conv_ovf_i1_un, ILOpCode.Conv_ovf_u1_un, ILOpCode.Conv_ovf_i2_un, ILOpCode.Conv_ovf_u2_un, + ILOpCode.Conv_ovf_i4_un, ILOpCode.Conv_ovf_u4_un, ILOpCode.Conv_ovf_i8_un, ILOpCode.Conv_ovf_u8_un, + ILOpCode.Conv_ovf_i_un, ILOpCode.Conv_ovf_u_un, + ILOpCode.Conv_ovf_i1, ILOpCode.Conv_ovf_u1, ILOpCode.Conv_ovf_i2, ILOpCode.Conv_ovf_u2, + ILOpCode.Conv_ovf_i4, ILOpCode.Conv_ovf_u4, ILOpCode.Conv_ovf_i8, ILOpCode.Conv_ovf_u8, + ILOpCode.Ckfinite, + ILOpCode.Conv_u2, ILOpCode.Conv_u1, ILOpCode.Conv_i, ILOpCode.Conv_ovf_i, ILOpCode.Conv_ovf_u, ILOpCode.Conv_u, + ILOpCode.Add_ovf, ILOpCode.Add_ovf_un, ILOpCode.Mul_ovf, ILOpCode.Mul_ovf_un, ILOpCode.Sub_ovf, ILOpCode.Sub_ovf_un, + ILOpCode.Endfinally, + ILOpCode.Arglist, ILOpCode.Ceq, ILOpCode.Cgt, ILOpCode.Cgt_un, ILOpCode.Clt, ILOpCode.Clt_un, + ILOpCode.Localloc, ILOpCode.Endfilter, + ILOpCode.Volatile, ILOpCode.Tail, ILOpCode.Cpblk, ILOpCode.Initblk, + ILOpCode.Rethrow, ILOpCode.Refanytype, ILOpCode.Readonly); + + // 1-byte operand. + Add (table, 1, ILOpCode.Starg_s, ILOpCode.Ldloc_s, ILOpCode.Ldloca_s, ILOpCode.Stloc_s, + ILOpCode.Ldarg_s, ILOpCode.Ldarga_s, ILOpCode.Ldc_i4_s, ILOpCode.Unaligned, + ILOpCode.Br_s, ILOpCode.Brfalse_s, ILOpCode.Brtrue_s, + ILOpCode.Beq_s, ILOpCode.Bge_s, ILOpCode.Bgt_s, ILOpCode.Ble_s, ILOpCode.Blt_s, + ILOpCode.Bne_un_s, ILOpCode.Bge_un_s, ILOpCode.Bgt_un_s, ILOpCode.Ble_un_s, ILOpCode.Blt_un_s, + ILOpCode.Leave_s); + + // 2-byte operand. + Add (table, 2, ILOpCode.Starg, ILOpCode.Ldloc, ILOpCode.Ldloca, ILOpCode.Stloc, ILOpCode.Ldarg, ILOpCode.Ldarga); + + // 4-byte operand. + Add (table, 4, ILOpCode.Ldc_i4, ILOpCode.Ldc_r4, + ILOpCode.Br, ILOpCode.Brfalse, ILOpCode.Brtrue, + ILOpCode.Beq, ILOpCode.Bge, ILOpCode.Bgt, ILOpCode.Ble, ILOpCode.Blt, + ILOpCode.Bne_un, ILOpCode.Bge_un, ILOpCode.Bgt_un, ILOpCode.Ble_un, ILOpCode.Blt_un, + ILOpCode.Leave, + ILOpCode.Jmp, ILOpCode.Call, ILOpCode.Callvirt, ILOpCode.Newobj, ILOpCode.Ldftn, ILOpCode.Ldvirtftn, + ILOpCode.Calli, + ILOpCode.Ldfld, ILOpCode.Ldflda, ILOpCode.Stfld, ILOpCode.Ldsfld, ILOpCode.Ldsflda, ILOpCode.Stsfld, + ILOpCode.Cpobj, ILOpCode.Ldobj, ILOpCode.Stobj, ILOpCode.Castclass, ILOpCode.Isinst, + ILOpCode.Box, ILOpCode.Newarr, ILOpCode.Ldelema, ILOpCode.Ldelem, ILOpCode.Stelem, + ILOpCode.Unbox, ILOpCode.Unbox_any, ILOpCode.Refanyval, ILOpCode.Mkrefany, + ILOpCode.Initobj, ILOpCode.Constrained, ILOpCode.Sizeof, + ILOpCode.Ldstr, ILOpCode.Ldtoken); + + // 8-byte operand. + Add (table, 8, ILOpCode.Ldc_i8, ILOpCode.Ldc_r8); + + return table; + } + + static void Add (Dictionary table, int size, params ILOpCode [] codes) + { + foreach (var code in codes) { + table [(ushort) code] = size; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRewriteException.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRewriteException.cs new file mode 100644 index 00000000000..95213c9a174 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRewriteException.cs @@ -0,0 +1,22 @@ +#nullable enable + +using System; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// Thrown when an assembly cannot be rewritten: either it uses a metadata construct this + /// prototype does not know how to reproduce, or two conflicting JNI replacements were + /// requested for a single shared piece of data that cannot be split without moving tokens. + /// + sealed class JniRewriteException : Exception + { + public JniRewriteException (string message) : base (message) + { + } + + public JniRewriteException (string message, Exception innerException) : base (message, innerException) + { + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRewritePlan.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRewritePlan.cs new file mode 100644 index 00000000000..79a6335a3c2 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRewritePlan.cs @@ -0,0 +1,123 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// Walks a method body's IL, reporting each instruction's opcode together with the location + /// and length of its operand. Every legal opcode has a fixed operand size except + /// switch, whose length depends on its embedded case count. + /// + static class IlInstructionScanner + { + public delegate void InstructionVisitor (ushort opCode, int instructionOffset, int operandOffset, int operandSize); + + public static void Walk (byte [] il, InstructionVisitor visit) + { + int i = 0; + while (i < il.Length) { + int instructionOffset = i; + byte b0 = il [i]; + ushort code; + if (b0 == 0xFE) { + if (i + 1 >= il.Length) { + throw new JniRewriteException ("Malformed IL: truncated two-byte opcode."); + } + code = (ushort) (0xFE00 | il [i + 1]); + i += 2; + } else { + code = b0; + i += 1; + } + + int operandOffset = i; + int remaining = il.Length - operandOffset; + int operandSize; + if (code == (ushort) ILOpCode.Switch) { + if (remaining < sizeof (uint)) { + throw new JniRewriteException ("Malformed IL: truncated switch operand."); + } + uint caseCount = ReadUInt32 (il, i); + if (caseCount > (uint) ((remaining - sizeof (uint)) / sizeof (int))) { + throw new JniRewriteException ($"Malformed IL: operand of opcode 0x{code:X} at offset {instructionOffset} extends past the end of the method body."); + } + operandSize = sizeof (uint) + (int) caseCount * sizeof (int); + } else if (!IlOpcodeTable.OperandSizes.TryGetValue (code, out operandSize)) { + throw new JniRewriteException ($"Unrecognized IL opcode 0x{code:X} while scanning a method body."); + } + + if (operandSize > remaining) { + throw new JniRewriteException ($"Malformed IL: operand of opcode 0x{code:X} at offset {instructionOffset} extends past the end of the method body."); + } + + visit (code, instructionOffset, operandOffset, operandSize); + i = operandOffset + operandSize; + } + } + + public static uint ReadUInt32 (byte [] data, int offset) + => (uint) (data [offset] | (data [offset + 1] << 8) | (data [offset + 2] << 16) | (data [offset + 3] << 24)); + + public static void WriteUInt32 (byte [] data, int offset, uint value) + { + data [offset] = (byte) value; + data [offset + 1] = (byte) (value >> 8); + data [offset + 2] = (byte) (value >> 16); + data [offset + 3] = (byte) (value >> 24); + } + } + + /// + /// The exact set of edits the rebuilder must apply while cloning an assembly. Every entry is + /// keyed by the *use site* rather than by the shared heap entry it happens to resolve to, so + /// two ldstr instructions (or two custom attributes) that the compiler deduplicated + /// into one #US/#Blob entry can still be given different replacements: the + /// rebuilder emits a fresh handle per distinct value. + /// + sealed class JniRewritePlan + { + readonly Dictionary customAttributeBlobs = new (); + readonly Dictionary> userStrings = new (); + readonly Dictionary utf8FieldValues = new (); + + public int ReplacementCount { get; private set; } + + public void AddCustomAttributeBlob (CustomAttributeHandle handle, byte [] newValue) + { + customAttributeBlobs [handle] = newValue; + ReplacementCount++; + } + + /// + /// Records that the ldstr whose 4-byte token operand starts at + /// in 's body should load + /// instead. + /// + public void AddUserString (MethodDefinitionHandle method, int operandOffset, string newValue) + { + if (!userStrings.TryGetValue (method, out var perMethod)) { + userStrings [method] = perMethod = new Dictionary (); + } + perMethod [operandOffset] = newValue; + ReplacementCount++; + } + + public void AddUtf8FieldValue (FieldDefinitionHandle field, string newValue) + { + utf8FieldValues [field] = newValue; + ReplacementCount++; + } + + public byte []? GetCustomAttributeBlob (CustomAttributeHandle handle) + => customAttributeBlobs.TryGetValue (handle, out byte []? newValue) ? newValue : null; + + public Dictionary? GetUserStrings (MethodDefinitionHandle method) + => userStrings.TryGetValue (method, out Dictionary? replacements) ? replacements : null; + + public string? GetUtf8FieldValue (FieldDefinitionHandle field) + => utf8FieldValues.TryGetValue (field, out string? newValue) ? newValue : null; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/MetadataEncoding.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/MetadataEncoding.cs new file mode 100644 index 00000000000..dc80ffd99f3 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/MetadataEncoding.cs @@ -0,0 +1,74 @@ +#nullable enable + +using System; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// ECMA-335 II.23.2 compressed unsigned integer encode/decode helpers, used when + /// re-serializing CustomAttribute value blobs by hand. + /// + static class MetadataEncoding + { + public static byte [] EncodeCompressedInteger (int value) + { + if (value < 0) { + throw new ArgumentOutOfRangeException (nameof (value)); + } + if (value <= 0x7F) { + return new [] { (byte) value }; + } + if (value <= 0x3FFF) { + return new [] { + (byte) (0x80 | (value >> 8)), + (byte) (value & 0xFF), + }; + } + if (value <= 0x1FFFFFFF) { + return new [] { + (byte) (0xC0 | (value >> 24)), + (byte) ((value >> 16) & 0xFF), + (byte) ((value >> 8) & 0xFF), + (byte) (value & 0xFF), + }; + } + throw new ArgumentOutOfRangeException (nameof (value), "Value too large to encode as an ECMA-335 compressed integer."); + } + + /// + /// Reads the compressed integer at , returning its width in + /// bytes and (via ) the decoded value. + /// + public static int ReadCompressedInteger (byte [] data, int offset, out int value) + { + if (offset < 0 || offset >= data.Length) { + throw new JniRewriteException ("Malformed metadata blob: truncated compressed integer."); + } + + byte first = data [offset]; + if ((first & 0x80) == 0) { + value = first; + return 1; + } + if ((first & 0xC0) == 0x80) { + RequireLength (data, offset, 2); + value = ((first & 0x3F) << 8) | data [offset + 1]; + return 2; + } + if ((first & 0xE0) == 0xC0) { + RequireLength (data, offset, 4); + value = ((first & 0x1F) << 24) | (data [offset + 1] << 16) | (data [offset + 2] << 8) | data [offset + 3]; + return 4; + } + + throw new JniRewriteException ("Malformed metadata blob: invalid compressed integer prefix."); + } + + static void RequireLength (byte [] data, int offset, int width) + { + if (offset + width > data.Length) { + throw new JniRewriteException ("Malformed metadata blob: truncated compressed integer."); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/MetadataRawColumns.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/MetadataRawColumns.cs new file mode 100644 index 00000000000..5906ba41874 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/MetadataRawColumns.cs @@ -0,0 +1,69 @@ +#nullable enable + +using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// Reads the handful of metadata table columns that does not + /// surface, straight from the table stream. + /// + static class MetadataRawColumns + { + /// + /// The ImplMap.MemberForwarded coded index (ECMA-335 II.22.22). The public + /// metadata APIs expose method imports but not the field imports that this column also + /// permits. + /// + public static unsafe EntityHandle GetImplMapMemberForwarded (MetadataReader reader, int rowNumber) + { + int rowCount = reader.GetTableRowCount (TableIndex.ImplMap); + if (rowNumber <= 0 || rowNumber > rowCount) { + throw new ArgumentOutOfRangeException (nameof (rowNumber)); + } + + int fieldCount = reader.GetTableRowCount (TableIndex.Field); + int methodCount = reader.GetTableRowCount (TableIndex.MethodDef); + int codedIndexSize = Math.Max (fieldCount, methodCount) < (1 << 15) ? sizeof (ushort) : sizeof (uint); + int rowSize = reader.GetTableRowSize (TableIndex.ImplMap); + long rowOffset = reader.GetTableMetadataOffset (TableIndex.ImplMap) + (long) (rowNumber - 1) * rowSize; + if (rowSize < sizeof (ushort) + codedIndexSize || rowOffset < 0 || rowOffset + rowSize > reader.MetadataLength) { + throw new JniRewriteException ("The ImplMap table extends past the end of the metadata."); + } + + var blob = new BlobReader (reader.MetadataPointer + (int) rowOffset + sizeof (ushort), codedIndexSize); + uint codedIndex = codedIndexSize == sizeof (ushort) ? blob.ReadUInt16 () : blob.ReadUInt32 (); + int rowId = checked ((int) (codedIndex >> 1)); + + if ((codedIndex & 1) == 0) { + if (rowId == 0 || rowId > fieldCount) { + throw new JniRewriteException ("An ImplMap row references an invalid Field token."); + } + return MetadataTokens.FieldDefinitionHandle (rowId); + } + + if (rowId == 0 || rowId > methodCount) { + throw new JniRewriteException ("An ImplMap row references an invalid MethodDef token."); + } + return MetadataTokens.MethodDefinitionHandle (rowId); + } + + /// + /// The ExportedType.TypeDefId hint column (ECMA-335 II.22.14). It always occupies + /// bytes 4..7 of the row, after the fixed-width Flags column. + /// + public static unsafe int GetExportedTypeDefinitionId (MetadataReader reader, int rowNumber) + { + int rowSize = reader.GetTableRowSize (TableIndex.ExportedType); + long rowOffset = reader.GetTableMetadataOffset (TableIndex.ExportedType) + (long) (rowNumber - 1) * rowSize; + if (rowSize < 8 || rowOffset < 0 || rowOffset + rowSize > reader.MetadataLength) { + throw new JniRewriteException ("The ExportedType table extends past the end of the metadata."); + } + + var blob = new BlobReader (reader.MetadataPointer + (int) rowOffset + sizeof (uint), sizeof (uint)); + return blob.ReadInt32 (); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeResourceSectionCopier.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeResourceSectionCopier.cs new file mode 100644 index 00000000000..88e2aec6b83 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeResourceSectionCopier.cs @@ -0,0 +1,143 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// Re-emits an assembly's Win32 resource directory (.rsrc) at whatever address the + /// rebuilt PE places it. The directory tree is copied byte-for-byte; only the absolute RVAs + /// stored in each IMAGE_RESOURCE_DATA_ENTRY.OffsetToData are relocated, since every + /// other offset in the format is relative to the start of the directory. + /// + sealed class NativeResourceSectionCopier : ResourceSectionBuilder + { + const int ResourceDirectoryHeaderSize = 16; + const int ResourceDirectoryEntrySize = 8; + const int ResourceDataEntrySize = 16; + const uint HighBit = 0x80000000; + + readonly byte [] section; + readonly int originalRva; + readonly HashSet dataEntryRvaOffsets; + + NativeResourceSectionCopier (byte [] section, int originalRva, HashSet dataEntryRvaOffsets) + { + this.section = section; + this.originalRva = originalRva; + this.dataEntryRvaOffsets = dataEntryRvaOffsets; + } + + /// + /// Returns null when the assembly has no Win32 resources. + /// + public static NativeResourceSectionCopier? TryCreate (PEReader peReader) + { + PEHeader? peHeader = peReader.PEHeaders.PEHeader; + if (peHeader == null) { + return null; + } + + DirectoryEntry directory = peHeader.ResourceTableDirectory; + if (directory.RelativeVirtualAddress == 0 && directory.Size == 0) { + return null; + } + if (directory.RelativeVirtualAddress == 0 || directory.Size == 0) { + throw new JniRewriteException ("The Win32 resource directory has an invalid RVA or size."); + } + + PEMemoryBlock block = peReader.GetSectionData (directory.RelativeVirtualAddress); + if (block.Length < directory.Size) { + throw new JniRewriteException ("The Win32 resource directory extends past the end of its PE section."); + } + + byte [] section = block.GetReader (0, directory.Size).ReadBytes (directory.Size); + var offsets = new HashSet (); + CollectDataEntryOffsets (section, directory.RelativeVirtualAddress, directoryOffset: 0, depth: 0, offsets, new HashSet ()); + return new NativeResourceSectionCopier (section, directory.RelativeVirtualAddress, offsets); + } + + protected override void Serialize (BlobBuilder builder, SectionLocation location) + { + var relocated = (byte []) section.Clone (); + int delta = location.RelativeVirtualAddress - originalRva; + + foreach (int offset in dataEntryRvaOffsets) { + uint value = ReadUInt32 (relocated, offset); + WriteUInt32 (relocated, offset, unchecked ((uint) ((int) value + delta))); + } + + builder.WriteBytes (relocated); + } + + static void CollectDataEntryOffsets (byte [] section, int originalRva, int directoryOffset, int depth, HashSet offsets, HashSet visited) + { + if (depth > 8) { + throw new JniRewriteException ("The Win32 resource directory nests more deeply than the PE format allows."); + } + if (!visited.Add (directoryOffset)) { + throw new JniRewriteException ("The Win32 resource directory contains a cycle."); + } + RequireRange (section, directoryOffset, ResourceDirectoryHeaderSize); + + int namedEntries = ReadUInt16 (section, directoryOffset + 12); + int idEntries = ReadUInt16 (section, directoryOffset + 14); + int entryOffset = directoryOffset + ResourceDirectoryHeaderSize; + + for (int i = 0; i < namedEntries + idEntries; i++, entryOffset += ResourceDirectoryEntrySize) { + RequireRange (section, entryOffset, ResourceDirectoryEntrySize); + uint offsetToData = ReadUInt32 (section, entryOffset + 4); + + if ((offsetToData & HighBit) != 0) { + CollectDataEntryOffsets (section, originalRva, (int) (offsetToData & ~HighBit), depth + 1, offsets, visited); + continue; + } + + int dataEntryOffset = (int) offsetToData; + RequireRange (section, dataEntryOffset, ResourceDataEntrySize); + RequireDataWithinCopiedSection (section, originalRva, dataEntryOffset); + offsets.Add (dataEntryOffset); + } + } + + /// + /// Validates that an IMAGE_RESOURCE_DATA_ENTRY's data - its absolute RVA plus its + /// size - lies entirely within the resource section this class copied. Without this + /// check a corrupt or crafted data entry could point past the copied bytes and would only + /// be caught (if at all) once something tried to actually read the relocated resource. + /// + static void RequireDataWithinCopiedSection (byte [] section, int originalRva, int dataEntryOffset) + { + uint dataRva = ReadUInt32 (section, dataEntryOffset); + uint dataSize = ReadUInt32 (section, dataEntryOffset + 4); + + long relativeOffset = (long) dataRva - originalRva; + if (relativeOffset < 0 || relativeOffset + dataSize > section.Length) { + throw new JniRewriteException ($"A Win32 resource data entry references data (RVA 0x{dataRva:X}, {dataSize} byte(s)) outside of the copied resource section."); + } + } + + static void RequireRange (byte [] section, int offset, int length) + { + if (offset < 0 || length < 0 || offset > section.Length - length) { + throw new JniRewriteException ("The Win32 resource directory references data outside of the resource section."); + } + } + + static ushort ReadUInt16 (byte [] data, int offset) => (ushort) (data [offset] | (data [offset + 1] << 8)); + + static uint ReadUInt32 (byte [] data, int offset) + => (uint) (data [offset] | (data [offset + 1] << 8) | (data [offset + 2] << 16) | (data [offset + 3] << 24)); + + static void WriteUInt32 (byte [] data, int offset, uint value) + { + data [offset] = (byte) value; + data [offset + 1] = (byte) (value >> 8); + data [offset + 2] = (byte) (value >> 16); + data [offset + 3] = (byte) (value >> 24); + } + } +}