From 069a1256c6ebdc1142e525a44733bc32fd834061 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 04 Aug 2026 11:37:29 +0000
Subject: [PATCH] Fix CodeQL note-severity alerts: ignored error status of file and stream calls (#814)

---
 opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java                               |   59 ++++
 opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java        |    6 
 opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java                |    4 
 opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java                              |   21 -
 opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java                                      |   10 
 opendj-server-legacy/src/messages/org/opends/messages/core.properties                                              |    4 
 opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java                                     |   20 -
 opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java                                    |   23 -
 opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java                                     |    9 
 opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java                              |   31 -
 opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java                              |   76 ++++-
 opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java                               |    8 
 opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java                             |   17 
 opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java                                |   19 +
 opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java                              |   72 +++++
 opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java                                          |   20 +
 opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java                            |   11 
 opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java                       |    9 
 opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java |    6 
 opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java                                   |   20 
 opendj-server-legacy/src/messages/org/opends/messages/logger.properties                                            |    8 
 opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java                                 |    8 
 opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java                                       |   45 ++
 opendj-server-legacy/src/messages/org/opends/messages/plugin.properties                                            |    5 
 opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java                      |  316 ++++++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java                          |    2 
 26 files changed, 662 insertions(+), 167 deletions(-)

diff --git a/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java b/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
index a7afa17..a6571e4 100644
--- a/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
+++ b/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2009 Sun Microsystems, Inc.
  * Portions Copyright 2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package com.forgerock.opendj.util;
 
@@ -130,7 +131,12 @@
             n = readLimit - bytesRead;
         }
 
-        bytesRead += n;
-        return parentStream.skip(n);
+        // The parent stream is allowed to skip fewer bytes than requested, so only account for the
+        // bytes which were actually skipped.
+        final long skipped = parentStream.skip(n);
+        if (skipped > 0) {
+            bytesRead += (int) skipped;
+        }
+        return skipped;
     }
 }
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java b/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
index c6a1522..15ced3a 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.forgerock.opendj.io;
 
@@ -145,10 +146,14 @@
 
         // Ignore all unused trailing components.
         final SizeLimitInputStream subSq = (SizeLimitInputStream) in;
-        if (subSq.getSizeLimit() - subSq.getBytesRead() > 0) {
-            logger.trace("Ignoring %d unused trailing bytes in ASN.1 SEQUENCE",
-                    subSq.getSizeLimit() - subSq.getBytesRead());
-            subSq.skip(subSq.getSizeLimit() - subSq.getBytesRead());
+        final long unusedBytes = subSq.getSizeLimit() - subSq.getBytesRead();
+        if (unusedBytes > 0) {
+            logger.trace("Ignoring %d unused trailing bytes in ASN.1 SEQUENCE", unusedBytes);
+            if (skipFully(subSq, unusedBytes) != unusedBytes) {
+                // The remaining components could not be skipped, so the reader would be left
+                // positioned in the middle of the sequence.
+                throw DecodeException.fatalError(ERR_ASN1_SKIP_TRUNCATED_VALUE.get(unusedBytes));
+            }
         }
 
         logger.trace("READ ASN.1 END SEQUENCE");
@@ -385,7 +390,7 @@
         // Read the header if haven't done so already
         peekLength();
 
-        final long bytesSkipped = in.skip(peekLength);
+        final long bytesSkipped = skipFully(in, peekLength);
         if (bytesSkipped != peekLength) {
             final LocalizableMessage message = ERR_ASN1_SKIP_TRUNCATED_VALUE.get(peekLength);
             throw DecodeException.fatalError(message);
@@ -395,6 +400,36 @@
     }
 
     /**
+     * Skips the requested number of bytes, retrying as needed since {@link InputStream#skip} is
+     * allowed to skip fewer bytes than requested even when the end of the stream has not been
+     * reached.
+     *
+     * @param stream
+     *            The stream to skip bytes from.
+     * @param length
+     *            The number of bytes to skip.
+     * @return The number of bytes actually skipped, which is smaller than {@code length} only if the
+     *         end of the stream was reached.
+     * @throws IOException
+     *             If an error occurs while skipping bytes.
+     */
+    private static long skipFully(final InputStream stream, final long length) throws IOException {
+        long remaining = length;
+        while (remaining > 0) {
+            final long skipped = stream.skip(remaining);
+            if (skipped > 0) {
+                remaining -= skipped;
+            } else if (stream.read() < 0) {
+                // End of stream reached: no more bytes can be skipped.
+                break;
+            } else {
+                remaining--;
+            }
+        }
+        return length - remaining;
+    }
+
+    /**
      * Internal helper method reading the additional ASN.1 length bytes and
      * transition to the next state if successful.
      *
diff --git a/opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java b/opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java
new file mode 100644
index 0000000..39b629c
--- /dev/null
+++ b/opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java
@@ -0,0 +1,72 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package com.forgerock.opendj.util;
+
+import static org.fest.assertions.Assertions.*;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.testng.annotations.Test;
+
+/** Test {@code SizeLimitInputStream}. */
+@SuppressWarnings("javadoc")
+public final class SizeLimitInputStreamTestCase extends UtilTestCase {
+
+    @Test
+    public void testSkipAccountsForTheBytesActuallySkipped() throws Exception {
+        // A BufferedInputStream only skips what its buffer holds, without reaching the end of the
+        // parent stream, which is exactly the case getBytesRead() used to get wrong.
+        final InputStream parent = new BufferedInputStream(new ByteArrayInputStream(new byte[32]), 4);
+        // Fill the buffer with 4 bytes and consume one of them, leaving 3 skippable bytes in it.
+        parent.read();
+
+        final SizeLimitInputStream stream = new SizeLimitInputStream(parent, 32);
+        assertThat(stream.skip(16)).isEqualTo(3);
+        assertThat(stream.getBytesRead()).isEqualTo(3);
+    }
+
+    @Test
+    public void testSkipIsCappedToTheSizeLimit() throws Exception {
+        final SizeLimitInputStream stream =
+                new SizeLimitInputStream(new ByteArrayInputStream(new byte[32]), 8);
+
+        assertThat(stream.skip(20)).isEqualTo(8);
+        assertThat(stream.getBytesRead()).isEqualTo(8);
+        assertThat(stream.read()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testSkipStopsAtTheEndOfTheParentStream() throws Exception {
+        final SizeLimitInputStream stream =
+                new SizeLimitInputStream(new ByteArrayInputStream(new byte[2]), 8);
+
+        assertThat(stream.skip(8)).isEqualTo(2);
+        assertThat(stream.getBytesRead()).isEqualTo(2);
+    }
+
+    @Test
+    public void testReadAccountsForTheBytesActuallyRead() throws Exception {
+        final SizeLimitInputStream stream =
+                new SizeLimitInputStream(new ByteArrayInputStream(new byte[32]), 8);
+
+        assertThat(stream.read()).isEqualTo(0);
+        assertThat(stream.read(new byte[16])).isEqualTo(7);
+        assertThat(stream.getBytesRead()).isEqualTo(8);
+        assertThat(stream.read()).isEqualTo(-1);
+    }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
index b8c0687..3ae1bdd 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
+++ b/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
@@ -13,11 +13,17 @@
  *
  * Copyright 2010 Sun Microsystems, Inc.
  * Portions copyright 2013 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.forgerock.opendj.io;
 
+import static org.testng.Assert.*;
+
+import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 
+import org.testng.annotations.Test;
+
 /**
  * Test class for ASN1InputStreamReader.
  */
@@ -27,4 +33,57 @@
         final ByteArrayInputStream inStream = new ByteArrayInputStream(b);
         return new ASN1InputStreamReader(inStream, maxElementSize);
     }
+
+    /**
+     * Returns a reader whose underlying stream skips fewer bytes than requested without having
+     * reached its end, which is what {@code BufferedInputStream} does once its buffer is partially
+     * consumed.
+     */
+    private ASN1Reader getBufferedReader(final byte[] b) {
+        return new ASN1InputStreamReader(new BufferedInputStream(new ByteArrayInputStream(b), 4), 0);
+    }
+
+    /**
+     * Tests that the trailing components of a sequence are fully skipped even when the underlying
+     * stream skips fewer bytes than requested.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDecodeSequenceIncompleteReadOverBufferedStream() throws Exception {
+        // A sequence holding ten booleans, of which only the first one is read, followed by an
+        // integer which must still be decoded correctly once the sequence has been skipped.
+        final byte[] b =
+                new byte[] { 0x30, 0x0C, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+                    0x01, 0x01, 0x02, 0x01, 0x7F };
+        final ASN1Reader reader = getBufferedReader(b);
+
+        reader.readStartSequence();
+        assertFalse(reader.readBoolean());
+        reader.readEndSequence();
+
+        assertEquals(reader.readInteger(), 127);
+    }
+
+    /**
+     * Tests that {@code skipElement} does not report a truncated value when the underlying stream
+     * skips fewer bytes than requested.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testSkipElementOverBufferedStream() throws Exception {
+        final byte[] b =
+                new byte[] { 0x30, 0x0C, 0x02, 0x01, 0x05, 0x04, 0x04, 0x61, 0x62, 0x63, 0x64, 0x02,
+                    0x01, 0x7F };
+        final ASN1Reader reader = getBufferedReader(b);
+
+        reader.readStartSequence();
+        assertEquals(reader.readInteger(), 5);
+        reader.skipElement();
+        assertEquals(reader.readInteger(), 127);
+        reader.readEndSequence();
+    }
 }
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java
index e7d6212..f15ec37 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2010 Sun Microsystems, Inc.
  * Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.quicksetup;
 
@@ -22,7 +23,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.nio.file.Files;
 
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
 import org.opends.quicksetup.util.Utils;
 import org.opends.server.util.ServerConstants;
 
@@ -33,6 +37,8 @@
  */
 public class LicenseFile
 {
+  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
   private static final String INSTALL_ROOT_SYSTEM_PROPERTY = "INSTALL_ROOT";
   /** The license file name in Legal directory. */
   private static final String LICENSE_FILE_NAME = "Forgerock_License.txt";
@@ -61,9 +67,9 @@
     String instanceLegalDirName = Utils.getInstancePathFromInstallPath(getInstallDirectory())
         + File.separator + LEGAL_FOLDER_NAME;
     File instanceLegalDir = new File(instanceLegalDirName);
-    if (!instanceLegalDir.exists())
+    if (!instanceLegalDir.isDirectory() && !instanceLegalDir.mkdirs())
     {
-      instanceLegalDir.mkdir();
+      logger.warn(LocalizableMessage.raw("Unable to create the legal directory %s", instanceLegalDirName));
     }
     return instanceLegalDirName;
   }
@@ -164,17 +170,19 @@
       String instanceLegalDirName = instanceDirname + File.separator + LEGAL_FOLDER_NAME;
       File instanceLegalDir = new File(instanceLegalDirName);
 
+      File approvalFile = new File(instanceLegalDir, ACCEPTED_LICENSE_FILE_NAME);
       try
       {
-        if (!instanceLegalDir.exists())
+        Files.createDirectories(instanceLegalDir.toPath());
+        if (!approvalFile.createNewFile() && !approvalFile.isFile())
         {
-          instanceLegalDir.mkdir();
+          logger.warn(LocalizableMessage.raw("Unable to create the license approval file %s", approvalFile));
         }
-        new File(instanceLegalDir, ACCEPTED_LICENSE_FILE_NAME).createNewFile();
       }
       catch (IOException e)
       {
-        // do nothing
+        logger.warn(LocalizableMessage.raw(
+            "Unable to create the license approval file %s: %s", approvalFile, e.getLocalizedMessage()));
       }
     }
   }
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java
index 49c38ae..919ff3d 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java
@@ -977,17 +977,6 @@
         writer.write(getJavaArgPropertyForScript(scriptName) + "=" + argument);
       }
     }
-
-    String libDir = Utils.getPath(
-        Utils.getInstancePathFromInstallPath(installPath), LIBRARIES_PATH_RELATIVE);
-    // Create directory if it doesn't exist yet
-    File fLib = new File(libDir);
-    if (!fLib.exists())
-    {
-      fLib.mkdir();
-    }
-//    final String destinationFile = Utils.getPath(libDir, isWindows() ? SET_JAVA_PROPERTIES_FILE_WINDOWS
-//                                                                     : SET_JAVA_PROPERTIES_FILE_UNIX);
   }
 
   /**
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java b/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java
index e15de6a..33e2c12 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java
@@ -119,7 +119,7 @@
         generationDir = Files.createTempDirectory(CONFIG_GUIDE_DIR).toString();
       } else {
         // Create new dir if necessary
-        new File(generationDir).mkdir();
+        Files.createDirectories(new File(generationDir).toPath());
       }
     } catch (Exception e) {
       e.printStackTrace();
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java
index bbcded9..0249d60 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java
@@ -280,25 +280,13 @@
       throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), m);
     }
 
-    if (tempFile.exists())
-    {
-      // Rename the existing "live" file out of the way and move the new file
-      // into place.
-      try
-      {
-        oldFile.delete();
-      }
-      catch (Exception e)
-      {
-        logger.traceException(e);
-      }
-    }
-
+    // Rename the existing "live" file out of the way and move the new file into place.
+    // renameFile() deletes an existing ".old" file and reports a failure to do so.
     try
     {
       if (ldifFile.exists())
       {
-        ldifFile.renameTo(oldFile);
+        renameFile(ldifFile, oldFile);
       }
     }
     catch (Exception e)
@@ -308,7 +296,7 @@
 
     try
     {
-      tempFile.renameTo(ldifFile);
+      renameFile(tempFile, ldifFile);
     }
     catch (Exception e)
     {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java
index 97e006d..8f2216d 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java
@@ -1249,28 +1249,15 @@
       writer.close();
 
 
-      // See if there is a ".save" file.  If so, then delete it.
-      File saveFile = getFileForPath(backingFilePath + ".save");
-      try
-      {
-        if (saveFile.exists())
-        {
-          saveFile.delete();
-        }
-      }
-      catch (Exception e)
-      {
-        logger.traceException(e);
-      }
-
-
       // If there is an existing backing file, then rename it to ".save".
+      // renameFile() deletes an existing ".save" file and reports a failure to do so.
+      File saveFile = getFileForPath(backingFilePath + ".save");
       File backingFile = getFileForPath(backingFilePath);
       try
       {
         if (backingFile.exists())
         {
-          backingFile.renameTo(saveFile);
+          renameFile(backingFile, saveFile);
         }
       }
       catch (Exception e)
@@ -1289,7 +1276,7 @@
       File tmpFile = getFileForPath(tmpFilePath);
       try
       {
-        tmpFile.renameTo(backingFile);
+        renameFile(tmpFile, backingFile);
       }
       catch (Exception e)
       {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
index 892643d..011b250 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
@@ -817,25 +817,14 @@
     }
 
     // If a ".startok" file already exists, then move it to an ".old" file.
+    // renameFile() deletes an existing ".old" file and reports a failure to do so.
     File oldFile = new File(oldFilePath);
-    try
-    {
-      if (oldFile.exists())
-      {
-        oldFile.delete();
-      }
-    }
-    catch (Exception e)
-    {
-      logger.traceException(e);
-    }
-
     File startOKFile = new File(startOKFilePath);
     try
     {
       if (startOKFile.exists())
       {
-        startOKFile.renameTo(oldFile);
+        renameFile(startOKFile, oldFile);
       }
     }
     catch (Exception e)
@@ -846,7 +835,7 @@
     // Rename the temp file to the ".startok" file.
     try
     {
-      tempFile.renameTo(startOKFile);
+      renameFile(tempFile, startOKFile);
     }
     catch (Exception e)
     {
@@ -1609,20 +1598,12 @@
 
     // Move the current config file out of the way and replace it with the updated version.
     File oldSource = new File(sourceFile.getAbsolutePath() + ".prechanges");
-    if (oldSource.exists())
-    {
-      oldSource.delete();
-    }
-    sourceFile.renameTo(oldSource);
-    new File(tempFilePath).renameTo(sourceFile);
+    renameFile(sourceFile, oldSource);
+    renameFile(new File(tempFilePath), sourceFile);
 
     // Move the changes file out of the way so it doesn't get applied again.
     File newChanges = new File(changesFile.getAbsolutePath() + ".applied");
-    if (newChanges.exists())
-    {
-      newChanges.delete();
-    }
-    changesFile.renameTo(newChanges);
+    renameFile(changesFile, newChanges);
   }
 
   private void applyConfigChangesIfNeeded(File configFileToUse) throws InitializationException
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java b/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java
index 70e131d..04f975f 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008 Sun Microsystems, Inc.
  * Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.core;
 
@@ -23,6 +24,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
@@ -228,14 +230,21 @@
 
         if (liveFile.exists())
         {
+          // Keeping a copy of the previous version is best effort only: save() is called from the
+          // entry encoding path, so failing here would turn a backup problem into a failed update.
           final File saveFile = new File(liveFile.getAbsolutePath() + ".save");
-          if (saveFile.exists())
+          try
           {
-            saveFile.delete();
+            renameFile(liveFile, saveFile);
           }
-          liveFile.renameTo(saveFile);
+          catch (final IOException e)
+          {
+            logger.traceException(e);
+            logger.warn(WARN_COMPRESSEDSCHEMA_CANNOT_SAVE_PREVIOUS_DATA, liveFile, saveFile,
+                stackTraceToSingleLineString(e));
+          }
         }
-        tempFile.renameTo(liveFile);
+        renameFile(tempFile, liveFile);
       }
       catch (final Exception e)
       {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java b/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java
index 72fdca0..644e0b4 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions Copyright 2013-2015 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.core;
 
@@ -93,9 +94,9 @@
       File f = getFileForPath(lockFile);
       try
       {
-        if (! f.exists())
+        if (! f.createNewFile())
         {
-          f.createNewFile();
+          logger.trace("Lock file %s already exists", lockFile);
         }
       }
       catch (Exception e)
@@ -207,9 +208,9 @@
       File f = getFileForPath(lockFile);
       try
       {
-        if (! f.exists())
+        if (! f.createNewFile())
         {
-          f.createNewFile();
+          logger.trace("Lock file %s already exists", lockFile);
         }
       }
       catch (Exception e)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java b/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java
index 30372b1..b21b6b3 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java
@@ -84,6 +84,16 @@
   private long totalFilesRotated;
   private long totalFilesCleaned;
 
+  /**
+   * Set when the current file could not be renamed by {@link #rotate()} and cleared as soon as a
+   * rotation succeeds again. While it is set, size based rotations are not triggered from
+   * {@link #writeRecord(String)} and the failure is not logged again: the rotation is only retried
+   * from the {@code RotaterThread}, once per interval.
+   */
+  private boolean rotationFailed;
+  /** Same latch as {@link #rotationFailed}, for the log files a retention policy cannot delete. */
+  private boolean cleanupFailed;
+
   /** The underlying output stream. */
   private MeteredStream outputStream;
   /** The underlying buffered writer using the output stream. */
@@ -172,12 +182,7 @@
                                int bufferSize)
       throws IOException, DirectoryException
   {
-    // Create new file if it doesn't exist
-    if(!file.exists())
-    {
-      file.createNewFile();
-    }
-
+    // The file is created by the output stream below if it does not exist yet.
     FileOutputStream stream = new FileOutputStream(file, append);
     outputStream = new MeteredStream(stream, file.length());
 
@@ -426,17 +431,28 @@
             File[] files =
                 retentionPolicy.deleteFiles(writer.getNamingPolicy());
 
+            int cleanedCount = 0;
             for(File file : files)
             {
-              file.delete();
-              totalFilesCleaned++;
-              logger.trace("%s cleaned up log file %s", retentionPolicy, file);
+              if (file.delete())
+              {
+                cleanedCount++;
+                totalFilesCleaned++;
+                cleanupFailed = false;
+                logger.trace("%s cleaned up log file %s", retentionPolicy, file);
+              }
+              else if (!cleanupFailed)
+              {
+                // Only report the first failure: the same files are returned on every interval.
+                cleanupFailed = true;
+                logger.warn(WARN_LOGGER_ERROR_DELETING_FILE, file, retentionPolicy);
+              }
             }
 
-            if(files.length > 0)
+            if(cleanedCount > 0)
             {
               lastCleanTime = TimeThread.getCalendar();
-              lastCleanCount = files.length;
+              lastCleanCount = cleanedCount;
             }
           }
           catch(DirectoryException de)
@@ -554,7 +570,9 @@
 
     synchronized(this)
     {
-      if(sizeLimit > 0 && outputStream.written + size + 1 >= sizeLimit)
+      // Once a rotation has failed the file stays over the size limit, so rotating it again for
+      // every single record would only repeat the failure. Leave the retry to the RotaterThread.
+      if(sizeLimit > 0 && !rotationFailed && outputStream.written + size + 1 >= sizeLimit)
       {
         rotate();
       }
@@ -590,9 +608,12 @@
   }
 
   /**
-   * Tries to rotate the log files. If the new log file already exists, it
-   * tries to rename the file. On failure, all subsequent log write requests
-   * will throw exceptions.
+   * Tries to rotate the log files by renaming the current file to the name provided by the naming
+   * policy. When the rename fails, the current file is kept and appended to rather than truncated,
+   * the failure is reported once and the rotation is retried on the next interval.
+   * <p>
+   * Note that {@code File.renameTo} silently replaces the target on most platforms, so a rotation
+   * happening within the same second as a previous one overwrites the file it just rotated.
    */
   private synchronized void rotate()
   {
@@ -609,11 +630,17 @@
 
     File currentFile = namingPolicy.getInitialName();
     File newFile = namingPolicy.getNextName();
-    currentFile.renameTo(newFile);
+    final boolean renamed = currentFile.renameTo(newFile);
+    // The latch must be set before the writer is re-opened: constructWriter() logs warnings of its
+    // own, and when this writer backs the error log they come straight back into writeRecord() on
+    // this thread, where they must not trigger another rotation attempt.
+    final boolean report = !renamed && !rotationFailed;
+    rotationFailed = !renamed;
 
     try
     {
-      constructWriter(currentFile, filePermissions, encoding, append,
+      // If the file could not be rotated then keep appending to it rather than truncating it.
+      constructWriter(currentFile, filePermissions, encoding, append || !renamed,
                       bufferSize);
     }
     catch (Exception e)
@@ -622,9 +649,18 @@
       errorHandler.handleOpenError(currentFile, e);
     }
 
-    logger.trace("Log file %s rotated and renamed to %s", currentFile, newFile);
-    totalFilesRotated++;
-    lastRotationTime = TimeThread.getCalendar();
+    if (renamed)
+    {
+      logger.trace("Log file %s rotated and renamed to %s", currentFile, newFile);
+      totalFilesRotated++;
+      lastRotationTime = TimeThread.getCalendar();
+    }
+    else if (report)
+    {
+      logger.error(ERR_LOGGER_ERROR_ROTATING_FILE, currentFile, newFile);
+    }
+    // lastRotationTime is left untouched on failure so that a time based policy keeps asking for a
+    // rotation on every interval instead of waiting for a whole new period.
   }
 
   @Override
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java b/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
index 0413aa9..f9198c2 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
@@ -755,9 +755,9 @@
 
     try
     {
-      if(!logFile.exists())
+      if(!logFile.createNewFile())
       {
-        logFile.createNewFile();
+        logger.trace("Referential integrity update log file %s already exists", logFileName);
       }
     }
     catch (IOException io)
@@ -865,8 +865,9 @@
             }
           }
         }
-        logFile.delete();
-        logFile.createNewFile();
+        if (!logFile.delete() || !logFile.createNewFile()) {
+          logger.error(ERR_PLUGIN_REFERENT_CANNOT_REPLACE_LOGFILE, logFileName);
+        }
       } catch (IOException io) {
         logger.error(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE, io.getMessage());
       }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
index 285381d..98f5e21 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
@@ -21,6 +21,7 @@
 import static org.opends.server.util.StaticUtils.*;
 
 import java.io.File;
+import java.nio.file.Files;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -144,10 +145,7 @@
     final File dbDirectory = getFileForPath(dbDirName);
     try
     {
-      if (!dbDirectory.exists())
-      {
-        dbDirectory.mkdir();
-      }
+      Files.createDirectories(dbDirectory.toPath());
       return dbDirectory;
     }
     catch (Exception e)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java
index 16cb340..a385f58 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java
@@ -199,9 +199,9 @@
   {
     try
     {
-      if (!logfile.exists())
+      if (!logfile.createNewFile())
       {
-        logfile.createNewFile();
+        logger.trace("Log file %s already exists", logfile.getPath());
       }
     }
     catch (IOException e)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
index ee1282c..651da13 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
@@ -949,7 +949,11 @@
     final File newRotationFile = getLastRotationTimePath(lastRotationTime);
     try
     {
-      newRotationFile.createNewFile();
+      if (!newRotationFile.createNewFile() && !newRotationFile.isFile())
+      {
+        throw new ChangelogException(ERR_CHANGELOG_UNABLE_TO_CREATE_LAST_LOG_ROTATION_TIME_FILE.get(
+            newRotationFile.getPath(), lastRotationTime));
+      }
     }
     catch (IOException e)
     {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java b/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java
index 8f74982..fa4f9c1 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java
@@ -291,7 +291,7 @@
           matchingRuleUses, ldapSyntaxes);
 
       File upgradeDirectory = getUpgradeDirectory();
-      upgradeDirectory.mkdir();
+      Files.createDirectories(upgradeDirectory.toPath());
       File concatFile = new File(upgradeDirectory, SCHEMA_CONCAT_FILE_NAME);
       concatFilePath = concatFile.getAbsolutePath();
 
@@ -313,11 +313,7 @@
         writeLines(writer, ATTR_LDAP_SYNTAXES, ldapSyntaxes);
       }
 
-      if (concatFile.exists())
-      {
-        concatFile.delete();
-      }
-      tempFile.renameTo(concatFile);
+      renameFile(tempFile, concatFile);
     }
     catch (Exception e)
     {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java
index 1cd2940..eab5eab 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2010 Sun Microsystems, Inc.
  * Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 
 package org.opends.server.tools.upgrade;
@@ -21,6 +22,8 @@
 import java.io.FileReader;
 import java.io.IOException;
 
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
 import org.opends.server.util.StaticUtils;
 
 /**
@@ -31,6 +34,8 @@
  */
 class LicenseFile
 {
+  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
   private static final String INSTALL_ROOT_SYSTEM_PROPERTY = "INSTALL_ROOT";
 
   /**
@@ -80,9 +85,9 @@
     final String instanceDirname = UpgradeUtils.getInstancePathFromInstallPath(getInstallRootPathFromSystem("."));
     final String instanceLegalDirName = instanceDirname + File.separator + LEGAL_FOLDER_NAME;
     final File instanceLegalDir = new File(instanceLegalDirName);
-    if (!instanceLegalDir.exists())
+    if (!instanceLegalDir.isDirectory() && !instanceLegalDir.mkdirs())
     {
-      instanceLegalDir.mkdir();
+      logger.warn(LocalizableMessage.raw("Unable to create the legal directory %s", instanceLegalDirName));
     }
     return instanceLegalDirName;
   }
@@ -197,12 +202,18 @@
   {
     if (getApproval())
     {
+      final File approvalFile = new File(getInstanceLegalDirectory(), ACCEPTED_LICENSE_FILE_NAME);
       try
       {
-        new File(getInstanceLegalDirectory(), ACCEPTED_LICENSE_FILE_NAME).createNewFile();
+        if (!approvalFile.createNewFile() && !approvalFile.isFile())
+        {
+          logger.warn(LocalizableMessage.raw("Unable to create the license approval file %s", approvalFile));
+        }
       }
       catch (IOException e)
-      { // do  nothing
+      {
+        logger.warn(LocalizableMessage.raw(
+            "Unable to create the license approval file %s: %s", approvalFile, e.getLocalizedMessage()));
       }
     }
   }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
index 2b3596e..c7f43fe 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
@@ -24,6 +24,7 @@
 import java.io.FileReader;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -682,17 +683,14 @@
     {
       logger.debug(LocalizableMessage.raw("Parent file of %s doesn't exist", destination.getPath()));
 
-      parentDirectory.mkdirs();
+      Files.createDirectories(parentDirectory.toPath());
 
       logger.debug(LocalizableMessage.raw("Parent directory %s created.", parentDirectory.getPath()));
     }
-    if (!destination.exists())
-    {
-      destination.createNewFile();
-    }
 
     logger.debug(LocalizableMessage.raw("Writing entries in %s.", destination.getAbsolutePath()));
 
+    // The destination file is created by the output stream below if it does not exist yet.
     try (LDIFEntryWriter writer = new LDIFEntryWriter(new FileOutputStream(destination)))
     {
       writer.writeEntry(theNewSchemaEntry);
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java b/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java
index 5c505e3..189e2b6 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.types;
 
@@ -22,6 +23,7 @@
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -289,7 +291,7 @@
     // Rename the new descriptor file to match the previous one.
     try
     {
-      newDescriptorFile.renameTo(descriptorFile);
+      renameFile(newDescriptorFile, descriptorFile);
     }
     catch (Exception e)
     {
@@ -307,7 +309,7 @@
     {
       try
       {
-        dir.mkdirs();
+        Files.createDirectories(dir.toPath());
       }
       catch (Exception e)
       {
@@ -328,24 +330,11 @@
     {
       String savedDescriptorFilePath = descriptorFilePath + ".save";
       File savedDescriptorFile = new File(savedDescriptorFilePath);
-      if (savedDescriptorFile.exists())
-      {
-        try
-        {
-          savedDescriptorFile.delete();
-        }
-        catch (Exception e)
-        {
-          logger.traceException(e);
-          LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR.get(
-              savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath);
-          throw new IOException(message.toString());
-        }
-      }
 
       try
       {
-        descriptorFile.renameTo(savedDescriptorFile);
+        // renameFile() deletes an existing target and reports a failure to do so.
+        renameFile(descriptorFile, savedDescriptorFile);
       }
       catch (Exception e)
       {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java b/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java
index f7e4a61..5a383d7 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2009 Sun Microsystems, Inc.
  * Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.types;
 
@@ -195,31 +196,24 @@
         case APPEND:
           // Create new file if it doesn't exist ensuring that we can
           // set its permissions.
-          if (!f.exists())
-          {
-            f.createNewFile();
-            mustSetPermissions = true;
-          }
+          mustSetPermissions = f.createNewFile();
           ldifOutputStream = new FileOutputStream(ldifFile, true);
           break;
         case OVERWRITE:
           // Create new file if it doesn't exist ensuring that we can
           // set its permissions.
-          if (!f.exists())
-          {
-            f.createNewFile();
-            mustSetPermissions = true;
-          }
+          mustSetPermissions = f.createNewFile();
           ldifOutputStream = new FileOutputStream(ldifFile, false);
           break;
         case FAIL:
-          if (f.exists())
+          // Create new file ensuring that we can set its permissions. The creation is
+          // atomic, hence it also fails if the file was created by someone else in
+          // the mean time.
+          if (!f.createNewFile())
           {
             LocalizableMessage message = ERR_LDIF_FILE_EXISTS.get(ldifFile);
             throw new IOException(message.toString());
           }
-          // Create new file ensuring that we can set its permissions.
-          f.createNewFile();
           mustSetPermissions = true;
           ldifOutputStream = new FileOutputStream(ldifFile);
           break;
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core.properties b/opendj-server-legacy/src/messages/org/opends/messages/core.properties
index ee4a3e0..7be7a7d 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 #
@@ -1076,6 +1077,9 @@
  provided object class set because it used an undefined token %s
 ERR_COMPRESSEDSCHEMA_CANNOT_WRITE_UPDATED_DATA_622=Unable to write the \
  updated compressed schema token data: %s
+WARN_COMPRESSEDSCHEMA_CANNOT_SAVE_PREVIOUS_DATA_756=Unable to keep a copy of \
+ the previous compressed schema token data by renaming %s to %s: %s. The \
+ updated token data has still been written
 ERR_ENTRYENCODECFG_INVALID_LENGTH_623=Unable to decode the provided \
  entry encode configuration element because it has an invalid length
 INFO_RESULT_NO_OPERATION_624=No Operation
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/logger.properties b/opendj-server-legacy/src/messages/org/opends/messages/logger.properties
index de90779..8b0486c 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/logger.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/logger.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2008 Sun Microsystems, Inc.
 # Portions Copyright 2015-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 #
 # Global directives
@@ -100,3 +101,10 @@
  common audit log publisher %s, the keystore file %s could not be read: %s
 ERR_COMMON_AUDIT_KEYSTORE_FILE_IS_EMPTY_33=Error while processing \
  common audit log publisher %s, the keystore file %s is empty
+ERR_LOGGER_ERROR_ROTATING_FILE_34=Error occurred while rotating log file %s to \
+ %s. The current log file will be kept and appended to, and the rotation will be \
+ retried on the next interval. Any further rotation errors will be ignored until \
+ a rotation succeeds
+WARN_LOGGER_ERROR_DELETING_FILE_35=Error occurred while deleting log file %s \
+ while enforcing retention policy %s. Any further deletion errors will be ignored \
+ until a log file is deleted successfully
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties b/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
index 3046004..9913479 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2014-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -235,6 +236,10 @@
  Referential Integrity plugin update log file: %s
 ERR_PLUGIN_REFERENT_REPLACE_LOGFILE_84=An error occurred replacing the \
  Referential Integrity plugin update log file: %s
+ERR_PLUGIN_REFERENT_CANNOT_REPLACE_LOGFILE_130=The Referential Integrity \
+ plugin update log file %s could not be deleted and created again after its \
+ records were processed. Those records will be processed again the next time \
+ the log file is read
 INFO_PLUGIN_REFERENT_LOGFILE_CHANGE_REQUIRES_RESTART_85=The file name that \
  the Referential Integrity plugin logs changes to during background \
  processing has been changed from %s to %s, but this change will not take \
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java
new file mode 100644
index 0000000..adbc1c5
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java
@@ -0,0 +1,316 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.loggers;
+
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.*;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.server.config.server.SizeLimitLogRotationPolicyCfg;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.types.FilePermission;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** Tests the rotation of {@link MultifileTextWriter}, in particular when the rename fails. */
+@SuppressWarnings("javadoc")
+public class MultifileTextWriterTestCase extends DirectoryServerTestCase
+{
+  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+  private static final DN PUBLISHER_DN = DN.valueOf("cn=Test Logger,cn=Loggers,cn=config");
+  /** Small enough for two records to overflow it, large enough for one not to. */
+  private static final long SIZE_LIMIT = 64;
+  private static final String RECORD_A = "a".repeat(32);
+  private static final String RECORD_B = "b".repeat(32);
+  /** Long enough to leave the re-opened stream over the size limit together with the first record. */
+  private static final String STAND_IN_WARNING =
+      "stand-in for the permission warnings logged while the writer is re-opened";
+
+  /** Naming policy with fixed names, so that the test does not depend on the current second. */
+  private static final class FixedNamingPolicy implements FileNamingPolicy
+  {
+    private final File initialFile;
+    private final File nextFile;
+
+    private FixedNamingPolicy(File initialFile, File nextFile)
+    {
+      this.initialFile = initialFile;
+      this.nextFile = nextFile;
+    }
+
+    @Override
+    public File getInitialName()
+    {
+      return initialFile;
+    }
+
+    @Override
+    public File getNextName()
+    {
+      return nextFile;
+    }
+
+    @Override
+    public FilenameFilter getFilenameFilter()
+    {
+      return new FilenameFilter()
+      {
+        @Override
+        public boolean accept(File dir, String name)
+        {
+          return name.equals(nextFile.getName());
+        }
+      };
+    }
+
+    @Override
+    public File[] listFiles()
+    {
+      return new File[0];
+    }
+  }
+
+  /**
+   * A file which can run a hook from within constructWriter(): after a failed rotation the writer
+   * is re-opened, and {@code FilePermission.setPermissions} checks {@code exists()} at the exact
+   * point where the permission warnings are logged.
+   */
+  private static final class HookedFile extends File
+  {
+    private static final long serialVersionUID = 1L;
+    private transient Runnable onExists;
+
+    private HookedFile(File parent, String child)
+    {
+      super(parent, child);
+    }
+
+    @Override
+    public boolean exists()
+    {
+      if (onExists != null)
+      {
+        onExists.run();
+      }
+      return super.exists();
+    }
+  }
+
+  private File tempDir;
+  private File logFile;
+  private File rotatedFile;
+  private MultifileTextWriter writer;
+
+  @BeforeClass
+  public void startServer() throws Exception
+  {
+    // The writer registers itself as a shutdown listener, which requires a bootstrapped server.
+    TestCaseUtils.startServer();
+  }
+
+  @BeforeMethod
+  public void setUp() throws Exception
+  {
+    tempDir = Files.createTempDirectory("MultifileTextWriterTestCase").toFile();
+    logFile = new File(tempDir, "test.log");
+    rotatedFile = new File(tempDir, "test.log.rotated");
+  }
+
+  @AfterMethod
+  public void tearDown() throws Exception
+  {
+    if (writer != null)
+    {
+      writer.shutdown();
+      writer = null;
+    }
+    deleteRecursively(tempDir);
+  }
+
+  /**
+   * Makes the rename attempted by the rotation fail on every platform: renaming a file onto a
+   * non-empty directory is refused both on POSIX systems and on Windows.
+   */
+  private void breakRotation() throws Exception
+  {
+    assertTrue(rotatedFile.mkdir());
+    assertTrue(new File(rotatedFile, "blocker").createNewFile());
+  }
+
+  private MultifileTextWriter newWriter() throws Exception
+  {
+    MultifileTextWriter newWriter = new MultifileTextWriter("Multifile Text Writer for " + PUBLISHER_DN,
+        Long.MAX_VALUE, new FixedNamingPolicy(logFile, rotatedFile), FilePermission.decodeUNIXMode("600"),
+        new LogPublisherErrorHandler(PUBLISHER_DN), "UTF-8", true, true, 0);
+
+    SizeLimitLogRotationPolicyCfg config = mock(SizeLimitLogRotationPolicyCfg.class);
+    when(config.getFileSizeLimit()).thenReturn(SIZE_LIMIT);
+    SizeBasedRotationPolicy policy = new SizeBasedRotationPolicy();
+    policy.initializeLogRotationPolicy(config);
+    newWriter.addRotationPolicy(policy);
+
+    // The rotater thread is deliberately not started: the rotations under test are the ones
+    // triggered inline by writeRecord().
+    return newWriter;
+  }
+
+  private List<String> linesOf(File file) throws Exception
+  {
+    return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
+  }
+
+  private static void deleteRecursively(File file)
+  {
+    if (file.isDirectory())
+    {
+      for (File child : file.listFiles())
+      {
+        deleteRecursively(child);
+      }
+    }
+    file.delete();
+  }
+
+  @Test
+  public void testSuccessfulRotationRenamesTheFile() throws Exception
+  {
+    writer = newWriter();
+
+    writer.writeRecord(RECORD_A);
+    writer.writeRecord(RECORD_B);
+    writer.flush();
+
+    assertEquals(writer.getTotalFilesRotated(), 1);
+    assertEquals(linesOf(rotatedFile), List.of(RECORD_A));
+    assertEquals(linesOf(logFile), List.of(RECORD_B));
+  }
+
+  @Test
+  public void testFailedRotationAppendsInsteadOfTruncating() throws Exception
+  {
+    breakRotation();
+    writer = newWriter();
+
+    writer.writeRecord(RECORD_A);
+    assertEquals(writer.getTotalFilesRotated(), 0);
+
+    // Overflows the size limit, so a rotation is attempted and fails.
+    writer.writeRecord(RECORD_B);
+    writer.flush();
+
+    assertEquals(writer.getTotalFilesRotated(), 0, "a failed rotation must not be counted");
+    assertTrue(rotatedFile.isDirectory(), "the rotation target must have been left alone");
+    assertEquals(linesOf(logFile), List.of(RECORD_A, RECORD_B),
+        "the log file must have been appended to rather than truncated");
+  }
+
+  /**
+   * A failed rotation of the file backing the error log used to come back into writeRecord() on the
+   * same thread, from a writer which had just been closed and was still over the size limit, and to
+   * recurse until the stack blew up.
+   */
+  @Test
+  @SuppressWarnings({ "rawtypes", "unchecked" })
+  public void testFailedRotationOfTheErrorLogDoesNotRecurse() throws Exception
+  {
+    breakRotation();
+    writer = newWriter();
+
+    MultifileTextWriter publishing = writer;
+    ErrorLogPublisher publisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(publishing);
+    ErrorLogger.getInstance().addLogPublisher(publisher);
+    try
+    {
+      publishing.writeRecord(RECORD_A);
+      // Overflows the size limit: the rotation fails and reports the failure through the very
+      // logger this writer is backing.
+      publishing.writeRecord(RECORD_B);
+      publishing.flush();
+    }
+    finally
+    {
+      // Removing the publisher closes it, which already shuts this writer down.
+      ErrorLogger.getInstance().removeLogPublisher(publisher);
+      writer = null;
+    }
+
+    assertEquals(publishing.getTotalFilesRotated(), 0);
+
+    List<String> lines = linesOf(logFile);
+    assertEquals(lines.get(0), RECORD_A, "the log file must have been appended to");
+    assertTrue(lines.contains(RECORD_B), "the record which triggered the rotation must not be lost");
+    assertTrue(lines.stream().anyMatch(line -> line.contains("rotating log file")),
+        "the rotation failure must be reported in the log file, but it contained: " + lines);
+  }
+
+  /**
+   * constructWriter() logs permission warnings of its own, between re-seeding the stream with the
+   * over-limit file length and the point where the failed rotation used to set its latch. When the
+   * writer backs the error log, such a warning used to re-enter writeRecord() and to recurse until
+   * the stack blew up. The hook stands in for the permission warning, firing at the same point of
+   * the re-open.
+   */
+  @Test
+  @SuppressWarnings({ "rawtypes", "unchecked" })
+  public void testWarningDuringReopenAfterFailedRotationDoesNotRecurse() throws Exception
+  {
+    breakRotation();
+    HookedFile hookedLogFile = new HookedFile(tempDir, logFile.getName());
+    logFile = hookedLogFile;
+    writer = newWriter();
+
+    MultifileTextWriter publishing = writer;
+    ErrorLogPublisher publisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(publishing);
+    ErrorLogger.getInstance().addLogPublisher(publisher);
+    try
+    {
+      publishing.writeRecord(RECORD_A);
+      hookedLogFile.onExists = () -> logger.warn(LocalizableMessage.raw(STAND_IN_WARNING));
+      // Overflows the size limit: the rotation fails, and while the writer is re-opened the hook
+      // logs through the very logger this writer is backing, like the permission warnings do.
+      publishing.writeRecord(RECORD_B);
+      publishing.flush();
+    }
+    finally
+    {
+      // Removing the publisher closes it, which already shuts this writer down.
+      ErrorLogger.getInstance().removeLogPublisher(publisher);
+      writer = null;
+    }
+
+    assertEquals(publishing.getTotalFilesRotated(), 0);
+
+    List<String> lines = linesOf(logFile);
+    assertEquals(lines.get(0), RECORD_A, "the log file must have been appended to");
+    assertTrue(lines.stream().anyMatch(line -> line.contains(STAND_IN_WARNING)),
+        "the warning logged during the re-open must not be lost, but the log file contained: " + lines);
+    assertTrue(lines.contains(RECORD_B), "the record which triggered the rotation must not be lost");
+    assertEquals(lines.stream().filter(line -> line.contains("rotating log file")).count(), 1L,
+        "the rotation failure must be reported exactly once, but the log file contained: " + lines);
+  }
+}

--
Gitblit v1.10.0