Skip to content

Commit 4172afa

Browse files
committed
Prevent concurrent reuse of SQLite statements
1 parent c436e8d commit 4172afa

5 files changed

Lines changed: 81 additions & 10 deletions

File tree

‎Lib/test/test_sqlite3/test_dbapi.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import sqlite3 as sqlite
2727
import subprocess
2828
import sys
29+
import textwrap
2930
import threading
3031
import unittest
3132
import urllib.parse
@@ -1734,6 +1735,45 @@ def run(con, err):
17341735
t.join()
17351736
self.assertEqual(len(err), 0, "\n".join(err))
17361737

1738+
@requires_subprocess()
1739+
def test_concurrent_queries_on_shared_connection(self):
1740+
code = textwrap.dedent("""\
1741+
import hashlib
1742+
import os
1743+
import sqlite3
1744+
import threading
1745+
from concurrent.futures import ThreadPoolExecutor
1746+
1747+
connection = sqlite3.connect(":memory:", check_same_thread=False)
1748+
connection.execute("CREATE TABLE entries (key INTEGER, value BLOB)")
1749+
data = os.urandom(1_400_000)
1750+
digest = hashlib.blake2b(data).digest()
1751+
keys = range(4)
1752+
connection.executemany(
1753+
"INSERT INTO entries VALUES (?, ?)", ((key, data) for key in keys)
1754+
)
1755+
barrier = threading.Barrier(len(keys))
1756+
1757+
def load(key):
1758+
barrier.wait()
1759+
for _ in range(100):
1760+
row = connection.execute(
1761+
"SELECT value FROM entries WHERE key = ?", (key,)
1762+
).fetchone()
1763+
assert row is not None
1764+
assert hashlib.blake2b(row[0]).digest() == digest
1765+
1766+
with ThreadPoolExecutor(max_workers=len(keys)) as executor:
1767+
list(executor.map(load, keys))
1768+
""")
1769+
proc = subprocess.run(
1770+
[sys.executable, "-X", "faulthandler", "-c", code],
1771+
capture_output=True,
1772+
text=True,
1773+
timeout=SHORT_TIMEOUT,
1774+
)
1775+
self.assertEqual(proc.returncode, 0, proc.stderr)
1776+
17371777

17381778
class ConstructorTests(unittest.TestCase):
17391779
def test_date(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a crash when multiple threads execute queries on the same SQLite connection.

‎Modules/_sqlite/cursor.c‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,18 @@ pysqlite_cursor_init_impl(pysqlite_Cursor *self,
141141
}
142142

143143
static inline int
144-
stmt_reset(pysqlite_Statement *self)
144+
stmt_reset(pysqlite_Statement *self, int keep_in_use)
145145
{
146146
int rc = SQLITE_OK;
147147

148148
if (self->st != NULL) {
149149
Py_BEGIN_ALLOW_THREADS
150150
rc = sqlite3_reset(self->st);
151151
Py_END_ALLOW_THREADS
152+
153+
if (rc == SQLITE_OK && !keep_in_use) {
154+
pysqlite_statement_mark_in_use(self, 0);
155+
}
152156
}
153157

154158
return rc;
@@ -179,7 +183,7 @@ cursor_clear(PyObject *op)
179183
pysqlite_set_ref(op, &self->row_factory, NULL);
180184
if (self->statement) {
181185
/* Reset the statement if the user has not closed the cursor */
182-
int rc = stmt_reset(self->statement);
186+
int rc = stmt_reset(self->statement, 0);
183187
Py_CLEAR(self->statement);
184188
if (rc != SQLITE_OK) {
185189
cursor_cannot_reset_stmt_error(self, 0);
@@ -854,7 +858,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation
854858

855859
if (self->statement) {
856860
// Reset pending statements on this cursor.
857-
if (stmt_reset(self->statement) != SQLITE_OK) {
861+
if (stmt_reset(self->statement, 0) != SQLITE_OK) {
858862
goto reset_failure;
859863
}
860864
}
@@ -872,15 +876,18 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation
872876
goto error;
873877
}
874878

875-
if (sqlite3_stmt_busy(self->statement->st)) {
879+
/* Reserve cached statements before releasing the GIL. SQLite only marks
880+
a statement busy once sqlite3_step() starts, leaving a race window. */
881+
if (!pysqlite_statement_try_mark_in_use(self->statement)) {
876882
Py_SETREF(self->statement,
877883
pysqlite_statement_create(self->connection, operation));
878884
if (self->statement == NULL) {
879885
goto error;
880886
}
887+
pysqlite_statement_mark_in_use(self->statement, 1);
881888
}
882889

883-
if (stmt_reset(self->statement) != SQLITE_OK) {
890+
if (stmt_reset(self->statement, 1) != SQLITE_OK) {
884891
goto reset_failure;
885892
}
886893
self->rowcount = self->statement->is_dml ? 0L : -1L;
@@ -966,7 +973,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation
966973
if (self->statement->is_dml) {
967974
self->rowcount += (long)sqlite3_changes(self->connection->db);
968975
}
969-
if (stmt_reset(self->statement) != SQLITE_OK) {
976+
if (stmt_reset(self->statement, 0) != SQLITE_OK) {
970977
goto reset_failure;
971978
}
972979
}
@@ -1001,7 +1008,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation
10011008
int sqlite3_state = sqlite3_errcode(db);
10021009
// stmt_reset() may return a previously set exception,
10031010
// either triggered because of Python or sqlite3.
1004-
rc = stmt_reset(self->statement);
1011+
rc = stmt_reset(self->statement, 0);
10051012
Py_CLEAR(self->statement);
10061013
if (sqlite3_state == SQLITE_OK && rc != SQLITE_OK) {
10071014
cursor_cannot_reset_stmt_error(self, 1);
@@ -1170,15 +1177,15 @@ pysqlite_cursor_iternext(PyObject *op)
11701177
if (self->statement->is_dml) {
11711178
self->rowcount = (long)sqlite3_changes(self->connection->db);
11721179
}
1173-
rc = stmt_reset(self->statement);
1180+
rc = stmt_reset(self->statement, 0);
11741181
Py_CLEAR(self->statement);
11751182
if (rc != SQLITE_OK) {
11761183
goto reset_failure;
11771184
}
11781185
}
11791186
else if (rc != SQLITE_ROW) {
11801187
rc = set_error_from_db(self->connection->state, self->connection->db);
1181-
int reset_rc = stmt_reset(self->statement);
1188+
int reset_rc = stmt_reset(self->statement, 0);
11821189
Py_CLEAR(self->statement);
11831190
Py_DECREF(row);
11841191
if (rc == SQLITE_OK && reset_rc != SQLITE_OK) {
@@ -1352,7 +1359,7 @@ pysqlite_cursor_close_impl(pysqlite_Cursor *self)
13521359
}
13531360

13541361
if (self->statement) {
1355-
int rc = stmt_reset(self->statement);
1362+
int rc = stmt_reset(self->statement, 0);
13561363
// Force self->statement to be NULL even if stmt_reset() may have
13571364
// failed to avoid a possible double-free if someone calls close()
13581365
// twice as a leak here would be better than a double-free.

‎Modules/_sqlite/statement.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pysqlite_statement_create(pysqlite_Connection *connection, PyObject *sql)
9797
}
9898

9999
self->st = stmt;
100+
self->in_use = 0;
100101
self->is_dml = is_dml;
101102

102103
PyObject_GC_Track(self);

‎Modules/_sqlite/statement.h‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,31 @@ typedef struct
3232
{
3333
PyObject_HEAD
3434
sqlite3_stmt* st;
35+
int in_use;
3536
int is_dml;
3637
} pysqlite_Statement;
3738

39+
static inline int
40+
pysqlite_statement_try_mark_in_use(pysqlite_Statement *self)
41+
{
42+
int in_use;
43+
Py_BEGIN_CRITICAL_SECTION(self);
44+
in_use = self->in_use;
45+
if (!in_use) {
46+
self->in_use = 1;
47+
}
48+
Py_END_CRITICAL_SECTION();
49+
return !in_use;
50+
}
51+
52+
static inline void
53+
pysqlite_statement_mark_in_use(pysqlite_Statement *self, int in_use)
54+
{
55+
Py_BEGIN_CRITICAL_SECTION(self);
56+
self->in_use = in_use;
57+
Py_END_CRITICAL_SECTION();
58+
}
59+
3860
pysqlite_Statement *pysqlite_statement_create(pysqlite_Connection *connection, PyObject *sql);
3961

4062
int pysqlite_statement_setup_types(PyObject *module);

0 commit comments

Comments
 (0)