Skip to content

Assertion failure in free_callback_context via create_collation and missing PyErr_NoMemory in create_callback_context #146090

Description

@devdanzin

Crash report

What happened?

The interpreter will abort when running the following MRE.

Automated diagnosis:

Bug 1: When sqlite3_create_collation_v2() fails with SQLITE_BUSY, error cleanup at line 2201 calls free_callback_context(ctx) directly. But ctx has refcount=1 (set at creation, line 1064). free_callback_context asserts ctx->refcount == 0 (line 1076) -> crash in debug builds.

Fix: Use decref_callback_context(ctx) instead of free_callback_context(ctx).
File: Modules/_sqlite/connection.c, line 2201

Bug 2: create_callback_context returns NULL without exception when PyMem_Malloc fails. All 7 callers propagate NULL → SystemError.

Fix: Add if (ctx == NULL) { return (callback_context *)PyErr_NoMemory(); }.
File: Modules/_sqlite/connection.c, line 1061

Full report (contains both bugs)

MRE 1:

import sqlite3

conn = sqlite3.connect(":memory:")
conn.create_collation("mycoll", lambda a, b: (a > b) - (a < b))
conn.execute("CREATE TABLE t(x TEXT)")
for i in range(100):
    conn.execute("INSERT INTO t VALUES (?)", (f"item_{i:03d}",))
conn.commit()

cursor = conn.execute("SELECT x FROM t ORDER BY x COLLATE mycoll")
next(cursor)

# Replace the collation while the statement is active -> SQLITE_BUSY
# -> free_callback_context(ctx) with refcount=1 -> assertion failure
try:
    conn.create_collation("mycoll", lambda a, b: 0)
except sqlite3.OperationalError:
    pass  # We never get here in debug builds — assertion fires first

MRE 2:

import sqlite3, _testcapi

conn = sqlite3.connect(":memory:")
_testcapi.set_nomemory(1, 0)
try:
    conn.set_trace_callback(lambda s: None)
except (MemoryError, SystemError):
    pass
finally:
    _testcapi.remove_mem_hooks()

Backtrace 1:

python: ./Modules/_sqlite/connection.c:1076: void free_callback_context(callback_context *): Assertion `ctx->refcount == 0' failed.

Program received signal SIGABRT, Aborted.

#0  __pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (threadid=<optimized out>, signo=6) at ./nptl/pthread_kill.c:89
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:100
#3  0x00007ffff7c45e2e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7c28888 in __GI_abort () at ./stdlib/abort.c:77
#5  0x00007ffff7c287f0 in __assert_fail_base (fmt=<optimized out>, assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=<optimized out>) at ./assert/assert.c:118
#6  0x00007ffff7c3c19f in __assert_fail (assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=<optimized out>) at ./assert/assert.c:127
#7  0x00007ffff72e678f in free_callback_context (ctx=ctx@entry=0x7ffff74af940) at ./Modules/_sqlite/connection.c:1076
#8  0x00007ffff72e765b in pysqlite_connection_create_collation_impl (self=0x7ffff75b6450, cls=0x555555f5ec50, name=0x7ffff74e6688 "mycoll", callable=0x7ffff74f0a10)
    at ./Modules/_sqlite/connection.c:2201
#9  pysqlite_connection_create_collation (self=0x7ffff75b6450, cls=0x555555f5ec50, args=0x7fffffffb5e0, nargs=nargs@entry=2, kwnames=0x0) at ./Modules/_sqlite/clinic/connection.c.h:1289
#10 0x00005555556b6e0b in method_vectorcall_FASTCALL_KEYWORDS_METHOD (func=func@entry=0x7ffff7343350, args=args@entry=0x7fffffffb5d8, nargsf=nargsf@entry=9223372036854775811,
    kwnames=kwnames@entry=0x0) at Objects/descrobject.c:381
#11 0x00005555556a2ebe in _PyObject_VectorcallTstate (tstate=0x555555d99c08 <_PyRuntime+360664>, callable=0x7ffff7343350, args=0x7fffffffb5d8, nargsf=9223372036854775811, kwnames=0x0)
    at ./Include/internal/pycore_call.h:144
#12 0x000055555583f859 in _Py_VectorCallInstrumentation_StackRefSteal (callable=..., arguments=0x7ffff7fa7078, total_args=total_args@entry=3, kwnames=kwnames@entry=...,
    call_instrumentation=false, frame=frame@entry=0x7ffff7fa7020, this_instr=0x555555f392f8, tstate=0x555555d99c08 <_PyRuntime+360664>) at Python/ceval.c:775
#13 0x000055555584a8e4 in _PyEval_EvalFrameDefault (tstate=tstate@entry=0x555555d99c08 <_PyRuntime+360664>, frame=<optimized out>, frame@entry=0x7ffff7fa7020, throwflag=throwflag@entry=0)
    at Python/generated_cases.c.h:1838
#14 0x000055555583f08b in _PyEval_EvalFrame (tstate=0x555555d99c08 <_PyRuntime+360664>, frame=0x7ffff7fa7020, throwflag=0) at ./Include/internal/pycore_ceval.h:118
#15 _PyEval_Vector (tstate=tstate@entry=0x555555d99c08 <_PyRuntime+360664>, func=func@entry=0x7ffff7466690, locals=locals@entry=0x7ffff746a4b0, args=args@entry=0x0,
    argcount=argcount@entry=0, kwnames=kwnames@entry=0x0) at Python/ceval.c:2130
#16 0x000055555583ee1e in PyEval_EvalCode (co=co@entry=0x555555f390f0, globals=globals@entry=0x7ffff746a4b0, locals=locals@entry=0x7ffff746a4b0) at Python/ceval.c:686
#17 0x00005555559c8f8e in run_eval_code_obj (tstate=0x555555d99c08 <_PyRuntime+360664>, co=co@entry=0x555555f390f0, globals=globals@entry=0x7ffff746a4b0, locals=locals@entry=0x7ffff746a4b0)
    at Python/pythonrun.c:1368
#18 0x00005555559c8adb in run_mod (mod=mod@entry=0x555555f45cc8, filename=filename@entry=0x7ffff74f8040, globals=globals@entry=0x7ffff746a4b0, locals=locals@entry=0x7ffff746a4b0,
    flags=0x7fffffffc910, arena=arena@entry=0x7ffff74dac20, interactive_src=0x0, generate_new_source=0) at Python/pythonrun.c:1471

Backtrace 2:

Fatal Python error: _Py_CheckFunctionResult: a function returned NULL without setting an exception
Python runtime state: initialized
object address  : 0x7ffff75ac520
object refcount : 1
object type     : 0x555555d05978
object type name: MemoryError
object repr     :
lost sys.stderr

Current thread 0x00007ffff7e8b780 [python] (most recent call first):
  File "/home/danzin/crashers/sqlite_oom1.py", line 6 in <module>

Extension modules: _testcapi (total: 1)

Program received signal SIGABRT, Aborted.

#0  __pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (threadid=<optimized out>, signo=6) at ./nptl/pthread_kill.c:89
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:100
#3  0x00007ffff7c45e2e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7c28888 in __GI_abort () at ./stdlib/abort.c:77
#5  0x0000555555993a4d in fatal_error_exit (status=3064086, status@entry=-1) at Python/pylifecycle.c:3341
#6  0x0000555555993737 in fatal_error (fd=2, header=header@entry=1, prefix=prefix@entry=0x555555ac7235 "_Py_CheckFunctionResult",
    msg=msg@entry=0x555555ac724d "a function returned NULL without setting an exception", status=status@entry=-1) at Python/pylifecycle.c:3571
#7  0x0000555555991ca4 in _Py_FatalErrorFunc (func=0x555555ac7235 "_Py_CheckFunctionResult", msg=0x555555ac724d "a function returned NULL without setting an exception")
    at Python/pylifecycle.c:3587
#8  0x00005555556a2d05 in _Py_CheckFunctionResult (tstate=<optimized out>, callable=<optimized out>, result=<optimized out>, where=<optimized out>) at Objects/call.c:43
#9  0x000055555583f859 in _Py_VectorCallInstrumentation_StackRefSteal (callable=..., arguments=0x7ffff7fa7078, total_args=total_args@entry=2, kwnames=kwnames@entry=...,
    call_instrumentation=false, frame=frame@entry=0x7ffff7fa7020, this_instr=0x7ffff756d9ea, tstate=0x555555d99c08 <_PyRuntime+360664>) at Python/ceval.c:775
#10 0x000055555584a8e4 in _PyEval_EvalFrameDefault (tstate=tstate@entry=0x555555d99c08 <_PyRuntime+360664>, frame=<optimized out>, frame@entry=0x7ffff7fa7020, throwflag=throwflag@entry=0)
    at Python/generated_cases.c.h:1838
#11 0x000055555583f08b in _PyEval_EvalFrame (tstate=0x555555d99c08 <_PyRuntime+360664>, frame=0x7ffff7fa7020, throwflag=0) at ./Include/internal/pycore_ceval.h:118
#12 _PyEval_Vector (tstate=tstate@entry=0x555555d99c08 <_PyRuntime+360664>, func=func@entry=0x7ffff7466690, locals=locals@entry=0x7ffff746a4b0, args=args@entry=0x0,
    argcount=argcount@entry=0, kwnames=kwnames@entry=0x0) at Python/ceval.c:2130
#13 0x000055555583ee1e in PyEval_EvalCode (co=co@entry=0x7ffff756d8a0, globals=globals@entry=0x7ffff746a4b0, locals=locals@entry=0x7ffff746a4b0) at Python/ceval.c:686
#14 0x00005555559c8f8e in run_eval_code_obj (tstate=0x555555d99c08 <_PyRuntime+360664>, co=co@entry=0x7ffff756d8a0, globals=globals@entry=0x7ffff746a4b0, locals=locals@entry=0x7ffff746a4b0)
    at Python/pythonrun.c:1368
#15 0x00005555559c8adb in run_mod (mod=mod@entry=0x555555f3f5b0, filename=filename@entry=0x7ffff74e8580, globals=globals@entry=0x7ffff746a4b0, locals=locals@entry=0x7ffff746a4b0,
    flags=0x7fffffffc950, arena=arena@entry=0x7ffff74dad40, interactive_src=0x0, generate_new_source=0) at Python/pythonrun.c:1471

Found using cpython-review-toolkit with Claude Opus 4.6, using the /cpython-review-toolkit:explore Modules/_sqlite/ all deep command.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.15.0a7+ (heads/main:e0f7c1097e1, Mar 17 2026, 18:10:52) [Clang 21.1.2 (2ubuntu6)]

Linked PRs

Activity

  1. picnixz commented on Mar 17, 2026

    @picnixz
    Member

    I think this one is on me. I added the refcount logic here and in the tests I said that coverage was missing so this likely caused this issue to slip.

  2. self-assigned this
    on Mar 17, 2026
  3. picnixz commented on Mar 17, 2026

    @picnixz
    Member

    I will try to address this in the next two weeks. I am away this week but I may address it while travelling.

  4. devdanzin commented on Mar 18, 2026

    @devdanzin
    MemberAuthor

    The full report presents another crash, please let me know if I should open another issue for it.

  5. vstinner commented on Mar 18, 2026

    @vstinner
    Member

    @picnixz plans to write a PR to fix this issue.

    I confirm that the proposed fix works as expected:

    diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
    index af63271b9fd..76df1089ebb 100644
    --- a/Modules/_sqlite/connection.c
    +++ b/Modules/_sqlite/connection.c
    @@ -2198,7 +2198,7 @@ pysqlite_connection_create_collation_impl(pysqlite_Connection *self,
              * the context before returning.
              */
             if (callable != Py_None) {
    -            free_callback_context(ctx);
    +            decref_callback_context(ctx);
             }
             set_error_from_db(self->state, self->db);
             return NULL;
  6. added
    3.13only security fixes
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Mar 18, 2026
  7. picnixz commented on Mar 18, 2026

    @picnixz
    Member

    please let me know if I should open another issue for it

    No, it's fine. Just rename the issue and add the two bugs in that issue (that is, bugs concerning custom refcounting in sqlite3)

  8. changed the title [-]Assertion failure in `free_callback_context` via `create_collation`[/-] [+]Assertion failure in `free_callback_context` via `create_collation` and missing `PyErr_NoMemory` in `create_callback_context`[/+] on Mar 19, 2026
  9. added a commit that references this issue on Mar 28, 2026
  10. removed their assignment
    on Mar 28, 2026
  11. added a commit that references this issue on Mar 29, 2026
  12. added 2 commits that reference this issue on Mar 29, 2026
  13. added 2 commits that reference this issue on Mar 29, 2026
  14. added a commit that references this issue on Apr 16, 2026
  15. added a commit that references this issue on Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13only security fixes3.14bugs and security fixes3.15bugs and security fixesextension-modulesC modules in the Modules dirtopic-sqlite3type-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions