From 5fa8ed760437453c7694ce9e8bd76d21e2f5d683 Mon Sep 17 00:00:00 2001
From: Valera V Harseko <vharseko@3a-systems.ru>
Date: Fri, 17 Jul 2026 11:16:45 +0000
Subject: [PATCH] CVE-2026-62375 Unbounded VLV offset array allocation leading to memory-exhaustion DoS

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java         |    8 ++
 opendj-server-legacy/src/main/java/org/opends/server/controls/VLVRequestControl.java          |   16 +++++
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java   |   27 ++++++---
 opendj-server-legacy/src/test/java/org/opends/server/controls/VLVOffsetAllocationDoSTest.java |  126 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 167 insertions(+), 10 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
index a4a7eb2..04827b4 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -14,6 +14,7 @@
  * Copyright 2006-2010 Sun Microsystems, Inc.
  * Portions Copyright 2011-2016 ForgeRock AS.
  * Portions copyright 2013 Manuel Gaupp
+ * Portions Copyright 2026 3A Systems, LLC
  */
 package org.opends.server.backends.pluggable;
 
@@ -2688,21 +2689,29 @@
       afterCount = 0;
     }
 
-    int count = 1 + beforeCount + afterCount;
+    // Never allocate more longs than the number of entries actually available
+    // from startPos, and compute the size with long arithmetic so an
+    // attacker-supplied before/after count cannot overflow or drive an oversized
+    // array (GHSA-q4wx-wj4j-4657).
+    final int available = startPos >= 0 && startPos < sortMap.size() ? sortMap.size() - startPos : 0;
+    final int count = (int) Math.max(0L, Math.min(1L + beforeCount + afterCount, available));
     long[] sortedIDs = new long[count];
     int treePos = 0;
     int arrayPos = 0;
-    for (EntryID id : sortMap.values())
+    if (count > 0)
     {
-      if (treePos++ < startPos)
+      for (EntryID id : sortMap.values())
       {
-        continue;
-      }
+        if (treePos++ < startPos)
+        {
+          continue;
+        }
 
-      sortedIDs[arrayPos++] = id.longValue();
-      if (arrayPos >= count)
-      {
-        break;
+        sortedIDs[arrayPos++] = id.longValue();
+        if (arrayPos >= count)
+        {
+          break;
+        }
       }
     }
 
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java
index 2b96e9f..757aa99 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
  */
 package org.opends.server.backends.pluggable;
 
@@ -689,7 +690,12 @@
     }
 
     final long[] selectedIDs;
-    final int count = 1 + beforeCount + afterCount;
+    // Never allocate more longs than the number of entries actually available
+    // from startPos, and compute the size with long arithmetic so an
+    // attacker-supplied before/after count cannot overflow or drive an oversized
+    // array (GHSA-q4wx-wj4j-4657).
+    final int available = startPos >= 0 && startPos < currentCount ? currentCount - startPos : 0;
+    final int count = (int) Math.max(0L, Math.min(1L + beforeCount + afterCount, available));
     try (Cursor<ByteString, ByteString> cursor = txn.openCursor(getName()))
     {
       if (cursor.positionToIndex(startPos))
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/controls/VLVRequestControl.java b/opendj-server-legacy/src/main/java/org/opends/server/controls/VLVRequestControl.java
index d71910b..5a109c2 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/controls/VLVRequestControl.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/controls/VLVRequestControl.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008 Sun Microsystems, Inc.
  * Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
  */
 package org.opends.server.controls;
 import org.forgerock.i18n.LocalizableMessage;
@@ -74,6 +75,16 @@
         int beforeCount = (int)reader.readInteger();
         int afterCount  = (int)reader.readInteger();
 
+        // The VLV draft defines beforeCount/afterCount as INTEGER (0..maxInt).
+        // Reject negative values (including those produced by the (int) cast
+        // wrapping a wire value above 2^31-1): unchecked they would flow into an
+        // unbounded / overflowing array allocation downstream (GHSA-q4wx-wj4j-4657).
+        if (beforeCount < 0 || afterCount < 0)
+        {
+          throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
+              INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE.get("beforeCount and afterCount must not be negative"));
+        }
+
         int offset = 0;
         int contentCount = 0;
         ByteString greaterThanOrEqual = null;
@@ -85,6 +96,11 @@
             offset = (int)reader.readInteger();
             contentCount = (int)reader.readInteger();
             reader.readEndSequence();
+            if (offset < 0 || contentCount < 0)
+            {
+              throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
+                  INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE.get("offset and contentCount must not be negative"));
+            }
             break;
 
           case TYPE_TARGET_GREATERTHANOREQUAL:
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/controls/VLVOffsetAllocationDoSTest.java b/opendj-server-legacy/src/test/java/org/opends/server/controls/VLVOffsetAllocationDoSTest.java
new file mode 100644
index 0000000..9660e2b
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/controls/VLVOffsetAllocationDoSTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.controls;
+
+import static org.opends.server.protocols.internal.InternalClientConnection.*;
+import static org.opends.server.protocols.internal.Requests.*;
+import static org.testng.Assert.*;
+
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.protocols.internal.SearchRequest;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Regression test for GHSA-q4wx-wj4j-4657 / OPENDJ-003 — unbounded array
+ * allocation via VLV beforeCount/afterCount/offset (CWE-789 / CWE-770 / CWE-190).
+ * <p>
+ * A VLV-by-offset request carries attacker-controlled before/after counts. The
+ * server computed {@code count = 1 + beforeCount + afterCount} and allocated
+ * {@code new long[count]} without clamping it to the actual list size. With
+ * {@code afterCount = Integer.MAX_VALUE} the sum overflowed to a negative int,
+ * yielding a {@code NegativeArraySizeException} (or, with large non-overflowing
+ * values, a multi-gigabyte allocation / {@code OutOfMemoryError}) from a single
+ * search request. The fix computes the count with long arithmetic and clamps it
+ * to the number of entries actually available, so the oversized window is now
+ * bounded instead of faulting the search handler.
+ * <p>
+ * Note: this exercises the default in-memory sort path
+ * ({@code EntryContainer.sortByOffset}), which requires no VLV index — the same
+ * unclamped allocation also existed in {@code VLVIndex.readRange}.
+ */
+@SuppressWarnings("javadoc")
+public class VLVOffsetAllocationDoSTest extends ControlsTestCase
+{
+  @BeforeClass
+  public void startServer() throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+  private void populateDB() throws Exception
+  {
+    TestCaseUtils.clearBackend("userRoot", "dc=example,dc=com");
+    TestCaseUtils.addEntries(
+        "dn: uid=albert.zimmerman,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "uid: albert.zimmerman",
+        "givenName: Albert",
+        "sn: Zimmerman",
+        "cn: Albert Zimmerman",
+        "",
+        "dn: uid=aaron.zimmerman,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "uid: aaron.zimmerman",
+        "givenName: Aaron",
+        "sn: Zimmerman",
+        "cn: Aaron Zimmerman",
+        "",
+        "dn: uid=mary.jones,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "uid: mary.jones",
+        "givenName: Mary",
+        "sn: Jones",
+        "cn: Mary Jones");
+  }
+
+  /**
+   * A single VLV-by-offset search with {@code afterCount = Integer.MAX_VALUE}
+   * (which previously overflowed {@code 1 + beforeCount + afterCount} into a
+   * negative array size) must now be clamped to the list size and return the
+   * same bounded page as a normal request — not fault the search handler with a
+   * {@code NegativeArraySizeException} / {@code OTHER}.
+   */
+  @Test
+  public void offsetAllocationOverflowIsClamped() throws Exception
+  {
+    populateDB();
+
+    // Baseline: a normal, bounded window.
+    SearchRequest ok = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
+        .addControl(new ServerSideSortRequestControl("givenName"))
+        .addControl(new VLVRequestControl(0, 3, 1, 0));
+    InternalSearchOperation okOp = getRootConnection().processSearch(ok);
+    assertEquals(okOp.getResultCode(), ResultCode.SUCCESS);
+
+    // Oversized window: afterCount = Integer.MAX_VALUE. Before the fix this
+    // overflowed to new long[-2147483648]; a larger non-overflowing count would
+    // instead force a multi-gigabyte allocation (OutOfMemoryError).
+    SearchRequest bad = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
+        .addControl(new ServerSideSortRequestControl("givenName"))
+        .addControl(new VLVRequestControl(0, Integer.MAX_VALUE, 1, 0));
+    InternalSearchOperation badOp = getRootConnection().processSearch(bad);
+
+    assertEquals(badOp.getResultCode(), ResultCode.SUCCESS,
+        "oversized VLV afterCount must be clamped to the list size, not fault the search handler "
+            + "(GHSA-q4wx-wj4j-4657): " + badOp.getErrorMessage());
+    // The page is bounded by the number of entries that actually exist (offset 1
+    // onwards), i.e. the same result as the bounded baseline request.
+    assertEquals(badOp.getSearchEntries().size(), okOp.getSearchEntries().size());
+  }
+}

--
Gitblit v1.10.0