Skip to content

Commit 3fc499b

Browse files
ammaraskargpshead
authored andcommitted
bpo-31178: Avoid concatenating bytes with str in subprocess error (python#3066)
Avoid concatenating bytes with str in the typically rare subprocess error path (exec failed). Includes a mock based unittest to exercise the codepath.
1 parent 6877111 commit 3fc499b

File tree

3 files changed

+54
-3
lines changed

3 files changed

+54
-3
lines changed

Lib/subprocess.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,15 +1313,18 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
13131313
try:
13141314
exception_name, hex_errno, err_msg = (
13151315
errpipe_data.split(b':', 2))
1316+
# The encoding here should match the encoding
1317+
# written in by the subprocess implementations
1318+
# like _posixsubprocess
1319+
err_msg = err_msg.decode()
13161320
except ValueError:
13171321
exception_name = b'SubprocessError'
13181322
hex_errno = b'0'
1319-
err_msg = (b'Bad exception data from child: ' +
1320-
repr(errpipe_data))
1323+
err_msg = 'Bad exception data from child: {!r}'.format(
1324+
bytes(errpipe_data))
13211325
child_exception_type = getattr(
13221326
builtins, exception_name.decode('ascii'),
13231327
SubprocessError)
1324-
err_msg = err_msg.decode(errors="surrogatepass")
13251328
if issubclass(child_exception_type, OSError) and hex_errno:
13261329
errno_num = int(hex_errno, 16)
13271330
child_exec_never_called = (err_msg == "noexec")

Lib/test/test_subprocess.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,53 @@ def test_exception_bad_args_0(self):
15421542
else:
15431543
self.fail("Expected OSError: %s" % desired_exception)
15441544

1545+
# We mock the __del__ method for Popen in the next two tests
1546+
# because it does cleanup based on the pid returned by fork_exec
1547+
# along with issuing a resource warning if it still exists. Since
1548+
# we don't actually spawn a process in these tests we can forego
1549+
# the destructor. An alternative would be to set _child_created to
1550+
# False before the destructor is called but there is no easy way
1551+
# to do that
1552+
class PopenNoDestructor(subprocess.Popen):
1553+
def __del__(self):
1554+
pass
1555+
1556+
@mock.patch("subprocess._posixsubprocess.fork_exec")
1557+
def test_exception_errpipe_normal(self, fork_exec):
1558+
"""Test error passing done through errpipe_write in the good case"""
1559+
def proper_error(*args):
1560+
errpipe_write = args[13]
1561+
# Write the hex for the error code EISDIR: 'is a directory'
1562+
err_code = '{:x}'.format(errno.EISDIR).encode()
1563+
os.write(errpipe_write, b"OSError:" + err_code + b":")
1564+
return 0
1565+
1566+
fork_exec.side_effect = proper_error
1567+
1568+
with self.assertRaises(IsADirectoryError):
1569+
self.PopenNoDestructor(["non_existent_command"])
1570+
1571+
@mock.patch("subprocess._posixsubprocess.fork_exec")
1572+
def test_exception_errpipe_bad_data(self, fork_exec):
1573+
"""Test error passing done through errpipe_write where its not
1574+
in the expected format"""
1575+
error_data = b"\xFF\x00\xDE\xAD"
1576+
def bad_error(*args):
1577+
errpipe_write = args[13]
1578+
# Anything can be in the pipe, no assumptions should
1579+
# be made about its encoding, so we'll write some
1580+
# arbitrary hex bytes to test it out
1581+
os.write(errpipe_write, error_data)
1582+
return 0
1583+
1584+
fork_exec.side_effect = bad_error
1585+
1586+
with self.assertRaises(subprocess.SubprocessError) as e:
1587+
self.PopenNoDestructor(["non_existent_command"])
1588+
1589+
self.assertIn(repr(error_data), str(e.exception))
1590+
1591+
15451592
def test_restore_signals(self):
15461593
# Code coverage for both values of restore_signals to make sure it
15471594
# at least does not blow up.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix string concatenation bug in rare error path in the subprocess module

0 commit comments

Comments
 (0)