Используя Thread.stop() на тщательно заблокированном, но ненадежном коде

Я знаю, что Thread.stop() устарел, и по уважительной причине: он, в общем, не безопасен. Но это не означает, что это никогда не безопасно... насколько я могу судить, это безопасно в контексте, в котором я хочу его использовать; и, насколько я вижу, у меня нет другого выбора.

Контекст - сторонний плагин для стратегии игры с двумя игроками: шахматы будут работать как рабочий пример. Для стороннего кода необходимо предоставить текущее состояние платы и (скажем) 10 секунд, чтобы принять решение о его движении. Он может возвращать свое движение и заканчиваться в течение допустимого времени, или он может, когда захочет, сигнализировать о своем текущем предпочтительном ходу; если срок истекает, он должен быть остановлен на своих дорожках, и его последний предпочтительный ход должен быть воспроизведен.

Написание плагина для прерывания изящно по запросу не опция: мне нужно иметь возможность использовать произвольные ненадежные сторонние плагины. Поэтому у меня должен быть какой-то способ насильственного прекращения его.

Вот что я делаю, чтобы заблокировать его:

  • Классы для плагина помещаются в собственный поток в свою группу потоков.
  • Они загружаются загрузчиком классов, который имеет строго ограничивающий SecurityManager на месте: все классы могут выполнять операции с номером.
  • Классы не получают ссылки на любые другие потоки, поэтому они не могут использовать Thread.join() для всего, что они не создали.
  • Плагин получает только две ссылки на объекты из хост-системы. Одним из них является состояние шахматной доски; это глубокая копия, и впоследствии она выбрасывается, поэтому не имеет значения, попадает ли она в противоречивое состояние. Другой - простой объект, который позволяет плагину устанавливать текущий предпочтительный ход.
  • Я проверяю, превышено ли время CPU, и я периодически проверяю, что хотя бы один поток подключаемого модуля делает что-то (чтобы он не мог спать бесконечно и избегать процессорного времени, когда-либо ударяя предел).
  • Предпочтительный ход не имеет достаточного состояния, чтобы быть непоследовательным, но в любом случае он тщательно и защищенно клонируется после его возвращения, а тот, который был возвращен, отбрасывается. К этому моменту в хост-системе осталось ничего, к которому подключаемый модуль имеет ссылку: ни потоки, ни экземпляры объектов.

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

Мне кажется, что причины, по которым Thread.stop() устарели, не применяются здесь (по дизайну). Я что-то пропустил? Есть ли еще опасность? Или я достаточно тщательно изолировал вещи, чтобы не было проблемы?

И есть ли лучший способ? Единственная альтернатива, я думаю, состоит в том, чтобы запустить целую новую JVM для запуска ненадежного кода и принудительно убить процесс, когда он больше не нужен, но имеет тысячу других проблем (дорогостоящий, хрупкий, зависящий от ОС).

Обратите внимание: Меня не интересуют ответы в строках "Ох, это не рекомендуется по какой-то причине, вы хотите посмотреть, приятель". Я знаю, он устарел по какой-то причине, и я полностью понимаю, почему небезопасно выпускать из клетки вообще. Я спрашиваю, существует ли конкретная причина для того, чтобы думать, что она небезопасна в в этом контексте.

Для чего это стоит, это (сокращенный) соответствующий бит кода:

public void playMoveInternal(GameState game) throws IllegalMoveException,
        InstantiationException, IllegalAccessException,
        IllegalMoveSpecificationException {
    ThreadGroup group = new ThreadGroup("playthread group");
    Thread playthread = null;
    group.setMaxPriority(Thread.MIN_PRIORITY);
    GameMetaData meta = null;
    StrategyGamePlayer player = null;
    try {
        GameState newgame = (GameState) game.clone();
        SandboxedURLClassLoader loader = new SandboxedURLClassLoader(
          // recreating this each time means static fields don't persist
                urls[newgame.getCurPlayer() - 1], playerInterface);
        Class<?> playerClass = loader.findPlayerClass();
        GameTimer timer = new GameTimer(
                newgame.getCurPlayer() == 1 ? timelimit : timelimit2);
          // time starts ticking here!
        meta = new GameMetaData((GameTimer) timer.clone());
        try {
            player = (StrategyGamePlayer) playerClass.newInstance();
        } catch (Exception e) {
            System.err.println("Couldn't create player module instance!");
            e.printStackTrace();
            game.resign(GameMoveType.MOVE_ILLEGAL);
            return;
        }
        boolean checkSleepy = true;
        playthread = new Thread(group, new MoveMakerThread(player, meta,
                newgame), "MoveMaker thread");
        int badCount = 0;
        playthread.start();
        try {
            while ((timer.getTimeRemaining() > 0) && (playthread.isAlive())
                    && (!stopping) && (!forceMove)) {
                playthread.join(50);
                if (checkSleepy) {
                    Thread.State thdState = playthread.getState();
                    if ((thdState == Thread.State.TIMED_WAITING)
                            || (thdState == Thread.State.WAITING)) {
                        // normally, main thread will be busy
                        Thread[] allThreads = new Thread[group
                                .activeCount() * 2];
                        int numThreads = group.enumerate(allThreads);
                        boolean bad = true;
                        for (int i = 0; i < numThreads; i++) {
                            // check some player thread somewhere is doing something
                            thdState = allThreads[i].getState();
                            if ((thdState != Thread.State.TIMED_WAITING)
                                    && (thdState != Thread.State.WAITING)) {
                                bad = false;
                                break; // found a good thread, so carry on
                            }
                        }
                        if ((bad) && (badCount++ > 100))
                            // means player has been sleeping for an expected 5
                            // sec, which is naughty
                            break;
                    }
                }
            }
        } catch (InterruptedException e) {
            System.err.println("Interrupted: " + e);
        }
    } catch (Exception e) {
        System.err.println("Couldn't play the game: " + e);
        e.printStackTrace();
    }
    playthread.destroy();
    try {
        Thread.sleep(1000);
    } catch (Exception e) {
    }
    group.stop();
    forceMove = false;
    try {
        if (!stopping)
            try {
                if (!game.isLegalMove(meta.getBestMove())) {
                    game.resign(GameMoveType.MOVE_ILLEGAL);
                }
                else
                    game.makeMove((GameMove) (meta.getBestMove().clone()));
                // We rely here on the isLegalMove call to make sure that
                // the return type is the right (final) class so that the clone()
                // call can't execute dodgy code
            } catch (IllegalMoveException e) {
                game.resign(GameMoveType.MOVE_ILLEGAL);
            } catch (NullPointerException e) {
                // didn't ever choose a move to make
                game.resign(GameMoveType.MOVE_OUT_OF_TIME);
            }
    } catch (Exception e) {
        e.printStackTrace();
        game.resign(GameMoveType.MOVE_OUT_OF_TIME);
    }
}

Ответ 1

В то время как я могу представить сценарии, где код, который вы тщательно просмотрели, можно безопасно остановить с помощью Thread.stop(), вы говорите об обратном коде, который вы так недоверяете, что пытаетесь ограничить его действия с помощью SecurityManager.

Так что может пойти не так? Прежде всего, остановка не работает более надежно, чем прерывание. Хотя проще игнорировать прерывание, простого catch(Throwable t){} достаточно, чтобы сделать Thread.stop() больше не работать, и такой catch может появиться в коде даже без намерения предотвратить Thread.stop() от работы, хотя это плохо стиль кодирования. Но кодеры, желающие игнорировать Thread.stop(), могут даже намеренно добавить catch(Throwable t){} и/или catch(ThreadDeath t){}.

Говоря о стиле плохого кодирования, блокировка всегда должна быть реализована с помощью try … finally …, чтобы гарантировать, что блокировка будет освобождена даже в исключительном случае. Если остановленный код не смог сделать это правильно, блокировка может оставаться заблокированной, когда поток будет остановлен.

Суть в том, что в пределах одной JVM вы не можете защитить себя от ненадежного кода. JVM - это песочница, которая защищает снаружи. Просто один простой пример: вредоносный код может начать потреблять память, выполняя множество небольших распределений памяти. В JVM вы ничего не можете сделать, чтобы защитить какой-то код, запущенный в той же JVM, против него. Вы можете применять только ограничения для всей JVM.

Таким образом, единственным решением является запуск плагинов в другой JVM.