Repository: pyca/cryptography

Issue:
CRL X509_REVOKED Memory Issue
In OpenSSl, `X509_CRL *` have a stack of `X509_REVOKED *`. In our parsing code we iterate over that using the `sk_X509_CRL_*` functions. However, `X509_REVOKED *` does not have a refcount, so it is freed when `X509_CRL_free` is called (which we register as a `gc` callback so it is called when the crl reference drops out of scope). Unfortunately, this is not safe. A user may choose to iterate the crl and save a `RevokedCertificate` instance, but this will point to garbage memory if the crl drops out of scope.

Ideally we could resolve this by incrementing the refcount, but since `X509_REVOKED *` doesn't have a refcount we're going to need to either retain the entire CRL struct for the lifetime of any sub-component (cue handwaving about how to do that) or else dupe every X509_REVOKED and pay the performance/memory penalty associated with that.



Discussion:
It seems like all we need to do is add a private attribute on the `_RevokedCertificate` which points the the `_CRL`?

Good handwaving, that seems like it'd work perfectly. PR comin'.


Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py
--- a/src/cryptography/hazmat/backends/openssl/x509.py
+++ b/src/cryptography/hazmat/backends/openssl/x509.py
@@ -747,8 +747,16 @@ def _decode_cert_issuer(backend, ext):
 
 @utils.register_interface(x509.RevokedCertificate)
 class _RevokedCertificate(object):
-    def __init__(self, backend, x509_revoked):
+    def __init__(self, backend, crl, x509_revoked):
         self._backend = backend
+        # The X509_REVOKED_value is a X509_REVOKED * that has
+        # no reference counting. This means when X509_CRL_free is
+        # called then the CRL and all X509_REVOKED * are freed. Since
+        # you can retain a reference to a single revoked certificate
+        # and let the CRL fall out of scope we need to retain a
+        # private reference to the CRL inside the RevokedCertificate
+        # object to prevent the gc from being called inappropriately.
+        self._crl = crl
         self._x509_revoked = x509_revoked
 
     @property
@@ -861,7 +869,10 @@ def _revoked_certificates(self):
             for i in range(num):
                 r = self._backend._lib.sk_X509_REVOKED_value(revoked, i)
                 self._backend.openssl_assert(r != self._backend._ffi.NULL)
-                revoked_list.append(_RevokedCertificate(self._backend, r))
+                revoked_certificate = _RevokedCertificate(
+                    self._backend, self, r
+                )
+                revoked_list.append(revoked_certificate)
 
         return revoked_list
 


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -173,6 +173,20 @@ def test_revoked_cert_retrieval(self, backend):
         # Check that len() works for CRLs.
         assert len(crl) == 12
 
+    def test_revoked_cert_retrieval_retain_only_revoked(self, backend):
+        """
+        This test attempts to trigger the crash condition described in
+        https://github.com/pyca/cryptography/issues/2557
+        PyPy does gc at its own pace, so it will only be reliable on CPython.
+        """
+        revoked = _load_cert(
+            os.path.join("x509", "custom", "crl_all_reasons.pem"),
+            x509.load_pem_x509_crl,
+            backend
+        )[11]
+        assert revoked.revocation_date == datetime.datetime(2015, 1, 1, 0, 0)
+        assert revoked.serial_number == 11
+
     def test_extensions(self, backend):
         crl = _load_cert(
             os.path.join("x509", "custom", "crl_ian_aia_aki.pem"),