You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 88 Next »

To avoid data corruption in multithreaded Java programs, shared data must be protected from concurrent modifications and accesses. This can be performed at the object level by using synchronized methods or blocks, or by using dynamic lock objects. However, excessive use of locking may result in deadlocks (See CON08-J. Do not call alien methods that synchronize on the same objects as any callers in the execution chain). For instance, to avoid deadlocks, the paint(), dispose(), stop(), destroy() methods in an applet should not be synchronized because they are always called and used from dedicated threads.

"The Java programming language neither prevents nor requires detection of deadlock conditions." [[JLS 05]]. Deadlocks can arise when two or more threads request and release locks in different orders.

Noncompliant Code Example

This noncompliant code example can deadlock because of excessive synchronization. Assume that an attacker has two bank accounts and is capable of requesting two depositAllAmount() operations in succession, one each from the two threads started in main().

class BankAccount {
  private int balanceAmount;  // Total amount in bank account
	 
  private BankAccount(int balance) {
    this.balanceAmount = balance;
  }

  // Deposits the amount from this object instance to BankAccount instance argument ba 
  private void depositAllAmount(BankAccount ba) {
    synchronized (this) {
      synchronized(ba) {
        ba.balanceAmount += this.balanceAmount;
        this.balanceAmount = 0; // withdraw all amount from this instance
        ba.displayAllAmount();  // Display the new balanceAmount in ba (may cause deadlock)
      }
    } 
  }
  
  private synchronized void displayAllAmount() {
    System.out.println(balanceAmount);
  }

  public static void initiateTransfer(final BankAccount first, final BankAccount second) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        first.depositAllAmount(second);
      }
    });
    t.start();
  }
}

Objects of class BankAccount represent bank accounts. The balanceAmount field represents the total balance amount available for a particular object (bank account). A user is allowed to initiate an operation deposit all amount that transfers the balance amount from one account to another. This is equivalent to closing a bank account and transferring the balance to a different (existing or new) account.

Objects of this class are deadlock-prone. An attacker may cause the program to construct two threads that initiate balance transfers from two different BankAccount object instances, a and b. Consider the following code that does this:

BankAccount a = new BankAccount(5000);
BankAccount b = new BankAccount(6000);
initiateTransfer(a, b); // starts thread 1
initiateTransfer(b, a); // starts thread 2

The two transfers are performed in their own threads, from instance a to b and b to a. The first thread atomically transfers the amount from a to b by depositing the balance from a to b and withdrawing the entire balance from a. The second thread performs the reverse operation, that is, it transfers the balance from b to a and withdraws the balance from b. When executing depositAllAmount(), the first thread might acquire a lock on object a while the second thread may acquire a lock on object b. Subsequently, the first thread requests a lock on b which is already held by the second thread and the second thread requests a lock on a which is already held by the first thread. This constitutes a deadlock condition, as neither thread can proceed.

The threads in this program request monitors in varying order depending on the interleaving of method calls. If Thread T1 finishes executing before Thread T2, or T2 before T1, there are no issues because in these cases, locks are acquired and released in the same order. Sequences where the threads alternate, such as, T1, T2, T1, T2 may deadlock.

Compliant Solution (single private lock)

The deadlock can be avoided by using a single lock to acquire before doing any account transfers.

class BankAccount {
  private int balanceAmount;  // Total amount in bank account
	 
  private static final Object lock;

  private BankAccount(int balance) {
    this.balanceAmount = balance;
    this.lock = new Object();
  }

  // Deposits the amount from this object instance to BankAccount instance argument ba 
  private void depositAllAmount(BankAccount ba) {
    synchronized (lock) {
      ba.balanceAmount += this.balanceAmount;
      this.balanceAmount = 0; // withdraw all amount from this instance
      ba.displayAllAmount();  // Display the new balanceAmount in ba (may cause deadlock)
    } 
  }
  
  private void displayAllAmount() {
    synchronized (lock) {
      System.out.println(balanceAmount);
    }
  }

  public static void initiateTransfer(final BankAccount first, final BankAccount second) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        first.depositAllAmount(second);
      }
    });
    t.start();
  }
}

In this scenario, if two threads with two different BankAccount objects try to tranfer to each others' accounts simultaneously, deadlock cannot occur. One thread will acquire the private lock, complete its transfer, and release the lock, before the other thread may proceed.

This solution comes with a performance penalty, as a private static lock restricts the system to only performing one transfer at a time. Two transfers involving four distinct accounts (and distinct target accounts) may not happen concurrently. The impact of this penalty increases considerably as the number of BankAccount objects increase. Consequently this solution does not scale very well.

Compliant Solution (ordered locks)

This compliant solution ensures that multiple locks are acquired and released in the same order. It requires that an ordering over BankAccount objects is available. The ordering is enforced by having the class BankAccount extend the java.lang.Comparable interface and overriding the compareTo() method.

class BankAccount implements Comparable {
  private int balanceAmount;  // Total amount in bank account	 
  private final Object lock;

  private BankAccount(int balance) {
    this.balanceAmount = balance;
    this.lock = new Object();
  }

  // Deposits the amount from this object instance to BankAccount instance argument ba 
  private void depositAllAmount(BankAccount ba) {
    BankAccount former, latter;
    if (compareTo(ba) < 0) {
      former = this;
      latter = ba;
    } else {
      former = ba;
      latter = this;
    }
    synchronized (former) {
      synchronized (latter) {
        ba.balanceAmount += this.balanceAmount;
        this.balanceAmount = 0; // withdraw all amount from this instance
        ba.displayAllAmount(); // Display the new balanceAmount in ba (may cause deadlock)
      } 
    }
  }
 
  private synchronized void displayAllAmount() {
    System.out.println(balanceAmount);
  }

  public static void initiateTransfer(final BankAccount first, final BankAccount second) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        first.depositAllAmount(second);
      }
    });
    t.start();
  }

  public int compareTo(BankAccount ba) {
   if(this.balanceAmount < ba.balanceAmount) {
     return -1;
   } else if(this.balanceAmount > ba.balanceAmount) {
     return 1;
   } else {
     return 0;
   }
  }
}

In this compliant solution, whenever a transfer occurs, the two BankAccount objects are ordered, with the first object's lock being acquired before the second object's lock. Consequently if two threads attempt transfers between the same two accounts, they will both try to acquire the first account's lock first, with the result that one thread will acquire both locks, complete the transfer, and release both locks before the other may proceed.

Unlike the previous compliant solution, this solution incurs no performance penalty, as multiple transfers can occur concurrently as long as the transfers involve distinct target accounts.

Compliant Solution (ReentrantLock)

In this compliant solution, each BankAccount has a java.util.concurrent.locks.ReentrantLock associated with it. This permits the depositAllAmount() method to try acquiring both accounts' locks, but releasing its locks if it fails, and trying again later.

class BankAccount {
  private int balanceAmount;  // Total amount in bank account
  private final Lock lock = new ReentrantLock();
  private static final int TIME = 1000; // 1 second
	 
  private BankAccount(int balance) {
    this.balanceAmount = balance;
  }

  // Deposits the amount from this object instance to BankAccount instance argument ba 
  private void depositAllAmount(BankAccount ba) throws InterruptedException {
    while (true) {
      if (this.lock.tryLock()) {
        try {
          if (ba.lock.tryLock()) {
            try {
              ba.balanceAmount += this.balanceAmount;
              this.balanceAmount = 0; // withdraw all amount from this instance
              ba.displayAllAmount();  // Display the new balanceAmount in ba 
              break;
            } finally {
              ba.lock.unlock();
            }
          }
        } finally {
          this.lock.unlock();
        }
      }
      Thread.sleep(TIME);
    }
  }
  
  private void displayAllAmount() throws InterruptedException {
    while (true) {
      if (lock.tryLock()) {
        try {
          System.out.println(balanceAmount);
          break;
        } finally {
          lock.unlock();
        }
      }
      Thread.sleep(TIME);
    }
  }

  public static void initiateTransfer(final BankAccount first, final BankAccount second) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        try {
          first.depositAllAmount(second);
        } catch (InterruptedException e) {
          // Forward to handler
        }
      }
    });
    t.start();
  }
}

Deadlock is impossible in this code, because no method grabs a lock and holds it indefinitely. If a lock is acquired, but the system cannot proceed immediately, it releases the lock and sleeps before requesting the lock again.

Code that uses this lock behaves similar to synchronized code that uses the traditional monitor lock. ReentrantLock provides several other capabilities, for instance, the tryLock() method does not block waiting if another thread is already holding the lock. The class java.util.concurrent.locks.ReentrantReadWriteLock can be used when some thread requires a lock to write information while other threads require the lock to concurrently read the information.

Noncompliant Code Example

This noncompliant code example consists of an application that monitors web requests. Each request has a response time associated with it, as well as a measurement of network bandwidth. This example calculates the average bandwidth of all requests, and also calculates the avaerage reponse time of all requests.

public class WebRequest {
  private long bandwidth;
  private long responseTime;

  public long getBandwidth() {
    return bandwidth;
  }
  public long getResponseTime() {
    return responseTime;
  }
 
  public WebRequest(long bandwidth, long responseTime) {
    this.bandwidth = bandwidth;
    this.responseTime = responseTime;
  }
}

public class WebRequestAnalyzer {
  private final Vector<WebRequest> requests = new Vector<WebRequest>();
  
  public boolean addWebRequest(WebRequest request) {
    // lock on last element to prevent data race with calculate methods
    synchronized (requests.lastElement()) {
      return requests.add(new WebRequest( request.getBandwidth(), request.getResponseTime()));
    }
  }
  
  public double getAverageBandwidth() { 
    return calculateAverageBandwidth( 0, 0);
  }

  private double calculateAverageBandwidth(int i, long bandwidth) { 
    if (i > requests.size()) {
      return bandwidth / requests.size();
    }
    synchronized (requests.elementAt(i)) {
      numberOfPages += requests.get(i).getBandwidth();
      return calculateAverageBandwidth(++i, bandwidth);  
    }
  }

  public double getAverageResponseTime() { 
    return calculateAverageResponseTime( requests.size() - 1, 0);
  }

  private double calculateAverageResponseTime(int i, long responseTime) { 
    if (i <= -1) {		 
      return responseTime / requests.size();
    }     
    synchronized (requests.elementAt(i)) {
      responseTime += requests.get(i).getResponseTime();
      return calculateAverageResponseTime(--i, responseTime);
    }
  }
}

The monitoring application is built upon class WebRequestAnalyzer which maintains a list of requests. To be thread-safe, it starts off with an empty list, and defensively copies all data fed to it. Henceforth, any thread can get the average bandwidth or average response time of all web requests.

The statistics perform fine-grained locking on the individual requests' intrinsic locks. The locks permit new requests to be added up to when the computation is almost complete; at that point they prevent new requests from being added. Consequently, the statistics reported by the methods are accurate at the time the methods actually return their results.

Unfortunately, this implementation is prone to deadlock because the recursive calls within the synchronized regions of these methods acquire the intrinsic locks in opposite numerical orders. That is, calculateAverageBandwidth() requests locks from index 0 to requests.size() - 1 whereas calculateAverageResponseTime() requests them from index requests.size() - 1 to 0. Because of recursion, no previously acquired locks are released by either method. A deadlock occurs when two threads call these methods out of order in that, one thread calls calculateAverageBandwidth() while the other calls calculateAverageResponseTime() before either method has finished executing.

For example, if there are 20 requests in the vector, and one thread calls getAverageBandwidth(), it acquires the intrinsic lock for WebRequest 0, the first in the vector. Meanwhile if a second thread calls getAverageResponseTime(), it acquires the intrinsic lock for WebRequest 19, the last in the vector. Consequently, deadlock results, as neither thread can acquire all of the locks and proceed with the calculation.

Compliant Solution

In this compliant solution, the two calculation methods acquire and release locks in the same order.

public class WebRequestAnalyzer {
  // initial methods are unchanged

  public double getAverageBandwidth() { 
    return calculateAverageBandwidth( 0, 0);
  }

  private double calculateAverageBandwidth(int i, long bandwidth) { 
    if (i > requests.size()) {
      return bandwidth / requests.size();
    }
    synchronized (requests.elementAt(i)) {
      numberOfPages += requests.get(i).getBandwidth();
      return calculateAverageBandwidth(++i, bandwidth);  
    }
  }

  public double getAverageResponseTime() { 
    return calculateAverageResponseTime( 0, 0);
  }

  private double calculateAverageResponseTime(int i, long responseTime) { 
    if (i > requests.size()) {		 
      return responseTime / requests.size();
    }     
    synchronized (requests.elementAt(i)) {
      responseTime += requests.get(i).getResponseTime();
      return calculateAverageResponseTime(++i, responseTime);
    }
  }
}

Consequently, while one thread is calculating the average bandwidth or response time, another thread cannot interfere or induce a deadlock. This is because the other thread would first have to synchronize on the first WebRequest, which is impossible until the first calculation is complete.

Risk Assessment

Acquiring and releasing locks in the wrong order may result in deadlocks.

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

CON12- J

low

likely

high

P3

L3

Automated Detection

TODO

Related Vulnerabilities

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

References

[[JLS 05]] Chapter 17, Threads and Locks
[[Halloway 00]]
[[MITRE 09]] CWE ID 412 "Unrestricted Lock on Critical Resource"


CON11-J. Do not assume that declaring an object volatile guarantees visibility of its members      11. Concurrency (CON)      CON13-J. Do not try to force thread shutdown

  • No labels