Bug #115222 MySqlPool(_idlePool) is not thread-safe
Submitted: 5 Jun 5:33 Modified: 8 Jul 8:12
Reporter: SoonWoo Kwon Email Updates:
Status: Verified Impact on me:
None 
Category:Connector / NET Severity:S3 (Non-critical)
Version:8.2.0, 9.0.0 OS:Windows
Assigned to: CPU Architecture:Any

[5 Jun 5:33] SoonWoo Kwon
Description:
MySqlPool is not thread-safe

The critical Section of ​​_idlePool in MySqlPool is abnormal

from the link below
https://github.com/mysql/mysql-connector-net/blob/8.2.0/MySQL.Data/src/MySqlPool.cs#L304
To block simultaneous access to '_idlePool', SemaphoreSlim is declared and entered into a lock state.
However, since SemaphoreSlim is declared as a local variable, '_idlePool' can be accessed without being blocked when accessed simultaneously from other threads.

I think it should be declared inside the 'MySqlPool' class, not a local variable.

There seems to be no problem with the method used in the previous version(8.0.26) (there were no problems when using it for at least 3 years)
	lock ((_idlePool as ICollection).SyncRoot)       (https://github.com/mysql/mysql-connector-net/blob/8.0.26/MySQL.Data/src/MySqlPool.cs#L286)

This problem occurs in 8.2.0, but the same code is being used in the latest version, 8.4.0.

How to repeat:
1. Use MySql.Net 8.1.0 or higher
2. Access the '_idlePool' object at the same time (ex. GetPooledConnectionAsync is executed at the same time as ClearAsync is executed, etc.)
3. The following exception is likely to occur

System.InvalidOperationException: The LinkedList is empty.
   at System.Collections.Generic.LinkedList`1.RemoveLast()
   at MySql.Data.MySqlClient.MySqlPool.<ClearAsync>d__36.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at MySql.Data.MySqlClient.MySqlPoolManager.<ClearPoolByTextAsync>d__26.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at MySql.Data.MySqlClient.MySqlPoolManager.<ClearPoolAsync>d__25.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at MySql.Data.MySqlClient.MySqlConnection.<ClearPoolAsync>d__125.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at MySql.Data.MySqlClient.MySqlConnection.ClearPool(MySqlConnection connection)
   at OUR_MYSQL_WRAPPER.RunClearPool(MySqlConnection conn)

Suggested fix:
use 'lock ((_idlePool as ICollection).SyncRoot)' the same as the previous version
 or modify the declaration location of SemaphoreSlim
[5 Jun 5:55] SoonWoo Kwon
It seems there are several places that use SemaphoreSlim in this way.
[3 Jul 12:54] MySQL Verification Team
Hello!

Thank you for the bug report.
Could you please provide repeatable test case (sample project, etc. - please make it as private if you prefer) to reproduce this issue at our end?

Regards,
Ashwini Patil
[4 Jul 2:06] SoonWoo Kwon
Hello, you can reproduce it with the code below.
In actual use, multiple threads create a connection with the same ConnectionString and ClearPool is performed when an error occurs

Connection Open and Clear must be executed at the same time, so the test is performed each time Enter (or any key) is pressed
It becomes reproducible when you press Enter more than 3 to 4 times

When I reproduce the issue on my computer, the call stack appears as follows:

   / System.Collections.Generic.LinkedList`1.RemoveLast()
   / MySql.Data.MySqlClient.MySqlPool.<ClearAsync>d__36.MoveNext()
   / MySql.Data.MySqlClient.MySqlPoolManager.<ClearPoolByTextAsync>d__27.MoveNext()
   / MySql.Data.MySqlClient.MySqlPoolManager.<ClearPoolAsync>d__26.MoveNext()
   / MySql.Data.MySqlClient.MySqlConnection.<ClearPoolAsync>d__128.MoveNext()
   / MySql.Data.MySqlClient.MySqlConnection.ClearPool(MySqlConnection connection)
   / ConsoleApp1.Program.TryClear() 파일 D:\subs\mysqltest\ConsoleApp1\ConsoleApp1\Program.cs:줄 51
   / System.Threading.Thread.StartCallback()

Please change ConnectionString in the code

I'm using C# 6.0 and installed MySql.Data 8.2.0 NugetPackage

============================================================================================

using MySql.Data.MySqlClient;
namespace ConsoleApp1
{
    internal class Program
    {
        static string ConnectionString = "server=127.0.0.1;port=3306;user id=root;password=      ;sslmode=Disabled;pooling=True;checkparameters=False;";
        static ManualResetEvent trigger = new ManualResetEvent(false);
        static void Main(string[] args)
        {
            for(int i = 0; i < 8; i++)
            {
                Thread thread = new Thread(Alloc);
                thread.Start();
            }
            Thread t = new Thread(Clear);
            t.Start();
            Console.WriteLine("Ready");
            while(true)
            {
                Console.ReadKey();
                trigger.Set();
                Console.WriteLine("Set");
                trigger.Reset();
            }
        }

        static void Alloc()
        {
            while(true)
            {
                using(var conn = new MySqlConnection(ConnectionString))
                {
                    trigger.WaitOne();
                    conn.Open();
                    Console.WriteLine("1");

                }
            }
        }
        static void Clear()
        {
            while(true)
            {
                using(var conn = new MySqlConnection(ConnectionString))
                {
                    trigger.WaitOne();
                    MySqlConnection.ClearPool(conn);
                    Console.WriteLine("2");
                }
            }
        }        
    }
}
[4 Jul 2:54] SoonWoo Kwon
It can be reproduced in Mysql.Data's latest version, 9.0.0

pressing Enter about 4 times at 1 second intervals and then holding down the button.
[8 Jul 8:12] MySQL Verification Team
Hello!

Thank you for the details.
Verified as described.

Regards,
Ashwini Patil