Skip to content

[Android] Prevent race condition by synchronizing buffer access #117950

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

kotlarmilos
Copy link
Member

@kotlarmilos kotlarmilos commented Jul 22, 2025

Problem

Android SSL callbacks WriteToConnection/ReadFromConnection receive an IntPtr that should reference a GCHandle to SafeDeleteSslContext. The handle was freed while native code could still invoke the callbacks with the IntPtr referencing a different object. This led to a NullReferenceException or InvalidCastException:

System.InvalidCastException: Unable to cast object of type 'System.Threading.Thread' to type 'System.Net.SafeDeleteSslContext'

Description

This PR synchronizes all buffer mutations behind a shared _lock, and updates the managed callbacks to verify that the handle is still allocated and its target is a SafeDeleteSslContext before using it.

Changes

  • Implemented WriteToConnection/ReadFromConnection checks for allocated GCHandle
  • Implemented lock in all buffer mutations to prevent race condition

Testing

The System.Net.Http.Functional.Tests now pass without the crash on Android.

Fixes #117045

@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

Copy link
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a race condition in Android SSL implementation where GCHandle references were being freed while native callbacks could still access them, causing InvalidCastException or NullReferenceException. The fix extends the GCHandle lifetime to match the SafeDeleteSslContext object and adds proper synchronization for buffer operations.

Key changes:

  • Replace weak GCHandle allocation with persistent handle that lives for the full object lifetime
  • Add comprehensive locking around all buffer operations to prevent race conditions
  • Implement null/disposed checks in native callbacks with graceful error handling

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/libraries/tests.proj Re-enables System.Net.Http.Functional.Tests for Android and adds it to smoke tests
src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs Implements persistent GCHandle, thread-safe buffer access, and defensive callback handling

@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@kotlarmilos kotlarmilos changed the title [Android] Prevent race condition by extending GCHandle lifetime and synchronizing buffer access [Android] Prevent race condition by synchronizing buffer access Jul 23, 2025
@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

catch (Exception ex)
{
Debug.Write("Exception Caught. - " + ex);
throw;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exceptions thrown from UnmanagedCallersOnly methods turn into unhandled exceptions (on non-Windows). This means that the app is going to crash if any exceptions are thrown by this method. Is it what we want? Should the exceptions be propagated to the user instead?

If you want to keep the behavior and crash the app if any exception is thrown here, it is better to do that without the try/catch. It is better for diagnostics. try/catch/rethrow can overwrite some of the details that may be useful for diagnosing the exceptions, and the unhandled exception handler should print the same details as the Debug.Write to the console.

Copy link
Member Author

@kotlarmilos kotlarmilos Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context. I agree -- it should be propagated to the user.

Comment on lines 173 to 174
if (handle.IsAllocated)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (handle.IsAllocated)
{

This is unnecessary. Non-null managedContextHandle is always going to turn into Allocated GCHandle (even if the handle was freed somewhere else already).

GCHandle is struct that is as dangerous as unsafe code. If you call Free on one copy of the struct, the other copies will still report IsAllocated==true even through the handle was freed.

GCHandle h = GCHandle.FromIntPtr(connection);
Debug.Assert(h.IsAllocated);

SafeDeleteSslContext? context = (SafeDeleteSslContext?)h.Target;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can consider switching to new GCHandle<T> to avoid this cast

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Actually, WeakGCHandle<T>.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, updated. TryGetTarget handles both invalid GCHandle and null context.

@kotlarmilos
Copy link
Member Author

/azp run runtime-android,runtime-androidemulator

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Android] System.Net.Http.Functional.Tests.csproj fails with SIGABRT in libcoreclr.so
6 participants