This is an automated email from the ASF dual-hosted git repository.

smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 2ae0ca931e4 HDDS-15341. EC write can fail with 
ArrayIndexOutOfBoundsException due to CoderUtil emptyChunk resize race (#10324)
2ae0ca931e4 is described below

commit 2ae0ca931e4dbd7ba0c984b1b719124afd5bd137
Author: Siyao Meng <[email protected]>
AuthorDate: Thu May 28 14:32:07 2026 -0700

    HDDS-15341. EC write can fail with ArrayIndexOutOfBoundsException due to 
CoderUtil emptyChunk resize race (#10324)
    
    Generated-by: Claude Code (Opus 4.7)
---
 .../ozone/erasurecode/rawcoder/CoderUtil.java      |  16 +++-
 .../ozone/erasurecode/rawcoder/TestCoderUtil.java  | 100 +++++++++++++++++++++
 2 files changed, 112 insertions(+), 4 deletions(-)

diff --git 
a/hadoop-hdds/erasurecode/src/main/java/org/apache/ozone/erasurecode/rawcoder/CoderUtil.java
 
b/hadoop-hdds/erasurecode/src/main/java/org/apache/ozone/erasurecode/rawcoder/CoderUtil.java
index ebf45e88dda..1fdadcad15b 100644
--- 
a/hadoop-hdds/erasurecode/src/main/java/org/apache/ozone/erasurecode/rawcoder/CoderUtil.java
+++ 
b/hadoop-hdds/erasurecode/src/main/java/org/apache/ozone/erasurecode/rawcoder/CoderUtil.java
@@ -38,15 +38,23 @@ private CoderUtil() {
    * @return empty chunk of zero bytes
    */
   static byte[] getEmptyChunk(int leastLength) {
-    if (emptyChunk.length >= leastLength) {
-      return emptyChunk; // In most time
+    byte[] chunk = emptyChunk;
+    if (chunk.length >= leastLength) {
+      return chunk; // In most time
     }
 
     synchronized (CoderUtil.class) {
-      emptyChunk = new byte[leastLength];
+      // Recheck under the lock: another caller may already have grown the
+      // cache while this caller waited. A larger cached chunk is valid for a
+      // smaller request, so only allocate when the cache is still too small.
+      chunk = emptyChunk;
+      if (chunk.length < leastLength) {
+        chunk = new byte[leastLength];
+        emptyChunk = chunk;
+      }
     }
 
-    return emptyChunk;
+    return chunk;
   }
 
   /**
diff --git 
a/hadoop-hdds/erasurecode/src/test/java/org/apache/ozone/erasurecode/rawcoder/TestCoderUtil.java
 
b/hadoop-hdds/erasurecode/src/test/java/org/apache/ozone/erasurecode/rawcoder/TestCoderUtil.java
new file mode 100644
index 00000000000..2135a31ecc3
--- /dev/null
+++ 
b/hadoop-hdds/erasurecode/src/test/java/org/apache/ozone/erasurecode/rawcoder/TestCoderUtil.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ozone.erasurecode.rawcoder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for raw coder utility methods.
+ */
+public class TestCoderUtil {
+
+  private static final int INITIAL_LENGTH = 4096;
+  private static final int SMALL_LENGTH = INITIAL_LENGTH + 1;
+  private static final int LARGE_LENGTH = SMALL_LENGTH * 2;
+
+  @BeforeEach
+  public void resetEmptyChunk() throws Exception {
+    Field emptyChunk = CoderUtil.class.getDeclaredField("emptyChunk");
+    emptyChunk.setAccessible(true);
+    synchronized (CoderUtil.class) {
+      emptyChunk.set(null, new byte[INITIAL_LENGTH]);
+    }
+  }
+
+  @Test
+  // HDDS-15341: This can reproduce the race that can make getEmptyChunk()
+  // return a buffer shorter than requested, which later causes
+  // ArrayIndexOutOfBoundsException when resetBuffer() passes that buffer
+  // to System.arraycopy().
+  public void getEmptyChunkDoesNotShrinkWhenCacheGrowsConcurrently()
+      throws Exception {
+    AtomicReference<Thread> workerThread = new AtomicReference<>();
+    ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
+      Thread thread = new Thread(r, "get-empty-chunk-small");
+      workerThread.set(thread);
+      return thread;
+    });
+
+    try {
+      Future<byte[]> smallChunk;
+      synchronized (CoderUtil.class) {
+        smallChunk = executor.submit(() -> CoderUtil.getEmptyChunk(
+            SMALL_LENGTH));
+        waitUntilBlocked(workerThread);
+        assertThat(CoderUtil.getEmptyChunk(LARGE_LENGTH).length)
+            .isGreaterThanOrEqualTo(LARGE_LENGTH);
+      }
+
+      assertThat(smallChunk.get(10, TimeUnit.SECONDS).length)
+          .as("concurrent caller should return the larger chunk already 
cached")
+          .isGreaterThanOrEqualTo(LARGE_LENGTH);
+      assertThat(CoderUtil.getEmptyChunk(LARGE_LENGTH).length)
+          .as("empty chunk cache should not shrink")
+          .isGreaterThanOrEqualTo(LARGE_LENGTH);
+    } finally {
+      executor.shutdownNow();
+    }
+  }
+
+  private static void waitUntilBlocked(AtomicReference<Thread> threadRef)
+      throws InterruptedException {
+    long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+    while (System.nanoTime() < deadline) {
+      Thread thread = threadRef.get();
+      if (thread != null && thread.getState() == Thread.State.BLOCKED) {
+        return;
+      }
+      Thread.sleep(10);
+    }
+
+    Thread thread = threadRef.get();
+    fail("small getEmptyChunk caller did not block on CoderUtil.class; state="
+        + (thread == null ? "not started" : thread.getState()));
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to