Commit eb063011 authored by Victor Stinner's avatar Victor Stinner

Issue #22636: Avoid using a shell in the ctypes.util module

Replace os.popen() with subprocess.Popen.

If the "gcc", "cc" or "objdump" command is not available, the code was
supposed to raise an OSError exception. But there was a bug in the code. The
shell code returns the exit code 10 if the required command is missing, and the
code tries to check for the status 10. The problem is that os.popen() doesn't
return the exit code directly, but a status which should be processed by
os.WIFEXITED() and os.WEXITSTATUS(). In practice, the exception was never
raised. The OSError exception was not documented and ctypes.util.find_library()
is expected to return None if the library is not found.

Based on patch by Victor Stinner.
parent f46d3afc
import unittest import unittest
import os import os, os.path
import sys import sys
from test import test_support
from ctypes import * from ctypes import *
from ctypes.util import find_library from ctypes.util import find_library
from ctypes.test import is_resource_enabled from ctypes.test import is_resource_enabled
...@@ -65,6 +66,11 @@ class Test_OpenGL_libs(unittest.TestCase): ...@@ -65,6 +66,11 @@ class Test_OpenGL_libs(unittest.TestCase):
if self.gle: if self.gle:
self.gle.gleGetJoinStyle self.gle.gleGetJoinStyle
def test_shell_injection(self):
result = find_library('; echo Hello shell > ' + test_support.TESTFN)
self.assertFalse(os.path.lexists(test_support.TESTFN))
self.assertIsNone(result)
# On platforms where the default shared library suffix is '.so', # On platforms where the default shared library suffix is '.so',
# at least some libraries can be loaded as attributes of the cdll # at least some libraries can be loaded as attributes of the cdll
# object, since ctypes now tries loading the lib again # object, since ctypes now tries loading the lib again
......
import sys, os import os
import subprocess
import sys
# find_library(name) returns the pathname of a library, or None. # find_library(name) returns the pathname of a library, or None.
if os.name == "nt": if os.name == "nt":
...@@ -86,25 +88,28 @@ elif os.name == "posix": ...@@ -86,25 +88,28 @@ elif os.name == "posix":
import re, tempfile, errno import re, tempfile, errno
def _findLib_gcc(name): def _findLib_gcc(name):
# Run GCC's linker with the -t (aka --trace) option and examine the
# library name it prints out. The GCC command will fail because we
# haven't supplied a proper program with main(), but that does not
# matter.
expr = r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name) expr = r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name)
fdout, ccout = tempfile.mkstemp() cmd = 'if type gcc >/dev/null 2>&1; then CC=gcc; elif type cc >/dev/null 2>&1; then CC=cc;else exit; fi;' \
os.close(fdout) 'LANG=C LC_ALL=C $CC -Wl,-t -o "$2" 2>&1 -l"$1"'
cmd = 'if type gcc >/dev/null 2>&1; then CC=gcc; elif type cc >/dev/null 2>&1; then CC=cc;else exit 10; fi;' \
'LANG=C LC_ALL=C $CC -Wl,-t -o ' + ccout + ' 2>&1 -l' + name temp = tempfile.NamedTemporaryFile()
try: try:
f = os.popen(cmd) proc = subprocess.Popen((cmd, '_findLib_gcc', name, temp.name),
try: shell=True,
trace = f.read() stdout=subprocess.PIPE)
finally: [trace, _] = proc.communicate()
rv = f.close()
finally: finally:
try: try:
os.unlink(ccout) temp.close()
except OSError, e: except OSError, e:
# ENOENT is raised if the file was already removed, which is
# the normal behaviour of GCC if linking fails
if e.errno != errno.ENOENT: if e.errno != errno.ENOENT:
raise raise
if rv == 10:
raise OSError, 'gcc or cc command not found'
res = re.search(expr, trace) res = re.search(expr, trace)
if not res: if not res:
return None return None
...@@ -116,13 +121,17 @@ elif os.name == "posix": ...@@ -116,13 +121,17 @@ elif os.name == "posix":
def _get_soname(f): def _get_soname(f):
if not f: if not f:
return None return None
cmd = "/usr/ccs/bin/dump -Lpv 2>/dev/null " + f
f = os.popen(cmd) null = open(os.devnull, "wb")
try: try:
data = f.read() with null:
finally: proc = subprocess.Popen(("/usr/ccs/bin/dump", "-Lpv", f),
f.close() stdout=subprocess.PIPE,
res = re.search(r'\[.*\]\sSONAME\s+([^\s]+)', data) stderr=null)
except OSError: # E.g. command not found
return None
[data, _] = proc.communicate()
res = re.search(br'\[.*\]\sSONAME\s+([^\s]+)', data)
if not res: if not res:
return None return None
return res.group(1) return res.group(1)
...@@ -131,16 +140,12 @@ elif os.name == "posix": ...@@ -131,16 +140,12 @@ elif os.name == "posix":
# assuming GNU binutils / ELF # assuming GNU binutils / ELF
if not f: if not f:
return None return None
cmd = 'if ! type objdump >/dev/null 2>&1; then exit 10; fi;' \ cmd = 'if ! type objdump >/dev/null 2>&1; then exit; fi;' \
"objdump -p -j .dynamic 2>/dev/null " + f 'objdump -p -j .dynamic 2>/dev/null "$1"'
f = os.popen(cmd) proc = subprocess.Popen((cmd, '_get_soname', f), shell=True,
try: stdout=subprocess.PIPE)
dump = f.read() [dump, _] = proc.communicate()
finally: res = re.search(br'\sSONAME\s+([^\s]+)', dump)
rv = f.close()
if rv == 10:
raise OSError, 'objdump command not found'
res = re.search(r'\sSONAME\s+([^\s]+)', dump)
if not res: if not res:
return None return None
return res.group(1) return res.group(1)
...@@ -151,23 +156,30 @@ elif os.name == "posix": ...@@ -151,23 +156,30 @@ elif os.name == "posix":
def _num_version(libname): def _num_version(libname):
# "libxyz.so.MAJOR.MINOR" => [ MAJOR, MINOR ] # "libxyz.so.MAJOR.MINOR" => [ MAJOR, MINOR ]
parts = libname.split(".") parts = libname.split(b".")
nums = [] nums = []
try: try:
while parts: while parts:
nums.insert(0, int(parts.pop())) nums.insert(0, int(parts.pop()))
except ValueError: except ValueError:
pass pass
return nums or [ sys.maxint ] return nums or [sys.maxint]
def find_library(name): def find_library(name):
ename = re.escape(name) ename = re.escape(name)
expr = r':-l%s\.\S+ => \S*/(lib%s\.\S+)' % (ename, ename) expr = r':-l%s\.\S+ => \S*/(lib%s\.\S+)' % (ename, ename)
f = os.popen('/sbin/ldconfig -r 2>/dev/null')
null = open(os.devnull, 'wb')
try: try:
data = f.read() with null:
finally: proc = subprocess.Popen(('/sbin/ldconfig', '-r'),
f.close() stdout=subprocess.PIPE,
stderr=null)
except OSError: # E.g. command not found
data = b''
else:
[data, _] = proc.communicate()
res = re.findall(expr, data) res = re.findall(expr, data)
if not res: if not res:
return _get_soname(_findLib_gcc(name)) return _get_soname(_findLib_gcc(name))
...@@ -180,16 +192,32 @@ elif os.name == "posix": ...@@ -180,16 +192,32 @@ elif os.name == "posix":
if not os.path.exists('/usr/bin/crle'): if not os.path.exists('/usr/bin/crle'):
return None return None
env = dict(os.environ)
env['LC_ALL'] = 'C'
if is64: if is64:
cmd = 'env LC_ALL=C /usr/bin/crle -64 2>/dev/null' args = ('/usr/bin/crle', '-64')
else: else:
cmd = 'env LC_ALL=C /usr/bin/crle 2>/dev/null' args = ('/usr/bin/crle',)
paths = None paths = None
for line in os.popen(cmd).readlines(): null = open(os.devnull, 'wb')
line = line.strip() try:
if line.startswith('Default Library Path (ELF):'): with null:
paths = line.split()[4] proc = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=null,
env=env)
except OSError: # E.g. bad executable
return None
try:
for line in proc.stdout:
line = line.strip()
if line.startswith(b'Default Library Path (ELF):'):
paths = line.split()[4]
finally:
proc.stdout.close()
proc.wait()
if not paths: if not paths:
return None return None
...@@ -223,11 +251,20 @@ elif os.name == "posix": ...@@ -223,11 +251,20 @@ elif os.name == "posix":
# XXX assuming GLIBC's ldconfig (with option -p) # XXX assuming GLIBC's ldconfig (with option -p)
expr = r'\s+(lib%s\.[^\s]+)\s+\(%s' % (re.escape(name), abi_type) expr = r'\s+(lib%s\.[^\s]+)\s+\(%s' % (re.escape(name), abi_type)
f = os.popen('LC_ALL=C LANG=C /sbin/ldconfig -p 2>/dev/null')
env = dict(os.environ)
env['LC_ALL'] = 'C'
env['LANG'] = 'C'
null = open(os.devnull, 'wb')
try: try:
data = f.read() with null:
finally: p = subprocess.Popen(['/sbin/ldconfig', '-p'],
f.close() stderr=null,
stdout=subprocess.PIPE,
env=env)
except OSError: # E.g. command not found
return None
[data, _] = p.communicate()
res = re.search(expr, data) res = re.search(expr, data)
if not res: if not res:
return None return None
......
...@@ -13,6 +13,9 @@ Core and Builtins ...@@ -13,6 +13,9 @@ Core and Builtins
Library Library
------- -------
- Issue #22636: Avoid shell injection problems with
ctypes.util.find_library().
- Issue #27330: Fixed possible leaks in the ctypes module. - Issue #27330: Fixed possible leaks in the ctypes module.
- Issue #27238: Got rid of bare excepts in the turtle module. Original patch - Issue #27238: Got rid of bare excepts in the turtle module. Original patch
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment