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