Skip to content

Commit 1d8a341

Browse files
kotlarmilosjkotas
andauthored
[Android] Prevent race condition by synchronizing buffer access (#117950)
* Dispose SafeSslHandle and use thread-safe operations on PAL layer * Refactor SSL stream handling to remove atomic operations and use SafeHandle * Remove newline * Improve thread safety during disposal * Revert changes * Fix disposal logic in SafeDeleteSslContext to prevent double disposal. Fix race condition on _disposed * Update SafeDeleteSslContext to manage GC handles using a static ConcurrentDictionary * Update src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs Co-authored-by: Jan Kotas <[email protected]> * Eliminate GC handle ConcurrentDictionary * Add managed context cleanup * Improve exception handling in WriteToConnection and ReadFromConnection methods * Replace object lock with class * Always release native _sslContext * Improve error handling and refactor WriteToConnection and ReadFromConnection methods to use WeakGCHandle * Fix formatting * Update src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs Co-authored-by: Jan Kotas <[email protected]> * Replace handle.Free() with handle.Dispose() * Refactor SSL stream cleanup --------- Co-authored-by: Jan Kotas <[email protected]>
1 parent 2b52fb2 commit 1d8a341

File tree

5 files changed

+119
-51
lines changed

5 files changed

+119
-51
lines changed

src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ private static unsafe partial int SSLStreamInitializeImpl(
7878
IntPtr managedContextHandle,
7979
delegate* unmanaged<IntPtr, byte*, int*, PAL_SSLStreamStatus> streamRead,
8080
delegate* unmanaged<IntPtr, byte*, int, void> streamWrite,
81+
delegate* unmanaged<IntPtr, void> managedContextCleanup,
8182
int appBufferSize,
8283
[MarshalAs(UnmanagedType.LPUTF8Str)] string? peerHost);
8384
internal static unsafe void SSLStreamInitialize(
@@ -86,10 +87,11 @@ internal static unsafe void SSLStreamInitialize(
8687
IntPtr managedContextHandle,
8788
delegate* unmanaged<IntPtr, byte*, int*, PAL_SSLStreamStatus> streamRead,
8889
delegate* unmanaged<IntPtr, byte*, int, void> streamWrite,
90+
delegate* unmanaged<IntPtr, void> managedContextCleanup,
8991
int appBufferSize,
9092
string? peerHost)
9193
{
92-
int ret = SSLStreamInitializeImpl(sslHandle, isServer, managedContextHandle, streamRead, streamWrite, appBufferSize, peerHost);
94+
int ret = SSLStreamInitializeImpl(sslHandle, isServer, managedContextHandle, streamRead, streamWrite, managedContextCleanup, appBufferSize, peerHost);
9395
if (ret != SUCCESS)
9496
throw new SslException();
9597
}

src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs

Lines changed: 102 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Security.Authentication;
88
using System.Security.Cryptography;
99
using System.Security.Cryptography.X509Certificates;
10+
using System.Threading;
1011

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

3031
private readonly SafeSslHandle _sslContext;
3132

33+
private readonly Lock _lock = new Lock();
34+
3235
private ArrayBuffer _inputBuffer = new ArrayBuffer(InitialBufferSize);
3336
private ArrayBuffer _outputBuffer = new ArrayBuffer(InitialBufferSize);
3437

3538
public SslStream.JavaProxy SslStreamProxy { get; }
3639

3740
public SafeSslHandle SslContext => _sslContext;
3841

42+
private volatile bool _disposed;
43+
3944
public SafeDeleteSslContext(SslAuthenticationOptions authOptions)
4045
: base(IntPtr.Zero)
4146
{
@@ -59,13 +64,21 @@ public SafeDeleteSslContext(SslAuthenticationOptions authOptions)
5964

6065
protected override void Dispose(bool disposing)
6166
{
62-
if (disposing)
67+
lock (_lock)
6368
{
64-
if (_sslContext is SafeSslHandle sslContext)
69+
if (!_disposed)
6570
{
66-
_inputBuffer.Dispose();
67-
_outputBuffer.Dispose();
68-
sslContext.Dispose();
71+
_disposed = true;
72+
73+
// First dispose the SSL context to trigger native cleanup
74+
_sslContext.Dispose();
75+
76+
if (disposing)
77+
{
78+
// Then dispose the buffers
79+
_inputBuffer.Dispose();
80+
_outputBuffer.Dispose();
81+
}
6982
}
7083
}
7184

@@ -75,61 +88,105 @@ protected override void Dispose(bool disposing)
7588
[UnmanagedCallersOnly]
7689
private static unsafe void WriteToConnection(IntPtr connection, byte* data, int dataLength)
7790
{
78-
SafeDeleteSslContext? context = (SafeDeleteSslContext?)GCHandle.FromIntPtr(connection).Target;
79-
Debug.Assert(context != null);
91+
WeakGCHandle<SafeDeleteSslContext> h = WeakGCHandle<SafeDeleteSslContext>.FromIntPtr(connection);
92+
if (!h.TryGetTarget(out SafeDeleteSslContext? context))
93+
{
94+
Debug.Write("WriteToConnection: failed to get target context");
95+
return;
96+
}
97+
98+
lock (context._lock)
99+
{
100+
if (context._disposed)
101+
{
102+
Debug.Write("WriteToConnection: context is disposed");
103+
return;
104+
}
80105

81-
var inputBuffer = new ReadOnlySpan<byte>(data, dataLength);
106+
var inputBuffer = new ReadOnlySpan<byte>(data, dataLength);
82107

83-
context._outputBuffer.EnsureAvailableSpace(dataLength);
84-
inputBuffer.CopyTo(context._outputBuffer.AvailableSpan);
85-
context._outputBuffer.Commit(dataLength);
108+
context._outputBuffer.EnsureAvailableSpace(dataLength);
109+
inputBuffer.CopyTo(context._outputBuffer.AvailableSpan);
110+
context._outputBuffer.Commit(dataLength);
111+
}
86112
}
87113

88114
[UnmanagedCallersOnly]
89115
private static unsafe PAL_SSLStreamStatus ReadFromConnection(IntPtr connection, byte* data, int* dataLength)
90116
{
91-
SafeDeleteSslContext? context = (SafeDeleteSslContext?)GCHandle.FromIntPtr(connection).Target;
92-
Debug.Assert(context != null);
93-
94-
int toRead = *dataLength;
95-
if (toRead == 0)
96-
return PAL_SSLStreamStatus.OK;
97-
98-
if (context._inputBuffer.ActiveLength == 0)
117+
WeakGCHandle<SafeDeleteSslContext> h = WeakGCHandle<SafeDeleteSslContext>.FromIntPtr(connection);
118+
if (!h.TryGetTarget(out SafeDeleteSslContext? context))
99119
{
120+
Debug.Write("ReadFromConnection: failed to get target context");
100121
*dataLength = 0;
101-
return PAL_SSLStreamStatus.NeedData;
122+
return PAL_SSLStreamStatus.Error;
102123
}
103124

104-
toRead = Math.Min(toRead, context._inputBuffer.ActiveLength);
125+
lock (context._lock)
126+
{
127+
if (context._disposed)
128+
{
129+
Debug.Write("ReadFromConnection: context is disposed");
130+
*dataLength = 0;
131+
return PAL_SSLStreamStatus.Error;
132+
}
133+
134+
int toRead = *dataLength;
135+
if (toRead == 0)
136+
return PAL_SSLStreamStatus.OK;
137+
138+
if (context._inputBuffer.ActiveLength == 0)
139+
{
140+
*dataLength = 0;
141+
return PAL_SSLStreamStatus.NeedData;
142+
}
143+
144+
toRead = Math.Min(toRead, context._inputBuffer.ActiveLength);
105145

106-
context._inputBuffer.ActiveSpan.Slice(0, toRead).CopyTo(new Span<byte>(data, toRead));
107-
context._inputBuffer.Discard(toRead);
146+
context._inputBuffer.ActiveSpan.Slice(0, toRead).CopyTo(new Span<byte>(data, toRead));
147+
context._inputBuffer.Discard(toRead);
148+
149+
*dataLength = toRead;
150+
return PAL_SSLStreamStatus.OK;
151+
}
152+
}
108153

109-
*dataLength = toRead;
110-
return PAL_SSLStreamStatus.OK;
154+
[UnmanagedCallersOnly]
155+
private static void CleanupManagedContext(IntPtr managedContextHandle)
156+
{
157+
if (managedContextHandle != IntPtr.Zero)
158+
{
159+
WeakGCHandle<SafeDeleteSslContext> handle = WeakGCHandle<SafeDeleteSslContext>.FromIntPtr(managedContextHandle);
160+
handle.Dispose();
161+
}
111162
}
112163

113164
internal void Write(ReadOnlySpan<byte> buf)
114165
{
115-
_inputBuffer.EnsureAvailableSpace(buf.Length);
116-
buf.CopyTo(_inputBuffer.AvailableSpan);
117-
_inputBuffer.Commit(buf.Length);
166+
lock (_lock)
167+
{
168+
_inputBuffer.EnsureAvailableSpace(buf.Length);
169+
buf.CopyTo(_inputBuffer.AvailableSpan);
170+
_inputBuffer.Commit(buf.Length);
171+
}
118172
}
119173

120174
internal int BytesReadyForConnection => _outputBuffer.ActiveLength;
121175

122176
internal void ReadPendingWrites(ref ProtocolToken token)
123177
{
124-
if (_outputBuffer.ActiveLength == 0)
178+
lock (_lock)
125179
{
126-
token.Size = 0;
127-
token.Payload = null;
128-
return;
129-
}
180+
if (_outputBuffer.ActiveLength == 0)
181+
{
182+
token.Size = 0;
183+
token.Payload = null;
184+
return;
185+
}
130186

131-
token.SetPayload(_outputBuffer.ActiveSpan);
132-
_outputBuffer.Discard(_outputBuffer.ActiveLength);
187+
token.SetPayload(_outputBuffer.ActiveSpan);
188+
_outputBuffer.Discard(_outputBuffer.ActiveLength);
189+
}
133190
}
134191

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

142-
int limit = Math.Min(count, _outputBuffer.ActiveLength);
199+
lock (_lock)
200+
{
201+
int limit = Math.Min(count, _outputBuffer.ActiveLength);
143202

144-
_outputBuffer.ActiveSpan.Slice(0, limit).CopyTo(new Span<byte>(buf, offset, limit));
145-
_outputBuffer.Discard(limit);
203+
_outputBuffer.ActiveSpan.Slice(0, limit).CopyTo(new Span<byte>(buf, offset, limit));
204+
_outputBuffer.Discard(limit);
146205

147-
return limit;
206+
return limit;
207+
}
148208
}
149209

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

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

234294
if (authOptions.EnabledSslProtocols != SslProtocols.None)
235295
{

src/libraries/tests.proj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,6 @@
225225
</ItemGroup>
226226

227227
<ItemGroup Condition="'$(TargetOS)' == 'android' and '$(RuntimeFlavor)' == 'CoreCLR' and '$(RunDisabledAndroidTests)' != 'true'">
228-
<!-- https://github.com/dotnet/runtime/issues/117045 -->
229-
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Http\tests\FunctionalTests\System.Net.Http.Functional.Tests.csproj" />
230228
<!-- https://github.com/dotnet/runtime/issues/114951 -->
231229
<!-- https://github.com/dotnet/runtime/issues/62547 -->
232230
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Security.Cryptography\tests\System.Security.Cryptography.Tests.csproj" />
@@ -604,6 +602,7 @@
604602
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Diagnostics.Tracing\tests\System.Diagnostics.Tracing.Tests.csproj" />
605603
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Security.Cryptography\tests\System.Security.Cryptography.Tests.csproj" />
606604
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Net.WebSockets.Client\tests\System.Net.WebSockets.Client.Tests.csproj" />
605+
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Net.Http\tests\FunctionalTests\System.Net.Http.Functional.Tests.csproj" />
607606
<SmokeTestProject Include="$(RepoRoot)\src\tests\FunctionalTests\Android\Device_Emulator\JIT\*.Test.csproj"/>
608607
</ItemGroup>
609608

src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ ARGS_NON_NULL_ALL static void FreeSSLStream(JNIEnv* env, SSLStream* sslStream)
411411
ReleaseGRef(env, sslStream->netOutBuffer);
412412
ReleaseGRef(env, sslStream->netInBuffer);
413413
ReleaseGRef(env, sslStream->appInBuffer);
414+
415+
sslStream->managedContextCleanup(sslStream->managedContextHandle);
416+
414417
free(sslStream);
415418
}
416419

@@ -650,7 +653,7 @@ SSLStream* AndroidCryptoNative_SSLStreamCreateWithKeyStorePrivateKeyEntry(intptr
650653
}
651654

652655
int32_t AndroidCryptoNative_SSLStreamInitialize(
653-
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, int32_t appBufferSize, char* peerHost)
656+
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, MANAGED_CONTEXT_CLEANUP managedContextCleanup, int32_t appBufferSize, char* peerHost)
654657
{
655658
abort_if_invalid_pointer_argument (sslStream);
656659
abort_unless(sslStream->sslContext != NULL, "sslContext is NULL in SSL stream");
@@ -706,6 +709,7 @@ int32_t AndroidCryptoNative_SSLStreamInitialize(
706709
sslStream->managedContextHandle = managedContextHandle;
707710
sslStream->streamReader = streamReader;
708711
sslStream->streamWriter = streamWriter;
712+
sslStream->managedContextCleanup = managedContextCleanup;
709713

710714
ret = SUCCESS;
711715

src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.h

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
typedef intptr_t ManagedContextHandle;
1212
typedef void (*STREAM_WRITER)(ManagedContextHandle, uint8_t*, int32_t);
1313
typedef int32_t (*STREAM_READER)(ManagedContextHandle, uint8_t*, int32_t*);
14+
typedef void (*MANAGED_CONTEXT_CLEANUP)(ManagedContextHandle);
1415

1516
typedef struct SSLStream
1617
{
@@ -24,6 +25,7 @@ typedef struct SSLStream
2425
ManagedContextHandle managedContextHandle;
2526
STREAM_READER streamReader;
2627
STREAM_WRITER streamWriter;
28+
MANAGED_CONTEXT_CLEANUP managedContextCleanup;
2729
} SSLStream;
2830

2931
typedef struct ApplicationProtocolData_t ApplicationProtocolData;
@@ -67,15 +69,16 @@ PALEXPORT SSLStream* AndroidCryptoNative_SSLStreamCreateWithKeyStorePrivateKeyEn
6769

6870
/*
6971
Initialize an SSL context
70-
- isServer : true if the context should be created in server mode
71-
- streamReader : callback for reading data from the connection
72-
- streamWriter : callback for writing data to the connection
73-
- appBufferSize : initial buffer size for application data
72+
- isServer : true if the context should be created in server mode
73+
- streamReader : callback for reading data from the connection
74+
- streamWriter : callback for writing data to the connection
75+
- managedContextCleanup : callback for cleaning up the managed context
76+
- appBufferSize : initial buffer size for application data
7477
7578
Returns 1 on success, 0 otherwise
7679
*/
7780
PALEXPORT int32_t AndroidCryptoNative_SSLStreamInitialize(
78-
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, int32_t appBufferSize, char* peerHost);
81+
SSLStream* sslStream, bool isServer, ManagedContextHandle managedContextHandle, STREAM_READER streamReader, STREAM_WRITER streamWriter, MANAGED_CONTEXT_CLEANUP managedContextCleanup, int32_t appBufferSize, char* peerHost);
7982

8083
/*
8184
Set target host

0 commit comments

Comments
 (0)