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:
SecurityManager
in place: all the classes can do is number crunching.Thread.join()
on anything they haven't created.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);
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With