Related PR
#885
Description
A reference to item is incorrectly decremented after a failed PyList_SetItem call, leading to a potential use-after-free vulnerability or double-decrement crash.
Affected Code
|
if (PyList_SetItem(result, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(result); |
|
return 0; |
|
} |
|
if (PyList_SetItem(result, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(result); |
|
return 0; |
|
} |
|
if (PyList_SetItem(result, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(result); |
|
return 0; |
|
} |
|
if (PyList_SetItem(list, i, item)) { |
|
Py_DECREF(item); |
|
return -1; |
|
} /* PyList_SetItem stole a reference to the item automatically */ |
|
if (PyList_SetItem(list, VECTOR(vs)[i], item)) { |
|
Py_DECREF(item); |
|
igraph_vector_int_destroy(&vs); |
|
return -1; |
|
} /* PyList_SetItem stole a reference to the item automatically */ |
|
if (PyList_SetItem(list, i, Py_None)) { |
|
Py_DECREF(Py_None); |
|
Py_DECREF(list); |
|
igraph_vector_int_destroy(&vs); |
|
return -1; |
|
} |
|
if (PyList_SetItem(list, VECTOR(vs)[i], item)) { |
|
Py_DECREF(list); |
|
Py_DECREF(item); |
|
igraph_vector_int_destroy(&vs); |
|
return -1; |
|
} |
|
if (PyList_SetItem(result, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(result); |
|
return 0; |
|
} |
|
if (PyList_SetItem(values, i, Py_None)) { /* reference stolen */ |
|
Py_DECREF(values); |
|
Py_DECREF(Py_None); |
|
return 0; |
|
} |
|
if (PyList_SetItem(value, i + j, o)) { |
|
Py_DECREF(o); /* append failed */ |
|
o = NULL; /* indicate error */ |
|
} else { |
|
if (PyList_SetItem(newlist, i, o)) { |
|
PyErr_PrintEx(0); |
|
Py_DECREF(o); |
|
Py_DECREF(newlist); |
|
Py_DECREF(newdict); |
|
IGRAPH_ERROR("", IGRAPH_FAILURE); |
|
} |
|
if (PyList_SetItem(value, i + j, o)) { |
|
Py_DECREF(o); /* append failed */ |
|
o = NULL; /* indicate error */ |
|
} else { |
|
if (PyList_SetItem(newlist, i, o)) { |
|
PyErr_PrintEx(0); |
|
Py_DECREF(o); |
|
Py_DECREF(newlist); |
|
Py_DECREF(newdict); |
|
IGRAPH_ERROR("", IGRAPH_FAILURE); |
|
} |
|
if (PyList_SetItem(list, j, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(random_func); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(list, j, item)) { /* reference to item stolen */ |
|
Py_DECREF(item); |
|
Py_DECREF(list); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
if (PyList_SetItem(res, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(list); |
|
Py_DECREF(res); |
|
return 0; |
|
} |
|
r=PyList_SetItem(result, self->idx, v); |
|
if (r == -1) { Py_DECREF(v); } |
|
if (PyList_SetItem(result, i, Py_None) == -1) { |
|
Py_DECREF(Py_None); |
|
Py_DECREF(result); |
|
return -1; |
|
} |
|
if (PyList_SetItem(result, i, v) == -1) { |
|
Py_DECREF(v); |
|
Py_DECREF(result); |
|
return -1; |
|
} |
|
r=PyList_SetItem(result, self->idx, v); |
|
if (r == -1) { Py_DECREF(v); } |
|
if (PyList_SetItem(result, i, Py_None) == -1) { |
|
Py_DECREF(Py_None); |
|
Py_DECREF(result); |
|
return -1; |
|
} |
|
if (PyList_SetItem(result, i, v) == -1) { |
|
Py_DECREF(v); |
|
Py_DECREF(result); |
|
return -1; |
|
} |
|
if (PyList_SetItem(obj, i, edge)) { /* reference to v stolen, reference to idx discarded */ |
|
Py_DECREF(edge); |
|
return NULL; |
|
} |
|
if (PyList_SetItem(obj, i, v)) { /* reference to v stolen, reference to idx discarded */ |
|
Py_DECREF(v); |
|
return NULL; |
|
} |
|
if (PyList_SetItem(values, eid, item)) { |
|
Py_DECREF(item); |
|
igraph_vector_int_clear(&data->to_add); |
|
} |
|
if (PyList_SetItem(values, eid, new_value)) { |
|
Py_DECREF(new_value); |
|
igraph_vector_int_clear(&data->to_add); |
|
} |
|
if (PyList_SetItem(result, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(result); |
|
return 0; |
|
} |
|
if (PyList_SetItem(result, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(result); |
|
return 0; |
|
} |
|
if (PyList_SetItem(result, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(result); |
|
return 0; |
|
} |
|
if (PyList_SetItem(list, i, item)) { |
|
Py_DECREF(item); |
|
return -1; |
|
} /* PyList_SetItem stole a reference to the item automatically */ |
|
if (PyList_SetItem(list, i, item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(list); |
|
return -1; |
|
} |
|
if (PyList_SetItem(list, VECTOR(es)[i], item)) { |
|
Py_DECREF(item); |
|
igraph_vector_int_destroy(&es); |
|
return -1; |
|
} /* PyList_SetItem stole a reference to the item automatically */ |
|
if (PyList_SetItem(list, i, Py_None)) { |
|
Py_DECREF(Py_None); |
|
Py_DECREF(list); |
|
return -1; |
|
} |
|
if (PyList_SetItem(list, VECTOR(es)[i], item)) { |
|
Py_DECREF(item); |
|
Py_DECREF(list); |
|
return -1; |
|
} |
|
if (!dest || PyList_SetItem(emi, j, dest)) { |
|
igraph_vector_ptr_destroy(&gs); |
|
igraph_vector_int_list_destroy(&edgemaps); |
|
Py_XDECREF(dest); |
|
Py_DECREF(emi); |
|
Py_DECREF(em_list); |
|
return NULL; |
|
} |
|
if (!emi || PyList_SetItem(em_list, i, emi)) { |
|
igraph_vector_ptr_destroy(&gs); |
|
igraph_vector_int_list_destroy(&edgemaps); |
|
Py_XDECREF(emi); |
|
Py_DECREF(em_list); |
|
return NULL; |
|
} |
|
if (!dest || PyList_SetItem(emi, j, dest)) { |
|
igraph_vector_ptr_destroy(&gs); |
|
igraph_vector_int_list_destroy(&edgemaps); |
|
Py_XDECREF(dest); |
|
Py_DECREF(emi); |
|
Py_DECREF(em_list); |
|
return NULL; |
|
} |
|
if (!emi || PyList_SetItem(em_list, i, emi)) { |
|
igraph_vector_ptr_destroy(&gs); |
|
igraph_vector_int_list_destroy(&edgemaps); |
|
Py_XDECREF(emi); |
|
Py_DECREF(em_list); |
|
return NULL; |
|
} |
Root Cause
PyList_SetItem Source Code
PyList_SetItem Document
PyList_SetItem Python Forum Discussion
Therefore, whether the function succeeds or fails, it will steal a reference count of the third argument. It must not call Py_XDECREF/Py_DECREF in case of failure.
Evidence:Reference Counting and Ownership in CPython Native API
Borrowed Reference
A borrowed reference is a reference obtained from an object that you don't own. You don't need to decrement its reference count when you're done with it, but you must ensure the object stays alive while you're using it (e.g., by creating an owned reference with Py_INCREF if necessary). Borrowed references are typically returned by functions like PyList_GetItem(), which returns an item from a list without incrementing its reference count.
New Reference
A new reference (also called an "owned reference") is a reference that you have ownership of. When you receive a new reference from a function (such as PyObject_New() or Py_BuildValue()), you are responsible for calling Py_DECREF() on it when you no longer need it to properly decrement its reference count. Failure to do so causes memory leaks.
Stolen Reference (Stealing)
A stolen reference is when a function takes ownership of a reference you pass to it. When you pass an object reference to a function that "steals" it, you no longer own that reference, and you should not call Py_DECREF() on it afterward. The function assumes full responsibility for managing the reference count of that object.
Example: PyList_SetItem
PyList_SetItem is a classic example of a reference-stealing function. According to the documentation:
"Set the item at index index in list to item. Return 0 on success. If index is out of bounds, return -1 and set an IndexError exception. Note: This function 'steals' a reference to item and discards a reference to an item already in the list at the affected position."
However, a critical ambiguity in the documentation is that it does not clearly state whether the reference is stolen in the case of function failure. This is fundamentally an all-or-nothing problem: when you pass an object to a stealing function, you must understand whether ownership is unconditionally transferred or only transferred on success.
Looking at the CPython source code clarifies this behavior:
int
PyList_SetItem(PyObject *op, Py_ssize_t i,
PyObject *newitem)
{
if (!PyList_Check(op)) {
Py_XDECREF(newitem);
PyErr_BadInternalCall();
return -1;
}
// ...
}
As demonstrated in the source code above, PyList_SetItem unconditionally calls Py_XDECREF(newitem) when the type check fails—meaning it always steals the reference, even on failure. The function takes ownership of newitem regardless of whether it successfully inserts the item into the list.
This behavior has serious implications for correct API usage. Consider the following incorrect code:
if (PyList_SetItem(a, b, something) < 0) {
Py_DECREF(something); // DANGER: Use-After-Free!
}
This code is defective because it leads to a use-after-free vulnerability. Since PyList_SetItem already stole the reference (and decremented it on failure via Py_XDECREF), the additional Py_DECREF(something) in the error-handling block causes a double decrement, potentially leading to an assertion failure in debug builds or memory corruption and crashes in release builds.
The correct pattern is simply:
if (PyList_SetItem(a, b, something) < 0) {
// Do NOT call Py_DECREF on 'something' - the reference was already stolen
return NULL; // or handle error appropriately
}
In summary, when dealing with stealing functions in the CPython API, you must relinquish all ownership responsibility for the passed reference and never decrement it after the call, regardless of the return value. Always consult the source code or thoroughly documented behavior to confirm whether a function truly provides an all-or-nothing stealing guarantee.
Related PR
#885
Description
A reference to item is incorrectly decremented after a failed PyList_SetItem call, leading to a potential use-after-free vulnerability or double-decrement crash.
Affected Code
python-igraph/src/_igraph/vertexseqobject.c
Lines 297 to 301 in 5a451e6
python-igraph/src/_igraph/vertexseqobject.c
Lines 322 to 326 in 5a451e6
python-igraph/src/_igraph/vertexseqobject.c
Lines 345 to 349 in 5a451e6
python-igraph/src/_igraph/vertexseqobject.c
Lines 471 to 474 in 5a451e6
python-igraph/src/_igraph/vertexseqobject.c
Lines 532 to 536 in 5a451e6
python-igraph/src/_igraph/vertexseqobject.c
Lines 551 to 556 in 5a451e6
python-igraph/src/_igraph/vertexseqobject.c
Lines 568 to 573 in 5a451e6
python-igraph/src/_igraph/pyhelpers.c
Lines 86 to 90 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 283 to 287 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 661 to 664 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 722 to 728 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 880 to 883 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 936 to 942 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 984 to 988 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 998 to 1002 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1072 to 1076 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1116 to 1120 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1149 to 1153 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1209 to 1214 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1246 to 1250 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1292 to 1296 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1326 to 1331 in 5a451e6
python-igraph/src/_igraph/attributes.c
Lines 1385 to 1390 in 5a451e6
python-igraph/src/_igraph/edgeobject.c
Lines 395 to 396 in 5a451e6
python-igraph/src/_igraph/edgeobject.c
Lines 408 to 412 in 5a451e6
python-igraph/src/_igraph/edgeobject.c
Lines 416 to 420 in 5a451e6
python-igraph/src/_igraph/vertexobject.c
Lines 526 to 527 in 5a451e6
python-igraph/src/_igraph/vertexobject.c
Lines 539 to 543 in 5a451e6
python-igraph/src/_igraph/vertexobject.c
Lines 547 to 551 in 5a451e6
python-igraph/src/_igraph/vertexobject.c
Lines 641 to 644 in 5a451e6
python-igraph/src/_igraph/vertexobject.c
Lines 686 to 689 in 5a451e6
python-igraph/src/_igraph/indexing.c
Lines 342 to 345 in 5a451e6
python-igraph/src/_igraph/indexing.c
Lines 404 to 407 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 312 to 316 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 337 to 341 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 361 to 365 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 497 to 500 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 518 to 522 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 562 to 566 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 581 to 585 in 5a451e6
python-igraph/src/_igraph/edgeseqobject.c
Lines 598 to 602 in 5a451e6
python-igraph/src/_igraph/operators.c
Lines 156 to 163 in 5a451e6
python-igraph/src/_igraph/operators.c
Lines 167 to 173 in 5a451e6
python-igraph/src/_igraph/operators.c
Lines 281 to 288 in 5a451e6
python-igraph/src/_igraph/operators.c
Lines 292 to 298 in 5a451e6
Root Cause
PyList_SetItem Source Code
PyList_SetItem Document
PyList_SetItem Python Forum Discussion
Therefore, whether the function succeeds or fails, it will steal a reference count of the third argument. It must not call
Py_XDECREF/Py_DECREFin case of failure.Evidence:Reference Counting and Ownership in CPython Native API
Borrowed Reference
A borrowed reference is a reference obtained from an object that you don't own. You don't need to decrement its reference count when you're done with it, but you must ensure the object stays alive while you're using it (e.g., by creating an owned reference with
Py_INCREFif necessary). Borrowed references are typically returned by functions likePyList_GetItem(), which returns an item from a list without incrementing its reference count.New Reference
A new reference (also called an "owned reference") is a reference that you have ownership of. When you receive a new reference from a function (such as
PyObject_New()orPy_BuildValue()), you are responsible for callingPy_DECREF()on it when you no longer need it to properly decrement its reference count. Failure to do so causes memory leaks.Stolen Reference (Stealing)
A stolen reference is when a function takes ownership of a reference you pass to it. When you pass an object reference to a function that "steals" it, you no longer own that reference, and you should not call
Py_DECREF()on it afterward. The function assumes full responsibility for managing the reference count of that object.Example:
PyList_SetItemPyList_SetItemis a classic example of a reference-stealing function. According to the documentation:However, a critical ambiguity in the documentation is that it does not clearly state whether the reference is stolen in the case of function failure. This is fundamentally an all-or-nothing problem: when you pass an object to a stealing function, you must understand whether ownership is unconditionally transferred or only transferred on success.
Looking at the CPython source code clarifies this behavior:
As demonstrated in the source code above,
PyList_SetItemunconditionally callsPy_XDECREF(newitem)when the type check fails—meaning it always steals the reference, even on failure. The function takes ownership ofnewitemregardless of whether it successfully inserts the item into the list.This behavior has serious implications for correct API usage. Consider the following incorrect code:
This code is defective because it leads to a use-after-free vulnerability. Since
PyList_SetItemalready stole the reference (and decremented it on failure viaPy_XDECREF), the additionalPy_DECREF(something)in the error-handling block causes a double decrement, potentially leading to an assertion failure in debug builds or memory corruption and crashes in release builds.The correct pattern is simply:
In summary, when dealing with stealing functions in the CPython API, you must relinquish all ownership responsibility for the passed reference and never decrement it after the call, regardless of the return value. Always consult the source code or thoroughly documented behavior to confirm whether a function truly provides an all-or-nothing stealing guarantee.