diff --git a/Include/pythonrun.h b/Include/pythonrun.h --- a/Include/pythonrun.h +++ b/Include/pythonrun.h @@ -221,6 +221,7 @@ PyAPI_FUNC(void) _PyGC_DumpShutdownStats PyAPI_FUNC(void) _PyGC_Fini(void); PyAPI_FUNC(void) PySlice_Fini(void); PyAPI_FUNC(void) _PyType_Fini(void); +PyAPI_FUNC(void) _PyRandom_Fini(void); PyAPI_DATA(PyThreadState *) _Py_Finalizing; #endif diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -361,9 +361,14 @@ class CmdLineTest(unittest.TestCase): code = """if 1: import os, sys for i, s in enumerate({streams}): + print(i, s) if getattr(sys, s) is not None: - os._exit(i + 1) - os._exit(42)""".format(streams=streams) + sys.exit(i + 1) + if os.name == 'posix': + # urandom() uses an early-allocated fd on POSIX, check + # that the fd wasn't butchered. + os.urandom(1) + sys.exit(42)""".format(streams=streams) def preexec(): if 'stdin' in streams: os.close(0) @@ -379,7 +384,7 @@ class CmdLineTest(unittest.TestCase): preexec_fn=preexec) out, err = p.communicate() self.assertEqual(test.support.strip_python_stderr(err), b'') - self.assertEqual(p.returncode, 42) + self.assertEqual(p.returncode, 42, out) def test_no_stdin(self): self._test_no_stdio(['stdin']) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1020,17 +1020,23 @@ class URandomTests(unittest.TestCase): @unittest.skipUnless(resource, "test requires the resource module") def test_urandom_failure(self): - soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) - resource.setrlimit(resource.RLIMIT_NOFILE, (1, hard_limit)) - try: - with self.assertRaises(OSError) as cm: + # Spawn a new process to avoid breaking urandom() for the rest + # of the process' lifetime. + code = """if 1: + import errno + import os + import resource + + soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) + os.closerange(3, soft_limit) + try: os.urandom(16) - self.assertEqual(cm.exception.errno, errno.EMFILE) - finally: - # We restore the old limit as soon as possible. If doing it - # using addCleanup(), code running in between would fail - # creating any file descriptor. - resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, hard_limit)) + except OSError as e: + assert e.errno == errno.EBADF, e.errno + else: + raise AssertionError("OSError not raised") + """ + assert_python_ok('-c', code) @contextlib.contextmanager diff --git a/Python/pythonrun.c b/Python/pythonrun.c --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -625,6 +625,7 @@ Py_Finalize(void) PyDict_Fini(); PySlice_Fini(); _PyGC_Fini(); + _PyRandom_Fini(); /* Cleanup Unicode implementation */ _PyUnicode_Fini(); diff --git a/Python/random.c b/Python/random.c --- a/Python/random.c +++ b/Python/random.c @@ -90,20 +90,20 @@ vms_urandom(unsigned char *buffer, Py_ss #if !defined(MS_WINDOWS) && !defined(__VMS) +static int urandom_fd = -1; +static int urandom_errno = 0; /* Read size bytes from /dev/urandom into buffer. Call Py_FatalError() on error. */ static void dev_urandom_noraise(char *buffer, Py_ssize_t size) { - int fd; + int fd = urandom_fd; Py_ssize_t n; - assert (0 < size); - - fd = open("/dev/urandom", O_RDONLY); if (fd < 0) Py_FatalError("Failed to open /dev/urandom"); + assert (0 < size); while (0 < size) { @@ -119,7 +119,6 @@ dev_urandom_noraise(char *buffer, Py_ssi buffer += n; size -= (Py_ssize_t)n; } - close(fd); } /* Read size bytes from /dev/urandom into buffer. @@ -127,17 +126,15 @@ dev_urandom_noraise(char *buffer, Py_ssi static int dev_urandom_python(char *buffer, Py_ssize_t size) { - int fd; + int fd = urandom_fd; Py_ssize_t n; if (size <= 0) return 0; - Py_BEGIN_ALLOW_THREADS - fd = open("/dev/urandom", O_RDONLY); - Py_END_ALLOW_THREADS - if (fd < 0) - { + if (fd < 0) { + assert(urandom_errno != 0); + errno = urandom_errno; if (errno == ENOENT || errno == ENXIO || errno == ENODEV || errno == EACCES) PyErr_SetString(PyExc_NotImplementedError, @@ -162,18 +159,76 @@ dev_urandom_python(char *buffer, Py_ssiz if (n <= 0) { /* stop on error or if read(size) returned 0 */ - if (n < 0) + if (n < 0) { PyErr_SetFromErrno(PyExc_OSError); + } else PyErr_Format(PyExc_RuntimeError, "Failed to read %zi bytes from /dev/urandom", size); - close(fd); return -1; } - close(fd); return 0; } + +static void +dev_urandom_init(void) +{ + assert(urandom_fd < 0); +#ifdef O_CLOEXEC + urandom_fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); +#else + urandom_fd = open("/dev/urandom", O_RDONLY); +#endif + if (urandom_fd < 0) + goto error; + else { + urandom_errno = 0; + if (urandom_fd <= 2) { + /* Ok, Python was launched with at least a missing standard + stream, and our random fd took its place. Unfortunately, + now other parts of the interpreter (as well as, possibly, + some user code) will mistake the random fd for a proper + standard stream. So we'll try to reallocate the fd to + something else. */ + int i = 0, dups[3], new_fd; + errno = 0; + while (1) { + new_fd = dup(urandom_fd); + if (new_fd < 0 || new_fd > 2) + break; + /* On the third dup() we *must* get either an error or + something greater than 2. */ + assert(i < 2); + dups[i++] = new_fd; + } + assert(i < 3); + close(urandom_fd); + while (--i >= 0) + close(dups[i]); + if (new_fd < 0) + goto error; + urandom_fd = new_fd; + } + } + return; +error: + /* If something failed, we don't report an error here, as it must still + be possible to launch Python if PYTHONHASHSEED is set to some fixed + value. Therefore, just record the error status for later. */ + urandom_fd = -1; + urandom_errno = errno; +} + +static void +dev_urandom_close(void) +{ + if (urandom_fd >= 0) { + close(urandom_fd); + urandom_fd = -1; + } +} + #endif /* !defined(MS_WINDOWS) && !defined(__VMS) */ /* Fill buffer with pseudo-random bytes generated by a linear congruent @@ -230,10 +285,17 @@ void void *secret = &_Py_HashSecret; Py_ssize_t secret_size = sizeof(_Py_HashSecret_t); + /* _PyRandom_Init() can be called twice (first by Python's main(), + second by Py_Initialize()). */ + if (_Py_HashSecret_Initialized) return; _Py_HashSecret_Initialized = 1; +#if !defined(MS_WINDOWS) && !defined(__VMS) + dev_urandom_init(); +#endif + /* Hash randomization is enabled. Generate a per-process secret, using PYTHONHASHSEED if provided. @@ -271,3 +333,11 @@ void #endif } } + +void +_PyRandom_Fini(void) +{ +#if !defined(MS_WINDOWS) && !defined(__VMS) + dev_urandom_close(); +#endif +}