Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 119 additions & 2 deletions Lib/test/test_sqlite3/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@
import sqlite3 as sqlite
import unittest

from test.support import import_helper
from test.support.os_helper import TESTFN, unlink

from .util import memory_database, cx_limit, with_tracebacks
from .util import MemoryDatabaseMixin

# TODO(picnixz): increase test coverage for other callbacks
# such as 'func', 'step', 'finalize', and 'collation'.


class CollationTests(MemoryDatabaseMixin, unittest.TestCase):

Expand Down Expand Up @@ -129,8 +133,55 @@ def test_deregister_collation(self):
self.assertEqual(str(cm.exception), 'no such collation sequence: mycoll')


class AuthorizerTests(MemoryDatabaseMixin, unittest.TestCase):

def assert_not_authorized(self, func, /, *args, **kwargs):
with self.assertRaisesRegex(sqlite.DatabaseError, "not authorized"):
func(*args, **kwargs)

# When a handler has an invalid signature, the exception raised is
# the same that would be raised if the handler "negatively" replied.

def test_authorizer_invalid_signature(self):
self.cx.execute("create table if not exists test(a number)")
self.cx.set_authorizer(lambda: None)
self.assert_not_authorized(self.cx.execute, "select * from test")

# Tests for checking that callback context mutations do not crash.
# Regression tests for https://github.com/python/cpython/issues/142830.

@with_tracebacks(ZeroDivisionError, regex="hello world")
def test_authorizer_concurrent_mutation_in_call(self):
self.cx.execute("create table if not exists test(a number)")

def handler(*a, **kw):
self.cx.set_authorizer(None)
raise ZeroDivisionError("hello world")

self.cx.set_authorizer(handler)
self.assert_not_authorized(self.cx.execute, "select * from test")

@with_tracebacks(OverflowError)
def test_authorizer_concurrent_mutation_with_overflown_value(self):
_testcapi = import_helper.import_module("_testcapi")
self.cx.execute("create table if not exists test(a number)")

def handler(*a, **kw):
self.cx.set_authorizer(None)
# We expect 'int' at the C level, so this one will raise
# when converting via PyLong_Int().
return _testcapi.INT_MAX + 1

self.cx.set_authorizer(handler)
self.assert_not_authorized(self.cx.execute, "select * from test")


class ProgressTests(MemoryDatabaseMixin, unittest.TestCase):

def assert_interrupted(self, func, /, *args, **kwargs):
with self.assertRaisesRegex(sqlite.OperationalError, "interrupted"):
func(*args, **kwargs)

def test_progress_handler_used(self):
"""
Test that the progress handler is invoked once it is set.
Expand Down Expand Up @@ -219,11 +270,48 @@ def bad_progress():
create table foo(a, b)
""")

def test_progress_handler_keyword_args(self):
def test_set_progress_handler_keyword_args(self):
with self.assertRaisesRegex(TypeError,
'takes at least 1 positional argument'):
self.con.set_progress_handler(progress_handler=lambda: None, n=1)

# When a handler has an invalid signature, the exception raised is
# the same that would be raised if the handler "negatively" replied.

def test_progress_handler_invalid_signature(self):
self.cx.execute("create table if not exists test(a number)")
self.cx.set_progress_handler(lambda x: None, 1)
self.assert_interrupted(self.cx.execute, "select * from test")

# Tests for checking that callback context mutations do not crash.
# Regression tests for https://github.com/python/cpython/issues/142830.

@with_tracebacks(ZeroDivisionError, regex="hello world")
def test_progress_handler_concurrent_mutation_in_call(self):
self.cx.execute("create table if not exists test(a number)")

def handler(*a, **kw):
self.cx.set_progress_handler(None, 1)
raise ZeroDivisionError("hello world")

self.cx.set_progress_handler(handler, 1)
self.assert_interrupted(self.cx.execute, "select * from test")

def test_progress_handler_concurrent_mutation_in_conversion(self):
self.cx.execute("create table if not exists test(a number)")

class Handler:
def __bool__(_):
# clear the progress handler
self.cx.set_progress_handler(None, 1)
raise ValueError # force PyObject_True() to fail

self.cx.set_progress_handler(Handler.__init__, 1)
self.assert_interrupted(self.cx.execute, "select * from test")

# Running with tracebacks makes the second execution of this
# function raise another exception because of a database change.


class TraceCallbackTests(MemoryDatabaseMixin, unittest.TestCase):

Expand Down Expand Up @@ -345,11 +433,40 @@ def test_trace_bad_handler(self):
cx.set_trace_callback(lambda stmt: 5/0)
cx.execute("select 1")

def test_trace_keyword_args(self):
def test_set_trace_callback_keyword_args(self):
with self.assertRaisesRegex(TypeError,
'takes exactly 1 positional argument'):
self.con.set_trace_callback(trace_callback=lambda: None)

# When a handler has an invalid signature, the exception raised is
# the same that would be raised if the handler "negatively" replied,
# but for the trace handler, exceptions are never re-raised (only
# printed when needed).

@with_tracebacks(
TypeError,
regex=r".*<lambda>\(\) missing 6 required positional arguments",
)
def test_trace_handler_invalid_signature(self):
self.cx.execute("create table if not exists test(a number)")
self.cx.set_trace_callback(lambda x, y, z, t, a, b, c: None)
self.cx.execute("select * from test")

# Tests for checking that callback context mutations do not crash.
# Regression tests for https://github.com/python/cpython/issues/142830.

@with_tracebacks(ZeroDivisionError, regex="hello world")
def test_trace_callback_concurrent_mutation_in_call(self):
self.cx.execute("create table if not exists test(a number)")

def handler(statement):
# clear the progress handler
self.cx.set_trace_callback(None)
raise ZeroDivisionError("hello world")

self.cx.set_trace_callback(handler)
self.cx.execute("select * from test")


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion Lib/test/test_sqlite3/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def check_tracebacks(self, cm, exc, exc_regex, msg_regex, obj_name):
with contextlib.redirect_stderr(buf):
yield

self.assertEqual(cm.unraisable.exc_type, exc)
self.assertIsSubclass(cm.unraisable.exc_type, exc)
if exc_regex:
msg = str(cm.unraisable.exc_value)
self.assertIsNotNone(exc_regex.search(msg), (exc_regex, msg))
Expand Down
2 changes: 1 addition & 1 deletion Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -3429,7 +3429,7 @@ MODULE__SSL_DEPS=$(srcdir)/Modules/_ssl.h $(srcdir)/Modules/_ssl/cert.c $(srcdir
MODULE__TESTCAPI_DEPS=$(srcdir)/Modules/_testcapi/parts.h $(srcdir)/Modules/_testcapi/util.h
MODULE__TESTLIMITEDCAPI_DEPS=$(srcdir)/Modules/_testlimitedcapi/testcapi_long.h $(srcdir)/Modules/_testlimitedcapi/parts.h $(srcdir)/Modules/_testlimitedcapi/util.h
MODULE__TESTINTERNALCAPI_DEPS=$(srcdir)/Modules/_testinternalcapi/parts.h
MODULE__SQLITE3_DEPS=$(srcdir)/Modules/_sqlite/connection.h $(srcdir)/Modules/_sqlite/cursor.h $(srcdir)/Modules/_sqlite/microprotocols.h $(srcdir)/Modules/_sqlite/module.h $(srcdir)/Modules/_sqlite/prepare_protocol.h $(srcdir)/Modules/_sqlite/row.h $(srcdir)/Modules/_sqlite/util.h
MODULE__SQLITE3_DEPS=$(srcdir)/Modules/_sqlite/connection.h $(srcdir)/Modules/_sqlite/context.h $(srcdir)/Modules/_sqlite/cursor.h $(srcdir)/Modules/_sqlite/microprotocols.h $(srcdir)/Modules/_sqlite/module.h $(srcdir)/Modules/_sqlite/prepare_protocol.h $(srcdir)/Modules/_sqlite/row.h $(srcdir)/Modules/_sqlite/util.h
MODULE__ZSTD_DEPS=$(srcdir)/Modules/_zstd/_zstdmodule.h $(srcdir)/Modules/_zstd/buffer.h $(srcdir)/Modules/_zstd/zstddict.h

CODECS_COMMON_HEADERS=$(srcdir)/Modules/cjkcodecs/multibytecodec.h $(srcdir)/Modules/cjkcodecs/cjkcodecs.h
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:mod:`sqlite3`: fix use-after-free crashes when the connection's callbacks
are mutated during a callback execution. Patch by Bénédikt Tran.
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
# needs -lncurses[w] and -lpanel[w]
@MODULE__CURSES_PANEL_TRUE@_curses_panel _curses_panel.c

@MODULE__SQLITE3_TRUE@_sqlite3 _sqlite/blob.c _sqlite/connection.c _sqlite/cursor.c _sqlite/microprotocols.c _sqlite/module.c _sqlite/prepare_protocol.c _sqlite/row.c _sqlite/statement.c _sqlite/util.c
@MODULE__SQLITE3_TRUE@_sqlite3 _sqlite/blob.c _sqlite/connection.c _sqlite/context.c _sqlite/cursor.c _sqlite/microprotocols.c _sqlite/module.c _sqlite/prepare_protocol.c _sqlite/row.c _sqlite/statement.c _sqlite/util.c

# needs -lssl and -lcrypt
@MODULE__SSL_TRUE@_ssl _ssl.c
Expand Down
30 changes: 30 additions & 0 deletions Modules/_sqlite/clinic/context.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading