Repository navigation
sqlite3: concurrent execute() on a shared Connection crashes the process with a native access violation (threadsafety == 3) #160699
Description
Summary
Sharing a single sqlite3.Connection between multiple threads (check_same_thread=False) and calling execute() concurrently on it kills the process with a native access violation (0xC0000005) on 64-bit Windows. sqlite3.threadsafety on this build is 3 (SQLITE_THREADSAFE=1, serialized mode), and the docs for check_same_thread state that the connection may be shared between threads as long as writes are serialized by the user — here nothing is being written at all, only SELECTs of BLOBs.
No Python-level exception is raised; the interpreter dies silently. faulthandler reports the fault on the CONN.execute(...) line (or on the very next byte-consuming line), i.e. the heap has already been corrupted.
The same pattern with no third-party packages at all reproduces, so this is not a C-extension interaction problem.
Reproduction
import faulthandler
import hashlib
import os
import sqlite3
import threading
import time
from concurrent.futures import ThreadPoolExecutor
faulthandler.enable(all_threads=True)
CONN = sqlite3.connect(':memory:', check_same_thread=False)
CONN.execute('create table entries (key blob primary key, value blob)')
for i in range(8):
CONN.execute(
'insert into entries values (?, ?)',
(f'k{i}'.encode(), os.urandom(1_400_000)),
)
CONN.commit()
KEYS = [f'k{i}'.encode() for i in range(8)]
def load(key):
row = CONN.execute('select value from entries where key = ?', (key,)).fetchone()
blob = row[0][:]
return hashlib.blake2b(blob).digest()[0]
def burst():
with ThreadPoolExecutor(max_workers=4) as executor:
sum(executor.map(load, KEYS))
threads = []
start = time.time()
while time.time() - start < 70:
if sum(t.is_alive() for t in threads) < 4:
thread = threading.Thread(target=burst, daemon=True)
thread.start()
threads.append(thread)
time.sleep(0.05)
print('no crash', len(threads))Run it a few times; it usually dies within 10-70 seconds.
Observed output:
Windows fatal exception: access violation
Current thread 0x000079d0 [ThreadPoolExecutor-11_3] (most recent call first):
File "repro.py", line 29 in load
row = CONN.execute('select value from entries where key = ?', (key,)).fetchone()
File ".../concurrent/futures/thread.py", line 73 in run
File ".../concurrent/futures/thread.py", line 86 in run
File ".../concurrent/futures/thread.py", line 119 in _worker
File ".../threading.py", line 1160 in run
In other runs the fault is reported one line later (hashlib.blake2b(blob)), the process exits with code 3221225477 (0xC0000005).
Expected behavior
Either it works (as threadsafety == 3 implies), or a Python-level exception is raised. It should never be possible for concurrent execute() on a shared connection to corrupt the process heap.
Your environment
- CPython 3.15.1 (
tags/v3.15.1:c37e74c, Oct 21 2026), 64-bit, MSC v.1951 - Windows 11 (10.0.26200)
sqlite3.sqlite_version3.53.4,sqlite3.threadsafety3- No third-party packages involved in the reproduction
Additional context
- Adding heavy native work in other threads (e.g.
polars.read_ipc()on the returned blob) makes the crash appear inside that unrelated library instead, because the corruption has already happened. This is how the problem was originally found: a Streamlit app was dying with0xC0000005inside polars, and the crash site moved around randomly. - Serializing all
execute()/fetchone()calls with a singlethreading.Lockdid not help in the original application; my guess is that cursor teardown (pysqlite_cursor_dealloc->sqlite3_finalize) happens after the lock is released, and therefore races with another thread reusing the same cached prepared statement. - Suspected cause: the per-connection prepared-statement cache and cursor state are shared between threads, while
sqlite3_stepreleases the GIL. Another thread can reset/reuse the same cachedsqlite3_stmtwhile it is still being stepped, or finalize it concurrently. - Workaround that works here: one connection per thread (thread-local), which stops the crash.
Happy to add tracing, test a patch, or try a build with Py_DEBUG if that helps.
Linked PRs
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsNo status
Activity
pythongh-160699: use serialized SQLite connections
pythongh-160699: retry documentation preview
BlueDestination commentedon Oct 27, 2026
Confirmed on Python 3.14.9 as well (so this is not a 3.15 regression):
cpython-3.14-windows-x86_64-none), Windows 11 10.0.26200sqlite3.sqlite_version == 3.53.1,sqlite3.threadsafety == 3:memory:connection, 8 blobs of 1.4 MB, two concurrent bursts ofThreadPoolExecutor(max_workers=4))New detail: pysqlite raises a Python-level error right before the access violation, which may help locate the unsafe spot:
So a thread can observe the shared connection in a state where
sqlite3_prepare_v2/sqlite3_bind_*reports misuse, and the same code path then dereferences invalid memory instead of failing cleanly.