Skip to content
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

[Mono.Android] Data sharing and Close() overrides #9103

Merged
merged 1 commit into from
Jul 12, 2024

Conversation

jonpryor
Copy link
Member

Context: #9039
Context: #9039 (comment)
Context: 0315e89

In #9039, a customer reports that starting with the .NET for Android workload 34.0.113, their app would start crashing with an ObjectDisposedException:

ObjectDisposed_ObjectName_Name, Java.IO.InputStreamInvoker
   at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable )
   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String , IJavaPeerable , JniArgumentValue* )
   at Java.IO.InputStream.Close()
   at Android.Runtime.InputStreamInvoker.Close()
   at System.IO.Stream.Dispose()
   at System.IO.BufferedStream.Dispose(Boolean )
   at System.IO.Stream.Close()
   at System.IO.Stream.Dispose()
   at System.Net.Http.StreamContent.Dispose(Boolean )
   at System.Net.Http.HttpContent.Dispose()
   at System.Net.Http.HttpResponseMessage.Dispose(Boolean )
   at Xamarin.Android.Net.AndroidHttpResponseMessage.Dispose(Boolean )
   at System.Net.Http.HttpResponseMessage.Dispose()
   at System.Net.Http.HttpClient.HandleFailure(Exception , Boolean , HttpResponseMessage , CancellationTokenSource , CancellationToken , CancellationTokenSource )
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage , HttpCompletionOption , CancellationTokenSource , Boolean , CancellationTokenSource , CancellationToken )

This was rather confusing, as between 34.0.95 and 34.0.113 the only change to Mono.Android.dll was 0315e89, which has nothing to do with networking.

Additional consideration presented a hypothetical: IDisposable.Dispose() should be idempotent:

To help ensure that resources are always cleaned up appropriately,
a Dispose method should be idempotent, such that it's callable
multiple times without throwing an exception.

Is InputStreamInvoker.Dispose() idempotent?

An additional conundrum is that InputStreamInvoker.Dispose() doesn't exist; it's Stream.Dispose() that exists, and Stream.Dispose() invokes Stream.Close().

This in turn means that Stream.Close() must be idempotent, which in turn means that InputStreamInvoker.Close() must be idempotent.

Additionally, Stream.Close() docs say you shouldn't override it!

Notes to Inheritors

In derived classes, do not override the Close() method,
instead put all of the Stream cleanup logic in the
Dispose(Boolean) method. For more information, see
Implementing a Dispose Method.

So we have a theoretical concern that InputStreamInvoker.Close() might not be idempotent, and that might be responsible for an ObjectDisposedException.

Maybe.

(At least it's a start?)

Create the obvious idempotent unit test, let's see if it fails:

var javaInputStream = new Java.IO.ByteArrayInputStream (new byte[]{0x1, 0x2, 0x3, 0x4});
var invoker = new InputStreamInvoker (javaInputStream);
invoker.Dispose ();
invoker.Dispose ();

Calling invoker.Dispose() twice does not fail. It is idempotent, at least for this test data.

However, with a slight change to that logic, we're not only able to make things break, but the breakage looks rather similar to the original ObjectDisposedException!

var javaInputStream = new Java.IO.ByteArrayInputStream (new byte[]{0x1, 0x2, 0x3, 0x4});
var invoker = new InputStreamInvoker (javaInputStream);
javaInputStream.Dispose ();
invoker.Dispose ();

fails with:

   at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable self)
   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters)
   at Java.IO.InputStream.Close()
   at Android.Runtime.InputStreamInvoker.Close()
   at System.IO.Stream.Dispose()

Thus, a new hypothesis for #9039: we somehow have a Java.IO.InputStream instance being shared, and that shared instance is being .Dispose()d of from multiple places. Neither InputStreamInvoker.Close() nor InputStreamInvoker.Dispose() anticipated this, both invoke BaseInputStream.Close() on a now disposed instance, and the ObjectDisposedException is the result.

TODO: try to validate this hypothesis. Perhaps it's related to the use of BufferedStream in AndroidMessageHandler.GetContent()?

For now, follow the advice of the Stream.Close() docs, and "remove" the InputStreamInvoker.Close() and OutputStreamInvoker.Close() method overrides. Furthermore, update their Dispose(bool) methods to verify that BaseInputStream and/or BaseOutputStream are not disposed before invoking .Close().

(Note: the Close() methods aren't actually removed, because doing so makes the public API analyzers complain. Instead, Close() is retained, but it just calls base.Close().)

Context: #9039
Context: #9039 (comment)
Context: 0315e89

In #9039, a customer reports that starting with the
.NET for Android workload 34.0.113, their app would start crashing
with an `ObjectDisposedException`:

	ObjectDisposed_ObjectName_Name, Java.IO.InputStreamInvoker
	   at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable )
	   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String , IJavaPeerable , JniArgumentValue* )
	   at Java.IO.InputStream.Close()
	   at Android.Runtime.InputStreamInvoker.Close()
	   at System.IO.Stream.Dispose()
	   at System.IO.BufferedStream.Dispose(Boolean )
	   at System.IO.Stream.Close()
	   at System.IO.Stream.Dispose()
	   at System.Net.Http.StreamContent.Dispose(Boolean )
	   at System.Net.Http.HttpContent.Dispose()
	   at System.Net.Http.HttpResponseMessage.Dispose(Boolean )
	   at Xamarin.Android.Net.AndroidHttpResponseMessage.Dispose(Boolean )
	   at System.Net.Http.HttpResponseMessage.Dispose()
	   at System.Net.Http.HttpClient.HandleFailure(Exception , Boolean , HttpResponseMessage , CancellationTokenSource , CancellationToken , CancellationTokenSource )
	   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage , HttpCompletionOption , CancellationTokenSource , Boolean , CancellationTokenSource , CancellationToken )

This was rather confusing, as between [34.0.95 and 34.0.113][0] the
only change to `Mono.Android.dll` was 0315e89, which has nothing to
do with networking.

Additional consideration presented a hypothetical:
[`IDisposable.Dispose()` should be idempotent][1]:

> To help ensure that resources are always cleaned up appropriately,
> a Dispose method should be idempotent, such that it's callable
> multiple times without throwing an exception.

*Is* `InputStreamInvoker.Dispose()` idempotent?

An additional conundrum is that `InputStreamInvoker.Dispose()`
doesn't exist; it's `Stream.Dispose()` that exists, and
[`Stream.Dispose()` invokes `Stream.Close()`][2].

This in turn means that `Stream.Close()` must be idempotent, which
in turn means that `InputStreamInvoker.Close()` must be idempotent.

Additionally, [`Stream.Close()` docs say you shouldn't override it][3]!

> ## Notes to Inheritors
> In derived classes, do not override the [`Close()`][4] method,
> instead put all of the Stream cleanup logic in the
> [`Dispose(Boolean)`][5] method.  For more information, see
> [Implementing a Dispose Method][1].

So we have a theoretical concern that `InputStreamInvoker.Close()`
might not be idempotent, and that *might* be responsible for an
`ObjectDisposedException`.

Maybe.

(At least it's a start?)

Create the obvious idempotent unit test, let's see if it fails:

	var javaInputStream = new Java.IO.ByteArrayInputStream (new byte[]{0x1, 0x2, 0x3, 0x4});
	var invoker = new InputStreamInvoker (javaInputStream);
	invoker.Dispose ();
	invoker.Dispose ();

Calling `invoker.Dispose()` twice does not fail.  It *is* idempotent,
at least for this test data.

However, with a slight change to that logic, we're not only able to
make things break, but the breakage looks rather similar to the
original `ObjectDisposedException`!

	var javaInputStream = new Java.IO.ByteArrayInputStream (new byte[]{0x1, 0x2, 0x3, 0x4});
	var invoker = new InputStreamInvoker (javaInputStream);
	javaInputStream.Dispose ();
	invoker.Dispose ();

fails with:

	   at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable self)
	   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters)
	   at Java.IO.InputStream.Close()
	   at Android.Runtime.InputStreamInvoker.Close()
	   at System.IO.Stream.Dispose()

Thus, a new hypothesis for #9039: we *somehow* have
a `Java.IO.InputStream` instance being shared, and that shared
instance is being `.Dispose()`d of from multiple places.
Neither `InputStreamInvoker.Close()` nor `InputStreamInvoker.Dispose()`
anticipated this, both invoke `BaseInputStream.Close()` on a now
disposed instance, and the `ObjectDisposedException` is the result.

TODO: try to validate this hypothesis.  Perhaps it's related to the
use of `BufferedStream` in `AndroidMessageHandler.GetContent()`?

For now, follow the advice of the `Stream.Close()` docs, and "remove"
the `InputStreamInvoker.Close()` and `OutputStreamInvoker.Close()`
method overrides.  Furthermore, update their `Dispose(bool)` methods
to verify that `BaseInputStream` and/or `BaseOutputStream` are not
disposed before invoking `.Close()`.

(Note: the `Close()` methods aren't actually removed, because doing
so makes the public API analyzers complain.  Instead, `Close()` is
retained, but it just calls `base.Close()`.)

[0]: 34.0.95...34.0.113
[1]: https://learn.microsoft.com/dotnet/standard/garbage-collection/implementing-dispose
[2]: https://github.com/dotnet/runtime/blob/2ea6ae57874c452923af059cbcb57d109564353c/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs#L156
[3]: https://learn.microsoft.com/dotnet/api/system.io.stream.close?view=net-8.0#notes-to-inheritors
[4]: https://learn.microsoft.com/dotnet/api/system.io.stream.close?view=net-8.0#system-io-stream-close
[5]: https://learn.microsoft.com/dotnet/api/system.io.stream.dispose?view=net-8.0#system-io-stream-dispose(system-boolean)
@jonpryor jonpryor merged commit 83626c9 into main Jul 12, 2024
56 of 58 checks passed
@jonpryor jonpryor deleted the dev/jonp/jonp-remove-Invoker.Close branch July 12, 2024 11:32
grendello added a commit that referenced this pull request Jul 23, 2024
* main: (23 commits)
  Localized file check-in by OneLocBuild Task (#9129)
  [ci] Disable CodeQL on CI/PR pipelines (#9128)
  Refine 16k page alignment support (#9075)
  [build] fix `ConfigureLocalWorkload` target (#9124)
  Bump to NDK r27 (#9020)
  [ci] Use drop service for SDK insertion artifacts  (#9116)
  Fix up all mapping paths (#9121)
  [ci] Fix maestro publishing for stable packages (#9118)
  Bump to dotnet/sdk@2f14fea98b 9.0.100-preview.7.24367.21 (#9108)
  Missing androidx.window.[extensions|sidecar] warnings (#9085)
  [ci] Use sign-artifacts template for macOS signing (#9091)
  [ci] Use DotNetCoreCLI to sign macOS files (#9102)
  [ci] Disable CodeQL on macOS, Linux, non-main jobs (#9111)
  [tests] re-enable `JavaAbstractMethodTest` (#9097)
  [Microsoft.Android.Sdk.ILLink] preserve types with `IJniNameProviderAttribute` (#9099)
  [Mono.Android] Data sharing and Close() overrides (#9103)
  [AndroidManifest] Add `Android.App.PropertyAttribute` (#9016)
  [Mono.Android] Add support for AndroidMessageHandler ClientCertificates (#8961)
  [Mono.Android] Bind and enumify API-35 (#9043)
  Bump to dotnet/java-interop@7a058c0e (#9066)
  ...
jonathanpeppers pushed a commit that referenced this pull request Jul 29, 2024
Context: #9039
Context: #9039 (comment)
Context: 0315e89

In #9039, a customer reports that starting with the
.NET for Android workload 34.0.113, their app would start crashing
with an `ObjectDisposedException`:

	ObjectDisposed_ObjectName_Name, Java.IO.InputStreamInvoker
	   at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable )
	   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String , IJavaPeerable , JniArgumentValue* )
	   at Java.IO.InputStream.Close()
	   at Android.Runtime.InputStreamInvoker.Close()
	   at System.IO.Stream.Dispose()
	   at System.IO.BufferedStream.Dispose(Boolean )
	   at System.IO.Stream.Close()
	   at System.IO.Stream.Dispose()
	   at System.Net.Http.StreamContent.Dispose(Boolean )
	   at System.Net.Http.HttpContent.Dispose()
	   at System.Net.Http.HttpResponseMessage.Dispose(Boolean )
	   at Xamarin.Android.Net.AndroidHttpResponseMessage.Dispose(Boolean )
	   at System.Net.Http.HttpResponseMessage.Dispose()
	   at System.Net.Http.HttpClient.HandleFailure(Exception , Boolean , HttpResponseMessage , CancellationTokenSource , CancellationToken , CancellationTokenSource )
	   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage , HttpCompletionOption , CancellationTokenSource , Boolean , CancellationTokenSource , CancellationToken )

This was rather confusing, as between [34.0.95 and 34.0.113][0] the
only change to `Mono.Android.dll` was 0315e89, which has nothing to
do with networking.

Additional consideration presented a hypothetical:
[`IDisposable.Dispose()` should be idempotent][1]:

> To help ensure that resources are always cleaned up appropriately,
> a Dispose method should be idempotent, such that it's callable
> multiple times without throwing an exception.

*Is* `InputStreamInvoker.Dispose()` idempotent?

An additional conundrum is that `InputStreamInvoker.Dispose()`
doesn't exist; it's `Stream.Dispose()` that exists, and
[`Stream.Dispose()` invokes `Stream.Close()`][2].

This in turn means that `Stream.Close()` must be idempotent, which
in turn means that `InputStreamInvoker.Close()` must be idempotent.

Additionally, [`Stream.Close()` docs say you shouldn't override it][3]!

> ## Notes to Inheritors
> In derived classes, do not override the [`Close()`][4] method,
> instead put all of the Stream cleanup logic in the
> [`Dispose(Boolean)`][5] method.  For more information, see
> [Implementing a Dispose Method][1].

So we have a theoretical concern that `InputStreamInvoker.Close()`
might not be idempotent, and that *might* be responsible for an
`ObjectDisposedException`.

Maybe.

(At least it's a start?)

Create the obvious idempotent unit test, let's see if it fails:

	var javaInputStream = new Java.IO.ByteArrayInputStream (new byte[]{0x1, 0x2, 0x3, 0x4});
	var invoker = new InputStreamInvoker (javaInputStream);
	invoker.Dispose ();
	invoker.Dispose ();

Calling `invoker.Dispose()` twice does not fail.  It *is* idempotent,
at least for this test data.

However, with a slight change to that logic, we're not only able to
make things break, but the breakage looks rather similar to the
original `ObjectDisposedException`!

	var javaInputStream = new Java.IO.ByteArrayInputStream (new byte[]{0x1, 0x2, 0x3, 0x4});
	var invoker = new InputStreamInvoker (javaInputStream);
	javaInputStream.Dispose ();
	invoker.Dispose ();

fails with:

	   at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable self)
	   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters)
	   at Java.IO.InputStream.Close()
	   at Android.Runtime.InputStreamInvoker.Close()
	   at System.IO.Stream.Dispose()

Thus, a new hypothesis for #9039: we *somehow* have
a `Java.IO.InputStream` instance being shared, and that shared
instance is being `.Dispose()`d of from multiple places.
Neither `InputStreamInvoker.Close()` nor `InputStreamInvoker.Dispose()`
anticipated this, both invoke `BaseInputStream.Close()` on a now
disposed instance, and the `ObjectDisposedException` is the result.

TODO: try to validate this hypothesis.  Perhaps it's related to the
use of `BufferedStream` in `AndroidMessageHandler.GetContent()`?

For now, follow the advice of the `Stream.Close()` docs, and "remove"
the `InputStreamInvoker.Close()` and `OutputStreamInvoker.Close()`
method overrides.  Furthermore, update their `Dispose(bool)` methods
to verify that `BaseInputStream` and/or `BaseOutputStream` are not
disposed before invoking `.Close()`.

(Note: the `Close()` methods aren't actually removed, because doing
so makes the public API analyzers complain.  Instead, `Close()` is
retained, but it just calls `base.Close()`.)

[0]: 34.0.95...34.0.113
[1]: https://learn.microsoft.com/dotnet/standard/garbage-collection/implementing-dispose
[2]: https://github.com/dotnet/runtime/blob/2ea6ae57874c452923af059cbcb57d109564353c/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs#L156
[3]: https://learn.microsoft.com/dotnet/api/system.io.stream.close?view=net-8.0#notes-to-inheritors
[4]: https://learn.microsoft.com/dotnet/api/system.io.stream.close?view=net-8.0#system-io-stream-close
[5]: https://learn.microsoft.com/dotnet/api/system.io.stream.dispose?view=net-8.0#system-io-stream-dispose(system-boolean)
@github-actions github-actions bot locked and limited conversation to collaborators Aug 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants