From: A. Jesse Jiryu Davis Date: Mon, 16 Dec 2013 23:56:05 +0000 (-0500) Subject: Prevent a deadlock if ThreadedResolver resolves a unicode hostname at module-import... X-Git-Tag: v3.2.0b1~24^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F964%2Fhead;p=thirdparty%2Ftornado.git Prevent a deadlock if ThreadedResolver resolves a unicode hostname at module-import time. --- diff --git a/tornado/netutil.py b/tornado/netutil.py index 21db47557..8ebe604d2 100644 --- a/tornado/netutil.py +++ b/tornado/netutil.py @@ -27,7 +27,7 @@ import stat from tornado.concurrent import dummy_executor, run_on_executor from tornado.ioloop import IOLoop from tornado.platform.auto import set_close_exec -from tornado.util import Configurable +from tornado.util import u, Configurable if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ ssl_match_hostname = ssl.match_hostname @@ -37,6 +37,14 @@ else: ssl_match_hostname = backports.ssl_match_hostname.match_hostname SSLCertificateError = backports.ssl_match_hostname.CertificateError +# ThreadedResolver runs getaddrinfo on a thread. If the hostname is unicode, +# getaddrinfo attempts to import encodings.idna. If this is done at +# module-import time, the import lock is already held by the main thread, +# leading to deadlock. Avoid it by caching the idna encoder on the main +# thread now. +u('foo').encode('idna') + + def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): """Creates listening sockets bound to the given port and address. diff --git a/tornado/test/netutil_test.py b/tornado/test/netutil_test.py index c47e58fa3..2501eab50 100644 --- a/tornado/test/netutil_test.py +++ b/tornado/test/netutil_test.py @@ -1,6 +1,9 @@ from __future__ import absolute_import, division, print_function, with_statement import socket +from subprocess import Popen +import sys +import time from tornado.netutil import BlockingResolver, ThreadedResolver, is_valid_ip from tornado.testing import AsyncTestCase, gen_test @@ -57,6 +60,28 @@ class ThreadedResolverTest(AsyncTestCase, _ResolverTestMixin): super(ThreadedResolverTest, self).tearDown() +@unittest.skipIf(futures is None, "futures module not present") +class ThreadedResolverImportTest(unittest.TestCase): + def test_import(self): + # Test for a deadlock when importing a module that runs the + # ThreadedResolver at import-time. See resolve_test.py for + # full explanation. + command = [ + sys.executable, + '-c', + 'import tornado.test.resolve_test'] + + start = time.time() + popen = Popen(command) + while time.time() - start < 20: + return_code = popen.poll() + if return_code is not None: + self.assertEqual(0, return_code) + return # Success. + + self.fail("import timed out") + + @unittest.skipIf(pycares is None, "pycares module not present") class CaresResolverTest(AsyncTestCase, _ResolverTestMixin): def setUp(self): diff --git a/tornado/test/resolve_test.py b/tornado/test/resolve_test.py new file mode 100644 index 000000000..5df8061b7 --- /dev/null +++ b/tornado/test/resolve_test.py @@ -0,0 +1,11 @@ +from tornado.ioloop import IOLoop +from tornado.netutil import ThreadedResolver +from tornado.util import u + +# When this module is imported, it runs getaddrinfo on a thread. Since +# the hostname is unicode, getaddrinfo attempts to import encodings.idna +# but blocks on the import lock. Verify that ThreadedResolver avoids +# this deadlock. + +resolver = ThreadedResolver() +IOLoop.current().run_sync(lambda: resolver.resolve(u('tornadoweb.org'), 80))