Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cc97c25
Dispose SafeSslHandle and use thread-safe operations on PAL layer
kotlarmilos Jul 22, 2025
9c1b904
Refactor SSL stream handling to remove atomic operations and use Safe…
kotlarmilos Jul 22, 2025
1d36a5e
Remove newline
kotlarmilos Jul 22, 2025
e5524a6
Improve thread safety during disposal
kotlarmilos Jul 22, 2025
0c18e08
Revert changes
kotlarmilos Jul 22, 2025
b561265
Merge branch 'main' into bugfix/android-coreclr-ssl-context
kotlarmilos Jul 22, 2025
5ff5dfb
Fix disposal logic in SafeDeleteSslContext to prevent double disposal…
kotlarmilos Jul 23, 2025
063f5d4
Update SafeDeleteSslContext to manage GC handles using a static Concu…
kotlarmilos Jul 23, 2025
c813751
Update src/libraries/System.Net.Security/src/System/Net/Security/Pal.…
kotlarmilos Jul 23, 2025
4525c6d
Eliminate GC handle ConcurrentDictionary
kotlarmilos Jul 23, 2025
66dfb1f
Add managed context cleanup
kotlarmilos Jul 23, 2025
e228f91
Improve exception handling in WriteToConnection and ReadFromConnectio…
kotlarmilos Jul 24, 2025
e95bc03
Replace object lock with class
kotlarmilos Jul 24, 2025
e38433f
Always release native _sslContext
kotlarmilos Jul 24, 2025
d35aa7e
Improve error handling and refactor WriteToConnection and ReadFromCon…
kotlarmilos Jul 25, 2025
9fd0e8e
Fix formatting
kotlarmilos Jul 25, 2025
38e45d7
Update src/libraries/System.Net.Security/src/System/Net/Security/Pal.…
kotlarmilos Jul 28, 2025
f8a6b05
Replace handle.Free() with handle.Dispose()
kotlarmilos Jul 28, 2025
020ac2d
Refactor SSL stream cleanup
kotlarmilos Jul 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ private static unsafe partial int SSLStreamInitializeImpl(
IntPtr managedContextHandle,
delegate* unmanaged<IntPtr, byte*, int*, PAL_SSLStreamStatus> streamRead,
delegate* unmanaged<IntPtr, byte*, int, void> streamWrite,
delegate* unmanaged<IntPtr, void> managedContextCleanup,
int appBufferSize,
[MarshalAs(UnmanagedType.LPUTF8Str)] string? peerHost);
internal static unsafe void SSLStreamInitialize(
Expand All @@ -86,10 +87,11 @@ internal static unsafe void SSLStreamInitialize(
IntPtr managedContextHandle,
delegate* unmanaged<IntPtr, byte*, int*, PAL_SSLStreamStatus> streamRead,
delegate* unmanaged<IntPtr, byte*, int, void> streamWrite,
delegate* unmanaged<IntPtr, void> managedContextCleanup,
int appBufferSize,
string? peerHost)
{
int ret = SSLStreamInitializeImpl(sslHandle, isServer, managedContextHandle, streamRead, streamWrite, appBufferSize, peerHost);
int ret = SSLStreamInitializeImpl(sslHandle, isServer, managedContextHandle, streamRead, streamWrite, managedContextCleanup, appBufferSize, peerHost);
if (ret != SUCCESS)
throw new SslException();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

using PAL_KeyAlgorithm = Interop.AndroidCrypto.PAL_KeyAlgorithm;
using PAL_SSLStreamStatus = Interop.AndroidCrypto.PAL_SSLStreamStatus;
Expand All @@ -29,13 +30,17 @@ internal sealed class SafeDeleteSslContext : SafeDeleteContext

private readonly SafeSslHandle _sslContext;

private readonly Lock _lock = new Lock();

private ArrayBuffer _inputBuffer = new ArrayBuffer(InitialBufferSize);
private ArrayBuffer _outputBuffer = new ArrayBuffer(InitialBufferSize);

public SslStream.JavaProxy SslStreamProxy { get; }

public SafeSslHandle SslContext => _sslContext;

private volatile bool _disposed;

public SafeDeleteSslContext(SslAuthenticationOptions authOptions)
: base(IntPtr.Zero)
{
Expand All @@ -59,13 +64,21 @@ public SafeDeleteSslContext(SslAuthenticationOptions authOptions)

protected override void Dispose(bool disposing)
{
if (disposing)
lock (_lock)
{
if (_sslContext is SafeSslHandle sslContext)
if (!_disposed)
{
_inputBuffer.Dispose();
_outputBuffer.Dispose();
sslContext.Dispose();
_disposed = true;

// First dispose the SSL context to trigger native cleanup
_sslContext.Dispose();

if (disposing)
{
// Then dispose the buffers
_inputBuffer.Dispose();
_outputBuffer.Dispose();
}
}
}

Expand All @@ -75,61 +88,105 @@ protected override void Dispose(bool disposing)
[UnmanagedCallersOnly]
private static unsafe void WriteToConnection(IntPtr connection, byte* data, int dataLength)
{
SafeDeleteSslContext? context = (SafeDeleteSslContext?)GCHandle.FromIntPtr(connection).Target;
Debug.Assert(context != null);
WeakGCHandle<SafeDeleteSslContext> h = WeakGCHandle<SafeDeleteSslContext>.FromIntPtr(connection);
if (!h.TryGetTarget(out SafeDeleteSslContext? context))
{
Debug.Write("WriteToConnection: failed to get target context");
return;
}

lock (context._lock)
{
if (context._disposed)
{
Debug.Write("WriteToConnection: context is disposed");
return;
}

var inputBuffer = new ReadOnlySpan<byte>(data, dataLength);
var inputBuffer = new ReadOnlySpan<byte>(data, dataLength);

context._outputBuffer.EnsureAvailableSpace(dataLength);
inputBuffer.CopyTo(context._outputBuffer.AvailableSpan);
context._outputBuffer.Commit(dataLength);
context._outputBuffer.EnsureAvailableSpace(dataLength);
inputBuffer.CopyTo(context._outputBuffer.AvailableSpan);
context._outputBuffer.Commit(dataLength);
}
}

[UnmanagedCallersOnly]
private static unsafe PAL_SSLStreamStatus ReadFromConnection(IntPtr connection, byte* data, int* dataLength)
{
SafeDeleteSslContext? context = (SafeDeleteSslContext?)GCHandle.FromIntPtr(connection).Target;
Debug.Assert(context != null);

int toRead = *dataLength;
if (toRead == 0)
return PAL_SSLStreamStatus.OK;

if (context._inputBuffer.ActiveLength == 0)
WeakGCHandle<SafeDeleteSslContext> h = WeakGCHandle<SafeDeleteSslContext>.FromIntPtr(connection);
if (!h.TryGetTarget(out SafeDeleteSslContext? context))
{
Debug.Write("ReadFromConnection: failed to get target context");
*dataLength = 0;
return PAL_SSLStreamStatus.NeedData;
return PAL_SSLStreamStatus.Error;
}

toRead = Math.Min(toRead, context._inputBuffer.ActiveLength);
lock (context._lock)
{
if (context._disposed)
{
Debug.Write("ReadFromConnection: context is disposed");
*dataLength = 0;
return PAL_SSLStreamStatus.Error;
}

int toRead = *dataLength;
if (toRead == 0)
return PAL_SSLStreamStatus.OK;

if (context._inputBuffer.ActiveLength == 0)
{
*dataLength = 0;
return PAL_SSLStreamStatus.NeedData;
}

toRead = Math.Min(toRead, context._inputBuffer.ActiveLength);

context._inputBuffer.ActiveSpan.Slice(0, toRead).CopyTo(new Span<byte>(data, toRead));
context._inputBuffer.Discard(toRead);
context._inputBuffer.ActiveSpan.Slice(0, toRead).CopyTo(new Span<byte>(data, toRead));
context._inputBuffer.Discard(toRead);

*dataLength = toRead;
return PAL_SSLStreamStatus.OK;
}
}

*dataLength = toRead;
return PAL_SSLStreamStatus.OK;
[UnmanagedCallersOnly]
private static void CleanupManagedContext(IntPtr managedContextHandle)
{
if (managedContextHandle != IntPtr.Zero)
{
WeakGCHandle<SafeDeleteSslContext> handle = WeakGCHandle<SafeDeleteSslContext>.FromIntPtr(managedContextHandle);
handle.Dispose();
}
}

internal void Write(ReadOnlySpan<byte> buf)
{
_inputBuffer.EnsureAvailableSpace(buf.Length);
buf.CopyTo(_inputBuffer.AvailableSpan);
_inputBuffer.Commit(buf.Length);
lock (_lock)
{
_inputBuffer.EnsureAvailableSpace(buf.Length);
buf.CopyTo(_inputBuffer.AvailableSpan);
_inputBuffer.Commit(buf.Length);
}
}

internal int BytesReadyForConnection => _outputBuffer.ActiveLength;

internal void ReadPendingWrites(ref ProtocolToken token)
{
if (_outputBuffer.ActiveLength == 0)
lock (_lock)
{
token.Size = 0;
token.Payload = null;
return;
}
if (_outputBuffer.ActiveLength == 0)
{
token.Size = 0;
token.Payload = null;
return;
}

token.SetPayload(_outputBuffer.ActiveSpan);
_outputBuffer.Discard(_outputBuffer.ActiveLength);
token.SetPayload(_outputBuffer.ActiveSpan);
_outputBuffer.Discard(_outputBuffer.ActiveLength);
}
}

internal int ReadPendingWrites(byte[] buf, int offset, int count)
Expand All @@ -139,12 +196,15 @@ internal int ReadPendingWrites(byte[] buf, int offset, int count)
Debug.Assert(count >= 0);
Debug.Assert(count <= buf.Length - offset);

int limit = Math.Min(count, _outputBuffer.ActiveLength);
lock (_lock)
{
int limit = Math.Min(count, _outputBuffer.ActiveLength);

_outputBuffer.ActiveSpan.Slice(0, limit).CopyTo(new Span<byte>(buf, offset, limit));
_outputBuffer.Discard(limit);
_outputBuffer.ActiveSpan.Slice(0, limit).CopyTo(new Span<byte>(buf, offset, limit));
_outputBuffer.Discard(limit);

return limit;
return limit;
}
}

private static SafeSslHandle CreateSslContext(SslStream.JavaProxy sslStreamProxy, SslAuthenticationOptions authOptions)
Expand Down Expand Up @@ -225,11 +285,11 @@ private unsafe void InitializeSslContext(
throw new NotImplementedException(nameof(SafeDeleteSslContext));
}

// Make sure the class instance is associated to the session and is provided
// in the Read/Write callback connection parameter
// Make sure the class instance is associated to the session and is provided in the Read/Write callback connection parameter
// Additionally, all calls should be synchronous so there's no risk of the managed object being collected while native code is executing.
IntPtr managedContextHandle = GCHandle.ToIntPtr(GCHandle.Alloc(this, GCHandleType.Weak));
string? peerHost = !isServer && !string.IsNullOrEmpty(authOptions.TargetHost) ? authOptions.TargetHost : null;
Interop.AndroidCrypto.SSLStreamInitialize(handle, isServer, managedContextHandle, &ReadFromConnection, &WriteToConnection, InitialBufferSize, peerHost);
Interop.AndroidCrypto.SSLStreamInitialize(handle, isServer, managedContextHandle, &ReadFromConnection, &WriteToConnection, &CleanupManagedContext, InitialBufferSize, peerHost);

if (authOptions.EnabledSslProtocols != SslProtocols.None)
{
Expand Down
3 changes: 1 addition & 2 deletions src/libraries/tests.proj
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,6 @@
</ItemGroup>

<ItemGroup Condition="'$(TargetOS)' == 'android' and '$(RuntimeFlavor)' == 'CoreCLR' and '$(RunDisabledAndroidTests)' != 'true'">
<!-- https://github.com/dotnet/runtime/issues/117045 -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Http\tests\FunctionalTests\System.Net.Http.Functional.Tests.csproj" />
<!-- https://github.com/dotnet/runtime/issues/114951 -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Diagnostics.DiagnosticSource\tests\TestWithConfigSwitches\System.Diagnostics.DiagnosticSource.Switches.Tests.csproj" />
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Resources.ResourceManager.Tests\System.Resources.ResourceManager.Tests.csproj" />
Expand Down Expand Up @@ -604,6 +602,7 @@
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Diagnostics.Tracing\tests\System.Diagnostics.Tracing.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Security.Cryptography\tests\System.Security.Cryptography.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Net.WebSockets.Client\tests\System.Net.WebSockets.Client.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Net.Http\tests\FunctionalTests\System.Net.Http.Functional.Tests.csproj" />
<SmokeTestProject Include="$(RepoRoot)\src\tests\FunctionalTests\Android\Device_Emulator\JIT\*.Test.csproj"/>
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ ARGS_NON_NULL_ALL static void FreeSSLStream(JNIEnv* env, SSLStream* sslStream)
ReleaseGRef(env, sslStream->netOutBuffer);
ReleaseGRef(env, sslStream->netInBuffer);
ReleaseGRef(env, sslStream->appInBuffer);

sslStream->managedContextCleanup(sslStream->managedContextHandle);

free(sslStream);
}

Expand Down Expand Up @@ -650,7 +653,7 @@ SSLStream* AndroidCryptoNative_SSLStreamCreateWithKeyStorePrivateKeyEntry(intptr
}

int32_t AndroidCryptoNative_SSLStreamInitialize(
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, int32_t appBufferSize, char* peerHost)
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, MANAGED_CONTEXT_CLEANUP managedContextCleanup, int32_t appBufferSize, char* peerHost)
{
abort_if_invalid_pointer_argument (sslStream);
abort_unless(sslStream->sslContext != NULL, "sslContext is NULL in SSL stream");
Expand Down Expand Up @@ -706,6 +709,7 @@ int32_t AndroidCryptoNative_SSLStreamInitialize(
sslStream->managedContextHandle = managedContextHandle;
sslStream->streamReader = streamReader;
sslStream->streamWriter = streamWriter;
sslStream->managedContextCleanup = managedContextCleanup;

ret = SUCCESS;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
typedef intptr_t ManagedContextHandle;
typedef void (*STREAM_WRITER)(ManagedContextHandle, uint8_t*, int32_t);
typedef int32_t (*STREAM_READER)(ManagedContextHandle, uint8_t*, int32_t*);
typedef void (*MANAGED_CONTEXT_CLEANUP)(ManagedContextHandle);

typedef struct SSLStream
{
Expand All @@ -24,6 +25,7 @@ typedef struct SSLStream
ManagedContextHandle managedContextHandle;
STREAM_READER streamReader;
STREAM_WRITER streamWriter;
MANAGED_CONTEXT_CLEANUP managedContextCleanup;
} SSLStream;

typedef struct ApplicationProtocolData_t ApplicationProtocolData;
Expand Down Expand Up @@ -67,15 +69,16 @@ PALEXPORT SSLStream* AndroidCryptoNative_SSLStreamCreateWithKeyStorePrivateKeyEn

/*
Initialize an SSL context
- isServer : true if the context should be created in server mode
- streamReader : callback for reading data from the connection
- streamWriter : callback for writing data to the connection
- appBufferSize : initial buffer size for application data
- isServer : true if the context should be created in server mode
- streamReader : callback for reading data from the connection
- streamWriter : callback for writing data to the connection
- managedContextCleanup : callback for cleaning up the managed context
- appBufferSize : initial buffer size for application data

Returns 1 on success, 0 otherwise
*/
PALEXPORT int32_t AndroidCryptoNative_SSLStreamInitialize(
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, int32_t appBufferSize, char* peerHost);
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, MANAGED_CONTEXT_CLEANUP managedContextCleanup, int32_t appBufferSize, char* peerHost);

/*
Set target host
Expand Down
Loading