-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Android SslStream splits messages over 2048 bytes in multiple TLS records (breaks MS-TDS protocol) #95295
Comments
Tagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones Issue DetailsDescriptionThe Android SslStream implementation using the Android SSLEngine APIs splits application data over 2048 bytes in multiple TLS records, something which normally isn't important, except for SQL Server which expects TDS RPC requests (query messages) to be sent in a single TLS record, breaking Microsoft.Data.SqlClient on Android. This issue only affects SslStream Android implementation, all other platforms resize internal buffers to send larger TLS records that can be safely fragmented at the transport level instead. Reproduction StepsWrite a simple Android project that makes a TLS connection to a service and send a message of size larger than 2048 bytes - if it is encoded in multiple TLS records, the issue is successfully reproduced. As for reproducing the issue in a context where it matters enough to cause SQL Server to disconnect the client, a test project is available in the original SqlClient issue. From there, all you need is to connect to SQL Server with TLS configured and enforced and make a query using a string longer than 956 characters (UTF-16 doubles the byte size + header overhead), resulting in a TDS RPC request longer than 2048 bytes over the wire. Upon receiving the message split in two TLS records, SQL Server drops the connection: Expected behaviorSslStream.Write should send input buffers in the smallest number of output TLS records by default, resizing internal buffers accordingly, and taking into account the maximum TLS record data size (16384). This means that calling SslStream.Write with a buffer of 10000 bytes should result in a single TLS record even if internal buffer sizes were initialized to a smaller size. As long as we try sending the input buffers as complete TLS records instead of splitting them, edge cases like MS-TDS RPC requests sent to SQL Server should work. Actual behaviorSslStream.Write with input buffers larger than 2048 bytes are automatically split and sent in multiple TLS records, breaking long SQL queries made with SqlClient on Android. Regression?This used to work in the old Xamarin.Android runtime prior to .NET Core (yes, the ancient one!), but since we've been waiting for .NET 6,7,8 for other SslStream-related Android issues to be fixed, we only noticed this particular regression after finally switching to .NET 8. Known WorkaroundsIncreasing the internal initial buffer size from 2048 to 4096 is sufficient to work around the issue, and we have managed to inject a patched System.Net.Security.dll in our builds as a temporary solution. However, it's far from ideal and an upstream fix is needed in the .NET runtime, even if it means just increasing the initial buffer size upstream as a quick fix.
Configuration.NET 8 for Android - both x64 and ARM64. The same code using SqlClient on all other platforms (iOS, Windows, macOS, Linux) is unaffected by this issue. Other informationA lot of additional information is present in the original SqlClient issue.
|
Tagging subscribers to 'arch-android': @steveisok, @akoeplinger Issue DetailsDescriptionThe Android SslStream implementation using the Android SSLEngine APIs splits application data over 2048 bytes in multiple TLS records, something which normally isn't important, except for SQL Server which expects TDS RPC requests (query messages) to be sent in a single TLS record, breaking Microsoft.Data.SqlClient on Android. This issue only affects SslStream Android implementation, all other platforms resize internal buffers to send larger TLS records that can be safely fragmented at the transport level instead. Reproduction StepsWrite a simple Android project that makes a TLS connection to a service and send a message of size larger than 2048 bytes - if it is encoded in multiple TLS records, the issue is successfully reproduced. As for reproducing the issue in a context where it matters enough to cause SQL Server to disconnect the client, a test project is available in the original SqlClient issue. From there, all you need is to connect to SQL Server with TLS configured and enforced and make a query using a string longer than 956 characters (UTF-16 doubles the byte size + header overhead), resulting in a TDS RPC request longer than 2048 bytes over the wire. Upon receiving the message split in two TLS records, SQL Server drops the connection: Expected behaviorSslStream.Write should send input buffers in the smallest number of output TLS records by default, resizing internal buffers accordingly, and taking into account the maximum TLS record data size (16384). This means that calling SslStream.Write with a buffer of 10000 bytes should result in a single TLS record even if internal buffer sizes were initialized to a smaller size. As long as we try sending the input buffers as complete TLS records instead of splitting them, edge cases like MS-TDS RPC requests sent to SQL Server should work. Actual behaviorSslStream.Write with input buffers larger than 2048 bytes are automatically split and sent in multiple TLS records, breaking long SQL queries made with SqlClient on Android. Regression?This used to work in the old Xamarin.Android runtime prior to .NET Core (yes, the ancient one!), but since we've been waiting for .NET 6,7,8 for other SslStream-related Android issues to be fixed, we only noticed this particular regression after finally switching to .NET 8. Known WorkaroundsIncreasing the internal initial buffer size from 2048 to 4096 is sufficient to work around the issue, and we have managed to inject a patched System.Net.Security.dll in our builds as a temporary solution. However, it's far from ideal and an upstream fix is needed in the .NET runtime, even if it means just increasing the initial buffer size upstream as a quick fix.
Configuration.NET 8 for Android - both x64 and ARM64. The same code using SqlClient on all other platforms (iOS, Windows, macOS, Linux) is unaffected by this issue. Other informationA lot of additional information is present in the original SqlClient issue.
|
I spent most of the day digging into the Android .NET SslStream implementation and the corresponding Android SSLEngine API implementation. The quickest fix by far is just to increase the initial buffer size, as I'm a bit lost in all the buffer memory management that's being done. The SSLEngine docs mention the BUFFER_OVERFLOW result for the Wrap() calls, but only clarifies how it is used for the Unwrap() calls. I found it easier to understand how it truly works by looking at the Android source code implementation of Wrap, especially the part that checks for the output buffer size to return BUFFER_OVERFLOW:
I adapted the internal calculateOutNetBufSize function for C:
This would be useful to predict the packet output buffer size from the input buffer size, and re-allocate internal buffers accordingly to proactively avoid BUFFER_OVERFLOW. One thing that's a bit confusing to me is that the Android source code appears to not produce a TLS record when returning BUFFER_OVERFLOW, so I'm not entirely sure how we end up with 2048 chunks when called from the .NET runtime. Normally, it would reallocate and try again with a larger buffer size, unless I missed something. The DoWrap() code in the .NET runtime also just eats the BUFFER_OVERFLOW result to return SSLStreamStatus_OK instead. It's a bit difficult to understand how it works in the end just by looking at DoWrap. If we go one level higher in the call stack, AndroidCryptoNative_SSLStreamWrite is also quite important for the internal buffer management:
This function takes the raw input buffer bytes and length, but then creates a temporary 'data' jbyteArray to transfer those input bytes into sslStream->appOutBuffer. The crucial part here is probably that we max the temporary buffer size based on the number of bytes remaining in sslStream->appOutBuffer ( However, sslStream->appOutBuffer is just the application data buffer, we would also need to grow sslStream->netOutBuffer accordingly, and including sufficient space for the TLS overhead, before the actual call to SSLEngineWrap made at the beginning of DoWrap:
And in order to predict the TLS maximum overhead bytes, we may have no choice but to use some of the same constants defined in the SSLEngine source code, as they are not properly exposed through the Android SSLEngine APIs. This could be done using the GetNetOutBufferSize adapted function defined earlier in this post. This is my analysis so far, I couldn't find a clear way to fix it, but it should save some time to the person that knows this code a lot better than I do in the .NET runtime team. |
@awakecoding thanks for the thorough report! I will try looking into it tomorrow. |
we may build up on #87874. That one allows to allocate as needed - but I'm not sure if that works for Android. Increasing the InitialBufferSize is not great IMHO as it will make it more expensive for every single connection. |
I am using SMO and i need to Increase the internal initial buffer size from 2048 to 8192 to get it working. |
The fix should be soon available in .NET 9 Preview 7 |
Description
The Android SslStream implementation using the Android SSLEngine APIs splits application data over 2048 bytes in multiple TLS records, something which normally isn't important, except for SQL Server which expects TDS RPC requests (query messages) to be sent in a single TLS record, breaking Microsoft.Data.SqlClient on Android. This issue only affects SslStream Android implementation, all other platforms resize internal buffers to send larger TLS records that can be safely fragmented at the transport level instead.
Reproduction Steps
Write a simple Android project that makes a TLS connection to a service and send a message of size larger than 2048 bytes - if it is encoded in multiple TLS records, the issue is successfully reproduced. As for reproducing the issue in a context where it matters enough to cause SQL Server to disconnect the client, a test project is available in the original SqlClient issue. From there, all you need is to connect to SQL Server with TLS configured and enforced and make a query using a string longer than 956 characters (UTF-16 doubles the byte size + header overhead), resulting in a TDS RPC request longer than 2048 bytes over the wire. Upon receiving the message split in two TLS records, SQL Server drops the connection:
Expected behavior
SslStream.Write should send input buffers in the smallest number of output TLS records by default, resizing internal buffers accordingly, and taking into account the maximum TLS record data size (16384). This means that calling SslStream.Write with a buffer of 10000 bytes should result in a single TLS record even if internal buffer sizes were initialized to a smaller size. As long as we try sending the input buffers as complete TLS records instead of splitting them, edge cases like MS-TDS RPC requests sent to SQL Server should work.
Actual behavior
SslStream.Write with input buffers larger than 2048 bytes are automatically split and sent in multiple TLS records, breaking long SQL queries made with SqlClient on Android.
Regression?
This used to work in the old Xamarin.Android runtime prior to .NET Core (yes, the ancient one!), but since we've been waiting for .NET 6,7,8 for other SslStream-related Android issues to be fixed, we only noticed this particular regression after finally switching to .NET 8.
Known Workarounds
Increasing the internal initial buffer size from 2048 to 4096 is sufficient to work around the issue, and we have managed to inject a patched System.Net.Security.dll in our builds as a temporary solution. However, it's far from ideal and an upstream fix is needed in the .NET runtime, even if it means just increasing the initial buffer size upstream as a quick fix.
Configuration
.NET 8 for Android - both x64 and ARM64. The same code using SqlClient on all other platforms (iOS, Windows, macOS, Linux) is unaffected by this issue.
Other information
A lot of additional information is present in the original SqlClient issue.
The text was updated successfully, but these errors were encountered: