Skip to content

Change in behavior around Java.Lang.Object.Dispose leads to a crash when calling Dispose recursively #4989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
PureWeen opened this issue Aug 7, 2020 · 2 comments · Fixed by #5022
Assignees
Labels
Area: App Runtime Issues in `libmonodroid.so`.

Comments

@PureWeen
Copy link
Member

PureWeen commented Aug 7, 2020

Steps to Reproduce

Related Xamarin.Forms issue here
xamarin/Xamarin.Forms#11029

User started seeing a crash that they weren't seeing before

App34.zip

  1. Run the included app on vs2019 => crash
  2. Run the included app on vs2017 => no crash

I realize the code that causes this is kind of weird but I just wanted to highlight the change in behavior

            var thing = new ShaneTextView(this);
            thing.Dispose();
        }


        public class ShaneTextView : TextView
        {
            bool _isDisposed;
            public ShaneTextView(Context context) : base(context)
            {
            }

            protected override void Dispose(bool disposing)
            {
                if (_isDisposed)
                {
                    return;
                }

                _isDisposed = true;
                if (this.Handle != IntPtr.Zero)
                    this.Dispose();

                base.Dispose(disposing);
            }
        }

Expected Behavior

Not crash

Actual Behavior

Crashes with

08-07 18:23:15.735 I/MonoDroid(22890): UNHANDLED EXCEPTION:
08-07 18:23:15.737 I/MonoDroid(22890): System.ArgumentException: Must have a valid JNI object reference!
08-07 18:23:15.737 I/MonoDroid(22890): Parameter name: value
08-07 18:23:15.737 I/MonoDroid(22890):   at Android.Runtime.AndroidValueManager.RemovePeer (Java.Interop.IJavaPeerable value) [0x0001e] in <a522ecdee49144d0b8411a1cab7b9ada>:0 
08-07 18:23:15.737 I/MonoDroid(22890):   at Java.Interop.JniRuntime+JniValueManager.DisposePeer (Java.Interop.JniObjectReference h, Java.Interop.IJavaPeerable value) [0x0001f] in <bf671abdfa384ce99d758b134b9dd5bf>:0 
08-07 18:23:15.737 I/MonoDroid(22890):   at Java.Interop.JniRuntime+JniValueManager.DisposePeer (Java.Interop.IJavaPeerable value) [0x00038] in <bf671abdfa384ce99d758b134b9dd5bf>:0 
08-07 18:23:15.737 I/MonoDroid(22890):   at Java.Lang.Object.Dispose () [0x00009] in <a522ecdee49144d0b8411a1cab7b9ada>:0 
08-07 18:23:15.737 I/MonoDroid(22890):   at App34.MainActivity.OnCreate (Android.OS.Bundle savedInstanceState) [0x00057] in C:\Users\shane\Source\Repos\App34\MainActivity.cs:29 
08-07 18:23:15.737 I/MonoDroid(22890):   at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_ (System.IntPtr jnienv, System.IntPtr native__this, System.IntPtr native_savedInstanceState) [0x0000f] in <a522ecdee49144d0b8411a1cab7b9ada>:0 
08-07 18:23:15.737 I/MonoDroid(22890):   at (wrapper dynamic-method) Android.Runtime.DynamicMethodNameCounter.3(intptr,intptr,intptr)

Version Information

Microsoft Visual Studio Enterprise 2019
Version 16.7.0
VisualStudio.16.Release/16.7.0+30330.147
Microsoft .NET Framework
Version 4.8.04084

Installed Version: Enterprise

Visual C++ 2019   00435-60000-00000-AA022
Microsoft Visual C++ 2019

ASP.NET and Web Tools 2019   16.7.532.28833
ASP.NET and Web Tools 2019

ASP.NET Core Razor Language Services   16.1.0.2035807+72d099b977d3a85e65fa3b0614ca8cfc803fef02
Provides languages services for ASP.NET Core Razor.

ASP.NET Web Frameworks and Tools 2019   16.7.532.28833
For additional information, visit https://www.asp.net/

Azure App Service Tools v3.0.0   16.7.532.28833
Azure App Service Tools v3.0.0

Azure Functions and Web Jobs Tools   16.7.532.28833
Azure Functions and Web Jobs Tools

C# Tools   3.7.0-6.20375.2+34202cc2f3e869fd70a26d8237f4552cf9e192cf
C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Common Azure Tools   1.10
Provides common services for use by Azure Mobile Services and Microsoft Azure Tools.

Extensibility Message Bus   1.2.6 (master@34d6af2)
Provides common messaging-based MEF services for loosely coupled Visual Studio extension components communication and integration.

IntelliCode Extension   1.0
IntelliCode Visual Studio Extension Detailed Info

Markdown Editor   1.12.253
A full featured Markdown editor with live preview and syntax highlighting. Supports GitHub flavored Markdown.

Microsoft Azure Tools   2.9
Microsoft Azure Tools for Microsoft Visual Studio 2019 - v2.9.30701.1

Microsoft Continuous Delivery Tools for Visual Studio   0.4
Simplifying the configuration of Azure DevOps pipelines from within the Visual Studio IDE.

Microsoft JVM Debugger   1.0
Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines

Microsoft Library Manager   2.1.79+ge3567815aa.RR
Install client-side libraries easily to any web project

Microsoft MI-Based Debugger   1.0
Provides support for connecting Visual Studio to MI compatible debuggers

Microsoft Visual C++ Wizards   1.0
Microsoft Visual C++ Wizards

Microsoft Visual Studio Tools for Containers   1.1
Develop, run, validate your ASP.NET Core applications in the target environment. F5 your application directly into a container with debugging, or CTRL + F5 to edit & refresh your app without having to rebuild the container.

Microsoft Visual Studio VC Package   1.0
Microsoft Visual Studio VC Package

Mono Debugging for Visual Studio   16.7.5 (112c7bc)
Support for debugging Mono processes with Visual Studio.

NuGet Package Manager   5.7.0
NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/

Project System Tools   1.0
Tools for working with C#, VisualBasic, and F# projects.

ProjectServicesPackage Extension   1.0
ProjectServicesPackage Visual Studio Extension Detailed Info

Snapshot Debugging Extension   1.0
Snapshot Debugging Visual Studio Extension Detailed Info

SQL Server Data Tools   16.0.62007.09200
Microsoft SQL Server Data Tools

Syntax Visualizer   1.0
An extension for visualizing Roslyn SyntaxTrees.

TypeScript Tools   16.0.20702.2001
TypeScript Tools for Microsoft Visual Studio

Visual Basic Tools   3.7.0-6.20375.2+34202cc2f3e869fd70a26d8237f4552cf9e192cf
Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Visual F# Tools 10.10.0.0 for F# 4.7   16.7.0-beta.20361.3+3ef6f0b514198c0bfa6c2c09fefe41a740b024d5
Microsoft Visual F# Tools 10.10.0.0 for F# 4.7

Visual Studio Code Debug Adapter Host Package   1.0
Interop layer for hosting Visual Studio Code debug adapters in Visual Studio

Visual Studio Container Tools Extensions (Preview)   1.0
View, manage, and diagnose containers within Visual Studio.

Visual Studio Tools for Containers   1.0
Visual Studio Tools for Containers

VisualStudio.DeviceLog   1.0
Information about my package

VisualStudio.Foo   1.0
Information about my package

VisualStudio.Mac   1.0
Mac Extension for Visual Studio

Xamarin   16.7.000.440 (d16-7@358f3c6)
Visual Studio extension to enable development for Xamarin.iOS and Xamarin.Android.

Xamarin Designer   16.7.0.495 (remotes/origin/d16-7@79c0c522c)
Visual Studio extension to enable Xamarin Designer tools in Visual Studio.

Xamarin Templates   16.7.85 (1bcbbdf)
Templates for building iOS, Android, and Windows apps with Xamarin and Xamarin.Forms.

Xamarin.Android SDK   11.0.0.3 (d16-7/aca845b)
Xamarin.Android Reference Assemblies and MSBuild support.
    Mono: 83105ba
    Java.Interop: xamarin/java.interop/d16-7@1f3388a
    ProGuard: Guardsquare/proguard/proguard6.2.2@ebe9000
    SQLite: xamarin/sqlite/3.32.1@1a3276b
    Xamarin.Android Tools: xamarin/xamarin-android-tools/d16-7@017078f


Xamarin.iOS and Xamarin.Mac SDK   13.20.2.2 (817b6f72a)
Xamarin.iOS and Xamarin.Mac Reference Assemblies and MSBuild support.
@PureWeen PureWeen added the Area: App Runtime Issues in `libmonodroid.so`. label Aug 7, 2020
@grendello grendello added this to the Under Consideration milestone Aug 10, 2020
@grendello grendello removed their assignment Aug 10, 2020
jonpryor added a commit to jonpryor/java.interop that referenced this issue Aug 14, 2020
Context: dotnet/android#4989

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : JavaObject {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.PeerReference.IsValid)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `JavaObject.Dispose()`
when `PeerReference` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	  	Native stacktrace:
	  =================================================================
	  	0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	  	0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	  	0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	  	0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	  	0x700009b716b0 - Unknown
	  	0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	  	0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	  	0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	  	0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	  	0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	  	0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	  	0x107a1c636 - Unknown
	  	0x107aa2c73 - Unknown

	…
	  =================================================================
	  	Managed Stacktrace:
	  =================================================================
	  	  at <unknown> <0xffffffff>
	  	  at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	  	  at Types:GetObjectClass <0x0010a>
	  	  at Types:GetJniTypeNameFromInstance <0x000a2>
	  	  at JniValueManager:DisposePeer <0x002c2>
	  	  at JniValueManager:DisposePeer <0x000f2>
	  	  at Java.Interop.JavaObject:Dispose <0x000ea>
	  	  at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	  	  at System.Object:runtime_invoke_void__this__ <0x000b0>
	  	  at <unknown> <0xffffffff>
	  	  at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	  	  at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	  	  at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
 …. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.
jonpryor added a commit to jonpryor/java.interop that referenced this issue Aug 14, 2020
Context: dotnet/android#4989

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : JavaObject {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.PeerReference.IsValid)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `JavaObject.Dispose()`
when `PeerReference` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	  	Native stacktrace:
	  =================================================================
	  	0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	  	0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	  	0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	  	0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	  	0x700009b716b0 - Unknown
	  	0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	  	0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	  	0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	  	0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	  	0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	  	0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	  	0x107a1c636 - Unknown
	  	0x107aa2c73 - Unknown

	…
	  =================================================================
	  	Managed Stacktrace:
	  =================================================================
	  	  at <unknown> <0xffffffff>
	  	  at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	  	  at Types:GetObjectClass <0x0010a>
	  	  at Types:GetJniTypeNameFromInstance <0x000a2>
	  	  at JniValueManager:DisposePeer <0x002c2>
	  	  at JniValueManager:DisposePeer <0x000f2>
	  	  at Java.Interop.JavaObject:Dispose <0x000ea>
	  	  at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	  	  at System.Object:runtime_invoke_void__this__ <0x000b0>
	  	  at <unknown> <0xffffffff>
	  	  at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	  	  at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	  	  at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
11. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.
grendello pushed a commit to dotnet/java-interop that referenced this issue Aug 15, 2020
Context: dotnet/android#4989

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : JavaObject {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.PeerReference.IsValid)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `JavaObject.Dispose()`
when `PeerReference` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	  	Native stacktrace:
	  =================================================================
	  	0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	  	0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	  	0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	  	0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	  	0x700009b716b0 - Unknown
	  	0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	  	0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	  	0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	  	0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	  	0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	  	0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	  	0x107a1c636 - Unknown
	  	0x107aa2c73 - Unknown

	…
	  =================================================================
	  	Managed Stacktrace:
	  =================================================================
	  	  at <unknown> <0xffffffff>
	  	  at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	  	  at Types:GetObjectClass <0x0010a>
	  	  at Types:GetJniTypeNameFromInstance <0x000a2>
	  	  at JniValueManager:DisposePeer <0x002c2>
	  	  at JniValueManager:DisposePeer <0x000f2>
	  	  at Java.Interop.JavaObject:Dispose <0x000ea>
	  	  at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	  	  at System.Object:runtime_invoke_void__this__ <0x000b0>
	  	  at <unknown> <0xffffffff>
	  	  at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	  	  at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	  	  at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
11. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.
jonpryor added a commit to jonpryor/xamarin-android that referenced this issue Aug 17, 2020
Fixes: dotnet#4989

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : Java.Lang.Object {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.Handle != IntPtr.Zero)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `Object.Dispose()`
when `Object.Handle` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	        Native stacktrace:
	  =================================================================
	        0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	        0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	        0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	        0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	        0x700009b716b0 - Unknown
	        0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	        0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	        0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	        0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	        0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	        0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	        0x107a1c636 - Unknown
	        0x107aa2c73 - Unknown

	…
	  =================================================================
	        Managed Stacktrace:
	  =================================================================
	          at <unknown> <0xffffffff>
	          at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	          at Types:GetObjectClass <0x0010a>
	          at Types:GetJniTypeNameFromInstance <0x000a2>
	          at JniValueManager:DisposePeer <0x002c2>
	          at JniValueManager:DisposePeer <0x000f2>
	          at Java.Interop.JavaObject:Dispose <0x000ea>
	          at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	          at System.Object:runtime_invoke_void__this__ <0x000b0>
	          at <unknown> <0xffffffff>
	          at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	          at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	          at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
11. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.

Bump to dotnet/java-interop@007b35b, which contains fixes to
`JniRuntime.JniValueManager.DisposePeer()`, and add appropriate tests.
grendello pushed a commit that referenced this issue Aug 18, 2020
Fixes: #4989

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : Java.Lang.Object {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.Handle != IntPtr.Zero)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `Object.Dispose()`
when `Object.Handle` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	        Native stacktrace:
	  =================================================================
	        0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	        0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	        0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	        0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	        0x700009b716b0 - Unknown
	        0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	        0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	        0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	        0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	        0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	        0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	        0x107a1c636 - Unknown
	        0x107aa2c73 - Unknown

	…
	  =================================================================
	        Managed Stacktrace:
	  =================================================================
	          at <unknown> <0xffffffff>
	          at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	          at Types:GetObjectClass <0x0010a>
	          at Types:GetJniTypeNameFromInstance <0x000a2>
	          at JniValueManager:DisposePeer <0x002c2>
	          at JniValueManager:DisposePeer <0x000f2>
	          at Java.Interop.JavaObject:Dispose <0x000ea>
	          at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	          at System.Object:runtime_invoke_void__this__ <0x000b0>
	          at <unknown> <0xffffffff>
	          at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	          at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	          at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
11. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.

Bump to dotnet/java-interop@007b35b, which contains fixes to
`JniRuntime.JniValueManager.DisposePeer()`, and add appropriate tests.
jonpryor added a commit to dotnet/java-interop that referenced this issue Aug 31, 2020
Context: dotnet/android#4989

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : JavaObject {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.PeerReference.IsValid)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `JavaObject.Dispose()`
when `PeerReference` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	  	Native stacktrace:
	  =================================================================
	  	0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	  	0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	  	0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	  	0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	  	0x700009b716b0 - Unknown
	  	0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	  	0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	  	0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	  	0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	  	0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	  	0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	  	0x107a1c636 - Unknown
	  	0x107aa2c73 - Unknown

	…
	  =================================================================
	  	Managed Stacktrace:
	  =================================================================
	  	  at <unknown> <0xffffffff>
	  	  at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	  	  at Types:GetObjectClass <0x0010a>
	  	  at Types:GetJniTypeNameFromInstance <0x000a2>
	  	  at JniValueManager:DisposePeer <0x002c2>
	  	  at JniValueManager:DisposePeer <0x000f2>
	  	  at Java.Interop.JavaObject:Dispose <0x000ea>
	  	  at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	  	  at System.Object:runtime_invoke_void__this__ <0x000b0>
	  	  at <unknown> <0xffffffff>
	  	  at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	  	  at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	  	  at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
11. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.
jonpryor added a commit that referenced this issue Aug 31, 2020
Fixes: dotnet/java-interop#694
Fixes: #3776
Fixes: #4989

Changes: dotnet/java-interop@a92fd18...b4c2a67

  * dotnet/java-interop@b4c2a67c: [Xamarin.Android.Tools.Bytecode] Hide Kotlin synthetic constructors (#700)
  * dotnet/java-interop@4141d848: [Java.Interop] Avoid double-disposing PeerReferences (#690)
jonpryor added a commit to jonpryor/xamarin-android that referenced this issue Sep 4, 2020
Fixes: dotnet#4989
Fixes: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1193891

Commit 62a5709 forgot to apply the `Mono.Android.dll` changes! (Doh!)

Property cherry-pick d4e0738.

~~~

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : Java.Lang.Object {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.Handle != IntPtr.Zero)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `Object.Dispose()`
when `Object.Handle` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	        Native stacktrace:
	  =================================================================
	        0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	        0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	        0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	        0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	        0x700009b716b0 - Unknown
	        0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	        0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	        0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	        0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	        0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	        0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	        0x107a1c636 - Unknown
	        0x107aa2c73 - Unknown

	…
	  =================================================================
	        Managed Stacktrace:
	  =================================================================
	          at <unknown> <0xffffffff>
	          at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	          at Types:GetObjectClass <0x0010a>
	          at Types:GetJniTypeNameFromInstance <0x000a2>
	          at JniValueManager:DisposePeer <0x002c2>
	          at JniValueManager:DisposePeer <0x000f2>
	          at Java.Interop.JavaObject:Dispose <0x000ea>
	          at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	          at System.Object:runtime_invoke_void__this__ <0x000b0>
	          at <unknown> <0xffffffff>
	          at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	          at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	          at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
11. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.

Bump to dotnet/java-interop@007b35b, which contains fixes to
`JniRuntime.JniValueManager.DisposePeer()`, and add appropriate tests.
jonpryor added a commit that referenced this issue Sep 5, 2020
Fixes: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1193891
Fixes: #4989

Changes: dotnet/java-interop@b4c2a67...a807961

  * dotnet/java-interop@a8079616: [Java.Interop] Dispose, *then* Remove (#708)

~~~

Also cherry-picks d4e0738, which *should* have been done as part of
commit 62a5709, but was overlooked:

What happens if you dispose an instance *while disposing the instance*?

	// C#
	public class MyDisposableObject : Java.Lang.Object {
	    bool _isDisposed;

	    protected override void Dispose (bool disposing)
	    {
	        if (_isDisposed) {
	            return;
	        }
	        _isDisposed = true;
	        if (this.Handle != IntPtr.Zero)
	            this.Dispose ();
	        base.Dispose (disposing);
	    }
	}

	// …
	void MyTestMethod ()
	{
	    var value = new MyDisposableObject ();
	    value.Dispose ();
	    value.Dispose ();
	}

Here, `MyDisposableObject.Dispose(bool)` calls `Object.Dispose()`
when `Object.Handle` is valid.  This "feels" admittedly unusual, but
`IDisposable.Dispose()` is *supposed to be* Idempotent: it can be
called multiple times with no ill effects.  Shouldn't this be the same?

Unfortunately, it *isn't* the same; it crashes, hard:

	  =================================================================
	        Native stacktrace:
	  =================================================================
	        0x10245bc49 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_dump_native_crash_info
	        0x1023f3d35 - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : mono_handle_native_crash
	        0x10245b20f - /Library/Frameworks/Mono.framework/Versions/Current/Commands/mono : sigabrt_signal_handler
	        0x7fff6983d5fd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
	        0x700009b716b0 - Unknown
	        0x7fff69713808 - /usr/lib/system/libsystem_c.dylib : abort
	        0x10658377a - …/lib/server/libjvm.dylib : _ZN2os5abortEbPvPKv
	        0x10637ea5f - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_handleEP10JavaThreadP8_jobject
	        0x10637eb08 - …/lib/server/libjvm.dylib : _ZN8jniCheck15validate_objectEP10JavaThreadP8_jobject
	        0x1063818a1 - …/lib/server/libjvm.dylib : checked_jni_GetObjectClass
	        0x10594cfa8 - …/Java.Interop/bin/TestDebug/libjava-interop.dylib : java_interop_jnienv_get_object_class
	        0x107a1c636 - Unknown
	        0x107aa2c73 - Unknown

	…
	  =================================================================
	        Managed Stacktrace:
	  =================================================================
	          at <unknown> <0xffffffff>
	          at Java.Interop.NativeMethods:java_interop_jnienv_get_object_class <0x000a5>
	          at Types:GetObjectClass <0x0010a>
	          at Types:GetJniTypeNameFromInstance <0x000a2>
	          at JniValueManager:DisposePeer <0x002c2>
	          at JniValueManager:DisposePeer <0x000f2>
	          at Java.Interop.JavaObject:Dispose <0x000ea>
	          at Java.InteropTests.JavaObjectTest:NestedDisposeInvocations <0x00072>
	          at System.Object:runtime_invoke_void__this__ <0x000b0>
	          at <unknown> <0xffffffff>
	          at System.Reflection.RuntimeMethodInfo:InternalInvoke <0x000b8>
	          at System.Reflection.RuntimeMethodInfo:Invoke <0x00152>
	          at System.Reflection.MethodBase:Invoke <0x00058>

Ouch.

The cause of the crash isn't the "successive" `.Dispose()` invocations
within `MyTestMethod()`, but rather the *nested* `.Dispose()`
invocation within `MyDisposableObject.Dispose()`.

Runtime execution is thus:

 1. `JavaObject.Dispose()`
 2. `JniRuntime.JniValueManager.DisposePeer(this)`
 3. `var h = this.PeerReference`
 4. `JniRuntime.JniValueManager.DisposePeer(h, this)`
 5. `JavaObject.Disposed()`
 6. `MyDisposableObject.Dispose(disposing:true)`
 7. `JavaObject.Dispose()`              // back to (1)?
 8. `JniRuntime.JniValueManager.DisposePeer(this)`
 9. `var h = this.PeerReference`        // *second* ref to `h`
10. `JniRuntime.JniValueManager.DisposePeer(h, this)`, which passes
    `h` to e.g. `JniEnvironment.Types.GetJniTypeNameFromInstance()`,
    thus requiring that `h` be a valid JNI reference, and also calls
    `JniObjectReference.Dispose()`, invalidating `h`.
11. "Unwinding" (4), call `Types.GetJniTypeNameFromInstance()` with
    the `h` from (3).

The problem *appears to be* the recursive `Dispose()` invocation on
(7), but the *actual* problem is step (3): by holding a cached/"old"
value of `this.PeerReference` -- and then later using that *same* value
in `JniRuntime.JniValueManager.DisposePeer()`-- when the nested
`JavaObject.Dispose()` invocation continues execution,
`this.PeerReference` will be *invalidated*, but the copy of the handle
from (3) will still be used!  This causes the JVM to very loudly abort.

The fix is to defer the "caching" present in (3): instead of storing
the `PeerReference` value "immediately" -- and disposing the same
value "later" -- don't store the value until *after*
`IJavaPeerable.Disposed()` is called.  This gives the
`Dispose(disposing:true)` method a chance to execute *before*
retaining any cached references to `PeerReference` -- which may in
turn invalidate `PeerReference`! -- thus ensuring that we only attempt
to dispose *valid* JNI handles.

Commit dotnet/java-interop@4141d848 contains fixes to
`JniRuntime.JniValueManager.DisposePeer()`, and add appropriate tests.
@brendanzagaeski
Copy link
Contributor

Release status update

A new Preview version of Xamarin.Android has now been published that includes the fix for this item. The fix is not yet included in a Release version. I will update this item again when a Release version is available that includes the fix.

Fix included in Xamarin.Android SDK version 11.1.0.3.

Fix included on Windows in Visual Studio 2019 version 16.8 Preview 3. To try the Preview version that includes the fix, check for the latest updates in Visual Studio Preview.

Fix included on macOS in Visual Studio 2019 for Mac version 8.8 Preview 3. To try the Preview version that includes the fix, check for the latest updates on the Preview updater channel.

@brendanzagaeski
Copy link
Contributor

Release status update

A new Release version of Xamarin.Android has now been published that includes the fix for this item.

Fix included in Xamarin.Android SDK version 11.1.0.17.

Fix included on Windows in Visual Studio 2019 version 16.8. To get the new version that includes the fix, check for the latest updates or install the most recent release from https://visualstudio.microsoft.com/downloads/.

Fix included on macOS in Visual Studio 2019 for Mac version 8.8. To get the new version that includes the fix, check for the latest updates on the Stable updater channel.

@ghost ghost locked as resolved and limited conversation to collaborators Jun 4, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Area: App Runtime Issues in `libmonodroid.so`.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants