You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Concurrent opens of distinct Microsoft.Data.Sqlite connections can receive the same live native handle. Beginning independent transactions on those two connections then throws SQLite Error 1: cannot start a transaction within a transaction.
This was reproduced with the published Microsoft.Data.Sqlite 10.0.11 package, without EF Core, DataLinq, reflection, forced GC, shared connection operations, or concurrent pool clearing. All connection objects remain strongly referenced and open until their handles are inspected.
Mechanism
SqliteConnectionInternal.Activate currently sets _active = true before _outerConnection.SetTarget(outerConnection). Activation runs after SqliteConnectionPool.GetConnection releases its lock. A competing checkout can therefore see the intermediate active/ownerless state through Leaked, reclaim the connection, and lend the same internal connection to another owner.
The same ordering exists on main at 961b3cb6fe78daf720068a5166218138c6c04f73.
Reproduction
Run the following project in an empty directory with dotnet run -- pool-result.json 30000. It clears only its own pool between waves, with all previous owners closed, then starts 16 concurrent opens and holds every owner alive. It stops and exits 1 when duplicate ownership is detected. Since this is a race, the failing wave varies and a run can pass.
usingSystem.Collections.Concurrent;usingSystem.Diagnostics;usingSystem.Runtime.InteropServices;usingSystem.Security.Cryptography;usingSystem.Text.Json;usingMicrosoft.Data.Sqlite;// Public APIs only. All owners remain strongly held and open until each wave// has finished. A duplicate is then checked with two independent read-only transactions.// No writes, provider internals or forced GC.varrounds=int.Parse(args[1]);constintworkers=16;ThreadPool.SetMinThreads(workers+2,workers+2);varerrors=newConcurrentQueue<string>();varduplicates=newList<object>();vartransactionErrors=newList<string>();varfile=Path.Combine(Path.GetDirectoryName(Path.GetFullPath(args[0]))!,"pool-public-probe.db");varconnectionString=newSqliteConnectionStringBuilder{DataSource=file,Pooling=true}.ToString();usingvarcontrol=newSqliteConnection(connectionString);control.Open();control.Close();varstopwatch=Stopwatch.StartNew();varcompleted=0;for(intround=0;round<rounds;round++){// Clear this probe's pool only, with all preceding owners already closed.SqliteConnection.ClearPool(control);usingvarready=newBarrier(workers);varconnections=Enumerable.Range(0,workers).Select(_ =>newSqliteConnection(connectionString)).ToArray();try{awaitTask.WhenAll(connections.Select(connection =>Task.Run(()=>{ready.SignalAndWait();try{connection.Open();}catch(Exceptionexception){errors.Enqueue(exception.ToString());}})));varhandles=connections.Select((connection,owner)=>new{Owner=owner,State=connection.State,Handle=connection.Handle?.DangerousGetHandle().ToInt64()}).ToArray();foreach(varownerinhandles.Where(x =>x.State!=System.Data.ConnectionState.Open||!x.Handle.HasValue||x.Handle.Value==0))errors.Enqueue($"Round {round}, owner {owner.Owner}: expected an open live connection; got {owner.State}, handle {owner.Handle}.");foreach(vargroupinhandles.Where(x =>x.Handle.HasValue).GroupBy(x =>x.Handle).Where(g =>g.Count()>1)){varowners=group.Select(x =>x.Owner).ToArray();duplicates.Add(new{Round=round,Handle=group.Key,Owners=owners});usingvarfirstTransaction=connections[owners[0]].BeginTransaction(System.Data.IsolationLevel.Serializable,deferred:true);try{usingvarsecondTransaction=connections[owners[1]].BeginTransaction(System.Data.IsolationLevel.Serializable,deferred:true);secondTransaction.Rollback();}catch(Microsoft.Data.Sqlite.SqliteExceptionexception){transactionErrors.Add(exception.Message);}firstTransaction.Rollback();}completed++;}finally{foreach(varconnectioninconnections)try{connection.Dispose();}catch(Exceptionexception){errors.Enqueue(exception.ToString());}}if(duplicates.Count!=0||!errors.IsEmpty)break;if(completed%100==0)Console.WriteLine($"Completed {completed}/{rounds} waves ({workers} concurrent opens each).");}SqliteConnection.ClearPool(control);varassembly=typeof(SqliteConnection).Assembly;varreport=new{Kind="Published Microsoft.Data.Sqlite pool and transaction reproduction",Runtime=RuntimeInformation.FrameworkDescription,Assembly=assembly.FullName,AssemblySha256=Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(assembly.Location))),RequestedRounds=rounds,CompletedRounds=completed,WorkersPerRound=workers,DurationSeconds=stopwatch.Elapsed.TotalSeconds,DuplicateLiveNativeHandles=duplicates,SecondBeginErrors=transactionErrors,Errors=errors.ToArray(),Limitation="Public opens and deferred transaction begins only. A passing stress run cannot exclude rare interleavings."};varjson=JsonSerializer.Serialize(report,newJsonSerializerOptions{WriteIndented=true});File.WriteAllText(args[0],json);Console.WriteLine(json);returnduplicates.Count==0&&errors.IsEmpty?0:1;
One recorded run failed on wave 312 (zero-based index 311), with owners 13 and 15 sharing a native handle. Starting a deferred serializable transaction on each independently reproduced the transaction-within-transaction error. A previous checkout-only run failed on wave 774.
Environment: Windows x64, SDK 10.0.401, .NET runtime 10.0.12, Microsoft.Data.Sqlite 10.0.11, SQLitePCLRaw.bundle_e_sqlite3 3.0.5. The published Microsoft.Data.Sqlite assembly SHA-256 was 4abd9c2a61e580eb853e93ca8953a3cef2c05714ae28d2d1859d4dbc5e5700bc.
Proposed correction and validation
I have prepared a minimal change that sets the weak owner before the volatile active flag, plus a regression test in SqliteConnectionFactoryTest which checks distinct handles while every owner is still open. This preserves pooling and adds no locks, retries, or public API changes.
An isolated v10.0.11 source build with a scheduling callback reproduced duplicate ownership and the exact transaction error. Reversing the two writes removed both failures under the same controlled schedule. The callback is diagnostic only and is not in the proposed patch.
An uninstrumented original source build reproduced duplicate ownership on wave 351; the corrected source build completed 50,000 waves / 800,000 opens without duplicates or errors. These are correctness stress runs, not performance benchmarks.
The new regression test fails on unmodified main and passes with the correction.
I would like to contribute this correction and regression test. Since the issue also affects published 10.0.11, a servicing backport would be useful if the diagnosis is accepted.
AI disclosure
This fix was found, reproduced and posted with GPT-6 Astra on Extra High, with the repository owner's authorization.
Concurrent opens of distinct Microsoft.Data.Sqlite connections can receive the same live native handle. Beginning independent transactions on those two connections then throws
SQLite Error 1: cannot start a transaction within a transaction.This was reproduced with the published Microsoft.Data.Sqlite 10.0.11 package, without EF Core, DataLinq, reflection, forced GC, shared connection operations, or concurrent pool clearing. All connection objects remain strongly referenced and open until their handles are inspected.
Mechanism
SqliteConnectionInternal.Activatecurrently sets_active = truebefore_outerConnection.SetTarget(outerConnection). Activation runs afterSqliteConnectionPool.GetConnectionreleases its lock. A competing checkout can therefore see the intermediate active/ownerless state throughLeaked, reclaim the connection, and lend the same internal connection to another owner.The same ordering exists on main at
961b3cb6fe78daf720068a5166218138c6c04f73.Reproduction
Run the following project in an empty directory with
dotnet run -- pool-result.json 30000. It clears only its own pool between waves, with all previous owners closed, then starts 16 concurrent opens and holds every owner alive. It stops and exits 1 when duplicate ownership is detected. Since this is a race, the failing wave varies and a run can pass.Repro.csproj:
Program.cs:
One recorded run failed on wave 312 (zero-based index 311), with owners 13 and 15 sharing a native handle. Starting a deferred serializable transaction on each independently reproduced the transaction-within-transaction error. A previous checkout-only run failed on wave 774.
Environment: Windows x64, SDK 10.0.401, .NET runtime 10.0.12, Microsoft.Data.Sqlite 10.0.11, SQLitePCLRaw.bundle_e_sqlite3 3.0.5. The published Microsoft.Data.Sqlite assembly SHA-256 was
4abd9c2a61e580eb853e93ca8953a3cef2c05714ae28d2d1859d4dbc5e5700bc.Proposed correction and validation
I have prepared a minimal change that sets the weak owner before the volatile active flag, plus a regression test in
SqliteConnectionFactoryTestwhich checks distinct handles while every owner is still open. This preserves pooling and adds no locks, retries, or public API changes.category=failingexclusion). Six skips reference Tests in SqliteCommandTest are flaky #35585; one references sqlite3_create_collation(..., null) doesn't delete collations ericsink/SQLitePCL.raw#421. This is the SQLite driver test application, not the full EF Core provider matrix.I would like to contribute this correction and regression test. Since the issue also affects published 10.0.11, a servicing backport would be useful if the diagnosis is accepted.
AI disclosure
This fix was found, reproduced and posted with GPT-6 Astra on Extra High, with the repository owner's authorization.