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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new ef39bc927 StrBuilder.replaceImpl shrink-branch leaves residual chars 
in buffer (#1672)
ef39bc927 is described below

commit ef39bc92712d47c100af3b243a8bfcd84a45f116
Author: Gary Gregory <[email protected]>
AuthorDate: Sun May 24 13:14:58 2026 -0400

    StrBuilder.replaceImpl shrink-branch leaves residual chars in buffer (#1672)
    
    tail
---
 .../org/apache/commons/lang3/text/StrBuilder.java  |  3 ++
 .../commons/lang3/text/StrBuilderClearTest.java    | 62 +++++++++++++++-------
 2 files changed, 45 insertions(+), 20 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java 
b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
index aa98cd992..39277460c 100644
--- a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
@@ -2705,6 +2705,9 @@ private void replaceImpl(final int startIndex, final int 
endIndex, final int rem
         if (insertLen != removeLen) {
             ensureCapacity(newSize);
             System.arraycopy(buffer, endIndex, buffer, startIndex + insertLen, 
size - endIndex);
+            if (size > newSize) {
+                ArrayFill.clear(buffer, newSize, size);
+            }
             size = newSize;
         }
         if (insertLen > 0) {
diff --git 
a/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java 
b/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java
index e499a1d20..328d4c92a 100644
--- a/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java
+++ b/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java
@@ -143,28 +143,30 @@ public void 
testReadFromReaderDoesNotExposeStaleInternalBuffer() throws IOExcept
     }
 
     @Test
-    public void testStaleCharsNotLeakedAfterClear() throws Exception {
-        final StrBuilder sb = new StrBuilder("secret_password_xyzzy_leak");
-        // clear() resets logical size to 0 but leaves chars in buffer
-        sb.clear();
-        // append something shorter than the original
-        sb.append("ok");
-        // Stale content is serialized as UTF-16BE char[] data.
-        // "xyzzy_leak" was at positions 15+, well beyond "ok" (len=2), so 
must not appear.
-        assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), 
"xyzzy_leak"));
-    }
-
-    @Test
-    public void testStaleCharsNotLeakedAfterTruncate() throws Exception {
-        final StrBuilder sb = new StrBuilder("top_secret_key_material");
-        // truncate to a short length – tail remains in buffer
-        sb.delete(6, sb.length());
-        // sb now logically contains "top_se"
-        assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), 
"secret_key_material"));
+    void testReplaceImplLeavesResidue() throws Exception {
+        final String string = "SECRET_PASSWORD_DATA";
+        final StrBuilder sb = new StrBuilder(string);
+        assertEquals(20, sb.length());
+        // Shrink: replace [0,20) with "X" => removeLen=20, insertLen=1, 
newSize=1.
+        sb.replace(0, 20, "X");
+        assertEquals(1, sb.length());
+        assertEquals("X", sb.toString());
+        final char[] buf = sb.getBuffer();
+        assertTrue(buf.length >= 20);
+        // Tail [1..20) should be cleared but isn't on baseline => residue 
persists.
+        // Probe offset 5: original was '_' (underscore from "SECRET_..."). 
After
+        // arraycopy(buf, endIndex=20, buf, startIndex+insertLen=1, 
size-endIndex=0)
+        // the shift is a no-op, so buf[5] retains the original 'T' from 
"SECRET_".
+        // Either way it is non-NUL.
+        assertEquals(CharUtils.NUL, buf[5]);
+        // Dump the visible residue at the logical-unused tail.
+        for (int i = 1; i < 20; i++) {
+            assertEquals(CharUtils.NUL, buf[i]);
+        }
     }
 
     @Test
-    void testSetLengthShrinkLeavesResidual() throws Exception {
+    void testSetLengthShrinkLeavesResidue() throws Exception {
         final String string = "CONFIDENTIAL_TOKEN_VALUE";
         final int len = string.length();
         final StrBuilder sb = new StrBuilder(string);
@@ -177,9 +179,29 @@ void testSetLengthShrinkLeavesResidual() throws Exception {
         assertTrue(buf.length >= len);
         // Probe offset 10: original was 'L' (CONFIDENTIA*L*_TOKEN_VALUE).
         assertEquals(CharUtils.NUL, buf[10]);
-        final StringBuilder dump = new StringBuilder();
         for (int i = 5; i < len; i++) {
             assertEquals(CharUtils.NUL, buf[i]);
         }
     }
+
+    @Test
+    public void testStaleCharsNotLeakedAfterClear() throws Exception {
+        final StrBuilder sb = new StrBuilder("secret_password_xyzzy_leak");
+        // clear() resets logical size to 0 but leaves chars in buffer
+        sb.clear();
+        // append something shorter than the original
+        sb.append("ok");
+        // Stale content is serialized as UTF-16BE char[] data.
+        // "xyzzy_leak" was at positions 15+, well beyond "ok" (len=2), so 
must not appear.
+        assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), 
"xyzzy_leak"));
+    }
+
+    @Test
+    public void testStaleCharsNotLeakedAfterTruncate() throws Exception {
+        final StrBuilder sb = new StrBuilder("top_secret_key_material");
+        // truncate to a short length – tail remains in buffer
+        sb.delete(6, sb.length());
+        // sb now logically contains "top_se"
+        assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), 
"secret_key_material"));
+    }
 }

Reply via email to