Thanks to visit codestin.com
Credit goes to github.com

Skip to content

sqlite3 converter lookup can crash during concurrent registry updates in free-threaded builds #155781

Description

@cuishuang

Bug report

Bug description:

In Modules/_sqlite/cursor.c, _pysqlite_get_converter() currently looks up a converter with:

retval = PyDict_GetItemWithError(state->converters, upcase_key);
return retval;

PyDict_GetItemWithError() returns a borrowed reference.

In a free-threaded build, another thread can replace or remove the converter after the dictionary lookup has completed but before pysqlite_build_row_cast_map() appends the converter to row_cast_map.

The converter registry can be modified either through:

sqlite3.register_converter(name, replacement)

or directly through the public sqlite3.converters dictionary.

A possible sequence is:

  1. A cursor thread obtains a borrowed converter reference.
  2. Another thread replaces or removes that dictionary entry.
  3. The previous converter is finalized.
  4. The cursor thread passes the no-longer-valid reference to PyList_Append().

A stress test on a free-threaded debug build of CPython main reproduces an interpreter crash.

The adapter registry already uses PyDict_GetItemRef() when looking up adapters. The converter lookup should similarly retain a strong reference until the converter has been added to the row cast map.

A proposed fix is to make _pysqlite_get_converter() return a new reference using PyDict_GetItemRef() and update pysqlite_build_row_cast_map() to release that reference after PyList_Append().

Reproducer

import sqlite3
import threading

NAME = "CONVERTER_RACE"
NCOLUMNS = 64
NITER = 3000

QUERY = "select " + ", ".join(
    f"'value' as 'c{i} [{NAME}]'" for i in range(NCOLUMNS)
)

class Converter:
    __slots__ = ("value",)

    def __init__(self, value):
        self.value = value

    def __call__(self, value):
        return self.value

def reader():
    con = sqlite3.connect(
        ":memory:",
        detect_types=sqlite3.PARSE_COLNAMES,
    )
    try:
        for _ in range(NITER):
            con.execute(QUERY).fetchone()
    finally:
        con.close()

def mutator():
    for i in range(NITER * NCOLUMNS):
        sqlite3.register_converter(NAME, Converter(i))
        if i % 3 == 0:
            sqlite3.converters.pop(NAME, None)

threads = [
    *[threading.Thread(target=reader) for _ in range(8)],
    *[threading.Thread(target=mutator) for _ in range(2)],
]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

Activity

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

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions