pybind miss NONE type checking cause core dump - #50
Merged
Conversation
Signed-off-by: ericyuanhui <285521263@qq.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix issue : #49
Python 3.13 pybind Parameter Conversion Crash Analysis
Conclusion
This issue is located in Ladybug's pybind Python API C++ parameter conversion layer. It is not in the C API, the query execution engine, or the SQL
UNWIND/MERGEimplementation.The
lbug_tcdml03_gdb.pyscript explicitly creates a database withbackend="pybind". The core dump also confirms that the loaded extension is:The root cause is that the pybind binding code reinterprets a Python
Noneobject as apy::listwithout validation, then reads it according to CPython's internallistlayout. This is undefined behavior. Python 3.12 does not crash only because the adjacent memory happens to contain zeroes, and it incorrectly convertsNoneto an empty list. The uv-managed Python 3.13 binary has mimalloc global state at the same adjacent memory location, which reliably triggers SIGSEGV.Reproduction Script and Input
Reproduction script:
Relevant input:
The query is:
The list values in the preceding rows cause the pybind parameter binder to infer a type approximately equivalent to:
When processing the fourth row,
r3, the target type ofc_listhas already been inferred asLIST<STRING>, while its actual Python value isNone.Core Dump Evidence
Core dump:
It was generated by the uv-managed Python 3.13 interpreter. The relevant crash-thread stack trace is:
Relevant local variables from the stack frames:
The crash occurs in
pybind11::str::check_. It attempts to read the Python object type ofchild, butchildis not a valid Python object. It is mimalloc's empty-page sentinel.Defective Code Path
File:
tools/python_api/src_cpp/py_connection.cppPyConnection::transformPythonValueAs()already handles null values at its entry point:However, prepared-statement parameters use a different code path:
This function does not check
val.is_none()before dispatching to theLIST,MAP, orSTRUCTbranches.py::reinterpret_borrow<py::list>(val)is an unchecked type wrapper. It does not performPyList_Check, and it does not convertNoneinto a list. It only wraps thePy_Nonepointer in a C++ object declared aspy::list.The pybind11 list iterator then uses
PySequence_Fast_ITEMSandPyList_GET_SIZEto access internal object fields directly. These macros are only valid for actual list objects or objects returned byPySequence_Fast(). PassingNoneviolates the CPython C API preconditions.Why Python 3.12 Does Not Crash
The system interpreter is:
A read-only memory inspection around
id(None)shows the following consecutive 64-bit words:Py_Noneis actually only aPyObject; its valid memory range only covers the first 16 bytes. The following words are not fields ofNone, so reading them is already out-of-bounds access.However,
RelWithDebInfodefinesNDEBUG, so thePyList_Check()assertion inside CPython'sPyList_GET_SIZE()does not execute. The invalid path interpretsNone + 16asPyVarObject.ob_size; that word happens to be zero. Therefore:py::listas having length zero.begin() == end().for (auto child : lst)never executes.LIST<STRING>with no child values.Therefore, Python 3.12 does not correctly preserve the value as SQL NULL. It accidentally converts
Noneinto an empty list,[]. This explains why the code can execute many times without generating a core dump.Why uv Python 3.13 Reliably Crashes
The uv-managed interpreter is:
This interpreter binary includes mimalloc, as confirmed by
strings, GDB symbols such as_mi_heap_mainand_mi_page_empty, and the core dump. Its memory immediately after_Py_NoneStructis:The same invalid operation now behaves as follows:
None + 16is incorrectly interpreted as the list'sob_size; it is now a non-zero address value.begin() != end(), so the loop necessarily enters.None + 24is incorrectly treated as the first list item, yielding_mi_page_empty.transformPythonValueAs(..., STRING)callspy::isinstance<py::str>._mi_page_emptyand triggers SIGSEGV.The reliability of the crash comes from the actual global object layout and embedded mimalloc in the current uv Python 3.13 binary, not from random query data.
Python 3.12 vs. 3.13
The public
PyListObjectlayout and the relevant parts of thePySequence_Fast_ITEMSmacro are the same in the local Python 3.12 and Python 3.13 headers. The root cause must not be described as “Python 3.13 changed the list ABI.”The real difference is the actual binary layout of the two interpreter distributions:
Python 3.13 free-threading support, JIT support, and related language changes are not involved. The active interpreter is not
python3.13t. Python 3.13 merely exposes the pre-existing bug consistently through the layout of this particular uv-managed distribution.Missing Test Coverage
Existing tests cover null values inside a list:
In this case,
Noneeventually reachestransformPythonValueAs(), which already has a null check, so it works.The missing case is a field whose inferred nested type is
LIST,MAP, orSTRUCT, while the entire field value isNone, for example:This test should assert that
c_listremains SQL NULL rather than becoming an empty list, and that execution does not crash. Equivalent tests should cover completeNonevalues for inferredMAPandSTRUCTfields as well.Recommended Fix
Handle null values at the entry point of
PyConnection::transformPythonValueFromParameterAs()before theswitchstatement:This preserves a null value with its target logical type before entering any
py::reinterpret_borrow<py::list>orpy::reinterpret_borrow<py::dict>branch. It fixes nullable list, map, and struct parameters together.Upgrading pybind11 alone cannot fix this issue, because the root problem is that Ladybug's caller code disguises
Noneaspy::list, violating the preconditions of both pybind11 and the CPython C API.