[1792271 views]

[]

Odi's astoundingly incomplete notes

New entries | Code

Stackoverflow can break your locking scheme

Simple Java locking code, looking innocent, right?
    private ReentrantLock lock = new ReentrantLock();
    private void doIt() {
        boolean locked = lock.tryLock();
        try {
            ...
        } finally {
            if (locked) lock.unlock();
        }
    }

Well, only until you realize that a call to unlock() causes 4 nested method calls. So unlock() requires a little bit of free stack. But what happens when stack is tight? Well... unlock() will simply fail with a StackOverflowError of course and your lock is leaked.

Here is a little demo:
public class StackOfl implements Runnable {
    public static void main(String[] args) {
        new StackOfl().run();
    }

    @Override
    public void run() {
        try {
            test();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("still locked? "+ lock.isLocked());
        System.out.println(lock.toString());
    }
    
    private void test() {
        overflowStack();
    }

    private ReentrantLock lock = new ReentrantLock();
    private void overflowStack() {
        boolean locked = lock.tryLock();
        try {
            overflowStack(); // 1. StackOverflowError occurs here
        } finally {
            if (locked) lock.unlock(); // 2. StackOverflowError occurs within here
        }
        // avoid tail recursion optimization in compiler
        System.out.println("unreachable");
    }
}
This code overflows the stack on purpose within the try/finally block. Java dutifully executes the finally block on the first StackOverflowError just to run into another StackOverflowError during unlock(), which is the one that propagates up.

The output is something like this:
java.lang.StackOverflowError
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at StackOfl.overflowStack(StackOfl.java:39)
	at StackOfl.overflowStack(StackOfl.java:37)
	at StackOfl.overflowStack(StackOfl.java:37)
...
still locked? true
java.util.concurrent.locks.ReentrantLock@7852e922[Locked by thread main]
You can clearly see the lock is leaked.
posted on 2022-02-22 08:37 UTC in Code | 0 comments | permalink