From 2a8e7220a2b0cf84b29e71c605076cb449658da8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 14:59:06 +0000 Subject: [PATCH 1/5] Initial plan From a121db4450d8305773861a1312114dcd2d223355 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 15:22:35 +0000 Subject: [PATCH 2/5] Revise trimmable typemap unsafe boundaries Agent-Logs-Url: https://github.com/dotnet/android/sessions/f976dfb0-61ce-4e48-b20e-ebcb73ddb9cf Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 5 ++++- .../Scanner/ScannerHashingHelper.cs | 18 ++++++++---------- .../TrimmableTypeMap.cs | 13 ++++++++----- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index c624d72ec71..a67da08dc16 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -62,7 +62,10 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// JniNativeMethod* methods = stackalloc JniNativeMethod[2]; /// methods[0] = new JniNativeMethod(&__utf8_0, &__utf8_1, &n_OnCreate_uco_0); /// methods[1] = new JniNativeMethod(&__utf8_2, &__utf8_3, &nctor_0_uco); -/// JniEnvironment.Types.RegisterNatives(jniType.PeerReference, new ReadOnlySpan<JniNativeMethod>(methods, 2)); +/// unsafe { +/// // SAFETY: methods points to the stackalloc buffer filled above; the UTF-8 fields and UCO entry points are module-lifetime data. +/// JniEnvironment.Types.RegisterNatives(jniType.PeerReference, new ReadOnlySpan<JniNativeMethod>(methods, 2)); +/// } /// } /// } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index fe3aa9aaed0..67220506b8f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Buffers.Binary; +using System.Text; using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -45,20 +46,17 @@ static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); } - static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) + static int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) { - int bytesWritten = 0; - fixed (char* nsPtr = ns) - fixed (byte* destinationPtr = destination) { - bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); - } + byte[] nsBytes = Encoding.UTF8.GetBytes (ns); + nsBytes.AsSpan ().CopyTo (destination); + int bytesWritten = nsBytes.Length; destination [bytesWritten++] = (byte) ':'; - fixed (char* assemblyNamePtr = assemblyName) - fixed (byte* destinationPtr = destination) { - bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten); - } + byte[] assemblyNameBytes = Encoding.UTF8.GetBytes (assemblyName); + assemblyNameBytes.AsSpan ().CopyTo (destination.Slice (bytesWritten)); + bytesWritten += assemblyNameBytes.Length; return bytesWritten; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index a574c00a066..f1d65285e86 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -129,7 +129,7 @@ static void InitializeCore (ITypeMap typeMap) } } - internal static unsafe void RegisterNativeMethods () + internal static void RegisterNativeMethods () { lock (s_initLock) { if (s_nativeMethodsRegistered) { @@ -142,10 +142,13 @@ internal static unsafe void RegisterNativeMethods () } using var runtimeClass = new JniType ("mono/android/Runtime"u8); - fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) { - var onRegisterNatives = (IntPtr)(delegate* unmanaged)&OnRegisterNatives; - var method = new JniNativeMethod (name, sig, onRegisterNatives); - JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); + unsafe { + // SAFETY: the UTF-8 literals are pinned for the duration of RegisterNatives, and OnRegisterNatives is a static UCO entry point. + fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) { + var onRegisterNatives = (IntPtr)(delegate* unmanaged)&OnRegisterNatives; + var method = new JniNativeMethod (name, sig, onRegisterNatives); + JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); + } } s_nativeMethodsRegistered = true; } From a80e6a3ba95201821f3207d383b107637c02c40a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 15:27:09 +0000 Subject: [PATCH 3/5] Avoid temporary UTF8 arrays in typemap hashing Agent-Logs-Url: https://github.com/dotnet/android/sessions/f976dfb0-61ce-4e48-b20e-ebcb73ddb9cf Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/ScannerHashingHelper.cs | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index 67220506b8f..031046cf30e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -13,7 +13,7 @@ internal static string ToLegacyCrc64 (string ns, string assemblyName) int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); byte[] rented = ArrayPool.Shared.Rent (byteCount); try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented); ulong crc = ulong.MaxValue; ulong length = 0; Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); @@ -27,18 +27,18 @@ internal static string ToLegacyCrc64 (string ns, string assemblyName) internal static string ToCrc64 (string ns, string assemblyName) { - const int stackallocThresholdBytes = 256; int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - Span utf8Buffer = byteCount <= stackallocThresholdBytes - ? stackalloc byte [stackallocThresholdBytes] - : new byte [byteCount]; - - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount)); - Span hash = stackalloc byte [8]; - System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash); - ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash); - BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); - return ToHexString (hash); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented); + Span hash = stackalloc byte [8]; + System.IO.Hashing.Crc64.Hash (rented.AsSpan (0, bytesWritten), hash); + ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash); + BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); + return ToHexString (hash); + } finally { + ArrayPool.Shared.Return (rented); + } } static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) @@ -46,17 +46,13 @@ static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); } - static int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) + static int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, byte[] destination) { - byte[] nsBytes = Encoding.UTF8.GetBytes (ns); - nsBytes.AsSpan ().CopyTo (destination); - int bytesWritten = nsBytes.Length; + int bytesWritten = Encoding.UTF8.GetBytes (ns, 0, ns.Length, destination, 0); destination [bytesWritten++] = (byte) ':'; - byte[] assemblyNameBytes = Encoding.UTF8.GetBytes (assemblyName); - assemblyNameBytes.AsSpan ().CopyTo (destination.Slice (bytesWritten)); - bytesWritten += assemblyNameBytes.Length; + bytesWritten += Encoding.UTF8.GetBytes (assemblyName, 0, assemblyName.Length, destination, bytesWritten); return bytesWritten; } From 9f6abc4dcc8f4c1ad6c1079ecc84c71581b2a7ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 19:48:51 +0000 Subject: [PATCH 4/5] Address scanner unsafe review feedback Agent-Logs-Url: https://github.com/dotnet/android/sessions/b540e64f-426f-4e62-89e3-b09e5e497c93 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/ScannerHashingHelper.cs | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index 031046cf30e..e6c6d7f7ae6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -1,7 +1,6 @@ using System; using System.Buffers; using System.Buffers.Binary; -using System.Text; using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -13,7 +12,11 @@ internal static string ToLegacyCrc64 (string ns, string assemblyName) int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); byte[] rented = ArrayPool.Shared.Rent (byteCount); try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented); + int bytesWritten; + unsafe { + // SAFETY: rented is at least byteCount bytes, which is the exact UTF-8 byte count for the data written by GetNamespaceAssemblyUtf8Bytes. + bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + } ulong crc = ulong.MaxValue; ulong length = 0; Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); @@ -27,18 +30,22 @@ internal static string ToLegacyCrc64 (string ns, string assemblyName) internal static string ToCrc64 (string ns, string assemblyName) { + const int stackallocThresholdBytes = 256; int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - byte[] rented = ArrayPool.Shared.Rent (byteCount); - try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented); - Span hash = stackalloc byte [8]; - System.IO.Hashing.Crc64.Hash (rented.AsSpan (0, bytesWritten), hash); - ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash); - BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); - return ToHexString (hash); - } finally { - ArrayPool.Shared.Return (rented); + Span utf8Buffer = byteCount <= stackallocThresholdBytes + ? stackalloc byte [stackallocThresholdBytes] + : new byte [byteCount]; + + int bytesWritten; + unsafe { + // SAFETY: utf8Buffer is at least byteCount bytes, which is the exact UTF-8 byte count for the data written by GetNamespaceAssemblyUtf8Bytes. + bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount)); } + Span hash = stackalloc byte [8]; + System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash); + ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash); + BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); + return ToHexString (hash); } static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) @@ -46,13 +53,24 @@ static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); } - static int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, byte[] destination) + /// + /// must contain at least + /// bytes for and . + /// + static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) { - int bytesWritten = Encoding.UTF8.GetBytes (ns, 0, ns.Length, destination, 0); + int bytesWritten = 0; + fixed (char* nsPtr = ns) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); + } destination [bytesWritten++] = (byte) ':'; - bytesWritten += Encoding.UTF8.GetBytes (assemblyName, 0, assemblyName.Length, destination, bytesWritten); + fixed (char* assemblyNamePtr = assemblyName) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten); + } return bytesWritten; } From 625266073afb9a5893ea86aababd47864a542927 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 19:52:27 +0000 Subject: [PATCH 5/5] Clean up unsafe hashing helper assignment Agent-Logs-Url: https://github.com/dotnet/android/sessions/b540e64f-426f-4e62-89e3-b09e5e497c93 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/ScannerHashingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index e6c6d7f7ae6..163504d7a3c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -59,10 +59,10 @@ static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) /// static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) { - int bytesWritten = 0; + int bytesWritten; fixed (char* nsPtr = ns) fixed (byte* destinationPtr = destination) { - bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); + bytesWritten = System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); } destination [bytesWritten++] = (byte) ':';