From 44b37afa94f2aa6dc575835ac508ed214f6b983a Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 31 Jul 2026 17:03:13 +0000
Subject: [PATCH] Fix CodeQL warning-severity alerts: process stream ownership and two leaks (#799)

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java   |   36 ++++++++---
 opendj-server-legacy/src/main/java/org/opends/quicksetup/util/OutputReader.java         |   13 ++-
 opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java         |   21 ++++--
 opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java     |   36 ++++-------
 opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java              |    9 +++
 opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java |   13 ++--
 6 files changed, 76 insertions(+), 52 deletions(-)

diff --git a/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java b/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java
index 2294bce..ed6c628 100644
--- a/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java
+++ b/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java
@@ -12,6 +12,7 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Copyright 2015-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.forgerock.opendj.maven.doc;
 
@@ -75,11 +76,15 @@
      * @throws IOException  Failed to make the copy.
      */
     static void copyFile(File original, File copy) throws IOException {
-        copyInputStreamToFile(new FileInputStream(original), copy);
+        try (InputStream input = new FileInputStream(original)) {
+            copyInputStreamToFile(input, copy);
+        }
     }
 
     /**
      * Copies the content of the original input stream to the copy.
+     * <p>
+     * The original input stream is closed on return, whether the copy succeeded or not.
      * @param original      The original input stream.
      * @param copy          The copy.
      * @throws IOException  Failed to make the copy.
@@ -88,12 +93,14 @@
         if (original == null) {
             throw new IOException("Could not read input to copy.");
         }
-        createFile(copy);
-        try (OutputStream outputStream = new FileOutputStream(copy)) {
-            int bytesRead;
-            byte[] buffer = new byte[4096];
-            while ((bytesRead = original.read(buffer)) > 0) {
-                outputStream.write(buffer, 0, bytesRead);
+        try {
+            createFile(copy);
+            try (OutputStream outputStream = new FileOutputStream(copy)) {
+                int bytesRead;
+                byte[] buffer = new byte[4096];
+                while ((bytesRead = original.read(buffer)) > 0) {
+                    outputStream.write(buffer, 0, bytesRead);
+                }
             }
         } finally {
             closeSilently(original);
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 7f6b80a..49c38ae 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
@@ -33,7 +33,6 @@
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -152,8 +151,7 @@
     try
     {
       process = processBuilder.start();
-      final BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
-      new OutputReader(err)
+      new OutputReader(process.getErrorStream())
       {
         @Override
         public void processLine(final String line)
@@ -164,8 +162,7 @@
         }
       }.start();
 
-      final BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()));
-      new OutputReader(out)
+      new OutputReader(process.getInputStream())
       {
         @Override
         public void processLine(final String line)
@@ -182,8 +179,10 @@
     {
       if (process != null)
       {
-        closeProcessStream(process.getErrorStream(), "error");
-        closeProcessStream(process.getOutputStream(), "output");
+        // The error and output streams of the process are owned by the readers started above,
+        // which close them once drained. Only the stream writing to the standard input of the
+        // process, which is never used here, is left to close.
+        closeProcessStream(process.getOutputStream(), "input");
       }
     }
   }
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/OutputReader.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/OutputReader.java
index 5e0c2e7..d27c2b5 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/OutputReader.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/OutputReader.java
@@ -19,6 +19,8 @@
 package org.opends.quicksetup.util;
 
 import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
@@ -39,16 +41,17 @@
   /**
    * The protected constructor.
    * <p>
-   * The reader is consumed until end of stream and then closed by this reader's thread, which is
-   * only launched by {@link #start()}.
+   * The stream is consumed until end of stream and then closed by this reader's thread, which is
+   * only launched by {@link #start()}. Wrapping the stream is done by that thread as well, so that
+   * this reader is the sole owner of every resource built on top of the stream.
    *
-   * @param reader  the BufferedReader of the stop process.
+   * @param stream  the output stream of the process to read.
    */
-  public OutputReader(final BufferedReader reader) {
+  public OutputReader(final InputStream stream) {
     thread = new Thread(new Runnable() {
       @Override
       public void run() {
-        try (BufferedReader in = reader) {
+        try (BufferedReader in = new BufferedReader(new InputStreamReader(stream))) {
           String line;
           while (null != (line = in.readLine())) {
             processLine(line);
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java
index 3a7d24f..a51e1dd 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java
@@ -19,6 +19,7 @@
 
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.List;
@@ -181,16 +182,9 @@
           logger.info(LocalizableMessage.raw("Launching stop command, argList: "+argList));
           Process process = pb.start();
 
-          BufferedReader err =
-            new BufferedReader(
-                new InputStreamReader(process.getErrorStream()));
-          BufferedReader out =
-            new BufferedReader(
-                new InputStreamReader(process.getInputStream()));
-
           /* Create these objects to resend the stop process output to the details area. */
-          new StopReader(err, true);
-          new StopReader(out, false);
+          new StopReader(process.getErrorStream(), true);
+          new StopReader(process.getInputStream(), false);
 
           int returnValue = process.waitFor();
 
@@ -388,11 +382,8 @@
     String startedId = getStartedId();
     Process process = pb.start();
 
-    BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
-    BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()));
-
-    StartReader errReader = new StartReader(err, startedId, true);
-    StartReader outputReader = new StartReader(out, startedId, false);
+    StartReader errReader = new StartReader(process.getErrorStream(), startedId, true);
+    StartReader outputReader = new StartReader(process.getInputStream(), startedId, false);
 
     int returnValue = process.waitFor();
 
@@ -546,12 +537,11 @@
     /**
      * The protected constructor.
      *
-     * @param reader  the BufferedReader of the stop process.
-     * @param isError a boolean indicating whether the BufferedReader
+     * @param stream  the stream of the stop process to read.
+     * @param isError a boolean indicating whether the stream
      *        corresponds to the standard error or to the standard output.
      */
-    public StopReader(final BufferedReader reader,
-                                      final boolean isError) {
+    public StopReader(final InputStream stream, final boolean isError) {
       final LocalizableMessage errorTag =
               isError ?
                       INFO_ERROR_READING_ERROROUTPUT.get() :
@@ -562,7 +552,7 @@
         @Override
         public void run() {
           // The reader is owned by this thread, which closes it once the stream is drained.
-          try (BufferedReader in = reader) {
+          try (BufferedReader in = new BufferedReader(new InputStreamReader(stream))) {
             String line = in.readLine();
             while (line != null) {
               if (application != null) {
@@ -626,13 +616,13 @@
 
     /**
      * The protected constructor.
-     * @param reader the BufferedReader of the start process.
+     * @param stream the stream of the start process to read.
      * @param startedId the message ID that this class can use to know whether
      * the start is over or not.
-     * @param isError a boolean indicating whether the BufferedReader
+     * @param isError a boolean indicating whether the stream
      * corresponds to the standard error or to the standard output.
      */
-    public StartReader(final BufferedReader reader, final String startedId,
+    public StartReader(final InputStream stream, final String startedId,
         final boolean isError)
     {
       final LocalizableMessage errorTag =
@@ -648,7 +638,7 @@
         public void run()
         {
           // The reader is owned by this thread, which closes it once the stream is drained.
-          try (BufferedReader in = reader)
+          try (BufferedReader in = new BufferedReader(new InputStreamReader(stream)))
           {
             String line = in.readLine();
             while (line != null)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java
index 73cb4d4..be1fd64 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java
@@ -260,16 +260,7 @@
         {
           return Entry.decode(reader, compressedSchema);
         }
-        InputStream is = reader.asInputStream();
-        if ((format & ENCRYPT_ENTRY) == ENCRYPT_ENTRY)
-        {
-          is = getCryptoManager().getCipherInputStream(is);
-        }
-        if ((format & COMPRESS_ENTRY) == COMPRESS_ENTRY)
-        {
-          is = new InflaterInputStream(is);
-        }
-        try (InputStream entryStream = is)
+        try (InputStream entryStream = newEntryInputStream(reader, format))
         {
           byte[] data = new byte[encodedEntryLen];
           int readBytes;
@@ -295,6 +286,31 @@
       }
     }
 
+    /**
+     * Returns the stream to read the encoded entry from, decorated as mandated by the format.
+     * <p>
+     * The returned stream owns the streams it wraps, so closing it closes the whole chain.
+     *
+     * @param reader the reader positioned at the start of the encoded entry.
+     * @param format the format byte of the encoded entry.
+     * @return the stream to read the encoded entry from.
+     * @throws CryptoManagerException If the cipher input stream cannot be created.
+     */
+    private static InputStream newEntryInputStream(ByteSequenceReader reader, int format)
+        throws CryptoManagerException
+    {
+      InputStream is = reader.asInputStream();
+      if ((format & ENCRYPT_ENTRY) == ENCRYPT_ENTRY)
+      {
+        is = getCryptoManager().getCipherInputStream(is);
+      }
+      if ((format & COMPRESS_ENTRY) == COMPRESS_ENTRY)
+      {
+        is = new InflaterInputStream(is);
+      }
+      return is;
+    }
+
     private ByteString encode(Entry entry, DataConfig dataConfig) throws DirectoryException
     {
       encodeVolatile(entry, dataConfig);
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java b/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
index f2f12a2..5d69679 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
@@ -328,6 +328,15 @@
       }
     }
 
+    if (backend == null)
+    {
+      // Unreachable as long as the checks above hold: a null backend ID implies at least one
+      // include branch, and every include branch either resolves to a backend or is rejected.
+      // The guard mirrors the one in runTask() and keeps the dereference below safe.
+      LocalizableMessage message = ERR_LDIFIMPORT_NO_BACKENDS_FOR_ID.get();
+      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
+    }
+
     // Make sure the selected backend will handle all the include branches
     defaultIncludeBranches = new ArrayList<>(backend.getBaseDNs());
 

--
Gitblit v1.10.0