Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rthooks: secure temp directories used by matplotlib and win32com rthooks #7827

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ recursive-include PyInstaller/bootloader/Windows-64bit-intel *
recursive-include PyInstaller/bootloader/Darwin-64bit *
include pyproject.toml
# These files need to be explicitly included
include PyInstaller/fake-modules/*.py
recursive-include PyInstaller/fake-modules *.py
include PyInstaller/hooks/rthooks.dat
include PyInstaller/lib/README.rst
56 changes: 56 additions & 0 deletions PyInstaller/fake-modules/_pyi_rth_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# -----------------------------------------------------------------------------
# Copyright (c) 2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------

import os
import sys
import errno
import tempfile

# Helper for creating temporary directories with access restricted to the user running the process.
# On POSIX systems, this is already achieved by `tempfile.mkdtemp`, which uses 0o700 permissions mask.
# On Windows, however, the POSIX permissions semantics have no effect, and we need to provide our own implementation
# that restricts the access by passing appropriate security attributes to the `CreateDirectory` function.

if os.name == 'nt':
from . import _win32

def secure_mkdtemp(suffix=None, prefix=None, dir=None):
"""
Windows-specific replacement for `tempfile.mkdtemp` that restricts access to the user running the process.
Based on `mkdtemp` implementation from python 3.11 stdlib.
"""

prefix, suffix, dir, output_type = tempfile._sanitize_params(prefix, suffix, dir)

names = tempfile._get_candidate_names()
if output_type is bytes:
names = map(os.fsencode, names)

for seq in range(tempfile.TMP_MAX):
name = next(names)
file = os.path.join(dir, prefix + name + suffix)
sys.audit("tempfile.mkdtemp", file)
try:
_win32.secure_mkdir(file)
except FileExistsError:
continue # try again
except PermissionError:
# This exception is thrown when a directory with the chosen name already exists on windows.
if (os.name == 'nt' and os.path.isdir(dir) and os.access(dir, os.W_OK)):
continue
else:
raise
return file

raise FileExistsError(errno.EEXIST, "No usable temporary directory name found")

else:
secure_mkdtemp = tempfile.mkdtemp
262 changes: 262 additions & 0 deletions PyInstaller/fake-modules/_pyi_rth_utils/_win32.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# -----------------------------------------------------------------------------
# Copyright (c) 2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------

import ctypes
import ctypes.wintypes

# Constants from win32 headers
TOKEN_QUERY = 0x0008

TokenUser = 1 # from TOKEN_INFORMATION_CLASS enum

ERROR_INSUFFICIENT_BUFFER = 122

INVALID_HANDLE = -1

FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000

SDDL_REVISION1 = 1

# Structures for ConvertSidToStringSidW
PSID = ctypes.wintypes.LPVOID


class SID_AND_ATTRIBUTES(ctypes.Structure):
_fields_ = [
("Sid", PSID),
("Attributes", ctypes.wintypes.DWORD),
]


class TOKEN_USER(ctypes.Structure):
_fields_ = [
("User", SID_AND_ATTRIBUTES),
]


PTOKEN_USER = ctypes.POINTER(TOKEN_USER)

# SECURITY_ATTRIBUTES structure for CreateDirectoryW
PSECURITY_DESCRIPTOR = ctypes.wintypes.LPVOID


class SECURITY_ATTRIBUTES(ctypes.Structure):
_fields_ = [
("nLength", ctypes.wintypes.DWORD),
("lpSecurityDescriptor", PSECURITY_DESCRIPTOR),
("bInheritHandle", ctypes.wintypes.BOOL),
]


# win32 API functions, bound via ctypes.
# NOTE: we do not use ctypes.windll.<dll_name> to avoid modifying its (global) function prototypes, which might affect
# user's code.
kernel32 = ctypes.WinDLL("kernel32")
advapi32 = ctypes.WinDLL("advapi32")

kernel32.CloseHandle.restype = ctypes.wintypes.BOOL
kernel32.CloseHandle.argtypes = (ctypes.wintypes.HANDLE,)

kernel32.LocalFree.restype = ctypes.wintypes.BOOL
kernel32.LocalFree.argtypes = (ctypes.wintypes.HLOCAL,)

kernel32.GetCurrentProcess.restype = ctypes.wintypes.HANDLE

kernel32.OpenProcessToken.restype = ctypes.wintypes.BOOL
kernel32.OpenProcessToken.argtypes = (
ctypes.wintypes.HANDLE,
ctypes.wintypes.DWORD,
ctypes.wintypes.PHANDLE,
)

advapi32.ConvertSidToStringSidW.restype = ctypes.wintypes.BOOL
advapi32.ConvertSidToStringSidW.argtypes = (
PSID,
ctypes.POINTER(ctypes.wintypes.LPWSTR),
)

advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.restype = ctypes.wintypes.BOOL
advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.argtypes = (
ctypes.wintypes.LPCWSTR,
ctypes.wintypes.DWORD,
ctypes.POINTER(PSECURITY_DESCRIPTOR),
ctypes.wintypes.PULONG,
)


def _win_error_to_message(error_code):
"""
Convert win32 error code to message.
"""
message_wstr = ctypes.wintypes.LPWSTR(None)
ret = kernel32.FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
None, # lpSource
error_code, # dwMessageId
0x400, # dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)
ctypes.byref(message_wstr), # pointer to LPWSTR due to FORMAT_MESSAGE_ALLOCATE_BUFFER
64, # due to FORMAT_MESSAGE_ALLOCATE_BUFFER, this is minimum number of characters to allocate
)
if ret == 0:
return None

message = message_wstr.value
kernel32.LocalFree(message_wstr)

# Strip trailing CR/LF.
if message:
message = message.strip()
return message


def _get_user_sid():
"""
Obtain the SID for the current user.
"""
process_token = ctypes.wintypes.HANDLE(INVALID_HANDLE)

try:
# Get access token for the current process
ret = kernel32.OpenProcessToken(
kernel32.GetCurrentProcess(),
TOKEN_QUERY,
ctypes.pointer(process_token),
)
if ret == 0:
error_code = kernel32.GetLastError()
raise RuntimeError(f"Failed to open process token! Error code: 0x{error_code:X}")

# Query buffer size for user info structure
user_info_size = ctypes.wintypes.DWORD(0)

ret = advapi32.GetTokenInformation(
process_token,
TokenUser,
None,
0,
ctypes.byref(user_info_size),
)

# We expect this call to fail with ERROR_INSUFFICIENT_BUFFER
if ret == 0:
error_code = kernel32.GetLastError()
if error_code != ERROR_INSUFFICIENT_BUFFER:
raise RuntimeError(f"Failed to query token information buffer size! Error code: 0x{error_code:X}")
else:
raise RuntimeError("Unexpected return value from GetTokenInformation!")

# Allocate buffer
user_info = ctypes.create_string_buffer(user_info_size.value)
ret = advapi32.GetTokenInformation(
process_token,
TokenUser,
user_info,
user_info_size,
ctypes.byref(user_info_size),
)
if ret == 0:
error_code = kernel32.GetLastError()
raise RuntimeError(f"Failed to query token information! Error code: 0x{error_code:X}")

# Convert SID to string
# Technically, we need to pass user_info->User.Sid, but as they are at the beginning of the
# buffer, just pass the buffer instead...
sid_wstr = ctypes.wintypes.LPWSTR(None)
ret = advapi32.ConvertSidToStringSidW(
ctypes.cast(user_info, PTOKEN_USER).contents.User.Sid,
ctypes.pointer(sid_wstr),
)
if ret == 0:
error_code = kernel32.GetLastError()
raise RuntimeError(f"Failed to convert SID to string! Error code: 0x{error_code:X}")
sid = sid_wstr.value
kernel32.LocalFree(sid_wstr)
except Exception:
sid = None
finally:
# Close the process token
if process_token.value != INVALID_HANDLE:
kernel32.CloseHandle(process_token)

return sid


# Get and cache current user's SID
_user_sid = _get_user_sid()


def secure_mkdir(dir_name):
"""
Replacement for mkdir that limits the access to created directory to current user.
"""

# Create security descriptor
# Prefer actual user SID over SID S-1-3-4 (current owner), because at the time of writing, Wine does not properly
# support the latter.
sid = _user_sid or "S-1-3-4"

# DACL descriptor (D):
# ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;(resource_attribute)
# - ace_type = SDDL_ACCESS_ALLOWED (A)
# - rights = SDDL_FILE_ALL (FA)
# - account_sid = current user (queried SID)
security_desc_str = f"D:(A;;FA;;;{sid})"
security_desc = ctypes.wintypes.LPVOID(None)

ret = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW(
security_desc_str,
SDDL_REVISION1,
ctypes.byref(security_desc),
None,
)
if ret == 0:
error_code = kernel32.GetLastError()
raise RuntimeError(
f"Failed to create security descriptor! Error code: 0x{error_code:X}, "
f"message: {_win_error_to_message(error_code)}"
)

security_attr = SECURITY_ATTRIBUTES()
security_attr.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
security_attr.lpSecurityDescriptor = security_desc
security_attr.bInheritHandle = False

# Create directory
ret = kernel32.CreateDirectoryW(
dir_name,
security_attr,
)
if ret == 0:
# Call failed; store error code immediately, to avoid it being overwritten in cleanup below.
error_code = kernel32.GetLastError()

# Free security descriptor
kernel32.LocalFree(security_desc)

# Exit on succeess
if ret != 0:
return

# Construct OSError from win error code
error_message = _win_error_to_message(error_code)

# Strip trailing dot to match error message from os.mkdir().
if error_message and error_message[-1] == '.':
error_message = error_message[:-1]

raise OSError(
None, # errno
error_message, # strerror
dir_name, # filename
error_code, # winerror
None, # filename2
)
25 changes: 25 additions & 0 deletions PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -----------------------------------------------------------------------------
# Copyright (c) 2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------
"""
This hook allows discovery and collection of PyInstaller's internal _pyi_rth_utils module that provides utility
functions for run-time hooks.

The module is implemented in 'PyInstaller/fake-modules/_pyi_rth_utils.py'.
"""

import os

from PyInstaller import PACKAGEPATH


def pre_find_module_path(api):
module_dir = os.path.join(PACKAGEPATH, 'fake-modules')
api.search_dirs = [module_dir]
8 changes: 5 additions & 3 deletions PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ def _pyi_rthook():
import atexit
import os
import shutil
import tempfile

# Put matplot config dir to temp directory.
configdir = tempfile.mkdtemp()
import _pyi_rth_utils # PyInstaller's run-time hook utilities module

# Isolate matplotlib's config dir into temporary directory.
# Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
configdir = _pyi_rth_utils.secure_mkdtemp()
os.environ['MPLCONFIGDIR'] = configdir

try:
Expand Down
8 changes: 5 additions & 3 deletions PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ def _pyi_rthook():
import atexit
import os
import shutil
import tempfile

import win32com

# Create temporary directory. The actual cache directory needs to be named `gen_py`, so create a sub-directory.
supportdir = tempfile.mkdtemp()
import _pyi_rth_utils # PyInstaller's run-time hook utilities module

# Create temporary directory.
# Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
supportdir = _pyi_rth_utils.secure_mkdtemp()
# The actual cache directory needs to be named `gen_py`, so create a sub-directory.
genpydir = os.path.join(supportdir, 'gen_py')
os.makedirs(genpydir, exist_ok=True)

Expand Down
5 changes: 5 additions & 0 deletions news/7827.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(Windows) Ensure that the access to temporary directories created by
the ``matplotlib`` and ``win32com`` run-time hooks is restricted to
the user running the frozen application, even if ``TMP`` / ``TEMP``
environment directory points to a system-wide location that can be
accessed to all users.