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] Prevent ObjectDisposedException while reading HTTP response from InputStreamInvoker #9789

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

simonrozsival
Copy link
Member

@simonrozsival simonrozsival commented Feb 12, 2025

Fixes #9039

TODO:

  • Add tests
  • Find the root cause of the issue - why has this issue surfaced recently?
  • Find out what else could be affected by the same problem?

The issue manifests in the following code snippet, where we're downloading a large file from the server:

var request = new HttpRequestMessage(HttpMethod.Get, $"/images/{imageId}/download");
var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);

ProgressLabel.Text = "Downloading..."; // with this line commented out the exception won't be thrown (??)

response.EnsureSuccessStatusCode();

await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
await using var fileStream = File.Create(path);

int bytesRead = -1;
var buffer = new byte[512].AsMemory(); // Small buffer on purpose.
while (bytesRead != 0)
{
    bytesRead = await stream.ReadAsync(buffer, cancellationToken);
    await fileStream.WriteAsync(buffer[..bytesRead], cancellationToken);

    Console.WriteLine($"Downloaded {(int)(100 * fileStream.Length / size)}%");
}

My best attempt at explaining the bug is the following:

  1. the AndroidMessageHandler returns an instance of AndroidHttpResponseMessage which keeps a reference to the HttpURLConnection MCW which internally keeps a reference to the InputStream (wrapped by Android.Runtime.InputStreamInvoker)
  2. after line stream = await response.Content.ReadAsStreamAsync(cancellationToken)...
    1. stream is referencing the response's InputStreamInvoker which keeps a reference to the InputStream
    2. response can be collected by GC
  3. when the download is long enough for several .NET and Java GCs to occur, this happens:
    1. response is collected on the .NET side
    2. HttpURLConnection is collected on the Java side
    3. when the next .NET GC runs, our Java GC bridge will change the strong gref Handle in InputStream to a weak ref and initiate Java GC
    4. Java GC will collect the InputStream since there are no more references to it from the Java side
    5. the GC bridge will test if the weak ref is still valid after the Java GC and it notices it isn't, so it replaces the handle with IntPtr.Zero
    6. the .NET InputStream object won't be collected though, because InputStreamInvoker is still holding a reference to it
    7. the next time we try to read from the InputStreamInvoker, it's internal InputStream is disposed and reading from it will throw the ObjectDisposedException we are observing

The problem can be prevented by keeping a gref to the underlying InputStream in InputStreamInvoker. This way, the gc bridge cannot dispose the InputStream while the only reference to it is on the .NET side in an object which isn't a Java class wrapper. Without this patch, the app will fail every single time when using a local debug build. With the patch applied, the exception is not thrown anymore.

/cc @grendello @jonathanpeppers @jonpryor @AaronRobinsonMSFT

@simonrozsival simonrozsival added the Area: HTTP Issues with sockets / HttpClient. label Feb 12, 2025
@jonathanpeppers
Copy link
Member

@simonrozsival FYI CI is complaining about branch name being too long:

error NU5123: Warning As Error: The file 'package/services/metadata/core-properties/37a2a0f2bbc1473e941d1c1f18bc947a.psmdcp' path, name, or both are too long. Your package might not work without long file path support. Please shorten the file path or file name.

We have seen it on other PRs and just shortened the name...

@AaronRobinsonMSFT
Copy link
Member

i. response is collected on the .NET side

@simonrozsival Can you try placing a GC.KeepAlive(response); on the other side of the loop and see if that also addressed the issue? I'm trying to see if we can confirm where the missing reference in the graph occurs.

@simonrozsival
Copy link
Member Author

simonrozsival commented Feb 13, 2025

@AaronRobinsonMSFT adding GC.KeepAlive(response); doesn't make any difference. I can see the handle of the InputStream being collected between two Read(byte[], int, int) calls:

  • when I added logging to Android.Runtime.InputStreamInvoker.Read to print the value of the base input stream Handle, I get a valid value in one Read (0x6092), then the following monodroid-gref from the gc bridge print those two logs mentioning that handle, and in the next call to Read the Handle is 0:
02-13 12:58:24.268 16901 16901 I monodroid-gref: +w+ grefc 312 gwrefc 12 obj-handle 0x6092/G -> new-handle 0x817/W from thread 'finalizer'(16901)
02-13 12:58:24.268 16901 16901 I monodroid-gref: -g- grefc 311 gwrefc 12 handle 0x6092/G from thread 'finalizer'(16901)
  • the Android.Runtime.InputStreamInvoker object isn't collected on the .NET side - there is a call to Read after the Java object is collected
  • the Android.Runtime.InputStreamInvoker has a direct reference to the itnernal Java.IO.InputStreamInvoker object which has been passed to the GC bridge anyway and it converted the gref to a weak ref as seen in the logs so that the Java side could collect it and the GC bridge sees that and replaces the Handle with 0
  • I added logging to the Dispose and ~InputStream methods on the base class wrapper Java.IO.InputStream just to make sure we're not disposing the stream from the .NET side somewhere and I confirmed we aren't doing that. The logs from the finalizer appear much later, after the Read throws the ObjectDisposedException and the HTTP response is being disposed

@jonpryor
Copy link
Member

My problem is that this throws my entire understanding of how the GC and GC bridge work into doubt. (HHOS 😓)

My understanding is that the GC bridge should only process objects which are not otherwise referenced by managed code. If managed code holds a GC rooted reference:

static Java.Lang.Object globalInstance = new Java.Lang.Object();

then that instance should never reach the GC bridge.


From #9039, we have a call stack of:

   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 )

Something is holding a reference to InputStreamInvoker. (If they didn't, how are they calling Stream.Close() on that instance?) That something appears to be a BufferedStream, and something holds a reference to that, which holds a reference to that…

Which is to say, the InputStreamInvoker instance must be considered to be "alive" by the GC, just by nature of it being invoked.

AndroidMessageHandler.SendAsync() creates an AndroidHttpResponseMessage, and -- from context -- it appears that AndroidHttpResponseMessage.Content._content is the InputStreamInvoker, with AndroidHttpResponseMessage.Content being a StreamContent instance.

InputStreamInvoker holds a strong reference to an InputStream. If InputStreamInvoker is alive, then InputStreamInvoker.BaseInputStream should also be kept alive.

Right?

Which brings us to 2(i): stream is valid until end-of-scope (due to using var, which "appends" an implicit stream.Dispose() at the end of the block.) 2(ii) doesn't feel quite relevant; yes, response can be collected, but it has no finalizer, so collection won't "do" anything.

Which brings us to 3(ii): why does HttpURLConnection (via response.httpConnection) getting collected by .NET & Java impact the InputStream? As per above, my understanding is that because InputStreamInvoker is kept alive (it's called!), then the InputStream must also be kept alive by the .NET GC. The InputStream JNI handle should always be a GREF, and shouldn't be touched or changed so long as the InputStreamInvoker is kept alive.

3(iii) thus doesn't make sense to me. Why is InputStream even involved?


Updating InputStreamInvoker to hold a GREF to BaseInputStream may fix the issue, but I still fundamentally don't understand the issue in the first place; how is it happening? Why is it happening? What do we need to look for via an audit to find other scenarios?

@jonpryor
Copy link
Member

@simonrozsival wrote:

  • the Android.Runtime.InputStreamInvoker has a direct reference to the itnernal Java.IO.InputStreamInvoker object which has been passed to the GC bridge …

This, again, is the part that confuses and scares me: Android.Runtime.InputStreamInvoker has a direct reference to the Java.IO.InputStream instance. How/why is this instance even being processed by the GC bridge at all?

This feels like something that should not happen, yet it is. Why is it happening?

@AaronRobinsonMSFT
Copy link
Member

My understanding is that the GC bridge should only process objects which are not otherwise referenced by managed code.

That is mostly correct. The GC Bridge only looks at bridge objects that have been marked as unreachable, but I believe they could point at an object that is still reachable. I'm basing this on the fact that since each bridge object is walked it could be in a field. However, this would be a monumental flaw in the GC Bridge that should have manifested long ago.

@jonpryor
Copy link
Member

jonpryor commented Feb 13, 2025

@AaronRobinsonMSFT wrote:

That is mostly correct.

Yay! (I think.)

The GC Bridge only looks at bridge objects that have been marked as unreachable

Sounds good…

but I believe they could point at an object that is still reachable.

And this deeply concerns me.

My problem is that #9039 feels like a GC bug, and the fix in this PR is, at best, a workaround.

If this isn't a GC bug, then I need to understand what the bug is, so that we can audit the codebase to find any other scenarios which "rhyme" with this bug. Right now, I don't understand what the bug is (other than "clearly a GC bug!"), and thus have no idea what such an audit would even look like.

If we go with your belief that "[the GC bridge could process] an object that is still reachable," then I don't know how to fix that. I don't know what that means. Consider:

/* 1 */ partial class MyActivity : Activity {
/* 2 */   public override void OnCreate(Bundle? b)
/* 3 */   {
/* 4 */     base.OnCreate(b);
/* 5 */     var v = new Android.Widget.Button(this);
/* 6 */     SetContentView(v);
/* 7 */   }
/* 8 */ }

v (line 5) is reachable. But if the GC decides to process "an object that is still reachable" between line 5 and 6, v would be collected, and line 6 would become equivalent to SetContentView(null). This would be, in short, bananas. You can't write code in such an environment.

Is the only reason this doesn't happen because there's not enough time between lines 5 and 6 for the GC to stop the world and intervene? Does this not happen because we've been lucky? Would that change if we throw a Thread.Sleep(10000); GC.Collect(); Thread.Sleep(10000); (20 seconds) between lines 5 and 6?

Or will the above always work, but if you have a "large enough object graph" that, while everything is reachable, it's "complicated enough" that the GC has second thoughts…?

The prospect of the GC bridge being able to process reachable objects is horrifying.

@AaronRobinsonMSFT
Copy link
Member

but I believe they could point at an object that is still reachable.

And this deeply concerns me.

I'm sorry, I was wrong. I didn't look deeply enough into the "push" operation for object fields. It skips over objects that are alive.

https://github.com/dotnet/runtime/blob/eb99e9380129ff621132e6c60113793804713860/src/mono/mono/metadata/sgen-tarjan-bridge.c#L621-L627

@jonpryor
Copy link
Member

@AaronRobinsonMSFT wrote:

I'm sorry, I was wrong. I didn't look deeply enough into the "push" operation for object fields. It skips over objects that are alive.

Thank you, I've now avoided a minor heart attack.

Which just re-raises my original question: how is the instance referenced by InputStreamInvoker.BaseInputStream being processed by the GC bridge when the InputStreamInvoker instance should be reachable and still references it!

Continuing to beat a dead horse, this really feels like a GC bug to me.

@simonrozsival
Copy link
Member Author

simonrozsival commented Feb 14, 2025

3(iii) thus doesn't make sense to me. Why is InputStream even involved?

It also doesn't make sense to me, but it's what I observe (based on the +w+ and -g- logs). After adding the GC.KeepAlive(response) at the very end as @AaronRobinsonMSFT suggested, I don't believe 3(i) is happening and therefore also 3(ii). But if the URLConnection is still alive on the Java side, then there should be also another reference to the InputStream on the java side and so that stream object should not be collected even after the gref->weak switch in the GC bridge.

It almost seems like we're not holding a gref but an lref and this local reference is not valid anymore, but the logs clearly show it is a gref. Also I think the app would crash in a different way if that was the case.

My problem is that #9039 feels like a GC bug, and the fix in this PR is, at best, a workaround.

I 100% agree. The same problem probably affects OutputStreamInvoker and other wrapper classes in the Mono.Android codebase.

Which just re-raises my original question: how is the instance referenced by InputStreamInvoker.BaseInputStream being processed by the GC bridge when the InputStreamInvoker instance should be reachable and still references it!

I wonder what else could cause the InputStreamInvoker.BaseInputStream.Handle value to flip to 0. I will try to add even more logging to the codebase today and make sure this isn't caused by something other than the GC.

@jonpryor
Copy link
Member

@simonrozsival wrote:

But if the URLConnection is still alive on the Java side…

I don't think that will be true. We are the one creating the URLConnection instance, so I imagine that we have the only reference to that instance, short of Java-side caching or something like that.

Which is why you were seeing 3(ii): because nothing on the Java side references the HttpURLConnection, once it's no longer reachable in the managed side due to 2(ii) and 3(i), it can be collected.

I still don't see why collecting the HttpURLConnection would at all impact the InputStream returned by HttpsURLConnection.getInputStream(), but I'd need to read more Java source to see if there could be a relation between the two, Additionally, we should be holding a GREF to the InputStream, and so long as it's reachable -- which it certainly should be! -- then Java won't collect it.

I wonder what else could cause the InputStreamInvoker.BaseInputStream.Handle value to flip to 0.

That should be in the GREF logs. Do you have any to share?

@simonrozsival
Copy link
Member Author

@jonpryor this is the logs I'm getting when running the repro app with adb shell setprop debug.mono.log all + some additional logging (you can see what I'm logging here main...simonrozsival:xamarin-android:input-stream-gc-issue-extended-logging):

logcat-dump-19581.txt

I can confirm that the GC bridge collects the InputStreamInvoker based on these log messages:

02-14 03:01:03.586 19581 19650 I monodroid-gref: +g+ grefc 287 gwrefc 0 obj-handle 0x15/I -> new-handle 0x6092/G from thread '.NET TP Worker'(4)
...
02-14 03:01:03.589 19581 19650 D Android.Runtime.InputStreamInvoker: FromJniHandle: Created new instance (type: Java.IO.InputStreamInvoker, handle: 0x6092, identity hash: 162865424, peer reference is valid: True)
...
02-14 03:01:04.280 19581 19653 D Android.Runtime.InputStreamInvoker: Read res 1024 (type: Java.IO.InputStreamInvoker, handle: 0x6092, identity hash: 162865424, peer reference is valid: True)
...
02-14 03:01:04.373 19581 19581 I monodroid-gref: +w+ grefc 299 gwrefc 25 obj-handle 0x6092/G -> new-handle 0x9a3/W from thread 'finalizer'(19581)
02-14 03:01:04.373 19581 19581 I monodroid-gref: -g- grefc 298 gwrefc 25 handle 0x6092/G from thread 'finalizer'(19581)
...
02-14 03:01:04.377 19581 19581 I monodroid-gc: GC cleanup summary: 101 objects tested - resurrecting 95.
02-14 03:01:04.377 19581 19581 I monodroid-gc: GC cleanup [sccs 24]: DEAD Java.IO.InputStreamInvoker
02-14 03:01:04.377 19581 19581 I monodroid-gc: GC cleanup [sccs 25]: DEAD Java.Net.HttpURLConnectionInvoker
02-14 03:01:04.377 19581 19581 I monodroid-gc: GC cleanup [sccs 26]: DEAD Java.Net.URL
02-14 03:01:04.377 19581 19581 I monodroid-gc: GC cleanup [sccs 28]: DEAD Android.Graphics.Drawables.GradientDrawable
02-14 03:01:04.377 19581 19581 I monodroid-gc: GC cleanup [sccs 69]: DEAD Microsoft.Maui.Controls.Platform.GenericGlobalLayoutListener
02-14 03:01:04.377 19581 19581 I monodroid-gc: GC cleanup [sccs 97]: DEAD Android.Graphics.Drawables.GradientDrawable
...
02-14 03:01:04.433 19581 19653 D Android.Runtime.InputStreamInvoker: Read (type: Java.IO.InputStreamInvoker, handle: 0x00, identity hash: 162865424, peer reference is valid: False)

@simonrozsival
Copy link
Member Author

simonrozsival commented Feb 14, 2025

I also enabled adb shell setprop debug.mono.gc 1 and I got this additional information:

02-14 03:28:46.656 20253 20253 I monodroid-gc: cross references callback invoked with 101 sccs and 0 xrefs.
...
02-14 03:28:46.656 20253 20253 I monodroid-gc: group 6 with 1 objects
02-14 03:28:46.656 20253 20253 I monodroid-gc:  obj 0x703b05da08 [Java.IO::InputStreamInvoker] handle 0x6092 key_handle 0x0
...
02-14 03:28:46.657 20253 20253 I monodroid-gc: group 36 with 1 objects
02-14 03:28:46.657 20253 20253 I monodroid-gc:  obj 0x703b859800 [Java.Net::HttpURLConnectionInvoker] handle 0x5d96 key_handle 0x0
02-14 03:28:46.657 20253 20253 I monodroid-gc: group 37 with 1 objects
02-14 03:28:46.657 20253 20253 I monodroid-gc:  obj 0x703b859738 [Java.Net::URL] handle 0x60c6 key_handle 0x0
...
02-14 03:28:46.657 20253 20253 I monodroid-gref: +w+ grefc 317 gwrefc 7 obj-handle 0x6092/G -> new-handle 0x44b/W from thread 'finalizer'(20253)
02-14 03:28:46.657 20253 20253 I monodroid-gref: -g- grefc 316 gwrefc 7 handle 0x6092/G from thread 'finalizer'(20253)
...
02-14 03:28:46.662 20253 20253 I monodroid-gc: GC cleanup summary: 101 objects tested - resurrecting 95.
02-14 03:28:46.662 20253 20253 I monodroid-gc: GC cleanup [sccs 6]: DEAD Java.IO.InputStreamInvoker
02-14 03:28:46.662 20253 20253 I monodroid-gc: GC cleanup [sccs 36]: DEAD Java.Net.HttpURLConnectionInvoker
02-14 03:28:46.662 20253 20253 I monodroid-gc: GC cleanup [sccs 37]: DEAD Java.Net.URL
...

It's interesting that there is no SCC more than 1 item in the whole log and also there are always 0 xrefs.

@simonrozsival
Copy link
Member Author

@filipnavara do you have an idea what could be going on here?

@AaronRobinsonMSFT
Copy link
Member

@simonrozsival Checking the SCC graph might be interesting here. You can collect that using adb shell setprop debug.mono.gc 1. This will dump out the graph that is sent to the bridge during the cross reference analysis phase. If it is sent over, then I'd tend to agree that the GC is marking a .NET object as dead when it shouldn't be.

@simonrozsival
Copy link
Member Author

@AaronRobinsonMSFT I think that should be what I posted in #9789 (comment), or should I send the whole logs?

@simonrozsival
Copy link
Member Author

simonrozsival commented Feb 14, 2025

I experimented with the repro some more and I realized I simplified the code from the customer's project in #9039 too much. In the original repro, progress is not reported via Console.WriteLine, but it updates a text in a label:

 var request = new HttpRequestMessage(HttpMethod.Get, $"/images/{imageId}/download");
 var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ProgressLabel.Text = "Downloading...";

 response.EnsureSuccessStatusCode();

 await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
 await using var fileStream = File.Create(path);

 int bytesRead = -1;
 var buffer = new byte[512].AsMemory(); // Small buffer on purpose.
 while (bytesRead != 0)
 {
    bytesRead = await stream.ReadAsync(buffer, cancellationToken);
    await fileStream.WriteAsync(buffer[..bytesRead], cancellationToken);

    Console.WriteLine($"Downloaded {(int)(100 * fileStream.Length / size)}%");
 }

With this single line, the GC bug manifests. Without updating the label text, it does not. I will update the code in the main description.


EDIT: I don't know what's so special about updating the label text (this is MAUI Label, but it boils down to Android.Widget.TextView.set_Text, which does several JNI calls to allocate a new Java string, invoke the setText(String) method via JNI and release the temporary string object's lref) that it seemingly confuses the SGen GC. Replacing this with a call to some other method (such as Thread.Sleep) doesn't seem to cause the issue.

I briefly explored the idea that making changes to the UI via JNI calls would somehow switch the JNI thread to a UI thread and reading from the network input stream would somehow cause a Java exception because networking on the UI thread is not allowed. If that were the case, I would expect the app to crash or at least print something to logcat. So I did not explore this idea any further.

I think I will try to find a better/smaller repro than what we got from the customer in #9039 on Monday without the additional dependencies such as MAUI.

@filipnavara
Copy link
Member

filipnavara commented Feb 22, 2025

@filipnavara do you have an idea what could be going on here?

So, here's what I think is happening. The sample uses a Task with synchronization context (Android.App.SyncContext), so the continuations get queued to the UI thread, including the continuation of the await stream.ReadAsync(...) call. If you time the GC just right, you will end up with the task continuation only being held by the machinery behind SyncContext, and the cross-GC callback will look something like this:

monodroid-gc: cross references callback invoked with 5 sccs and 0 xrefs
monodroid-gc: group 0 with 1 objects
monodroid-gc:     obj 0x723341bebbd0 [Android.OS::Looper]
monodroid-gc: group 1 with 1 objects
monodroid-gc:     obj 0x723341bea7b0 [Java.IO::InputStreamInvoker]
monodroid-gc: group 2 with 1 objects
monodroid-gc:     obj 0x723341bea660 [Java.Net::Proxy]
monodroid-gc: group 3 with 1 objects
monodroid-gc:     obj 0x72333c4c0b30 [::RunnableImplementor]
monodroid-gc: group 4 with 1 objects
monodroid-gc:     obj 0x723341bea698 [AndroidApp1::MainActivity]

Presumably the Looper is rooted by the Java side and so is RunnableImplementor. However, the RunnableImplementor is the one holding the reference to the continuation delegate and in turn the task state, including the whole hierarchy leading to InputStreamInvoker. Since RunnableImplementor is considered dead, the whole graph of ANY Java objects pointed only from the Task are deemed collectible. The Java side will recognize that RunnableImplementor is alive, but not anything held by its delegate. Objects on the C# side will survive but not the ones on the Java side.

(Technically, you can probably argue that there's a reference from RunnableImplementor to InputStreamInvoker that should be reported by MonoVM. Presumably it may not traverse delegates correctly?)

@filipnavara
Copy link
Member

filipnavara commented Feb 22, 2025

Also, here's a smaller version of the repro:
AndroidApp1.zip

Use the HTTP server from the original repro, plug the address in the source code (ie. replace http://172.16.14.188:5218/ with http://10.0.2.2:5218/ in the source code) and run it. It has no MAUI and it crashes in few seconds, usually on the first download loop.

Side-note: If you modify the sample to add .ConfigureAwait(false) behind the .ReadAsync(...) call it won't crash.

@filipnavara
Copy link
Member

filipnavara commented Feb 22, 2025

Here's even smaller repro, no HTTP involved:

        // Run this on UI thread (eg. in MainActivity.OnCreate)  
        Task.Run(GCLoop); // <== Repeat this line up to 4 times depending on HW/emulator to make the repro more reliable
        _ = AsyncStreamWriter();

        public static async Task GCLoop()
        {
            while (true)
            {
                GC.Collect();
                await Task.Delay(10);
            }
        }

        public static async Task AsyncStreamWriter()
        {
            var bs = new ByteArrayOutputStream();
            var osi = new Android.Runtime.OutputStreamInvoker(bs);
            try
            {
                while (true)
                    await osi.WriteAsync(new byte[2]);
            }
            catch (ObjectDisposedException ex)
            {
                System.Environment.FailFast(ex.ToString());
            }
        }

@AaronRobinsonMSFT
Copy link
Member

@filipnavara Great analysis.

Presumably it may not traverse delegates correctly?

I would agree this seems like the issue. However, if this is true it seems odd we've not observed this before. We should also be able to easily confirm this.

@filipnavara
Copy link
Member

filipnavara commented Feb 22, 2025

It's not exactly that it doesn't process delegates correctly, but it definitely does produce incorrect SCC/XREF set for this case. Now, the DUMP_GRAPH debugging option is largely broken but I have played with it on one of my branches and I can produce some traces (parts are missing because the dumping code would crash or corrupt state): https://gist.github.com/filipnavara/b18980f244a83b9a9856c486f601a410

Also, seemingly running with the "new" bridge (which is older than the "tarjan" bridge) I don't get the crash. I enabled it using adb shell setprop debug.mono.env MONO_GC_PARAMS=bridge-implementation=new.

UPD: Observations suggests that there are multiple conditions necessary for this to happen. The loop between AsyncStateMachineBox'1 and <AsyncStreamWriter>d__2 (task data) seems to cause the algorithm to lose track of the relation in the color graph to ByteArrayOutputStream which is then used for forming the xrefs sent to the callback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: HTTP Issues with sockets / HttpClient.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

HttpClient ObjectDisposed after SDK upgrade from 34.0.95 -> 34.0.113
5 participants