Как динамически импортировать небезопасный модуль Python с таймаутом? - PullRequest
0 голосов
/ 26 ноября 2018

Мне нужно динамически загрузить несколько потенциально небезопасных модулей для целей тестирования.

Что касается безопасности, мой скрипт выполняется пользователем с низким уровнем доступа.

Хотя мне все еще нужен способэлегантно сделайте тайм-аут процесса импорта, так как я не гарантирую, что скрипт модуля завершится.Например, он может содержать вызов input или бесконечный цикл.

В настоящее время я использую Thread.join с timeout, но это не полностью решает проблему, так как сценарий тогда ещежив в фоновом режиме и нет способа уничтожить поток.

from threading import Thread
import importlib.util

class ReturnThread(Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._return = None

    def run(self):
        if self._target is not None:
            self._return = self._target(*self._args, **self._kwargs)

    def join(self, *args, **kwargs):
        super().join(*args, **kwargs)
        return self._return

def loader(name, path):
    spec = importlib.util.spec_from_file_location(name, path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module) # This may run into an infinite loop
    return module

module_loader = ReturnThread(loader, ('module_name', 'module/path'))
module_loader.start()
module = module_loader.join(timeout=0.1)

# The thread might still be alive here
if module is None:
    ...
else:
    ...

Как я могу импортировать модуль, но вернуть None, если время ожидания скрипта истекло?

1 Ответ

0 голосов
/ 28 ноября 2018

Вы не можете надежно убить импорт модуля.По сути, вы выполняете живой код в своем собственном интерпретаторе, поэтому все ставки отключены.

Никогда не импортируйте ненадежный код

Прежде всего, нет способа безопасно импортировать небезопасные модули из ненадежного источника.,Не имеет значения, используете ли вы пользователя с низким доступом. НИКОГДА НЕ ИМПОРТИРУЙТЕ НЕПРАВИЛЬНЫЙ КОД .В тот момент, когда код импортируется, он может использовать дыры в безопасности вашей системы далеко за пределами самого процесса Python.Python - это язык программирования общего назначения, а не среда с песочницей, и любой импортируемый вами код имеет полный прогон вашей системы

Вместо использования пользователя с низким доступом, по крайней мере запустить это виртуальная машина.Среду виртуальной машины можно настроить на основе удачного снимка без сетевого доступа и закрыть по истечении определенного времени.Затем вы можете сравнить снимки, чтобы увидеть, что, если вообще, пытался сделать код.Любое нарушение безопасности на этом уровне является кратковременным и бесполезным.См. Также Рекомендации по выполнению ненадежного кода over в Software Engineering Stack Exchange.

Вы не можете помешать коду отменить вашу работу

Далее, поскольку вы не можете контролировать действия импортируемого кода, он может просто мешать любым попыткам тайм-аута кода.Первое, что может сделать импортированный код, это отменить защиту, которую вы установили!Импортированный код может получить доступ ко всему глобальному состоянию Python, включая код, вызвавший импорт.Код мог бы установить максимальное значение интервала переключения потоков 1020 * (внутреннее значение, длинное моделирование без знака, миллисекунды, так что максимальное значение составляет ((2 ** 32) - 1) миллисекунды, всего лишь smidgen менее 71 минуты 35 секунд), чтобы помешать планированию.

Вы не можете надежно останавливать потоки, если они не хотят останавливаться

Выход из потока в Python обрабатывается путем вызова исключения :

Поднимите исключение SystemExit. Если не поймано , это приведет к бесшумному завершению потока.

(выделение жирным шрифтом.)

Из чистого кода Python вы можете выйти только изпоток из кода , работающий в этом потоке , но есть способ обойти это, см. ниже.

Но вы не можете гарантировать, что импортируемый код не просто перехватывает и обрабатываетвсе исключения;если это так, код будет продолжать работать.В этот момент он становится гонкой вооружений;ваш поток может вставить исключение в тот момент, когда другой поток находится внутри обработчика исключения?Тогда вы можете выйти из этой ветки, иначе проиграете.Вам придется продолжать попытки, пока вы не добьетесь успеха.

Поток, который ожидает блокирования ввода-вывода или запускает операцию блокировки в собственном расширении, не может (легко) быть уничтожен

Еслиимпортируемый код ожидает блокировки ввода-вывода (например, input()), после чего вы не можете прервать этот вызов.Возбуждение исключения ничего не делает, и вы не можете использовать сигналы (поскольку Python обрабатывает их в основном потоке только ).Вам нужно будет найти и закрыть каждый открытый канал ввода-вывода, на котором они могут быть заблокированы.Это выходит за рамки моего ответа здесь, слишком много способов запустить операции ввода-вывода.

Если код начал что-то реализованное в собственном коде (расширение Python) и , что блоков, все ставки полностью отключены.

Состояние вашего интерпретатора может быть изменено к тому моменту, когда вы его остановите

Код, который вы импортировали, мог сделать что-нибудь к тому времени, когда вам удалось их остановить.,Импортированные модули могли быть заменены.Исходный код на диске может быть изменен.Вы не можете быть уверены, что никакие другие темы не были запущены.В Python все возможно, поэтому предположим, что это произошло.

Если вы хотите сделать это, в любом случае

С учетом этих предостережений, так что вы принимаете, что

  • Код, который вы импортируете, может совершать вредоносные действия в ОС, в которой он работает, без возможности остановить их в рамках одного и того же процесса или даже в ОС
  • Код, который вы импортируете, может помешать работе вашего кода.
  • Код, который вы импортировали, мог импортировать и запускать вещи, которые вы не хотели импортировать или запускать.
  • Код может запускать операции, которые не позволяют полностью остановить поток

затем вы можете установить время ожидания импорта, запустив их в отдельном потоке, а затем вызвать исключение SystemExit в потоке.Вы можете вызвать исключения в другом потоке, вызвав функцию PyThreadState_SetAsyncExc C-API через объект ctypes.pythonapi .Набор тестов Python фактически использует этот путь в тесте , я использовал его в качестве шаблона для своего решения ниже.

Итак, вот полная реализация, которая делает именно это и поднимает пользовательскийUninterruptableImport исключение (подкласс ImportError), если импорт не может быть прерван.Если при импорте возникло исключение, то это исключение повторно вызывается в потоке, который запустил процесс импорта:

"""Import a module within a timeframe

Uses the PyThreadState_SetAsyncExc C API and a signal handler to interrupt
the stack of calls triggered from an import within a timeframe

No guarantees are made as to the state of the interpreter after interrupting

"""

import ctypes
import importlib
import random
import sys
import threading
import time

_set_async_exc = ctypes.pythonapi.PyThreadState_SetAsyncExc
_set_async_exc.argtypes = (ctypes.c_ulong, ctypes.py_object)
_system_exit = ctypes.py_object(SystemExit)


class UninterruptableImport(ImportError):
    pass


class TimeLimitedImporter():
    def __init__(self, modulename, timeout=5):
        self.modulename = modulename
        self.module = None
        self.exception = None
        self.timeout = timeout

        self._started = None
        self._started_event = threading.Event()
        self._importer = threading.Thread(target=self._import, daemon=True)
        self._importer.start()
        self._started_event.wait()

    def _import(self):
        self._started = time.time()
        self._started_event.set()
        timer = threading.Timer(self.timeout, self.exit)
        timer.start()
        try:
            self.module = importlib.import_module(self.modulename)
        except Exception as e:
            self.exception = e
        finally:
            timer.cancel()

    def result(self, timeout=None):
        # give the importer a chance to finish first
        if timeout is not None:
            timeout += max(time.time() + self.timeout - self._started, 0)
        self._importer.join(timeout)
        if self._importer.is_alive():
            raise UninterruptableImport(
                f"Could not interrupt the import of {self.modulename}")
        if self.module is not None:
            return self.module
        if self.exception is not None:
            raise self.exception

    def exit(self):
        target_id = self._importer.ident
        if target_id is None:
            return
        # set a very low switch interval to be able to interrupt an exception
        # handler if SystemExit is being caught
        old_interval = sys.getswitchinterval()
        sys.setswitchinterval(1e-6)

        try:
            # repeatedly raise SystemExit until the import thread has exited.
            # If the exception is being caught by a an exception handler,
            # our only hope is to raise it again *while inside the handler*
            while True:
                _set_async_exc(target_id, _system_exit)

                # short randomised wait times to 'surprise' an exception
                # handler
                self._importer.join(
                    timeout=random.uniform(1e-4, 1e-5)
                )
                if not self._importer.is_alive():
                    return
        finally:
            sys.setswitchinterval(old_interval)


def import_with_timeout(modulename, import_timeout=5, exit_timeout=1):
    importer = TimeLimitedImporter(modulename, import_timeout)
    return importer.result(exit_timeout)

Если код не может быть уничтожен, он будет запущен в потоке демонаЭто означает, что вы можете по крайней мере грациозно завершить работу с Python.

Используйте его следующим образом:

module = import_with_timeout(modulename)

для тайм-аута по умолчанию, равного 5 секундам, и 1 секунда ожидания, чтобы убедиться, что импорт действительно не удаляется.

...