原文:www.securecoding.cert.org,THI04-J. Ensure thatthreads performing blocking operations can be terminated。
来自开涛的博客:http://mp.weixin.qq.com/s/RPX9GygSuCx0f84BsZOIxw
在网络或文件I/O上操作阻塞的线程和任务,必须给调用者提供一种明确终止执行的机制,防止出现拒绝服务(DoS)漏洞。
违规代码示例(Blocking I/O, Volatile Flag)
该违规代码示例使用了 THI05-J. Do not use Thread.stop() to terminate threads中的建议,使用一个volatile变量done判断是否安全的终止了线程。 不过当线程被网络I/O上的readLine() 操作阻塞时,在网络I/O完成之前,它是不能响应心设置的done变量的。因此,线程终止操作可以被无限期的延迟,直到readLine()网络I/O完成。
public class SocketReader implements Runnable{
private final Socket socket;
private final BufferedReader in;
private volatile boolean done=false;
private final Object lock=new Object();
public SocketReader(String host,int port) throws IOException{
this.socket=new Socket(host,port);
this.in=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
}
public void run() {
try{
synchronized(lock){
readData();
}
}catch(IOException in){
//forward to handler
}
}
public void readData() throws IOException{
String string;
while(!done&& (string=in.readLine())!=null){
//Blocks until end of stream(null)
}
}
public void shutDown(){
done=true;
}
public static void main(String[] args) throws IOException, InterruptedException {
SocketReader reader=new SocketReader("somehost",25);
Thread thread=new Thread(reader);
thread.start();
Thread.sleep(1000);
reader.shutDown();
}
}
违规代码示例(Blocking I/O, Interruptible)
该违规代码示例和上面的例子类似,但使用线程中断来终止线程。java.net.Socket上的网络I/O操作是阻塞的,线程中断引起的线程终止操作也可以被无限期的延迟。
public class SocketReader1 implements Runnable {
// other methods...
private final Socket socket;
private final BufferedReader in;
private final Object lock = new Object();
public SocketReader1(String host, int port) throws IOException {
this.socket = new Socket(host, port);
this.in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
}
public void readData() throws IOException {
String string;
while (!Thread.interrupted() && (string = in.readLine()) != null) {
// Blocks until end of stream(null)
}
}
public void run() {
try {
synchronized (lock) {
readData();
}
} catch (IOException in) {
// forward to handler
}
}
public static void main(String[] args) throws IOException, InterruptedException {
SocketReader reader = new SocketReader("somehost", 25);
Thread thread = new Thread(reader);
thread.start();
thread.sleep(1000);
thread.interrupt();
}
}
合规解决方案(Close Socket Connection)
合规解决方案是通过在shutdown方法关闭socket来终止阻塞的网络I/O操作。当socket关闭后,readLine()方法会抛出SocketException异常,从而继续线程中的其他处理。注意,想要即保持连接存活,同时干净地立即停止线程,这是不可能的。
public class SocketReaderRight implements Runnable{
private final Socket socket;
private final BufferedReader in;
private volatile boolean done=false;
private final Object lock=new Object();
public SocketReaderRight(String host,int port) throws IOException{
this.socket=new Socket(host,port);
this.in=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
}
public void run() {
try{
synchronized(lock){
readData();
}
}catch(IOException in){
//forward to handler
}
}
public void readData() throws IOException{
String string;
try {
while((string=in.readLine())!=null){
//Blocks until end of stream(null)
}
} catch (Exception e) {
shutDown();
}
}
public void shutDown() throws IOException{
socket.close();
}
public static void main(String[] args) throws IOException, InterruptedException {
SocketReader reader=new SocketReader("somehost",25);
Thread thread=new Thread(reader);
thread.start();
Thread.sleep(1000);
reader.shutDown();
}
}
当从main()方法调用了shutdown()方法后,readData()中的finally块又调用了一次shutdown(),再次关闭socket。这种情况不用担心,当socket已经被关闭,再次关闭不作任何操作。
当执行异步I/O时,可以调用java.nio.channels.Selector的close()或者wakeup()来立即中断java.nio.channels.Selector上的阻塞操作。(译者注:可以参考笔者的下一篇)
当出现阻塞状态之后必须执行额外的操作,可以使用一个boolean值标识等待终止。当需要用这种标志补全代码时,shutdown()方法应设置标志为false从而让线程退出while循环。
合规解决方案(InterruptibleChannel)
该合规解决方案没使用Socket连接而是使用一个可中断channeljava.nio.channels.SocketChannel。如果线程执行的网络I/O在读取数据时使用Thread.interrupt() 方法中断,线程会收到一个ClosedByInterruptException异常,且channel立即关闭,线程状态状态也会被设置。
public class SocketReaderRight1 implements Runnable{
private final SocketChannel sc;
private final String host;
private final int port;
private final Object lock=new Object();
public SocketReaderRight1(String host,int port) throws IOException{
this.host=host;
this.port=port;
this.sc=SocketChannel.open(new InetSocketAddress(host,port));
}
public void run() {
ByteBuffer buf=ByteBuffer.allocate(1024);
try{
synchronized(lock){
while(!Thread.interrupted()){
sc.read(buf);
//...
}
}
}catch(IOException ie){
//forward to handler
}
}
public static void main(String[] args) throws IOException, InterruptedException {
SocketReader reader=new SocketReader("somehost",25);
Thread thread=new Thread(reader);
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
此技术中断当前线程。然而,它停止线程只是通过轮询 Thread.interrupted() 方法的线程状态状态并在线程中断状态时终止线程实现的。使用SocketChannel 时要确保while循环中的条件在收到中断时立即进行测试,即使读取通常是一个阻塞操作。同样地,调用 java.nio.channels.Selector 所在的阻塞线程上的 interrupt() 方法也导致线程被唤醒。
Database Connection中查询的中断
违规代码示例(Database Connection)
该不合规示例展示了一个线程安全的DBConnector类,其每个线程创建一个JDBC连接。每一个连接属于一个线程,不会与其他线程共享。这是一个常见的用例,因为JDBC连接有意设计为单线程。
public class DataBaseConnection implements Runnable {
private final String query;
DataBaseConnection(String query) {
this.query = query;
}
public void run() {
Connection connection;
try {
connection = DriverManager.getConnection("jdbc:driver:name", "username", "password");
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
} catch (Exception e) {
// TODO: handle exception
}
}
public static void main(String[] args) throws InterruptedException {
DataBaseConnection connector = new DataBaseConnection("suitable query");
Thread thread = new Thread(connector);
thread.start();
thread.sleep(5000);
thread.interrupt();
}
}
数据库连接像socket,没有内在的可中断性。因此,该设计无法实现当相应的线程阻塞在一个慢查询上时,客户端通过关闭资源来尝试取消任务, 如一个join操作。
合规解决方案(Statement.cancel())
该合规解决方案使用ThreadLocal 包装连接,线程调用initialValue()方法获取一个唯一的连接实例。这种方法可以提供一个cancelStatement(),以便其他线程或客户端可以在需要时中断慢查询。 cancelStatement() 方法调用 Statement.cancel()方法实现。
public class DBConnectorRight implements Runnable{
final String query;
private volatile Statement stmt;
DBConnectorRight(String query){
this.query=query;
}
private static final ThreadLocal<Connection> connectionHolder=
new ThreadLocal<Connection>(){
Connection connection=null;
@Override
public Connection initialValue(){
try {
connection=DriverManager.getConnection("jdbc:driver:name","username","password");
} catch (SQLException e) {
// Forward to handler
}
return connection;
}
};
public Connection getConnection(){
return connectionHolder.get();
}
public boolean cancelStatement(){
Statement tmpStmt=stmt;
if(tmpStmt!=null){
try {
tmpStmt.cancel();
return true;
} catch (Exception e) {
// Forword to handler
}
}
return false;
}
@Override
public void run() {
try {
if(getConnection()!=null){
stmt=getConnection().createStatement();
}
if(stmt==null||(stmt.getConnection()!=getConnection())){
throw new IllegalStateException();
}
ResultSet rs=stmt.executeQuery(query);
} catch (Exception e) {
// forward to handler
}
}
public static void main(String[] args) throws InterruptedException {
DBConnectorRight connector=new DBConnectorRight("suitable query");
Thread thread=new Thread(connector);
thread.start();
thread.sleep(4000);
connector.cancelStatement();
}
}
Statement.cancel() 方法取消查询需要数据库和驱动两者都支持(译者注:比如mysql通过发送KILL QUERY 命令进行取消)。如果数据库或驱动不支持取消,则取消查询是不可能的。
根据Java API,Statement接口文档:
默认情况下,每个Statement同一时刻尽可以打开一个ResultSet对象。因此,如果一个ResultSet对象上的读取与另一个上的读取是交替的,那么每个ResultSet对象必须由不同的Statement对象生成。
该合规解决方案可以确保只有一个与Statement关联的ResultSet属于一个实例,且只有一个线程能访问到查询结果。