From 7e3d3625aa8e66af97e898c1914dd234fd742a81 Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Thu, 31 Mar 2016 11:02:52 +0100
Subject: [PATCH] Don't allow reentrant transactions.

The database's transaction lock is reentrant, meaning that a thread that's already holding the lock can acquire it again. This would allow a thread that already has a transaction in progress to start another transaction, which could cause transaction isolation issues and/or lock timeouts on the database's internal locks.

Check that the current thread isn't already holding the lock when starting a transaction.
---
 .../db/DatabaseComponentImpl.java             |  7 ++-
 .../db/DatabaseComponentImplTest.java         | 50 +++++++++++++++++++
 2 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index d001599035..4247040468 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -59,7 +59,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.logging.Logger;
 
@@ -80,7 +79,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	private final EventBus eventBus;
 	private final ShutdownManager shutdown;
 	private final AtomicBoolean closed = new AtomicBoolean(false);
-	private final ReadWriteLock lock = new ReentrantReadWriteLock(true);
+	private final ReentrantReadWriteLock lock =
+			new ReentrantReadWriteLock(true);
 
 	private volatile int shutdownHandle = -1;
 
@@ -119,6 +119,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	}
 
 	public Transaction startTransaction(boolean readOnly) throws DbException {
+		// Don't allow reentrant locking
+		if (lock.getReadHoldCount() > 0) throw new IllegalStateException();
+		if (lock.getWriteHoldCount() > 0) throw new IllegalStateException();
 		if (readOnly) lock.readLock().lock();
 		else lock.writeLock().lock();
 		try {
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index 02531d08d1..eed704adde 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -65,6 +65,7 @@ import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDO
 import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
 public class DatabaseComponentImplTest extends BriarTestCase {
@@ -1447,4 +1448,53 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		context.assertIsSatisfied();
 	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testCannotStartReadTransactionDuringReadTransaction()
+			throws Exception {
+		testCannotStartTransactionDuringTransaction(true, true);
+	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testCannotStartWriteTransactionDuringReadTransaction()
+			throws Exception {
+		testCannotStartTransactionDuringTransaction(true, false);
+	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testCannotStartReadTransactionDuringWriteTransaction()
+			throws Exception {
+		testCannotStartTransactionDuringTransaction(false, true);
+	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testCannotStartWriteTransactionDuringWriteTransaction()
+			throws Exception {
+		testCannotStartTransactionDuringTransaction(false, false);
+	}
+
+	private void testCannotStartTransactionDuringTransaction(
+			boolean firstTxnReadOnly, boolean secondTxnReadOnly)
+			throws Exception {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
+		final EventBus eventBus = context.mock(EventBus.class);
+
+		context.checking(new Expectations() {{
+			oneOf(database).startTransaction();
+			will(returnValue(new Object()));
+		}});
+
+		DatabaseComponent db = createDatabaseComponent(database, eventBus,
+				shutdown);
+
+		assertNotNull(db.startTransaction(firstTxnReadOnly));
+		try {
+			db.startTransaction(secondTxnReadOnly);
+		} finally {
+			context.assertIsSatisfied();
+		}
+	}
 }
-- 
GitLab