Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dd55a5f
bpo-36085: Enable better DLL resolution on Windows
zooba Mar 13, 2019
b01f83b
Improves error handling
zooba Mar 13, 2019
51ade83
Improve ctypes error message and enables adjacent dependency loading
zooba Mar 13, 2019
e8b54a4
Remove invalid ctypes flag
zooba Mar 14, 2019
98db2ef
Merge remote-tracking branch 'cpython/master' into bpo-36085
zooba Mar 14, 2019
49b757b
Merge remote-tracking branch 'cpython/master' into bpo-36085
zooba Mar 18, 2019
ae48b59
Fix uninitialised variable
zooba Mar 18, 2019
3f29c0a
Add NEWS and make nicer interface for add_dll_directory
zooba Mar 18, 2019
e022bb8
Add after_test step to collect more info
zooba Mar 21, 2019
56fa1bd
Change to on_finish
zooba Mar 21, 2019
c50b0ea
Attempt a workaround for the test
zooba Mar 21, 2019
4fd36e4
Try a broader workaround
zooba Mar 21, 2019
88ef129
Handle errors in workaround
zooba Mar 21, 2019
34614af
Test retrieving specific counter instead of all counters
zooba Mar 22, 2019
8311618
Remove default DLL directory setting
zooba Mar 22, 2019
8823f31
Add dll_load_flags to ctypes.CDLL for use on Windows
zooba Mar 26, 2019
5882477
Remove unused import
zooba Mar 26, 2019
f867be2
Remove logging from AppVeyor definition
zooba Mar 26, 2019
f82fce4
Update what's new and ctypes docs
zooba Mar 27, 2019
492ab38
Adds ctypes test for new parameter and add_dll_directory
zooba Mar 27, 2019
01b9c76
Improve ctypes test
zooba Mar 27, 2019
92e1983
Add bpo reference to What's New
zooba Mar 27, 2019
e654a09
Ensure test cleans up added DLL directory
zooba Mar 27, 2019
4a1c712
Improve documentation
zooba Mar 28, 2019
1af7230
Improve error handling
zooba Mar 28, 2019
864c34e
Merge remote-tracking branch 'cpython/master' into bpo-36085
zooba Mar 28, 2019
6ae0b6f
Fix calling convention for 32-bit builds
zooba Mar 28, 2019
7cf3055
Adds import test
zooba Mar 29, 2019
6b1d038
Ensure CRT DLLs are copied for test
zooba Mar 29, 2019
a9f7ae0
Remove trailing whitespace
zooba Mar 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1322,14 +1322,14 @@ There are several ways to load shared libraries into the Python process. One
way is to instantiate one of the following classes:


.. class:: CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
.. class:: CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=0)

Instances of this class represent loaded shared libraries. Functions in these
libraries use the standard C calling convention, and are assumed to return
:c:type:`int`.


.. class:: OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
.. class:: OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=0)

Windows only: Instances of this class represent loaded shared libraries,
functions in these libraries use the ``stdcall`` calling convention, and are
Expand All @@ -1342,7 +1342,7 @@ way is to instantiate one of the following classes:
:exc:`WindowsError` used to be raised.


.. class:: WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
.. class:: WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=0)

Windows only: Instances of this class represent loaded shared libraries,
functions in these libraries use the ``stdcall`` calling convention, and are
Expand Down Expand Up @@ -1394,6 +1394,15 @@ the Windows error code which is managed by the :func:`GetLastError` and
:func:`ctypes.set_last_error` are used to request and change the ctypes private
copy of the windows error code.

The *winmode* parameter is used on Windows to specify how the library is loaded
(since *mode* is ignored). It takes any value that is valid for the Win32 API
``LoadLibraryEx`` flags parameter. When omitted, the default is to use the flags
that result in the most secure DLL load, avoiding issues such as DLL hijacking.

.. versionchanged:: 3.8
Added *winmode* parameter.


.. data:: RTLD_GLOBAL
:noindex:

Expand Down
20 changes: 20 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3079,6 +3079,26 @@ to be ignored.
:func:`signal.signal`.


.. function:: add_dll_directory(path)

Add a path to the DLL search path.

This search path is used when resolving dependencies for imported
extension modules (the module itself is resolved through sys.path),
and also by ctypes.

Remove the directory by calling **close()** on the returned object
or using it in a :keyword:`with` statement.

See the `Microsoft documentation
<https://msdn.microsoft.com/44228cf2-6306-466c-8f16-f513cd3ba8b5>`_
for more information about how DLLs are loaded.

.. availability:: Windows.

.. versionadded:: 3.8


.. function:: execl(path, arg0, arg1, ...)
execle(path, arg0, arg1, ..., env)
execlp(file, arg0, arg1, ...)
Expand Down
25 changes: 25 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ asyncio
On Windows, the default event loop is now :class:`~asyncio.ProactorEventLoop`.


ctypes
------

On Windows, :class:`~ctypes.CDLL` and subclasses now accept a *winmode* parameter
to specify flags for the underlying ``LoadLibraryEx`` call. The default flags are
set to only load DLL dependencies from trusted locations, including the path
where the DLL is stored (if a full or partial path is used to load the initial
DLL) and paths added by :func:`~os.add_dll_directory`.


gettext
-------

Expand Down Expand Up @@ -224,6 +234,13 @@ Added new function, :func:`math.prod`, as analogous function to :func:`sum`
that returns the product of a 'start' value (default: 1) times an iterable of
numbers. (Contributed by Pablo Galindo in :issue:`35606`)

os
--

Added new function :func:`~os.add_dll_directory` on Windows for providing
additional search paths for native dependencies when importing extension
modules or loading DLLs using :mod:`ctypes`.


os.path
-------
Expand Down Expand Up @@ -697,6 +714,14 @@ Changes in the Python API
environment variable and does not use :envvar:`HOME`, which is not normally
set for regular user accounts.

* DLL dependencies for extension modules and DLLs loaded with :mod:`ctypes` on
Windows are now resolved more securely. Only the system paths, the directory
containing the DLL or PYD file, and directories added with
:func:`~os.add_dll_directory` are searched for load-time dependencies.
Specifically, :envvar:`PATH` is no longer used, and modifications to this
environment variable will no longer have any effect on normal DLL resolution.
(See :issue:`36085`.)


CPython bytecode changes
------------------------
Expand Down
12 changes: 11 additions & 1 deletion Lib/ctypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ class CDLL(object):

def __init__(self, name, mode=DEFAULT_MODE, handle=None,
use_errno=False,
use_last_error=False):
use_last_error=False,
winmode=None):
self._name = name
flags = self._func_flags_
if use_errno:
Expand All @@ -341,6 +342,15 @@ def __init__(self, name, mode=DEFAULT_MODE, handle=None,
"""
if name and name.endswith(")") and ".a(" in name:
mode |= ( _os.RTLD_MEMBER | _os.RTLD_NOW )
if _os.name == "nt":
if winmode is not None:
mode = winmode
else:
import nt
mode = nt._LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
if '/' in name or '\\' in name:
self._name = nt._getfullpathname(self._name)
mode |= nt._LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR

class _FuncPtr(_CFuncPtr):
_flags_ = flags
Expand Down
63 changes: 63 additions & 0 deletions Lib/ctypes/test/test_loading.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from ctypes import *
import os
import shutil
import subprocess
import sys
import sysconfig
import unittest
import test.support
from ctypes.util import find_library
Expand Down Expand Up @@ -112,5 +115,65 @@ def test_1703286_B(self):
# This is the real test: call the function via 'call_function'
self.assertEqual(0, call_function(proc, (None,)))

@unittest.skipUnless(os.name == "nt",
'test specific to Windows')
def test_load_dll_with_flags(self):
_sqlite3 = test.support.import_module("_sqlite3")
src = _sqlite3.__file__
if src.lower().endswith("_d.pyd"):
ext = "_d.dll"
else:
ext = ".dll"

with test.support.temp_dir() as tmp:
# We copy two files and load _sqlite3.dll (formerly .pyd),
# which has a dependency on sqlite3.dll. Then we test
# loading it in subprocesses to avoid it starting in memory
# for each test.
target = os.path.join(tmp, "_sqlite3.dll")
shutil.copy(src, target)
shutil.copy(os.path.join(os.path.dirname(src), "sqlite3" + ext),
os.path.join(tmp, "sqlite3" + ext))

def should_pass(command):
with self.subTest(command):
subprocess.check_output(
[sys.executable, "-c",
"from ctypes import *; import nt;" + command],
cwd=tmp
)

def should_fail(command):
with self.subTest(command):
with self.assertRaises(subprocess.CalledProcessError):
subprocess.check_output(
[sys.executable, "-c",
"from ctypes import *; import nt;" + command],
cwd=tmp, stderr=subprocess.STDOUT,
)

# Default load should not find this in CWD
should_fail("WinDLL('_sqlite3.dll')")

# Relative path (but not just filename) should succeed
should_pass("WinDLL('./_sqlite3.dll')")

# Insecure load flags should succeed
should_pass("WinDLL('_sqlite3.dll', winmode=0)")

# Full path load without DLL_LOAD_DIR shouldn't find dependency
should_fail("WinDLL(nt._getfullpathname('_sqlite3.dll'), " +
"winmode=nt._LOAD_LIBRARY_SEARCH_SYSTEM32)")

# Full path load with DLL_LOAD_DIR should succeed
should_pass("WinDLL(nt._getfullpathname('_sqlite3.dll'), " +
"winmode=nt._LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR)")

# User-specified directory should succeed
should_pass("import os; p = os.add_dll_directory(os.getcwd());" +
"WinDLL('_sqlite3.dll'); p.close()")



if __name__ == "__main__":
unittest.main()
37 changes: 37 additions & 0 deletions Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -1070,3 +1070,40 @@ def __fspath__(self):
@classmethod
def __subclasshook__(cls, subclass):
return hasattr(subclass, '__fspath__')


if name == 'nt':
class _AddedDllDirectory:
def __init__(self, path, cookie, remove_dll_directory):
self.path = path
self._cookie = cookie
self._remove_dll_directory = remove_dll_directory
def close(self):
self._remove_dll_directory(self._cookie)
self.path = None
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def __repr__(self):
if self.path:
return "<AddedDllDirectory({!r})>".format(self.path)
return "<AddedDllDirectory()>"

def add_dll_directory(path):
"""Add a path to the DLL search path.

This search path is used when resolving dependencies for imported
extension modules (the module itself is resolved through sys.path),
and also by ctypes.

Remove the directory by calling close() on the returned object or
using it in a with statement.
"""
import nt
cookie = nt._add_dll_directory(path)
return _AddedDllDirectory(
path,
cookie,
nt._remove_dll_directory
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Enable better DLL resolution on Windows by using safe DLL search paths and
adding :func:`os.add_dll_directory`.
34 changes: 25 additions & 9 deletions Modules/_ctypes/callproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1267,31 +1267,44 @@ static PyObject *format_error(PyObject *self, PyObject *args)
}

static const char load_library_doc[] =
"LoadLibrary(name) -> handle\n\
"LoadLibrary(name, load_flags) -> handle\n\
\n\
Load an executable (usually a DLL), and return a handle to it.\n\
The handle may be used to locate exported functions in this\n\
module.\n";
module. load_flags are as defined for LoadLibraryEx in the\n\
Windows API.\n";
static PyObject *load_library(PyObject *self, PyObject *args)
{
const WCHAR *name;
PyObject *nameobj;
PyObject *ignored;
int load_flags = 0;
HMODULE hMod;
DWORD err;

if (!PyArg_ParseTuple(args, "U|O:LoadLibrary", &nameobj, &ignored))
if (!PyArg_ParseTuple(args, "U|i:LoadLibrary", &nameobj, &load_flags))
return NULL;

name = _PyUnicode_AsUnicode(nameobj);
if (!name)
return NULL;

Py_BEGIN_ALLOW_THREADS
hMod = LoadLibraryW(name);
/* bpo-36085: Limit DLL search directories to avoid pre-loading
* attacks and enable use of the AddDllDirectory function.
*/
hMod = LoadLibraryExW(name, NULL, (DWORD)load_flags);
err = hMod ? 0 : GetLastError();
Py_END_ALLOW_THREADS

if (!hMod)
return PyErr_SetFromWindowsErr(GetLastError());
if (err == ERROR_MOD_NOT_FOUND) {
PyErr_Format(PyExc_FileNotFoundError,
("Could not find module '%.500S'. Try using "
"the full path with constructor syntax."),
nameobj);
return NULL;
} else if (err) {
return PyErr_SetFromWindowsErr(err);
}
#ifdef _WIN64
return PyLong_FromVoidPtr(hMod);
#else
Expand All @@ -1307,15 +1320,18 @@ static PyObject *free_library(PyObject *self, PyObject *args)
{
void *hMod;
BOOL result;
DWORD err;
if (!PyArg_ParseTuple(args, "O&:FreeLibrary", &_parse_voidp, &hMod))
return NULL;

Py_BEGIN_ALLOW_THREADS
result = FreeLibrary((HMODULE)hMod);
err = result ? 0 : GetLastError();
Py_END_ALLOW_THREADS

if (!result)
return PyErr_SetFromWindowsErr(GetLastError());
if (!result) {
return PyErr_SetFromWindowsErr(err);
}
Py_RETURN_NONE;
}

Expand Down
Loading