Skip to content

Commit d3d3584

Browse files
authored
Fixes: #9862 Changes: dotnet/java-interop@8221b7d...d3d3a1b * dotnet/java-interop@d3d3a1bf: [Java.Interop] JNIEnv::NewObject and Replaceable instances (dotnet/java-interop#1323) Context: 5c23bcd Issue #9862 is an observed "race condition" around `Android.App.Application` subclass creation. *Two* instances of `AndroidApp` were created, one from the "normal" app startup: at MailClient.Mobile.Droid.AndroidApp..ctor(IntPtr handle, JniHandleOwnership ownership) … at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type ) at Java.Lang.Object._GetObject[Application](IntPtr , JniHandleOwnership ) at Java.Lang.Object.GetObject[Application](IntPtr handle, JniHandleOwnership transfer) at Java.Lang.Object.GetObject[Application](IntPtr jnienv, IntPtr handle, JniHandleOwnership transfer) at Android.App.Application.n_OnCreate(IntPtr jnienv, IntPtr native__this) at crc647fae2f69c19dcd0d.AndroidApp.n_onCreate(Native Method) at crc647fae2f69c19dcd0d.AndroidApp.onCreate(AndroidApp.java:25) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1316) and another from an `androidx.work.WorkerFactory`, via parameter: at MailClient.Mobile.Droid.AndroidApp..ctor(IntPtr handle, JniHandleOwnership ownership) … at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type ) at Android.Runtime.JNIEnv.<>c.<CreateNativeArrayElementToManaged>b__70_9(Type type, IntPtr source, Int32 index) at Android.Runtime.JNIEnv.GetObjectArray(IntPtr , Type[] ) at Java.Interop.TypeManager.n_Activate(IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr) at mono.android.TypeManager.n_activate(Native Method) at mono.android.TypeManager.Activate(TypeManager.java:7) at crc647fae2f69c19dcd0d.SyncWorker.<init>(SyncWorker.java:23) at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at androidx.work.WorkerFactory.createWorkerWithDefaultFallback(WorkerFactory.java:95) However, what was odd about this "race condition" was that the *second* instance created would reliably win! Further investigation suggested that this was less of a "race condition" and more a bug in `AndroidValueManager`, wherein when "Replaceable" instances were created, an existing instance would *always* be replaced, even if the new instance was also Replaceable! This feels bananas; yes, Replaceable should be replaceable, but the expectation was that it would be replaced by *non*-Replaceable instances, not just any instance that came along later. Aside: a "Replaceable" instance is an instance created via `JniRuntime.JniValueManager.CreatePeer()` / `TypeManager.CreateInstance()`. "Replaceable" instances can be replaced in the `JniRuntime.JniValueManager` by instances created via the constructor, e.g. `new Integer(42)`. JniPeerReference h; using (var x = new Java.Lang.Integer(1)) h = x.PeerReference.NewLocalRef(); // No mapping for `h` in JniValueManager, as x was disposed. var a = JniEnvironment.Runtime.ValueManager.CreatePeer (ref h, JniObjectReferenceOptions.Copy, null); // a is "Replaceable" var b = new Java.Lang.Integer(x.Handle, JniHandleOwnership.DoNotTransfer); // b is *not* Replaceable; created via constructor var p = JniEnvironment.Runtime.ValueManager.PeekPeer(h); // p should be the non-Replaceable value b, *not* a dotnet/java-interop@d3d3a1bf updates `JniRuntimeJniValueManagerContract` to add a new `CreatePeer_ReplaceableDoesNotReplace()` test to codify the desired semantic that Replaceable instances do not replace Replaceable instances. Update `AndroidValueManager` so that the new `CreatePeer_ReplaceableDoesNotReplace()` test passes. Supporting this new semantic required "extending" Replaceable lifetime; it turns out that the existing code within `TypeManager.CreateInstance()`: var result = CreateProxy (type, handle, transfer); if (Runtime.IsGCUserPeer (result.PeerReference.Handle)) { result.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); } return result; sets `.Replaceable` *too late*, as during execution of the activation constructor the instance thinks it *isn't* replaceable, and thus creation of a new instance via the activation constructor will replace an already existing replaceable instance; it's not until *after* the constructor finished executing that we set `.Replaceable`. Address this by updating `CreatePeer()` to split up instance creation: 1. Create an *un-constructed* instance using [`RuntimeHelpers.GetUninitializedObject(Type)`][0]. 2. Set `.Replaceable` on the instance from (1) 3. *Then* invoke the activation constructor. This allows `JniRuntime.JniValueManager.AddPeer()` to reliably determine that an instance is `.Replaceable`, and thus *not* replace a `.Replaceable` instance with another `.Replaceable` instance. dotnet/java-interop@d3d3a1bf does similar things with `JniRuntime.JniValueManager`. Update `ManagedValueManager` to override the new `TryConstructPeer()` method, as `TryCreatePeer()` has been removed. Finally, an "interesting" crash happened when adding the `RuntimeHelpers.GetUninitializedObject()` + invoke constructor fix: E droid.NET_Test: JNI ERROR (app bug): accessed deleted Global 0x3056 F droid.NET_Test: java_vm_ext.cc:570] JNI DETECTED ERROR IN APPLICATION: use of deleted global reference 0x3056 or, when it didn't up and crash, we could see "bizarre" unit test failures from `JnienvTest.MoarThreadingTests()`: I NUnit : 1) Java.InteropTests.JnienvTest.MoarThreadingTests (Mono.Android.NET-Tests) I NUnit : No exception should be thrown [t2]! Got: System.ObjectDisposedException: Cannot access disposed object with JniIdentityHashCode=158880748. I NUnit : Object name: 'Java.Lang.Integer'. I NUnit : at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable self) I NUnit : at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeAbstractInt32Method(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) I NUnit : at Java.Lang.Integer.IntValue() I NUnit : at Java.Lang.Integer.System.IConvertible.ToInt32(IFormatProvider provider) I NUnit : at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider) I NUnit : at Android.Runtime.JNIEnv.GetObjectArray(IntPtr array_ptr, Type[] element_types) I NUnit : at Java.InteropTests.JnienvTest.<>c__DisplayClass26_0.<MoarThreadingTests>b__1() I NUnit : Expected: null I NUnit : But was: <System.ObjectDisposedException: Cannot access disposed object with JniIdentityHashCode=158880748. or: I NUnit : 1) Java.InteropTests.JnienvTest.MoarThreadingTests (Mono.Android.NET-Tests) I NUnit : No exception should be thrown [t2]! Got: System.ArgumentException: Handle must be valid. (Parameter 'instance') I NUnit : at Java.Interop.JniEnvironment.InstanceMethods.CallIntMethod(JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args) I NUnit : at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeAbstractInt32Method(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) I NUnit : at Java.Lang.Integer.IntValue() I NUnit : at Java.Lang.Integer.System.IConvertible.ToInt32(IFormatProvider provider) I NUnit : at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider) I NUnit : at Android.Runtime.JNIEnv.GetObjectArray(IntPtr array_ptr, Type[] element_types) I NUnit : at Java.InteropTests.JnienvTest.<>c__DisplayClass26_0.<MoarThreadingTests>b__1() The thread using the invalid global reference is `t1` in `MoarThreadingTests()`, which uses `JNIEnv.CopyObjectArray<int>(IntPtr, int[])`, which in turn uses `JavaConvert.FromJniHandle<T>()`. Why would that be failing? The answer: consider `JavaConvert.JniHandleConverters`: partial class JavaConvert { static Dictionary<Type, Func<IntPtr, JniHandleOwnership, object>> JniHandleConverters = new() { { typeof (int), (handle, transfer) => { using (var value = new Java.Lang.Integer (handle, transfer | JniHandleOwnership.DoNotRegister)) return value.IntValue (); } }, } } Note that we're invoking the `Integer(IntPtr, JniHandleOwnership)` constructor. As per the above Aside about Replaceable instances, this means that `value` should *replace* any existing mapping within the `JniRuntime.JniValueManager`. *However*, here was *also* provide `JniHandleOwnership.DoNotRegister`, which *prevents* the instance from being placed into the instance mapping, i.e. `Object.PeekObject()` *should not find the instance*. Next, turn to `Object.SetHandle()` from 5c23bcd: partial class /* Java.Lang. */ Object { protected void SetHandle (IntPtr value, JniHandleOwnership transfer) { var reference = new JniObjectReference (value); JNIEnvInit.ValueManager?.ConstructPeer ( this, ref reference, value == IntPtr.Zero ? JniObjectReferenceOptions.None : JniObjectReferenceOptions.Copy); JNIEnv.DeleteRef (value, transfer); } } What's missing? A check for `JniHandleOwnership.DoNotRegister`! Which means that the `new Integer(…)` will be in the instance map, *and replace any existing mappings*, only to be subsequently disposed. This permits the following "total execution order" to happen between threads `t1` and `t2` in `MoarThreadingTests()`: * Main thread: // JNI equivalent to Java `new Object[]{ new Integer (1) }` IntPtr lrefJliArray = JNIEnv.NewObjectArray<int>(new[]{1}) * `t1` Thread: IntPtr lrefInteger = JNIEnv.GetObjectArrayElement(lrefJliArray, 0); var value = new Integer(lrefInteger, .TransferLocalRef); // via JavaConvert.JniHandleConverters * `t2` Thread: IntPtr lrefInteger = JNIEnv.GetObjectArrayElement(lrefJliArray, 0); var x = Java.Lang.Object.GetObject (lrefInteger, .TransferLocalRef); // via JNIEnv.NativeArrayElementToManaged[typeof(IJavaObject)] Note that `x` is the same instance as `value`. * `t1` Thread: int v = value.IntValue(); value.Dispose(); * `t2` Thread: does *anything* with `x`. As `x` and `value` are the same instance, `x` has been disposed by thread `t1`. Depending on where thread scheduling, this would explain both the `ObjectDisposedException` as well as the `JNI ERROR (app bug)`. The fix is to correct the oversight from 5c23bcd, and forward the `JniHandleOwnership.DoNotRegister` flag to `JniObjectReferenceOptions`. *On the way to investigating this…* Update `tests/Mono.Android-Tests` to use `@(AndroidEnvironment)` in Debug builds to set `debug.mono.debug=1`. This allows for filenames and line numbers in stack traces. Update `src/native` so that the GREF log can be created; previously, `adb logcat` would contain: W monodroid: Failed to create directory '/data/user/0/Mono.Android.NET_Tests/files/.__override__/arm64-v8a'. No such file or directory … E monodroid: fopen failed for file /data/user/0/Mono.Android.NET_Tests/files/.__override__/arm64-v8a/grefs.txt: No such file or directory The apparent cause for this is that the ABI is now part of the path, `…/.__override__/arm64-v8a/grefs.txt` and not `…/.__override__/grefs.txt`. Additionally, `create_public_directory()` was a straight `mkdir()` call, which *does not* create intermediate directories. Update `create_public_directory()` to use `create_directory()` instead, which *does* create intermediate directories. This allows `grefs.txt` to be created. Next, the contents of `grefs.txt` occasionally looked "wrong". Turns out, `WriteGlobalReferenceLine()` and `WriteLocalReferenceLine()` didn't consistently append a newline to the message, which impacts e.g. the `Created …` message: Created PeerReference=0x3c06/G IdentityHashCode=0x2e29bd Instance=0xbe0aab36 Instance.Type=Android.OS.Bundle, Java.Type=android/os/Bundle+g+ grefc 19 gwrefc 0 obj-handle 0x706f711035/L -> new-handle 0x3d06/G from thread '<null>'(1) Update `WriteGlobalReferenceLine()` and `WriteLocalReferenceLine()` to always append a newline to the message. [0]: https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.runtimehelpers.getuninitializedobject?view=net-9.0
1 parent f03662c commit d3d3584

File tree

8 files changed

+51
-14
lines changed

8 files changed

+51
-14
lines changed

src/Mono.Android/Android.Runtime/AndroidRuntime.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,13 @@ public override IntPtr ReleaseLocalReference (ref JniObjectReference value, ref
173173
public override void WriteLocalReferenceLine (string format, params object?[] args)
174174
{
175175
RuntimeNativeMethods._monodroid_gref_log ("[LREF] " + string.Format (CultureInfo.InvariantCulture, format, args));
176+
RuntimeNativeMethods._monodroid_gref_log ("\n");
176177
}
177178

178179
public override void WriteGlobalReferenceLine (string format, params object?[] args)
179180
{
180181
RuntimeNativeMethods._monodroid_gref_log (string.Format (CultureInfo.InvariantCulture, format, args));
182+
RuntimeNativeMethods._monodroid_gref_log ("\n");
181183
}
182184

183185
public override JniObjectReference CreateGlobalReference (JniObjectReference value)
@@ -689,7 +691,7 @@ internal void AddPeer (IJavaPeerable value, JniObjectReference reference, IntPtr
689691
for (int i = 0; i < targets.Count; ++i) {
690692
IJavaPeerable? target;
691693
var wref = targets [i];
692-
if (ShouldReplaceMapping (wref!, reference, out target)) {
694+
if (ShouldReplaceMapping (wref!, reference, value, out target)) {
693695
found = true;
694696
targets [i] = IdentityHashTargets.CreateWeakReference (value);
695697
break;
@@ -747,7 +749,7 @@ internal void AddPeer (IJavaPeerable value, IntPtr handle, JniHandleOwnership tr
747749
}
748750
}
749751

750-
bool ShouldReplaceMapping (WeakReference<IJavaPeerable> current, JniObjectReference reference, out IJavaPeerable? target)
752+
bool ShouldReplaceMapping (WeakReference<IJavaPeerable> current, JniObjectReference reference, IJavaPeerable value, out IJavaPeerable? target)
751753
{
752754
target = null;
753755

@@ -771,12 +773,17 @@ bool ShouldReplaceMapping (WeakReference<IJavaPeerable> current, JniObjectRefere
771773
// we want the 2nd MCW to replace the 1st, as the 2nd is
772774
// the one the dev created; the 1st is an implicit intermediary.
773775
//
776+
// Meanwhile, a new "replaceable" instance should *not* replace an
777+
// existing "replaceable" instance; see dotnet/android#9862.
778+
//
774779
// [0]: If Java ctor invokes overridden virtual method, we'll
775780
// transition into managed code w/o a registered instance, and
776781
// thus will create an "intermediary" via
777782
// (IntPtr, JniHandleOwnership) .ctor.
778-
if ((target.JniManagedPeerState & JniManagedPeerStates.Replaceable) == JniManagedPeerStates.Replaceable)
783+
if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) &&
784+
!value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) {
779785
return true;
786+
}
780787

781788
return false;
782789
}

src/Mono.Android/Java.Interop/TypeManager.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,22 +382,34 @@ internal static object CreateProxy (
382382
{
383383
// Skip Activator.CreateInstance() as that requires public constructors,
384384
// and we want to hide some constructors for sanity reasons.
385+
var peer = GetUninitializedObject (type);
385386
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
386387
var c = type.GetConstructor (flags, null, XAConstructorSignature, null);
387388
if (c != null) {
388-
return c.Invoke (new object [] { handle, transfer });
389+
c.Invoke (peer, new object[] { handle, transfer });
390+
return peer;
389391
}
390392
c = type.GetConstructor (flags, null, JIConstructorSignature, null);
391393
if (c != null) {
392394
JniObjectReference r = new JniObjectReference (handle);
393395
JniObjectReferenceOptions o = JniObjectReferenceOptions.Copy;
394-
var peer = (IJavaPeerable) c.Invoke (new object [] { r, o });
396+
c.Invoke (peer, new object [] { r, o });
395397
JNIEnv.DeleteRef (handle, transfer);
396398
return peer;
397399
}
400+
GC.SuppressFinalize (peer);
398401
throw new MissingMethodException (
399402
"No constructor found for " + type.FullName + "::.ctor(System.IntPtr, Android.Runtime.JniHandleOwnership)",
400403
CreateJavaLocationException ());
404+
405+
static IJavaPeerable GetUninitializedObject (
406+
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
407+
Type type)
408+
{
409+
var v = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (type);
410+
v.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable);
411+
return v;
412+
}
401413
}
402414

403415
public static void RegisterType (string java_class, Type t)

src/Mono.Android/Java.Lang/Object.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,22 @@ protected override void Dispose (bool disposing)
110110
protected void SetHandle (IntPtr value, JniHandleOwnership transfer)
111111
{
112112
var reference = new JniObjectReference (value);
113+
var options = FromJniHandleOwnership (transfer);
113114
JNIEnvInit.ValueManager?.ConstructPeer (
114115
this,
115116
ref reference,
116-
value == IntPtr.Zero ? JniObjectReferenceOptions.None : JniObjectReferenceOptions.Copy);
117+
value == IntPtr.Zero ? JniObjectReferenceOptions.None : options);
117118
JNIEnv.DeleteRef (value, transfer);
118119
}
119120

121+
static JniObjectReferenceOptions FromJniHandleOwnership (JniHandleOwnership transfer)
122+
{
123+
var options = JniObjectReferenceOptions.Copy;
124+
if (transfer.HasFlag (JniHandleOwnership.DoNotRegister))
125+
options |= JniObjectReferenceOptions.CopyAndDoNotRegister;
126+
return options;
127+
}
128+
120129
internal static IJavaPeerable? PeekObject (IntPtr handle, Type? requiredType = null)
121130
{
122131
var peeked = JNIEnvInit.ValueManager?.PeekPeer (new JniObjectReference (handle));

src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,8 @@ public override List<JniSurfacedPeerInfo> GetSurfacedPeers ()
265265

266266
static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) };
267267

268-
protected override IJavaPeerable? TryCreatePeer (
268+
protected override bool TryConstructPeer (
269+
IJavaPeerable self,
269270
ref JniObjectReference reference,
270271
JniObjectReferenceOptions options,
271272
[DynamicallyAccessedMembers (Constructors)]
@@ -277,10 +278,10 @@ public override List<JniSurfacedPeerInfo> GetSurfacedPeers ()
277278
reference.Handle,
278279
JniHandleOwnership.DoNotTransfer,
279280
};
280-
var p = (IJavaPeerable) c.Invoke (args);
281+
c.Invoke (self, args);
281282
JniObjectReference.Dispose (ref reference, options);
282-
return p;
283+
return true;
283284
}
284-
return base.TryCreatePeer (ref reference, options, type);
285+
return base.TryConstructPeer (self, ref reference, options, type);
285286
}
286287
}

src/native/mono/runtime-base/util.cc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,10 @@ Util::path_combine (const char *path1, const char *path2)
147147
void
148148
Util::create_public_directory (const char *dir)
149149
{
150-
mode_t m = umask (0);
151-
int ret = mkdir (dir, 0777);
150+
int ret = create_directory (dir, 0777);
152151
if (ret < 0) {
153152
log_warn (LOG_DEFAULT, "Failed to create directory '{}'. {}", dir, std::strerror (errno));
154153
}
155-
umask (m);
156154
}
157155

158156
int

tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@
5252
<_DefaultValueAttributeSupport Condition="'$(TrimMode)' == 'full'">true</_DefaultValueAttributeSupport>
5353
</PropertyGroup>
5454

55+
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
56+
<!-- trying to track:
57+
JNI ERROR (app bug): accessed deleted Global 0x3056
58+
-->
59+
<AndroidEnvironment Include="env.txt" />
60+
</ItemGroup>
61+
5562
<ItemGroup>
5663
<ProjectReference Include="..\..\TestRunner.Core\TestRunner.Core.NET.csproj" />
5764
<ProjectReference Include="..\..\TestRunner.NUnit\TestRunner.NUnit.NET.csproj" />
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Environment Variables and system properties
2+
# debug.mono.log=gref,default
3+
debug.mono.debug=1

0 commit comments

Comments
 (0)