【问题标题】:Using Thread.stop() on carefully locked down, but untrusted, code在仔细锁定但不受信任的代码上使用 Thread.stop()
【发布时间】:2014-08-25 15:51:10
【问题描述】:

我知道Thread.stop() 已被弃用,这是有充分理由的:一般来说,它并不安全。但这并不意味着它是从不安全的......据我所知,它在我想要使用它的上下文中是安全的;而且,据我所知,我别无选择。

上下文是用于两人战略游戏的第三方插件:国际象棋将作为工作示例。需要为第三方代码提供当前的棋盘状态,并且(例如)10 秒来决定其移动。它可以返回它的移动并在允许的时间内终止,或者它可以在任何时候发出它当前首选移动的信号;如果时间限制到期,它应该停止在它的轨道上,并且应该播放它最近的首选移动。

编写插件以根据请求正常停止是不是一种选择:我需要能够采用任意不受信任的第三方插件。所以我必须有一些方法来强制终止它。

这是我正在做的事情来锁定它:

  1. 插件的类被放入它们自己的线程中,放入它们自己的线程组中。
  2. 它们加载了一个类加载器,该类加载器具有高度限制性的SecurityManager:所有类都可以做的就是数字运算。
  3. 这些类没有获得对任何其他线程的引用,因此它们不能在它们尚未创建的任何东西上使用Thread.join()
  4. 插件仅从主机系统中获得两个对对象的引用。一是棋盘状态;这是一个深拷贝,之后会被丢弃,所以如果它进入不一致的状态也没关系。另一个是允许插件设置其当前首选移动的简单对象。
  5. 我正在检查 CPU 时间何时超过限制,并且我会定期检查插件的至少一个线程是否正在做某事(这样它就不能无限期地休眠并避免 CPU 时间达到限制)。
  6. 首选招式没有足够的状态导致不一致,但无论如何它在返回后会被小心且防御性地克隆,并且返回的那个被丢弃。至此,主机系统中什么都没有可供插件引用:没有线程,没有对象实例。

因此,插件似乎不能让任何东西处于不一致的状态(除了它可能创建的任何对象,然后这些对象将被丢弃);并且它不会影响任何其他线程(除了它产生的任何线程,它们将在同一个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);
    }
}

【问题讨论】:

  • 对于您正在尝试做的事情,使用 ExecutorService 可能是一个更优雅的解决方案。
  • 看起来这在某些方面可能更干净一些,但它不会解决根本问题。 ExecutorService 的 Javadoc 说:“除了尽力停止处理正在执行的任务之外,没有任何保证。例如,典型的实现将通过 Thread.interrupt() 取消,因此任何无法响应中断的任务都可能永远不会终止。”
  • 一个插件仍然可能会扰乱 JDK 内部的状态(如果有人想要恶意),并且您的 ClassLoader 可能不会加载另一组 JDK 类?你需要下定决心:你的方法对于防御大多数 sloppy 插件来说听起来很实用。但是有人想搞砸虚拟机,他们可能还是找到了办法(毕竟他们从来没有设法堵住小程序的所有漏洞)。你的目标是什么?
  • @Durandal 我确实想知道这一点。即使在限制性安全管理器下,JVM 中是否存在任何类都可以进入和离开的同步对象?你有什么例子吗?我想不出来。
  • “首选动作没有足够的状态来导致不一致,但是……在它返回后……被防御性克隆”这没有任何意义。如果它是您可以克隆的东西,它也可能是不一致的。在停止线程的上下文中可以安全使用的东西并不多。一旦它包含多个变量,或者它不是原始值或真正不可变的对象,它就会变得不一致。对于原始值或真正不可变的对象,克隆毫无意义。

标签: java multithreading


【解决方案1】:

虽然我可以想象您仔细审查过的代码可能会被 Thread.stop() 安全停止的场景,但您说的是相反的代码,您非常不信任以至于您试图通过 SecurityManager 限制其操作.

那么会出什么问题呢?首先,停止并不比中断更可靠。虽然更容易忽略中断,但一个简单的catch(Throwable t){} 足以使Thread.stop() 不再工作,即使无意阻止Thread.stop() 工作,这样的catch 也可能出现在代码中,尽管它很糟糕编码风格。但是想要忽略Thread.stop()的编码人员甚至可能会添加catch(Throwable t){}和/或catch(ThreadDeath t){}有意

说到糟糕的编码风格,锁定应该始终使用try … finally … 来实现,以确保即使在异常情况下也能保证释放锁定。如果停止的代码未能正确执行此操作,则当线程停止时,锁可能会保持锁定状态。

底线是在同一个 JVM 中,您无法保护自己免受不受信任的代码的侵害。 JVM保护外部的沙箱。举一个简单的例子:恶意代码可以通过执行大量小内存分配来开始占用内存。您无法在 JVM 中执行任何操作来保护在同一 JVM 中运行的某些代码免受它的侵害。您只能对整个 JVM 实施限制。

所以唯一的解决方案是在不同的 JVM 中运行插件。

【讨论】:

  • 谢谢。我没有意识到可以捕获并忽略Thread.stop() 呼叫。我认为这是关键点,这意味着我必须使用不同的 JVM。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-24
  • 2014-04-15
  • 2012-07-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多