Change Entry initialisation protocol
authorIustin Pop <iustin@k1024.org>
Fri, 29 Nov 2019 13:28:06 +0000 (14:28 +0100)
committerIustin Pop <iustin@k1024.org>
Fri, 29 Nov 2019 13:28:06 +0000 (14:28 +0100)
This fixes very large and significant bugs - segfaults and memory
leaks - that were present for uninitialised object, more precisely
created but not init'ed ones.

I spent quite a bit of time thinking back on forth how to fix this,
and from the two options of:

- check initialised status on all code paths, or
- don't ever allow invalid/un-initialised objects

The latter one seems the correct one, even though the Python C API
docs imply that doing actual stuff in `__new__` should be "rare".

Tests for reference leaks and wrong re-init added as well; these would
have caught at least memory leaks before.

NEWS
acl.c
tests/test_acls.py

diff --git a/NEWS b/NEWS
index 9e8f2e815203b49c7044a2cd97320c871dd292b7..389f7fd1af8cc849ecd8d991b4772b11af5e9798 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,15 @@ and new features, such as:
   when dealing with file names, removing custom code (with the
   associated benefits).
 
+Important API changes/bug fixes:
+
+- Initialisation protocol has been changed, to disallow uninitialised
+  objects; this means that `__new__` will always create valid objects,
+  to prevent the need for checking initialisation status in all code
+  paths; this also (implicitly) fixes memory leaks on re-initialisation
+  (calling `__init__(…)` on an existing object) and segfaults (!) on
+  non-initialised object attribute access.
+
 Additionally, test suite has changed to `pytest`.
 
 Version 0.5.4
diff --git a/acl.c b/acl.c
index 2556389d7c2e43eb4dc501c872d2047a9fb38442..16004b56a8b6ac38ddca15b59beba025fe50f944 100644 (file)
--- a/acl.c
+++ b/acl.c
@@ -709,14 +709,27 @@ static int get_tag_qualifier(acl_entry_t entry, tag_qual *tq) {
 static PyObject* Entry_new(PyTypeObject* type, PyObject* args,
                            PyObject *keywds) {
     PyObject* newentry;
+    Entry_Object* entry;
+    ACL_Object* parent = NULL;
+
+    if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent))
+        return NULL;
 
     newentry = PyType_GenericNew(type, args, keywds);
 
-    if(newentry != NULL) {
-        ((Entry_Object*)newentry)->entry = NULL;
-        ((Entry_Object*)newentry)->parent_acl = NULL;
+    if(newentry == NULL) {
+        return NULL;
     }
 
+    entry = (Entry_Object*)newentry;
+
+    if(acl_create_entry(&parent->acl, &entry->entry) == -1) {
+        PyErr_SetFromErrno(PyExc_IOError);
+        Py_DECREF(newentry);
+        return NULL;
+    }
+    Py_INCREF(parent);
+    entry->parent_acl = (PyObject*)parent;
     return newentry;
 }
 
@@ -728,14 +741,11 @@ static int Entry_init(PyObject* obj, PyObject* args, PyObject *keywds) {
     if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent))
         return -1;
 
-    if(acl_create_entry(&parent->acl, &self->entry) == -1) {
-        PyErr_SetFromErrno(PyExc_IOError);
+    if ((PyObject*)parent != self->parent_acl) {
+        PyErr_SetString(PyExc_ValueError,
+                        "Can't reinitialize with a different parent");
         return -1;
     }
-
-    self->parent_acl = (PyObject*)parent;
-    Py_INCREF(parent);
-
     return 0;
 }
 
@@ -833,10 +843,6 @@ static PyObject* Entry_get_tag_type(PyObject *obj, void* arg) {
     Entry_Object *self = (Entry_Object*) obj;
     acl_tag_t value;
 
-    if (self->entry == NULL) {
-        PyErr_SetString(PyExc_AttributeError, "entry attribute");
-        return NULL;
-    }
     if(acl_get_tag_type(self->entry, &value) == -1) {
         PyErr_SetFromErrno(PyExc_IOError);
         return NULL;
index e170addb3552e534dd1951a8c146cbb8e91d31fb..b827b8db3df7ba6d676f11251dabe84b0710a3ba 100644 (file)
@@ -520,6 +520,22 @@ class TestModification:
         with pytest.raises(TypeError):
           posix1e.Entry(object())
 
+    def test_entry_reinitialisations(self):
+        a = posix1e.ACL()
+        b = posix1e.ACL()
+        e = posix1e.Entry(a)
+        e.__init__(a)
+        with pytest.raises(ValueError, match="different parent"):
+            e.__init__(b)
+
+    @NOT_PYPY
+    def test_entry_reinit_leaks_refcount(self):
+        acl = posix1e.ACL()
+        e = acl.append()
+        ref = sys.getrefcount(acl)
+        e.__init__(acl)
+        assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..."
+
     def test_delete(self):
         """Test delete Entry from the ACL"""
         acl = posix1e.ACL()