diff --git a/docs/api.rst b/docs/api.rst index a79cd4b9..1364440d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -575,10 +575,19 @@ Router Class object destructors called, including TLS usage by native extension code, triggering many new variants of all the issues above. + * Pseudo-Random Number Generator state that is easily observable by + network peers to be duplicate, violating requirements of + cryptographic protocols through one-time state reuse. In the worst + case, children continually reuse the same state due to repeatedly + forking from a static parent. + :py:meth:`fork` cleans up Mitogen-internal objects, in addition to - locks held by the :py:mod:`logging` package. You must arrange for your - program's state, including any third party packages in use, to be - cleaned up by specifying an `on_fork` function. + locks held by the :py:mod:`logging` package, reseeds + :py:func:`random.random`, and the OpenSSL PRNG via + :py:func:`ssl.RAND_add`, but only if the :py:mod:`ssl` module is + already loaded. You must arrange for your program's state, including + any third party packages in use, to be cleaned up by specifying an + `on_fork` function. The associated stream implementation is :py:class:`mitogen.fork.Stream`. diff --git a/mitogen/fork.py b/mitogen/fork.py index 7fc9b543..7a6c87c1 100644 --- a/mitogen/fork.py +++ b/mitogen/fork.py @@ -28,6 +28,8 @@ import logging import os +import random +import sys import threading import mitogen.core @@ -37,6 +39,17 @@ import mitogen.parent LOG = logging.getLogger('mitogen') +def fixup_prngs(): + """ + Add 256 bits of /dev/urandom to OpenSSL's PRNG in the child, and re-seed + the random package with the same data. + """ + s = os.urandom(256 / 8) + random.seed(s) + if 'ssl' in sys.modules: + sys.modules['ssl'].RAND_add(s, 75.0) + + def break_logging_locks(): """ After fork, ensure any logging.Handler locks are recreated, as a variety of @@ -87,6 +100,7 @@ class Stream(mitogen.parent.Stream): mitogen.core.Latch._on_fork() mitogen.core.Side._on_fork() break_logging_locks() + fixup_prngs() if self.on_fork: self.on_fork() mitogen.core.set_block(childfp.fileno()) diff --git a/tests/fork_test.py b/tests/fork_test.py index ced39e2c..bb15d791 100644 --- a/tests/fork_test.py +++ b/tests/fork_test.py @@ -1,5 +1,10 @@ +import ctypes import os +import random +import ssl +import struct +import sys import mitogen import unittest2 @@ -8,12 +13,44 @@ import testlib import plain_old_module +IS_64BIT = struct.calcsize('P') == 8 +PLATFORM_TO_PATH = { + ('darwin', False): '/usr/lib/libssl.dylib', + ('darwin', True): '/usr/lib/libssl.dylib', + ('linux2', False): '/usr/lib/libssl.so', + ('linux2', True): '/usr/lib/x86_64-linux-gnu/libssl.so', +} + +c_ssl = ctypes.CDLL(PLATFORM_TO_PATH[sys.platform, IS_64BIT]) +c_ssl.RAND_pseudo_bytes.argtypes = [ctypes.c_char_p, ctypes.c_int] +c_ssl.RAND_pseudo_bytes.restype = ctypes.c_int + + +def random_random(): + return random.random() + + +def RAND_pseudo_bytes(n=32): + buf = ctypes.create_string_buffer(n) + assert 1 == c_ssl.RAND_pseudo_bytes(buf, n) + return buf[:] + + class ForkTest(testlib.RouterMixin, unittest2.TestCase): def test_okay(self): context = self.router.fork() self.assertNotEqual(context.call(os.getpid), os.getpid()) self.assertEqual(context.call(os.getppid), os.getpid()) + def test_random_module_diverges(self): + context = self.router.fork() + self.assertNotEqual(context.call(random_random), random_random()) + + def test_ssl_module_diverges(self): + context = self.router.fork() + self.assertNotEqual(context.call(RAND_pseudo_bytes), + RAND_pseudo_bytes()) + if __name__ == '__main__': unittest2.main()