summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteve Dower <steve.dower@microsoft.com>2019-03-29 23:37:16 (GMT)
committerGitHub <noreply@github.com>2019-03-29 23:37:16 (GMT)
commit2438cdf0e932a341c7613bf4323d06b91ae9f1f1 (patch)
tree231cdf3f22e1d5eb9f88fe7a511ab47e3cf8d225
parent32119e10b792ad7ee4e5f951a2d89ddbaf111cc5 (diff)
downloadcpython-2438cdf0e932a341c7613bf4323d06b91ae9f1f1.zip
cpython-2438cdf0e932a341c7613bf4323d06b91ae9f1f1.tar.gz
cpython-2438cdf0e932a341c7613bf4323d06b91ae9f1f1.tar.bz2
bpo-36085: Enable better DLL resolution on Windows (GH-12302)
-rw-r--r--Doc/library/ctypes.rst17
-rw-r--r--Doc/library/os.rst30
-rw-r--r--Doc/whatsnew/3.8.rst30
-rw-r--r--Lib/ctypes/__init__.py12
-rw-r--r--Lib/ctypes/test/test_loading.py63
-rw-r--r--Lib/os.py37
-rw-r--r--Lib/test/test_import/__init__.py48
-rw-r--r--Misc/NEWS.d/next/Windows/2019-03-18-11-44-49.bpo-36085.mLfxfc.rst2
-rw-r--r--Modules/_ctypes/callproc.c34
-rw-r--r--Modules/clinic/posixmodule.c.h98
-rw-r--r--Modules/posixmodule.c133
-rw-r--r--Python/dynload_win.c10
12 files changed, 492 insertions, 22 deletions
diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst
index 500aad8..baab0de 100644
--- a/Doc/library/ctypes.rst
+++ b/Doc/library/ctypes.rst
@@ -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
@@ -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
@@ -1394,6 +1394,17 @@ 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 to avoiding issues such as DLL
+hijacking. Passing the full path to the DLL is the safest way to ensure the
+correct library and dependencies are loaded.
+
+.. versionchanged:: 3.8
+ Added *winmode* parameter.
+
+
.. data:: RTLD_GLOBAL
:noindex:
diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index f8803af..85e240a 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -3079,6 +3079,36 @@ 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 :mod:`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
+ Previous versions of CPython would resolve DLLs using the default
+ behavior for the current process. This led to inconsistencies,
+ such as only sometimes searching :envvar:`PATH` or the current
+ working directory, and OS functions such as ``AddDllDirectory``
+ having no effect.
+
+ In 3.8, the two primary ways DLLs are loaded now explicitly
+ override the process-wide behavior to ensure consistency. See the
+ :ref:`porting notes <bpo-36085-whatsnew>` for information on
+ updating libraries.
+
+
.. function:: execl(path, arg0, arg1, ...)
execle(path, arg0, arg1, ..., env)
execlp(file, arg0, arg1, ...)
diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
index 0ffbcab..f0423c3 100644
--- a/Doc/whatsnew/3.8.rst
+++ b/Doc/whatsnew/3.8.rst
@@ -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
-------
@@ -238,6 +248,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
-------
@@ -727,6 +744,19 @@ Changes in the Python API
environment variable and does not use :envvar:`HOME`, which is not normally
set for regular user accounts.
+.. _bpo-36085-whatsnew:
+
+* 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` and the current working directory are no longer
+ used, and modifications to these will no longer have any effect on normal DLL
+ resolution. If your application relies on these mechanisms, you should check
+ for :func:`~os.add_dll_directory` and if it exists, use it to add your DLLs
+ directory while loading your library.
+ (See :issue:`36085`.)
+
Changes in the C API
--------------------
diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py
index 5f78bed..4107db3 100644
--- a/Lib/ctypes/__init__.py
+++ b/Lib/ctypes/__init__.py
@@ -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:
@@ -341,6 +342,15 @@ class CDLL(object):
"""
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
diff --git a/Lib/ctypes/test/test_loading.py b/Lib/ctypes/test/test_loading.py
index f3b65b9..be367c6 100644
--- a/Lib/ctypes/test/test_loading.py
+++ b/Lib/ctypes/test/test_loading.py
@@ -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
@@ -112,5 +115,65 @@ class LoaderTest(unittest.TestCase):
# 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()
diff --git a/Lib/os.py b/Lib/os.py
index 7741c75..79ff7a2 100644
--- a/Lib/os.py
+++ b/Lib/os.py
@@ -1070,3 +1070,40 @@ class PathLike(abc.ABC):
@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
+ )
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index 7306e0f..a0bfe1a 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -8,6 +8,8 @@ import os
import platform
import py_compile
import random
+import shutil
+import subprocess
import stat
import sys
import threading
@@ -17,6 +19,7 @@ import unittest.mock as mock
import textwrap
import errno
import contextlib
+import glob
import test.support
from test.support import (
@@ -460,6 +463,51 @@ class ImportTests(unittest.TestCase):
finally:
del sys.path[0]
+ @unittest.skipUnless(sys.platform == "win32", "Windows-specific")
+ def test_dll_dependency_import(self):
+ from _winapi import GetModuleFileName
+ dllname = GetModuleFileName(sys.dllhandle)
+ pydname = importlib.util.find_spec("_sqlite3").origin
+ depname = os.path.join(
+ os.path.dirname(pydname),
+ "sqlite3{}.dll".format("_d" if "_d" in pydname else ""))
+
+ with test.support.temp_dir() as tmp:
+ tmp2 = os.path.join(tmp, "DLLs")
+ os.mkdir(tmp2)
+
+ pyexe = os.path.join(tmp, os.path.basename(sys.executable))
+ shutil.copy(sys.executable, pyexe)
+ shutil.copy(dllname, tmp)
+ for f in glob.glob(os.path.join(sys.prefix, "vcruntime*.dll")):
+ shutil.copy(f, tmp)
+
+ shutil.copy(pydname, tmp2)
+
+ env = None
+ env = {k.upper(): os.environ[k] for k in os.environ}
+ env["PYTHONPATH"] = tmp2 + ";" + os.path.dirname(os.__file__)
+
+ # Test 1: import with added DLL directory
+ subprocess.check_call([
+ pyexe, "-Sc", ";".join([
+ "import os",
+ "p = os.add_dll_directory({!r})".format(
+ os.path.dirname(depname)),
+ "import _sqlite3",
+ "p.close"
+ ])],
+ stderr=subprocess.STDOUT,
+ env=env,
+ cwd=os.path.dirname(pyexe))
+
+ # Test 2: import with DLL adjacent to PYD
+ shutil.copy(depname, tmp2)
+ subprocess.check_call([pyexe, "-Sc", "import _sqlite3"],
+ stderr=subprocess.STDOUT,
+ env=env,
+ cwd=os.path.dirname(pyexe))
+
@skip_if_dont_write_bytecode
class FilePermissionTests(unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Windows/2019-03-18-11-44-49.bpo-36085.mLfxfc.rst b/Misc/NEWS.d/next/Windows/2019-03-18-11-44-49.bpo-36085.mLfxfc.rst
new file mode 100644
index 0000000..41f23e6
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2019-03-18-11-44-49.bpo-36085.mLfxfc.rst
@@ -0,0 +1,2 @@
+Enable better DLL resolution on Windows by using safe DLL search paths and
+adding :func:`os.add_dll_directory`.
diff --git a/Modules/_ctypes/callproc.c b/Modules/_ctypes/callproc.c
index 7c25e2e..5a943d3 100644
--- a/Modules/_ctypes/callproc.c
+++ b/Modules/_ctypes/callproc.c
@@ -1251,19 +1251,21 @@ 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);
@@ -1271,11 +1273,22 @@ static PyObject *load_library(PyObject *self, PyObject *args)
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
@@ -1291,15 +1304,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;
}
diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h
index 55f2cbb..43f8ba6 100644
--- a/Modules/clinic/posixmodule.c.h
+++ b/Modules/clinic/posixmodule.c.h
@@ -7961,6 +7961,94 @@ exit:
#endif /* defined(HAVE_GETRANDOM_SYSCALL) */
+#if defined(MS_WINDOWS)
+
+PyDoc_STRVAR(os__add_dll_directory__doc__,
+"_add_dll_directory($module, /, path)\n"
+"--\n"
+"\n"
+"Add a path to the DLL search path.\n"
+"\n"
+"This search path is used when resolving dependencies for imported\n"
+"extension modules (the module itself is resolved through sys.path),\n"
+"and also by ctypes.\n"
+"\n"
+"Returns an opaque value that may be passed to os.remove_dll_directory\n"
+"to remove this directory from the search path.");
+
+#define OS__ADD_DLL_DIRECTORY_METHODDEF \
+ {"_add_dll_directory", (PyCFunction)(void(*)(void))os__add_dll_directory, METH_FASTCALL|METH_KEYWORDS, os__add_dll_directory__doc__},
+
+static PyObject *
+os__add_dll_directory_impl(PyObject *module, path_t *path);
+
+static PyObject *
+os__add_dll_directory(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ static const char * const _keywords[] = {"path", NULL};
+ static _PyArg_Parser _parser = {NULL, _keywords, "_add_dll_directory", 0};
+ PyObject *argsbuf[1];
+ path_t path = PATH_T_INITIALIZE("_add_dll_directory", "path", 0, 0);
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ if (!path_converter(args[0], &path)) {
+ goto exit;
+ }
+ return_value = os__add_dll_directory_impl(module, &path);
+
+exit:
+ /* Cleanup for path */
+ path_cleanup(&path);
+
+ return return_value;
+}
+
+#endif /* defined(MS_WINDOWS) */
+
+#if defined(MS_WINDOWS)
+
+PyDoc_STRVAR(os__remove_dll_directory__doc__,
+"_remove_dll_directory($module, /, cookie)\n"
+"--\n"
+"\n"
+"Removes a path from the DLL search path.\n"
+"\n"
+"The parameter is an opaque value that was returned from\n"
+"os.add_dll_directory. You can only remove directories that you added\n"
+"yourself.");
+
+#define OS__REMOVE_DLL_DIRECTORY_METHODDEF \
+ {"_remove_dll_directory", (PyCFunction)(void(*)(void))os__remove_dll_directory, METH_FASTCALL|METH_KEYWORDS, os__remove_dll_directory__doc__},
+
+static PyObject *
+os__remove_dll_directory_impl(PyObject *module, PyObject *cookie);
+
+static PyObject *
+os__remove_dll_directory(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ static const char * const _keywords[] = {"cookie", NULL};
+ static _PyArg_Parser _parser = {NULL, _keywords, "_remove_dll_directory", 0};
+ PyObject *argsbuf[1];
+ PyObject *cookie;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ cookie = args[0];
+ return_value = os__remove_dll_directory_impl(module, cookie);
+
+exit:
+ return return_value;
+}
+
+#endif /* defined(MS_WINDOWS) */
+
#ifndef OS_TTYNAME_METHODDEF
#define OS_TTYNAME_METHODDEF
#endif /* !defined(OS_TTYNAME_METHODDEF) */
@@ -8480,4 +8568,12 @@ exit:
#ifndef OS_GETRANDOM_METHODDEF
#define OS_GETRANDOM_METHODDEF
#endif /* !defined(OS_GETRANDOM_METHODDEF) */
-/*[clinic end generated code: output=1a9c62f5841221ae input=a9049054013a1b77]*/
+
+#ifndef OS__ADD_DLL_DIRECTORY_METHODDEF
+ #define OS__ADD_DLL_DIRECTORY_METHODDEF
+#endif /* !defined(OS__ADD_DLL_DIRECTORY_METHODDEF) */
+
+#ifndef OS__REMOVE_DLL_DIRECTORY_METHODDEF
+ #define OS__REMOVE_DLL_DIRECTORY_METHODDEF
+#endif /* !defined(OS__REMOVE_DLL_DIRECTORY_METHODDEF) */
+/*[clinic end generated code: output=ab36ec0376a422ae input=a9049054013a1b77]*/
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 3f76018..7c4e5f0 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -1442,17 +1442,23 @@ win32_error(const char* function, const char* filename)
}
static PyObject *
-win32_error_object(const char* function, PyObject* filename)
+win32_error_object_err(const char* function, PyObject* filename, DWORD err)
{
/* XXX - see win32_error for comments on 'function' */
- errno = GetLastError();
if (filename)
return PyErr_SetExcFromWindowsErrWithFilenameObject(
PyExc_OSError,
- errno,
+ err,
filename);
else
- return PyErr_SetFromWindowsErr(errno);
+ return PyErr_SetFromWindowsErr(err);
+}
+
+static PyObject *
+win32_error_object(const char* function, PyObject* filename)
+{
+ errno = GetLastError();
+ return win32_error_object_err(function, filename, errno);
}
#endif /* MS_WINDOWS */
@@ -13161,6 +13167,113 @@ error:
}
#endif /* HAVE_GETRANDOM_SYSCALL */
+#ifdef MS_WINDOWS
+/* bpo-36085: Helper functions for managing DLL search directories
+ * on win32
+ */
+
+typedef DLL_DIRECTORY_COOKIE (WINAPI *PAddDllDirectory)(PCWSTR newDirectory);
+typedef BOOL (WINAPI *PRemoveDllDirectory)(DLL_DIRECTORY_COOKIE cookie);
+
+/*[clinic input]
+os._add_dll_directory
+
+ path: path_t
+
+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.
+
+Returns an opaque value that may be passed to os.remove_dll_directory
+to remove this directory from the search path.
+[clinic start generated code]*/
+
+static PyObject *
+os__add_dll_directory_impl(PyObject *module, path_t *path)
+/*[clinic end generated code: output=80b025daebb5d683 input=1de3e6c13a5808c8]*/
+{
+ HMODULE hKernel32;
+ PAddDllDirectory AddDllDirectory;
+ DLL_DIRECTORY_COOKIE cookie = 0;
+ DWORD err = 0;
+
+ /* For Windows 7, we have to load this. As this will be a fairly
+ infrequent operation, just do it each time. Kernel32 is always
+ loaded. */
+ Py_BEGIN_ALLOW_THREADS
+ if (!(hKernel32 = GetModuleHandleW(L"kernel32")) ||
+ !(AddDllDirectory = (PAddDllDirectory)GetProcAddress(
+ hKernel32, "AddDllDirectory")) ||
+ !(cookie = (*AddDllDirectory)(path->wide))) {
+ err = GetLastError();
+ }
+ Py_END_ALLOW_THREADS
+
+ if (err) {
+ return win32_error_object_err("add_dll_directory",
+ path->object, err);
+ }
+
+ return PyCapsule_New(cookie, "DLL directory cookie", NULL);
+}
+
+/*[clinic input]
+os._remove_dll_directory
+
+ cookie: object
+
+Removes a path from the DLL search path.
+
+The parameter is an opaque value that was returned from
+os.add_dll_directory. You can only remove directories that you added
+yourself.
+[clinic start generated code]*/
+
+static PyObject *
+os__remove_dll_directory_impl(PyObject *module, PyObject *cookie)
+/*[clinic end generated code: output=594350433ae535bc input=c1d16a7e7d9dc5dc]*/
+{
+ HMODULE hKernel32;
+ PRemoveDllDirectory RemoveDllDirectory;
+ DLL_DIRECTORY_COOKIE cookieValue;
+ DWORD err = 0;
+
+ if (!PyCapsule_IsValid(cookie, "DLL directory cookie")) {
+ PyErr_SetString(PyExc_TypeError,
+ "Provided cookie was not returned from os.add_dll_directory");
+ return NULL;
+ }
+
+ cookieValue = (DLL_DIRECTORY_COOKIE)PyCapsule_GetPointer(
+ cookie, "DLL directory cookie");
+
+ /* For Windows 7, we have to load this. As this will be a fairly
+ infrequent operation, just do it each time. Kernel32 is always
+ loaded. */
+ Py_BEGIN_ALLOW_THREADS
+ if (!(hKernel32 = GetModuleHandleW(L"kernel32")) ||
+ !(RemoveDllDirectory = (PRemoveDllDirectory)GetProcAddress(
+ hKernel32, "RemoveDllDirectory")) ||
+ !(*RemoveDllDirectory)(cookieValue)) {
+ err = GetLastError();
+ }
+ Py_END_ALLOW_THREADS
+
+ if (err) {
+ return win32_error_object_err("remove_dll_directory",
+ NULL, err);
+ }
+
+ if (PyCapsule_SetName(cookie, NULL)) {
+ return NULL;
+ }
+
+ Py_RETURN_NONE;
+}
+
+#endif
static PyMethodDef posix_methods[] = {
@@ -13349,6 +13462,10 @@ static PyMethodDef posix_methods[] = {
OS_SCANDIR_METHODDEF
OS_FSPATH_METHODDEF
OS_GETRANDOM_METHODDEF
+#ifdef MS_WINDOWS
+ OS__ADD_DLL_DIRECTORY_METHODDEF
+ OS__REMOVE_DLL_DIRECTORY_METHODDEF
+#endif
{NULL, NULL} /* Sentinel */
};
@@ -13826,6 +13943,14 @@ all_ins(PyObject *m)
if (PyModule_AddIntConstant(m, "_COPYFILE_DATA", COPYFILE_DATA)) return -1;
#endif
+#ifdef MS_WINDOWS
+ if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_DEFAULT_DIRS", LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)) return -1;
+ if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_APPLICATION_DIR", LOAD_LIBRARY_SEARCH_APPLICATION_DIR)) return -1;
+ if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_SYSTEM32", LOAD_LIBRARY_SEARCH_SYSTEM32)) return -1;
+ if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_USER_DIRS", LOAD_LIBRARY_SEARCH_USER_DIRS)) return -1;
+ if (PyModule_AddIntConstant(m, "_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR", LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR)) return -1;
+#endif
+
return 0;
}
diff --git a/Python/dynload_win.c b/Python/dynload_win.c
index 36918c3..457d47f 100644
--- a/Python/dynload_win.c
+++ b/Python/dynload_win.c
@@ -215,12 +215,14 @@ dl_funcptr _PyImport_FindSharedFuncptrWindows(const char *prefix,
#if HAVE_SXS
cookie = _Py_ActivateActCtx();
#endif
- /* We use LoadLibraryEx so Windows looks for dependent DLLs
- in directory of pathname first. */
- /* XXX This call doesn't exist in Windows CE */
+ /* bpo-36085: We use LoadLibraryEx with restricted search paths
+ to avoid DLL preloading attacks and enable use of the
+ AddDllDirectory function. We add SEARCH_DLL_LOAD_DIR to
+ ensure DLLs adjacent to the PYD are preferred. */
Py_BEGIN_ALLOW_THREADS
hDLL = LoadLibraryExW(wpathname, NULL,
- LOAD_WITH_ALTERED_SEARCH_PATH);
+ LOAD_LIBRARY_SEARCH_DEFAULT_DIRS |
+ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);
Py_END_ALLOW_THREADS
#if HAVE_SXS
_Py_DeactivateActCtx(cookie);