11

I'm aware that Thread.stop() is deprecated, and for good reason: it is not, in general, safe. But that doesn't mean that it is never safe... as far as I can see, it is safe in the context in which I want to use it; and, as far as I can see, I have no other option.

The context is a third-party plug-in for a two-player strategy game: chess will do as the working example. The third-party code needs to be given a current board state, and (say) 10 seconds to decide on its move. It can return its move and terminate within the allowable time, or it can, whenever it wants to, signal its current preferred move; if the time limit expires, it should be stopped in its tracks, and its most recent preferred move should be played.

Writing the plug-in to stop gracefully on request is not an option: I need to be able to take arbitrary untrusted third-party plug-ins. So I have to have some way of forcibly terminating it.

Here's what I'm doing to lock it down:

  1. The classes for the plug-in get put into their own thread, into their own thread group.
  2. They are loaded with a class loader that has a highly restrictive SecurityManager in place: all the classes can do is number crunching.
  3. The classes don't get a reference to any other threads, so they can't use Thread.join() on anything they haven't created.
  4. The plug-in gets only two references to objects from the host system. One is the state of the chess board; this is a deep copy, and is thrown away afterwards, so it doesn't matter if it gets into an inconsistent state. The other is a simple object that allows the plug-in to set its current preferred move.
  5. I'm checking when CPU time has exceeded the limit, and I'm periodically checking that at least one thread of the plug-in is doing something (so that it can't sleep indefinitely and avoid CPU time ever hitting the limit).
  6. The preferred move doesn't have enough state to be inconsistent, but in any case it is carefully and defensively cloned after it is returned, and the one that was returned is discarded. By this point, there is nothing left in the host system to which the plug-in had a reference: no threads, no object instances.

In consequence, it seems, the plug-in can't leave anything in an inconsistent state (except for any objects it might create, which will then be discarded); and it can't affect any other threads (except for any threads it spawns, which will be in the same ThreadGroup, and so will also be killed off).

It looks to me as though the reasons that Thread.stop() is deprecated don't apply here (by design). Have I missed anything? Is there still danger here? Or have I isolated things carefully enough that there can't be a problem?

And is there a better way? The only alternative, I think, is to start up a whole new JVM to run the untrusted code, and forcibly kill the process when it's no longer wanted, but that has a thousand other problems (expensive, fragile, OS-dependent).

Please note: I'm not interested in answers along the lines of "Ooh, it's deprecated for a reason, you want to watch it, mate." I know it's deprecated for a reason, and I completely understand why it is not safe to be let out of the cage in general. What I am asking is whether there is a specific reason for thinking that it is unsafe in this context.

For what it's worth, here is the (abridged) relevant bit of the code:

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);
    }
}
chiastic-security
  • 20,430
  • 4
  • 39
  • 67
  • Using an ExecutorService may be a more elegant solution for what you are trying to do. – ControlAltDel Aug 25 '14 at 16:03
  • Looks like that might be a bit cleaner in some respects, but it won't solve the fundamental problem. Javadoc for `ExecutorService` says: "There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. For example, typical implementations will cancel via `Thread.interrupt()`, so any task that fails to respond to interrupts may never terminate." – chiastic-security Aug 25 '14 at 16:07
  • 2
    A plugin could still mess with state inside the JDK (if someone wanted to be malicious) and your ClassLoader probably doesn't load another set of JDK classes? You need to make up your mind: your approach sounds practical for defending against the majority of *sloppy* plugins. But someone wants to mess up the VM, they probably still find a way (after all they never managed to plug all the holes for applets). Whats your goal? – Durandal Aug 25 '14 at 16:09
  • @Durandal I did wonder about this. Are there synchronized objects inside the JVM that any class can get to and leave in an inconsistent state, even under a restrictive security manager? Do you have any examples? I can't think of any. – chiastic-security Aug 25 '14 at 16:17
  • @chiastic-security Well, that might depend on how *restrictive* the SecurityManager ultimately is, I/O completely forbidden? Thread creation forbidden? And what about places where you can "add" something to the JDK, e.g. loading a Resource (e.g. Font) or hijack a JDK spawned thread (e.g. EventQueue.invokeLater). For concrete examples of past security problems (I never really cared), the list of bug fixes with regards to applets is probably also a good source. – Durandal Aug 25 '14 at 16:58
  • 1
    “The preferred move doesn't have enough state to be inconsistent, but … is … defensively cloned after it is returned” That makes no sense. If it is something you can clone, it can be inconsistent as well. There is not much that can be safely used in the context of a stopped thread. As soon as it consist of more than one variable or if it is something other than a primitive value or truly immutable object, it can become inconsistent. And for a primitive value or a truly immutable object cloning makes no sense. – Holger Aug 25 '14 at 17:20
  • @Holger Yes, I phrased it badly. The returned value represents a move on the board. It gets cloned and examined; if it is inconsistent, it is treated as an illegal move. It doesn't have anything in it that assumes a consistent state. – chiastic-security Aug 25 '14 at 18:22
  • I'd say the bigger problem is that sandboxing in all JVMs is everything but trustworthy - that's one reason why the java browser plugin is such a big security risk. Also there are do many things one could overlook (e.g. can you forbid the untrusted code to acquire a lock on a class? Otherwise locking on Thread will disable thread creation for everyone.) Since you're copying the input and output anyhow, what about spawning an extra process? In that case you'd have to rely on OS security measures (not platform independent and work to set up), but those are much better tested I'd say. – Voo Aug 25 '14 at 18:30
  • http://stackoverflow.com/a/831227/1121883 – Liviu Stirb Aug 26 '14 at 08:38

1 Answers1

3

While I can imagine scenarios where code that you have carefully reviewed may be safely stopped with Thread.stop(), you are talking about the opposite, code that you distrust so much that you are attempting to restrict its actions by a SecurityManager.

So what can go wrong? First of all, stopping does not work more reliable than interruption. While it is easier to ignore an interruption, a simple catch(Throwable t){} is enough to make Thread.stop() not work anymore and such a catch may appear in code even without the intention of preventing Thread.stop() from working, though it is bad coding style. But coders wanting to ignore Thread.stop() might even add a catch(Throwable t){} and/or catch(ThreadDeath t){} intentionally.

Speaking of bad coding style, locking should always be implemented using try … finally … to ensure that the lock is guaranteed to be freed even in the exceptional case. If the stopped code failed to do this right, the lock may remain locked when the thread will be stopped.

The bottom line is that within the same JVM you can’t protect yourself against untrusted code. The JVM is the sandbox which protects the outside. Just one simple example: a malicious code could start eating up memory by performing lots of small memory allocations. There is nothing you can do within the JVM to protect some code running in the same JVM against it. You can only enforce limits for the entire JVM.

So the only solution is to run the plugins in a different JVM.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks. I hadn't realised it was possible to catch and ignore a `Thread.stop()` call. That is the key point, I think, and means I have to go with a different JVM. – chiastic-security Aug 26 '14 at 10:35