diff options
| author | Steve Dower <steve.dower@microsoft.com> | 2019-03-29 16:37:16 -0700 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-03-29 16:37:16 -0700 | 
| commit | 2438cdf0e932a341c7613bf4323d06b91ae9f1f1 (patch) | |
| tree | 231cdf3f22e1d5eb9f88fe7a511ab47e3cf8d225 | |
| parent | 32119e10b792ad7ee4e5f951a2d89ddbaf111cc5 (diff) | |
| download | cpython-git-2438cdf0e932a341c7613bf4323d06b91ae9f1f1.tar.gz | |
bpo-36085: Enable better DLL resolution on Windows (GH-12302)
| -rw-r--r-- | Doc/library/ctypes.rst | 17 | ||||
| -rw-r--r-- | Doc/library/os.rst | 30 | ||||
| -rw-r--r-- | Doc/whatsnew/3.8.rst | 30 | ||||
| -rw-r--r-- | Lib/ctypes/__init__.py | 12 | ||||
| -rw-r--r-- | Lib/ctypes/test/test_loading.py | 63 | ||||
| -rw-r--r-- | Lib/os.py | 37 | ||||
| -rw-r--r-- | Lib/test/test_import/__init__.py | 48 | ||||
| -rw-r--r-- | Misc/NEWS.d/next/Windows/2019-03-18-11-44-49.bpo-36085.mLfxfc.rst | 2 | ||||
| -rw-r--r-- | Modules/_ctypes/callproc.c | 34 | ||||
| -rw-r--r-- | Modules/clinic/posixmodule.c.h | 98 | ||||
| -rw-r--r-- | Modules/posixmodule.c | 133 | ||||
| -rw-r--r-- | Python/dynload_win.c | 10 | 
12 files changed, 492 insertions, 22 deletions
diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 500aad8858..baab0de8f8 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 f8803af952..85e240a000 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 0ffbcab353..f0423c376f 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 5f78beda58..4107db3e39 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 f3b65b9d6e..be367c6fa3 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() @@ -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 7306e0f7f7..a0bfe1a6c1 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 0000000000..41f23e6556 --- /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 7c25e2e796..5a943d3c37 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 55f2cbb91a..43f8ba6b4e 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 3f76018357..7c4e5f082b 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 36918c3579..457d47f5ee 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);  | 
