I have developing skills in Python/C API and memory allocation, and was expecting the following Python-embedded C++ code to be problematic and lead to something like a segmentation fault:
#include <Python.h>
#include <iostream>
int main(){
Py_Initialize();
PyObject* pythonList = Py_BuildValue("[i i]",1,2);
Py_DECREF(pythonList); // I checked with Py_REFCNT(pythonList) that reference count is now 0
PyList_Check(pythonList); // hence, I was expecting here something like a segmentation fault, but this does not happen...
std::cout << "Ok, goodbye" << std::endl;
return 0;
}
However, nothing bad happens at runtime (and "Ok, goodbye" is displayed).
Is this code actually OK, in spite of accessing (in PyList_Check(pythonList);
) a PyObject that has been decref-ed to zero ?
Or, is this code wrong and it was just a matter of luck that no segmentation fault here happens (why ?) ?
The code is wrong -- the call to Py_DECREF()
has freed the object, which means that pythonList
is a dangling-pointer, so when PyList_Check()
tries to deference that pointer, undefined behavior is invoked.
As for why that did not cause a segmentation fault, the formal answer is that undefined behavior is not required to result in any particular observable consequence (such as a segmentation fault). Undefined behavior can result in the program doing literally anything, and it's the programmer's responsibility to avoid invoking it.
As a practical matter, there is a more satisfying explanation, though: on most popular systems, segmentation faults are caused when a program tries to access a page of virtual memory that is unmapped to any physical memory age, or mapped to a physical page that program is not allowed to have access to. So if you had set pythonList
to point to some random/invalid memory location, and then tried to dereference it, you'd probably get a segmentation fault. However, pythonList
isn't pointing to a random memory location, it's pointing to the memory location where a valid Python List object was located (right up until a moment ago, when Py_DECREF()
freed it). The "freeing" of that memory merely means that the process's heap-data-structure now includes that portion of memory in its "free memory list" as memory that can be reused the next time some other part of the process wants to allocate memory. It doesn't involve telling the MMU that that memory location is now inaccessible (and generally speaking, it can't, since the MMU checks the validity of memory pages, not of individual bytes, and it's quite common to have a memory page that contains both valid objects and freed-memory-regions mixed together). So the MMU/segmentation-fault sanity checking system won't catch your reading of freed memory (valgrind might catch it, but at the cost of your program running 10-100 times slower than normal).