Wiki Markup |
---|
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]). |
...
"The Java programming language neither prevents nor requires detection of deadlock conditions." \[[JLS 05|AA. Java References#JLS 05]\]. Deadlocks can arise when two or more threads request and release locks in different orders. Consequently, to avoid deadlock, locks should be acquired and released in the same order and synchronization should be limited to where it is absolutely necessary. For instance, to avoid deadlocks in an applet, the {{paint()}}, {{dispose()}}, {{stop()}}, and {{destroy()}} methods should not be synchronized because they are always called and used from dedicated threads. |
...
The advice of this guideline also applies to programs that need to work with a limited set of resources. For instance, deadlocks that can arise when two or more threads are waiting for each other to release resources such as database connections, can be mitigated by letting each thread retry the operation after some random amount of time if a requested lock is unavailable. h2. |
...
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 {{depositAmount()}} operations in succession, one each from the two threads started in {{main() |
...
Code Block | ||
---|---|---|
| ||
}}. {code:bgColor=#FFcccc} final class BankAccount { private double balanceAmount; // Total amount in bank account BankAccount(double balance) { this.balanceAmount = balance; } // Deposits the amount from this object instance to BankAccount instance argument ba private void depositAmount(BankAccount ba, double amount) { synchronized (this) { synchronized(ba) { if(amount > balanceAmount) { throw new IllegalArgumentException("Transfer cannot be completed"); } ba.balanceAmount += amount; this.balanceAmount -= amount; } } } public static void initiateTransfer(final BankAccount first, final BankAccount second, final double amount) { Thread t = new Thread(new Runnable() { public void run() { first.depositAmount(second, amount); } }); t.start(); } } {code} 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 amount_ that atomically transfers a specified amount from one account to another. |
...
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: |
...
{code |
} BankAccount a = new BankAccount(5000); BankAccount b = new BankAccount(6000); initiateTransfer(a, b, 1000); // starts thread 1 initiateTransfer(b, a, 1000); // starts thread 2 {code} The two transfers, from instance {{a}} to {{b}} and {{b}} to {{a}}, are performed in their own threads. The first thread atomically transfers the amount from {{a}} to {{b}} by depositing it in account {{b}} and then withdrawing the same amount from {{a}}. The second thread performs the reverse operation, that is, it transfers the balance from {{b}} to {{a}}. When executing {{depositAmount()}}, the first thread acquires a lock on object {{a}}. It is possible for the second thread to acquire a lock on object {{b}} before the first thread can lock on {{b}}. Subsequently, the first thread would request a lock on {{b}} which is already held by the second thread. The second thread would request a lock on {{a}} which is already held by the first thread. This constitutes a deadlock condition, as neither thread can proceed. |
...
This noncompliant code example may or may not deadlock depending on the scheduling details of the platform. Deadlock can occur when two threads request the same two locks in different orders and each thread obtains a lock that prevents the other thread from completing its transfer. Deadlock may not occur when two threads request the same two locks, but one thread completes its transfer before the other thread begins. Deadlock also cannot occur if the two threads request the same two locks in the same order (which would happen if they both transfer money from one account to a second account), or if two transfers involving distinct accounts occur concurrently. |
...
Compliant Solution (static
internal private lock)
The deadlock can be avoided by using a private static final
internal lock object before performing any account transfers.
Code Block | ||
---|---|---|
| ||
h2. Compliant Solution ({{static}} internal private lock) The deadlock can be avoided by using a {{private static final}} internal lock object before performing any account transfers. {code:bgColor=#ccccff} final class BankAccount { private double balanceAmount; // Total amount in bank account private static final Object lock = new Object(); BankAccount(double balance) { this.balanceAmount = balance; } // Deposits the amount from this object instance to BankAccount instance argument ba private void depositAmount(BankAccount ba, double amount) { synchronized (lock) { if (amount > balanceAmount) { throw new IllegalArgumentException("Transfer cannot be completed"); } ba.balanceAmount += amount; this.balanceAmount -= amount; } } public static void initiateTransfer(final BankAccount first, final BankAccount second, final double amount) { Thread t = new Thread(new Runnable() { public void run() { first.depositAmount(second, amount); } }); t.start(); } } {code} In this scenario, if two threads with two different {{BankAccount}} objects try to transfer 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 can proceed. |
...
This solution comes with a performance penalty because a {{private static}} lock restricts the system to performing only one transfer at a time. Two transfers involving four distinct accounts (with 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. |
...
h2. 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}} implement the {{java.lang.Comparable}} interface and overriding the {{compareTo()}} method. |
...
{code | ||
:bgColor | =#ccccff | } final class BankAccount implements Comparable<BankAccount> { private double balanceAmount; // Total amount in bank account private final Object lock; private final long id; // Unique for each BankAccount private static long nextID = 0; // Next unused id BankAccount(double balance) { this.balanceAmount = balance; this.lock = new Object(); this.id = this.nextID++; } public int compareTo(BankAccount ba) { // No need to check for overflow because the type long is large // enough to hold the required number of bank accounts return (this.id - ba.id); } // Deposits the amount from this object instance to BankAccount instance argument ba public void depositAmount(BankAccount ba, double amount) { BankAccount former, latter; if (compareTo(ba) < 0) { former = this; latter = ba; } else { former = ba; latter = this; } synchronized (former) { synchronized (latter) { if (amount > balanceAmount) { throw new IllegalArgumentException("Transfer cannot be completed"); } ba.balanceAmount += amount; this.balanceAmount -= amount; } } } public static void initiateTransfer(final BankAccount first, final BankAccount second, final double amount) { Thread t = new Thread(new Runnable() { public void run() { first.depositAmount(second, amount); } }); t.start(); } } {code} Whenever a transfer occurs, the two {{BankAccount}} objects are ordered so that the {{first}} object's lock is 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 before the second account's lock, with the result that one thread will acquire both locks, complete the transfer, and release both locks before the other thread can proceed. |
...
Unlike the previous compliant solution, this solution incurs no performance penalty because multiple transfers can occur concurrently as long as the transfers involve distinct target accounts. h2. |
...
Compliant Solution (ReentrantLock) |
...
In this compliant solution, each {{BankAccount}} has a {{java.util.concurrent.locks.ReentrantLock}} associated with it. This permits the {{depositAmount()}} method to try acquiring both accounts' locks, but releasing the locks if it fails, and trying again later. |
...
{code | ||
:bgColor | =#ccccff | } final class BankAccount { private double balanceAmount; // Total amount in bank account private final Lock lock = new ReentrantLock(); private final Random number = new Random(123L); BankAccount(double balance) { this.balanceAmount = balance; } // Deposits the amount from this object instance to BankAccount instance argument ba private void depositAmount(BankAccount ba, double amount) throws InterruptedException { while (true) { if (this.lock.tryLock()) { try { if (ba.lock.tryLock()) { try { if (amount > balanceAmount) { throw new IllegalArgumentException("Transfer cannot be completed"); } ba.balanceAmount += amount; this.balanceAmount -= amount; break; } finally { ba.lock.unlock(); } } } finally { this.lock.unlock(); } } int n = number.nextInt(1000); int TIME = 1000 + n; // 1 second + random delay to prevent livelock Thread.sleep(TIME); } } public static void initiateTransfer(final BankAccount first, final BankAccount second, final double amount) { Thread t = new Thread(new Runnable() { public void run() { try { first.depositAmount(second, amount); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Reset interrupted status } } }); t.start(); } } {code} Deadlock is impossible in this compliant solution because no method grabs a lock and holds it indefinitely. If the current object's lock is acquired, but the the second lock is unavailable, the first lock is released and the thread sleeps for some specified amount of time before retrying. |
...
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
Consider an immutable class WebRequest
that encapsulates a web request to a server.
Code Block |
---|
h2. Noncompliant Code Example Consider an immutable class {{WebRequest}} that encapsulates a web request to a server. {code} // Immutable WebRequest public final class WebRequest { private final long bandwidth; private final long responseTime; public WebRequest(long bandwidth, long responseTime) { this.bandwidth = bandwidth; this.responseTime = responseTime; } public long getBandwidth() { return bandwidth; } public long getResponseTime() { return responseTime; } } {code} Each request has a response time associated with it along with a measurement of network bandwidth required to fulfill the request. |
...
This noncompliant code example consists of an application that monitors web requests. It calculates the average bandwidth and average response time required to service all incoming requests. |
...
{code | ||
:bgColor | =#FFcccc | } public final class WebRequestAnalyzer { private final Vector<WebRequest> requests = new Vector<WebRequest>(); public boolean addWebRequest(WebRequest request) { // Lock on last element to prevent data race with the calculateAverageResponseTime() method synchronized (requests.lastElement()) {if(requests.size() == 0) { return requests.add(new WebRequest(request.getBandwidth(), request.getResponseTime())); } // Lock on last element to prevent data race with the calculateAverageResponseTime() method synchronized (requests.lastElement()) { // Defensive copying return requests.add(new WebRequest(request.getBandwidth(), request.getResponseTime())); } } public double getAverageBandwidth() { return calculateAverageBandwidth(0,if (requests.size() == 0); { } public double getAverageResponseTime() {throw new IllegalStateException("The vector is empty!"); } return calculateAverageResponseTime(requests.size() - 1calculateAverageBandwidth(0, 0); } privatepublic double calculateAverageBandwidth(int i, long bandwidthgetAverageResponseTime() { if (i > requests.size() == 0) { return bandwidth / requests.size(); throw new IllegalStateException("The vector is empty!"); } synchronized (requests.elementAt(i)) {} bandwidth += requests.get(i).getBandwidth(); return calculateAverageBandwidth(++i, bandwidth); // Acquires locks in increasing order }return calculateAverageResponseTime(requests.size() - 1, 0); } private double calculateAverageResponseTimecalculateAverageBandwidth(int i, long responseTimebandwidth) { if (i <== -1requests.size()) { System.out.println(bandwidth); return responseTimebandwidth / requests.size(); } synchronized (requests.elementAt(i)) { responseTimebandwidth += requests.get(i).getResponseTimegetBandwidth(); return calculateAverageResponseTimecalculateAverageBandwidth(--++i, responseTimebandwidth); // Acquires locks in decreasingincreasing order } } } |
The monitoring application is built around class WebRequestAnalyzer
that maintains a list of web requests using vector requests
. The vector requests
is suitably initialized after defensively copying the requests. Any thread can get the average bandwidth or average response time of all web requests by invoking the getAverageBandwidth()
and getAverageResponseTime()
methods.
These methods use fine-grained locking by holding locks on individual elements (web requests) of the vector. The locks permit new requests to be added while the computations are still underway. Consequently, the statistics reported by the methods are accurate at the time they return the 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 of WebRequest
0, the first element in the vector. Meanwhile, if a second thread calls getAverageResponseTime()
, it acquires the intrinsic lock of WebRequest
19, the last element in the vector. Consequently, a deadlock results because neither thread can acquire all of the locks and proceed with the calculations.
Compliant Solution
In this compliant solution, the two calculation methods acquire and release locks in the same order, beginning with the first web request in the vector.
Code Block | ||
---|---|---|
| ||
public final class WebRequestAnalyzer { private final Vector<WebRequest> requests = new Vector<WebRequest>(); public boolean addWebRequest(WebRequest request 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); // Acquires locks in decreasing order } } } {code} {mc} // Hidden main() method public static void main(String[] args) { final WebRequestAnalyzer wra = new WebRequestAnalyzer(); wra.addWebRequest(new WebRequest(10,20)); wra.addWebRequest(new WebRequest(30,60)); new Thread(new Runnable() { public void run() { wra.getAverageResponseTime(); } }).start(); new Thread(new Runnable() { public void run() { // No need to lock on the last element because locks are acquired in increasing order return requests.add(new WebRequest(request.getBandwidth(), request.getResponseTime())); } public double getAverageBandwidth() { return calculateAverageBandwidth(0, 0); } public double getAverageResponseTime() { return calculateAverageResponseTime(0, 0); } private double calculateAverageBandwidth(int i, long bandwidth) { if (i > requests.size()) { return bandwidth / requests.size(); } synchronized (requests.elementAt(i)) { // Acquires locks in increasing order bandwidth += requests.get(i).getBandwidth(); return calculateAverageBandwidth(++i, bandwidth); } } 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); // Acquires locks in increasing order } } } |
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 need 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
Wiki Markup |
---|
\[[JLS 05|AA. Java References#JLS 05]\] [Chapter 17, Threads and Locks|http://java.sun.com/docs/books/jls/third_edition/html/memory.html]
\[[Halloway 00|AA. Java References#Halloway 00]\]
\[[MITRE 09|AA. Java References#MITRE 09]\] [CWE ID 412|http://cwe.mitre.org/data/definitions/412.html] "Unrestricted Lock on Critical Resource" |
...
wra.getAverageBandwidth();
}
}).start();
}
{mc}
The monitoring application is built around class {{WebRequestAnalyzer}} that maintains a list of web requests using vector {{requests}}. The vector {{requests}} is suitably initialized after defensively copying the requests. Any thread can get the average bandwidth or average response time of all web requests by invoking the {{getAverageBandwidth()}} and {{getAverageResponseTime()}} methods.
These methods use fine-grained locking by holding locks on individual elements (web requests) of the vector. The locks permit new requests to be added while the computations are still underway. Consequently, the statistics reported by the methods are accurate at the time they return the 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 of {{WebRequest}} 0, the first element in the vector. Meanwhile, if a second thread calls {{getAverageResponseTime()}}, it acquires the intrinsic lock of {{WebRequest}} 19, the last element in the vector. Consequently, a deadlock results because neither thread can acquire all of the locks and proceed with the calculations.
h2. Compliant Solution
In this compliant solution, the two calculation methods acquire and release locks in the same order, beginning with the first web request in the vector.
{code:bgColor=#ccccff}
public final class WebRequestAnalyzer {
private final Vector<WebRequest> requests = new Vector<WebRequest>();
public boolean addWebRequest(WebRequest request) {
// No need to lock on the last element because locks are acquired in increasing order
return requests.add(new WebRequest(request.getBandwidth(), request.getResponseTime()));
}
public double getAverageBandwidth() {
return calculateAverageBandwidth(0, 0);
}
public double getAverageResponseTime() {
return calculateAverageResponseTime(0, 0);
}
private double calculateAverageBandwidth(int i, long bandwidth) {
if (i == requests.size()) {
return bandwidth / requests.size();
}
synchronized (requests.elementAt(i)) { // Acquires locks in increasing order
bandwidth += requests.get(i).getBandwidth();
return calculateAverageBandwidth(++i, bandwidth);
}
}
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); // Acquires locks in increasing order
}
}
}
{code}
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 need to synchronize on the first {{WebRequest}}, which is impossible until the first calculation is complete.
h2. 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 | {color:green}{*}P3{*}{color} | {color:green}{*}L3{*}{color} |
h3. Automated Detection
TODO
h3. Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the [CERT website|https://www.kb.cert.org/vulnotes/bymetric?searchview&query=FIELD+KEYWORDS+contains+CON34-J].
h2. References
\[[JLS 05|AA. Java References#JLS 05]\] [Chapter 17, Threads and Locks|http://java.sun.com/docs/books/jls/third_edition/html/memory.html]
\[[Halloway 00|AA. Java References#Halloway 00]\]
\[[MITRE 09|AA. Java References#MITRE 09]\] [CWE ID 412|http://cwe.mitre.org/data/definitions/412.html] "Unrestricted Lock on Critical Resource"
----
[!The CERT Sun Microsystems Secure Coding Standard for Java^button_arrow_left.png!|CON11-J. Do not assume that declaring an object reference volatile guarantees visibility of its members] [!The CERT Sun Microsystems Secure Coding Standard for Java^button_arrow_up.png!|11. Concurrency (CON)] [!The CERT Sun Microsystems Secure Coding Standard for Java^button_arrow_right.png!|CON13-J. Ensure that threads are stopped cleanly]
|