From 4ca4ffcaa974f5f32954b54bec87604d614b00ae Mon Sep 17 00:00:00 2001 From: Greg DiCristofaro Date: Thu, 11 Dec 2025 15:34:32 -0500 Subject: [PATCH 1/2] CT-10807 readonly transaction option --- .../sleuthkit/datamodel/SleuthkitCase.java | 60 +++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java index 08986de050..3fc3b439fa 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java +++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java @@ -3080,9 +3080,33 @@ public String getBackupDatabasePath() { * @throws TskCoreException */ public CaseDbTransaction beginTransaction() throws TskCoreException { - return new CaseDbTransaction(this); + return beginTransaction(false); } + /** + * Create a new transaction on the case database. The transaction object + * that is returned can be passed to methods that take a CaseDbTransaction. + * The caller is responsible for calling either commit() or rollback() on + * the transaction object. + * + * Note that this beginning the transaction also acquires the single user + * case write lock if {@code readonly} is {@code false} or the read lock if + * {@code readonly} is {@code true}, which will be automatically released + * when the transaction is closed. + * + * @param readonly True if the transaction does not perform any writes to + * the database. + * + * @return A CaseDbTransaction object. + * + * @throws TskCoreException + */ + @Beta + public CaseDbTransaction beginTransaction(boolean readonly) throws TskCoreException { + return new CaseDbTransaction(this, readonly); + } + + /** * Gets the case database name. * @@ -14458,6 +14482,7 @@ void executeCommand(DbCommand command) throws SQLException { public static final class CaseDbTransaction { private final CaseDbConnection connection; + private final boolean readOnlyTransaction; private SleuthkitCase sleuthkitCase; /* This class can store information about what was @@ -14474,15 +14499,32 @@ public static final class CaseDbTransaction { private List deletedOsAccountObjectIds = new ArrayList<>(); private List deletedResultObjectIds = new ArrayList<>(); + // Keep track of which threads have connections to debug deadlocks private static Set threadsWithOpenTransaction = new HashSet<>(); private static final Object threadsWithOpenTransactionLock = new Object(); - private CaseDbTransaction(SleuthkitCase sleuthkitCase) throws TskCoreException { + /** + * Constructor for a case database transaction. + * + * @param sleuthkitCase The TSK case. + * @param readOnlyTransaction True if the transaction will not make any + * writes to the database and therefore does + * not need the write lock. + * + * @throws TskCoreException + */ + private CaseDbTransaction(SleuthkitCase sleuthkitCase, boolean readOnlyTransaction) throws TskCoreException { this.sleuthkitCase = sleuthkitCase; + this.readOnlyTransaction = readOnlyTransaction; - sleuthkitCase.acquireSingleUserCaseWriteLock(); + if (readOnlyTransaction) { + sleuthkitCase.acquireSingleUserCaseReadLock(); + } else { + sleuthkitCase.acquireSingleUserCaseWriteLock(); + } + this.connection = sleuthkitCase.getConnection(); try { synchronized (threadsWithOpenTransactionLock) { @@ -14490,7 +14532,11 @@ private CaseDbTransaction(SleuthkitCase sleuthkitCase) throws TskCoreException { threadsWithOpenTransaction.add(Thread.currentThread().getId()); } } catch (SQLException ex) { - sleuthkitCase.releaseSingleUserCaseWriteLock(); + if (readOnlyTransaction) { + sleuthkitCase.releaseSingleUserCaseReadLock(); + } else { + sleuthkitCase.releaseSingleUserCaseWriteLock(); + } throw new TskCoreException("Failed to create transaction on case database", ex); } @@ -14671,7 +14717,11 @@ public void rollback() throws TskCoreException { */ void close() { this.connection.close(); - sleuthkitCase.releaseSingleUserCaseWriteLock(); + if (readOnlyTransaction) { + sleuthkitCase.releaseSingleUserCaseReadLock(); + } else { + sleuthkitCase.releaseSingleUserCaseWriteLock(); + } synchronized (threadsWithOpenTransactionLock) { threadsWithOpenTransaction.remove(Thread.currentThread().getId()); } From 3da87edaaa77a23e98074560098ecdd6d81c597c Mon Sep 17 00:00:00 2001 From: Greg DiCristofaro Date: Mon, 15 Dec 2025 11:30:09 -0500 Subject: [PATCH 2/2] CT-10807 API change --- .../sleuthkit/datamodel/SleuthkitCase.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java index 3fc3b439fa..81d537d7be 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java +++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java @@ -3080,30 +3080,30 @@ public String getBackupDatabasePath() { * @throws TskCoreException */ public CaseDbTransaction beginTransaction() throws TskCoreException { - return beginTransaction(false); + return new CaseDbTransaction(this, false); } /** - * Create a new transaction on the case database. The transaction object + *

Create a new transaction on the case database. The transaction object * that is returned can be passed to methods that take a CaseDbTransaction. * The caller is responsible for calling either commit() or rollback() on - * the transaction object. + * the transaction object.

* - * Note that this beginning the transaction also acquires the single user - * case write lock if {@code readonly} is {@code false} or the read lock if - * {@code readonly} is {@code true}, which will be automatically released - * when the transaction is closed. - * - * @param readonly True if the transaction does not perform any writes to - * the database. + *

Note that this beginning the transaction also acquires the single user + * case read lock, which will be automatically released when the + * transaction is closed.

+ * + *

WARNING: This API should only be used if the transaction is + * guaranteed to only ever perform reads and no updates to the database. + * Undefined behavior can occur if this API is used with database updates.

* * @return A CaseDbTransaction object. * * @throws TskCoreException */ @Beta - public CaseDbTransaction beginTransaction(boolean readonly) throws TskCoreException { - return new CaseDbTransaction(this, readonly); + public CaseDbTransaction beginReadOnlyTransaction() throws TskCoreException { + return new CaseDbTransaction(this, true); }