While working on our C extension, I noticed what I believe is a bug/crash in the list implementation when using _PyList_AsTupleAndClear (which creates a list from a tuple then clears the list), sort of live std::move-ing a Python list into a tuple.
Long story short, the function sets self->ob_item = NULL and Py_SET_SIZE(self, 0) ("clear the list and disown the memory") but does NOT reset self->allocated to 0.
After this, the list "believes" it has some memory allocated but it actually has none.
If code then tries to append something to the list (through PyList_Append),
it fails because it directly writes to self->ob_item[len] -- which is null.
Currently, it seems like there's only one caller to _PyList_AsTupleAndClear, and it discards the list right after gettings its tuple, so the bug has been flying under the radar I believe...
gives (I have a TSan build but it does give me what I expect...)
<frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'ext', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
Attempting append...
ThreadSanitizer:DEADLYSIGNAL
==35116==ERROR: ThreadSanitizer: SEGV on unknown address 0x000000000000 (pc 0x000101639e50 bp 0x00016f72d570 sp 0x00016f72d530 T25592900)
==35116==The signal is caused by a WRITE memory access.
==35116==Hint: address points to the zero page.
^Z
zsh: suspended ../python.exe -c "import ext; ext.test()"
Next steps
I have a branch/PR ready with a fix for that: #145680.
_PyList_AsTupleAndClear is only used internally in CPython, so it's OK for it to only do enough to convince list_dealloc do the right thing.
That said, a bit of defensive programming shouldn't hurt.
I don't think we need to add an adjustment for self->allocated, though. We can instead skip freeing the memory & setting self->ob_item to NULL, and leave deallocation to list_dealloc.
I don't think we need to add an adjustment for self->allocated, though. We can instead skip freeing the memory & setting self->ob_item to NULL, and leave deallocation to list_dealloc.
Ah, yeah, that makes sense to me as well.
EDIT: Now that I think about it, won't that be a problem (when we deallocate the list) since the items in the list will still "be" in ob_item but now belong to the resulting tuple?
The bits of the pointers will “be” in ob_item, but they'll be treated as uninitialized memory, reserved so that the next few appends don't need to reallocate the array.
The deallocator should not look at them, much less DECREF them.
Ah, I think I get it now. Sorry for the confusion -- what you mean is to remove setting ob_item to NULL, remove allocated = 0 but keep the Py_SET_SIZE so that the list deallocator considers everything inside ob_item as uninitialised memory / not something it needs to clear. Correct? (Sorry for the back and forth.)
Activity
encukou commented on Mar 9, 2026
_PyList_AsTupleAndClear is only used internally in CPython, so it's OK for it to only do enough to convince list_dealloc do the right thing.
That said, a bit of defensive programming shouldn't hurt.
I don't think we need to add an adjustment for self->allocated, though. We can instead skip freeing the memory & setting self->ob_item to NULL, and leave deallocation to list_dealloc.
KowalskiThomas commented on Mar 9, 2026
Ah, yeah, that makes sense to me as well.
EDIT: Now that I think about it, won't that be a problem (when we deallocate the list) since the items in the list will still "be" in ob_item but now belong to the resulting tuple?
encukou commented on Mar 10, 2026
The bits of the pointers will “be” in ob_item, but they'll be treated as uninitialized memory, reserved so that the next few appends don't need to reallocate the array.
The deallocator should not look at them, much less DECREF them.
KowalskiThomas commented on Mar 10, 2026
Ah, I think I get it now. Sorry for the confusion -- what you mean is to remove setting ob_item to NULL, remove allocated = 0 but keep the Py_SET_SIZE so that the list deallocator considers everything inside ob_item as uninitialised memory / not something it needs to clear. Correct? (Sorry for the back and forth.)
KowalskiThomas commented on Mar 11, 2026
I updated #145680 to do what you suggested.
gh-145681: do not deallocate list buffer in `_PyList_AsTupleAndClear` (…
encukou commented on Mar 12, 2026
Thank you!
pythongh-145681: do not deallocate list buffer in `_PyList_AsTupleAnd…