Commit 0acad8eb authored by Martin Panter's avatar Martin Panter

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

Replace os.popen() with subprocess.Popen. Based on patch by Victor Stinner.

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.
parent a94e5d49
import unittest import unittest
import os import os, os.path
import sys import sys
import test.support import test.support
from ctypes import * from ctypes import *
...@@ -64,6 +64,11 @@ class Test_OpenGL_libs(unittest.TestCase): ...@@ -64,6 +64,11 @@ class Test_OpenGL_libs(unittest.TestCase):
self.skipTest('lib_gle not available') self.skipTest('lib_gle not available')
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 contextlib import shutil
import subprocess 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":
...@@ -94,28 +95,43 @@ elif os.name == "posix": ...@@ -94,28 +95,43 @@ elif os.name == "posix":
import re, tempfile import re, tempfile
def _findLib_gcc(name): def _findLib_gcc(name):
expr = r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name) # Run GCC's linker with the -t (aka --trace) option and examine the
fdout, ccout = tempfile.mkstemp() # library name it prints out. The GCC command will fail because we
os.close(fdout) # haven't supplied a proper program with main(), but that does not
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;' \ # matter.
'LANG=C LC_ALL=C $CC -Wl,-t -o ' + ccout + ' 2>&1 -l' + name expr = os.fsencode(r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name))
try:
f = os.popen(cmd) c_compiler = shutil.which('gcc')
if not c_compiler:
c_compiler = shutil.which('cc')
if not c_compiler:
# No C compiler available, give up
return None
temp = tempfile.NamedTemporaryFile()
try: try:
trace = f.read() args = [c_compiler, '-Wl,-t', '-o', temp.name, '-l' + name]
finally:
rv = f.close() env = dict(os.environ)
env['LC_ALL'] = 'C'
env['LANG'] = 'C'
proc = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env)
with proc:
trace = proc.stdout.read()
finally: finally:
try: try:
os.unlink(ccout) temp.close()
except FileNotFoundError: except FileNotFoundError:
# Raised if the file was already removed, which is the normal
# behaviour of GCC if linking fails
pass pass
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
return res.group(0) return os.fsdecode(res.group(0))
if sys.platform == "sunos5": if sys.platform == "sunos5":
...@@ -123,55 +139,65 @@ elif os.name == "posix": ...@@ -123,55 +139,65 @@ 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
with contextlib.closing(os.popen(cmd)) as f: proc = subprocess.Popen(("/usr/ccs/bin/dump", "-Lpv", f),
data = f.read() stdout=subprocess.PIPE,
res = re.search(r'\[.*\]\sSONAME\s+([^\s]+)', data) stderr=subprocess.DEVNULL)
with proc:
data = proc.stdout.read()
res = re.search(br'\[.*\]\sSONAME\s+([^\s]+)', data)
if not res: if not res:
return None return None
return res.group(1) return os.fsdecode(res.group(1))
else: else:
def _get_soname(f): def _get_soname(f):
# 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;' \ objdump = shutil.which('objdump')
"objdump -p -j .dynamic 2>/dev/null " + f if not objdump:
f = os.popen(cmd) # objdump is not available, give up
try: return None
dump = f.read()
finally: proc = subprocess.Popen((objdump, '-p', '-j', '.dynamic', f),
rv = f.close() stdout=subprocess.PIPE,
if rv == 10: stderr=subprocess.DEVNULL)
raise OSError('objdump command not found') with proc:
res = re.search(r'\sSONAME\s+([^\s]+)', dump) dump = proc.stdout.read()
res = re.search(br'\sSONAME\s+([^\s]+)', dump)
if not res: if not res:
return None return None
return res.group(1) return os.fsdecode(res.group(1))
if sys.platform.startswith(("freebsd", "openbsd", "dragonfly")): if sys.platform.startswith(("freebsd", "openbsd", "dragonfly")):
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.maxsize ] return nums or [sys.maxsize]
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)
with contextlib.closing(os.popen('/sbin/ldconfig -r 2>/dev/null')) as f: expr = os.fsencode(expr)
data = f.read()
proc = subprocess.Popen(('/sbin/ldconfig', '-r'),
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
with proc:
data = proc.stdout.read()
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))
res.sort(key=_num_version) res.sort(key=_num_version)
return res[-1] return os.fsdecode(res[-1])
elif sys.platform == "sunos5": elif sys.platform == "sunos5":
...@@ -179,17 +205,24 @@ elif os.name == "posix": ...@@ -179,17 +205,24 @@ 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
with contextlib.closing(os.popen(cmd)) as f: proc = subprocess.Popen(args,
for line in f.readlines(): stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=env)
with proc:
for line in proc.stdout:
line = line.strip() line = line.strip()
if line.startswith('Default Library Path (ELF):'): if line.startswith(b'Default Library Path (ELF):'):
paths = line.split()[4] paths = os.fsdecode(line).split()[4]
if not paths: if not paths:
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 #16182: Fix various functions in the "readline" module to use the - Issue #16182: Fix various functions in the "readline" module to use the
locale encoding, and fix get_begidx() and get_endidx() to return code point locale encoding, and fix get_begidx() and get_endidx() to return code point
indexes. indexes.
......
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