From 6c4b3e1b75f917c4a1d609c357d5f68df96056d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Csahvx655-wq=E2=80=9D?= <“sahvx655@gmail.com”> Date: Fri, 29 May 2026 11:56:43 +0530 Subject: [PATCH] Fix race condition in MemoryUserDatabase.save() by correctly scoping the lock --- .../catalina/users/MemoryUserDatabase.java | 54 ++++++++--------- .../TestMemoryUserDatabaseConcurrency.java | 58 +++++++++++++++++++ 2 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 test/org/apache/catalina/users/TestMemoryUserDatabaseConcurrency.java diff --git a/java/org/apache/catalina/users/MemoryUserDatabase.java b/java/org/apache/catalina/users/MemoryUserDatabase.java index dc01463191de..f958909d7451 100644 --- a/java/org/apache/catalina/users/MemoryUserDatabase.java +++ b/java/org/apache/catalina/users/MemoryUserDatabase.java @@ -585,37 +585,37 @@ public void save() throws Exception { throw ioe; } this.lastModified = fileNew.lastModified(); - } finally { - writeLock.unlock(); - } - // Perform the required renames to permanently save this file - File fileOld = new File(pathnameOld); - if (!fileOld.isAbsolute()) { - fileOld = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathnameOld); - } - if (fileOld.exists() && !fileOld.delete()) { - throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld)); - } - File fileOrig = new File(pathname); - if (!fileOrig.isAbsolute()) { - fileOrig = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathname); - } - if (fileOrig.exists()) { - if (!fileOrig.renameTo(fileOld)) { - throw new IOException(sm.getString("memoryUserDatabase.renameOld", fileOld.getAbsolutePath())); + // Perform the required renames to permanently save this file + File fileOld = new File(pathnameOld); + if (!fileOld.isAbsolute()) { + fileOld = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathnameOld); } - } - if (!fileNew.renameTo(fileOrig)) { - if (fileOld.exists()) { - if (!fileOld.renameTo(fileOrig)) { - log.warn(sm.getString("memoryUserDatabase.restoreOrig", fileOld)); + if (fileOld.exists() && !fileOld.delete()) { + throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld)); + } + File fileOrig = new File(pathname); + if (!fileOrig.isAbsolute()) { + fileOrig = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathname); + } + if (fileOrig.exists()) { + if (!fileOrig.renameTo(fileOld)) { + throw new IOException(sm.getString("memoryUserDatabase.renameOld", fileOld.getAbsolutePath())); } } - throw new IOException(sm.getString("memoryUserDatabase.renameNew", fileOrig.getAbsolutePath())); - } - if (fileOld.exists() && !fileOld.delete()) { - throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld)); + if (!fileNew.renameTo(fileOrig)) { + if (fileOld.exists()) { + if (!fileOld.renameTo(fileOrig)) { + log.warn(sm.getString("memoryUserDatabase.restoreOrig", fileOld)); + } + } + throw new IOException(sm.getString("memoryUserDatabase.renameNew", fileOrig.getAbsolutePath())); + } + if (fileOld.exists() && !fileOld.delete()) { + throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld)); + } + } finally { + writeLock.unlock(); } } diff --git a/test/org/apache/catalina/users/TestMemoryUserDatabaseConcurrency.java b/test/org/apache/catalina/users/TestMemoryUserDatabaseConcurrency.java new file mode 100644 index 000000000000..939a036ced71 --- /dev/null +++ b/test/org/apache/catalina/users/TestMemoryUserDatabaseConcurrency.java @@ -0,0 +1,58 @@ +package org.apache.catalina.users; + +import java.io.File; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Assert; +import org.junit.Test; + +public class TestMemoryUserDatabaseConcurrency { + + @Test + public void testConcurrentSave() throws Exception { + MemoryUserDatabase db = new MemoryUserDatabase("TestDB"); + db.setPathname("test-users.xml"); + db.setReadonly(false); + db.init(); + + // Create some data + db.createRole("role1", "description1"); + db.createUser("user1", "pass1", "User One"); + + int numThreads = 10; + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger successCount = new AtomicInteger(0); + AtomicInteger errorCount = new AtomicInteger(0); + + for (int i = 0; i < numThreads; i++) { + executor.submit(() -> { + try { + latch.await(); + db.save(); + successCount.incrementAndGet(); + } catch (Exception e) { + errorCount.incrementAndGet(); + } + }); + } + + latch.countDown(); + executor.shutdown(); + executor.awaitTermination(30, TimeUnit.SECONDS); + + System.out.println("Success count: " + successCount.get()); + System.out.println("Error count: " + errorCount.get()); + + Assert.assertEquals("Some saves failed due to race condition", numThreads, successCount.get()); + + File file = new File("test-users.xml"); + file.delete(); + new File("test-users.xml.new").delete(); + new File("test-users.xml.old").delete(); + } +}