Skip to content

Commit

Permalink
Fix crash during deallocation of self-referencing lists
Browse files Browse the repository at this point in the history
When a list or any of its nodes references itself in a way that creates a cycle,
their PyObjects may be visited multiple times during deallocation of the list.
To handle this case cleanly, Python documentation recommends to release
references held by such PyObject with Py_CLEAR.
The macro works similarly to Py_XDECREF, but also sets the reference to NULL,
which prevents repeated deallocation of the reference during subsequent visits
of its owner PyObject.

Most of the references held by llist PyObjects are released by Py_CLEAR.
One of the exceptions was Py_None, which is a global variable that may not be
set to NULL.
To work around this limitation, llist module simply invoked Py_DECREF on Py_None
during each visit of a garbage collected PyObject. Unfortunately this could
release Py_None too many times, effectively 'stealing' references from other
objects and eventually leading to a crash.

To fix this issue, each linked list and node which holds a Py_None reference
will now set a flag in its PyObject structure. The first release of the Py_None
reference will clear the flag to prevent future traversal of this object from
repeating the operation.

Closes issue #11.
  • Loading branch information
ajakubek committed Aug 26, 2019
1 parent 01603ae commit bde1ee8
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- fixed repr() and str() for self-referencing lists and nodes
(closes issue #10)
- fixed duplicated release of Py_None during destruction of
self-referencing lists and nodes (closes issue #11)

-----------------------------------------------------------------------

Expand Down
19 changes: 17 additions & 2 deletions src/dllist.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

#include <Python.h>
#include <structmember.h>

#include "config.h"
#include "flags.h"
#include "py23macros.h"
#include "utils.h"

Expand All @@ -28,6 +30,7 @@ typedef struct
PyObject* prev;
PyObject* next;
PyObject* list_weakref;
unsigned char flags;
} DLListNodeObject;

/* Convenience function for linking list nodes.
Expand Down Expand Up @@ -187,7 +190,12 @@ static int dllistnode_clear_refs(DLListNodeObject* self)
{
Py_CLEAR(self->value);
Py_CLEAR(self->list_weakref);
Py_DECREF(Py_None);

if ((self->flags & LLIST_HAS_PY_NONE_REF) != 0)
{
self->flags &= ~LLIST_HAS_PY_NONE_REF;
Py_DECREF(Py_None);
}

return 0;
}
Expand Down Expand Up @@ -219,6 +227,7 @@ static PyObject* dllistnode_new(PyTypeObject* type,
self->prev = Py_None;
self->next = Py_None;
self->list_weakref = Py_None;
self->flags = LLIST_HAS_PY_NONE_REF;

Py_INCREF(self->value);
Py_INCREF(self->list_weakref);
Expand Down Expand Up @@ -333,6 +342,7 @@ typedef struct
Py_ssize_t last_accessed_idx;
Py_ssize_t size;
PyObject* weakref_list;
unsigned char flags;
} DLListObject;

static Py_ssize_t py_ssize_t_abs(Py_ssize_t x)
Expand Down Expand Up @@ -578,7 +588,11 @@ static int dllist_clear_refs(DLListObject* self)
}
}

Py_DECREF(Py_None);
if ((self->flags & LLIST_HAS_PY_NONE_REF) != 0)
{
self->flags &= ~LLIST_HAS_PY_NONE_REF;
Py_DECREF(Py_None);
}

return 0;
}
Expand Down Expand Up @@ -612,6 +626,7 @@ static PyObject* dllist_new(PyTypeObject* type,
self->last_accessed_idx = -1;
self->size = 0;
self->weakref_list = NULL;
self->flags = LLIST_HAS_PY_NONE_REF;

return (PyObject*)self;
}
Expand Down
10 changes: 10 additions & 0 deletions src/flags.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* Copyright (c) 2019 Adam Jakubek, Rafał Gałczyński
* Released under the MIT license (see attached LICENSE file).
*/

#ifndef FLAGS_H
#define FLAGS_H

#define LLIST_HAS_PY_NONE_REF (0x01)

#endif /* FLAGS_H */
19 changes: 17 additions & 2 deletions src/sllist.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

#include <Python.h>
#include <structmember.h>

#include "config.h"
#include "flags.h"
#include "py23macros.h"
#include "utils.h"

Expand All @@ -27,6 +29,7 @@ typedef struct
PyObject* value;
PyObject* next;
PyObject* list_weakref;
unsigned char flags;
} SLListNodeObject;


Expand Down Expand Up @@ -115,7 +118,12 @@ static int sllistnode_clear_refs(SLListNodeObject* self)
{
Py_CLEAR(self->value);
Py_CLEAR(self->list_weakref);
Py_DECREF(Py_None);

if ((self->flags & LLIST_HAS_PY_NONE_REF) != 0)
{
self->flags &= ~LLIST_HAS_PY_NONE_REF;
Py_DECREF(Py_None);
}

return 0;
}
Expand Down Expand Up @@ -168,6 +176,7 @@ static PyObject* sllistnode_new(PyTypeObject* type,
self->next = Py_None;
self->value = Py_None;
self->list_weakref = Py_None;
self->flags = LLIST_HAS_PY_NONE_REF;

Py_INCREF(self->value);
Py_INCREF(self->list_weakref);
Expand Down Expand Up @@ -309,6 +318,7 @@ typedef struct
PyObject* last;
Py_ssize_t size;
PyObject* weakref_list;
unsigned char flags;
} SLListObject;


Expand Down Expand Up @@ -351,7 +361,11 @@ static int sllist_clear_refs(SLListObject* self)
}
}

Py_DECREF(Py_None);
if ((self->flags & LLIST_HAS_PY_NONE_REF) != 0)
{
self->flags &= ~LLIST_HAS_PY_NONE_REF;
Py_DECREF(Py_None);
}

return 0;
}
Expand Down Expand Up @@ -383,6 +397,7 @@ static PyObject* sllist_new(PyTypeObject* type,
self->last = Py_None;
self->weakref_list = NULL;
self->size = 0;
self->flags = LLIST_HAS_PY_NONE_REF;

return (PyObject*)self;
}
Expand Down
39 changes: 39 additions & 0 deletions tests/llist_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,26 @@ def test_list_node_can_be_subclassed(self):
class DerivedNode(sllistnode):
pass

def test_cyclic_list_destruction_does_not_release_extra_None_refs(self):
original_ref_count = sys.getrefcount(None)

for _ in range(original_ref_count * 10):
ll = sllist()
ll.append(sllistnode(ll))
del ll

self.assertGreater(sys.getrefcount(None), 0)

def test_cyclic_node_destruction_does_not_release_extra_None_refs(self):
original_ref_count = sys.getrefcount(None)

for _ in range(original_ref_count * 10):
ll = self.make_recursive_node_list()
del ll

self.assertGreater(sys.getrefcount(None), 0)


class testdllist(unittest.TestCase):

def test_init_empty(self):
Expand Down Expand Up @@ -1911,6 +1931,25 @@ def test_list_node_can_be_subclassed(self):
class DerivedNode(dllistnode):
pass

def test_cyclic_list_destruction_does_not_release_extra_None_refs(self):
original_ref_count = sys.getrefcount(None)

for _ in range(original_ref_count * 10):
ll = dllist()
ll.append(dllistnode(ll))
del ll

self.assertGreater(sys.getrefcount(None), 0)

def test_cyclic_node_destruction_does_not_release_extra_None_refs(self):
original_ref_count = sys.getrefcount(None)

for _ in range(original_ref_count * 10):
ll = self.make_recursive_node_list()
del ll

self.assertGreater(sys.getrefcount(None), 0)


def suite():
suite = unittest.TestSuite()
Expand Down

0 comments on commit bde1ee8

Please sign in to comment.