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

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new a38eb3c581 Fix BZ 70126. permessage-deflate may drop bytes of inflated 
frame
a38eb3c581 is described below

commit a38eb3c5814d19d7f085cae7a5ca5ddaf9bb4a95
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Jun 24 16:22:33 2026 +0100

    Fix BZ 70126. permessage-deflate may drop bytes of inflated frame
    
    Fix written by GPT-5.5
    Test case written by Hironori Ichimiya
---
 .../apache/tomcat/websocket/PerMessageDeflate.java | 93 ++++++++++++++++++----
 .../tomcat/websocket/TestPerMessageDeflate.java    | 46 +++++++++++
 2 files changed, 123 insertions(+), 16 deletions(-)

diff --git a/java/org/apache/tomcat/websocket/PerMessageDeflate.java 
b/java/org/apache/tomcat/websocket/PerMessageDeflate.java
index 3789abb69f..5ad156dcc8 100644
--- a/java/org/apache/tomcat/websocket/PerMessageDeflate.java
+++ b/java/org/apache/tomcat/websocket/PerMessageDeflate.java
@@ -68,11 +68,14 @@ public class PerMessageDeflate implements Transformation {
     private final boolean isServer;
     private final Inflater inflater = new Inflater(true);
     private final ByteBuffer readBuffer = 
ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+    private final byte[] eomOverflowBuffer = new byte[1];
     private final Deflater deflater = new 
Deflater(Deflater.DEFAULT_COMPRESSION, true);
     private final byte[] EOM_BUFFER = new byte[EOM_BYTES.length + 1];
 
     private volatile Transformation next;
     private volatile boolean skipDecompression = false;
+    private volatile boolean eomBytesInserted = false;
+    private volatile boolean eomOverflowWritten = false;
     private volatile ByteBuffer writeBuffer = 
ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
     private volatile boolean firstCompressedFrameWritten = false;
     // Flag to track if a message is completely empty
@@ -211,10 +214,23 @@ public class PerMessageDeflate implements Transformation {
             return next.getMoreData(opCode, fin, rsv, dest);
         }
 
+        if (eomOverflowWritten) {
+            if (!dest.hasRemaining()) {
+                return TransformationResult.OVERFLOW;
+            }
+            dest.put(eomOverflowBuffer[0]);
+            eomOverflowWritten = false;
+            if (!dest.hasRemaining()) {
+                if (inflateEomBytes()) {
+                    return TransformationResult.OVERFLOW;
+                }
+                return endFrame(fin);
+            }
+        }
+
         int written;
-        boolean usedEomBytes = false;
 
-        while (dest.remaining() > 0 || usedEomBytes) {
+        while (dest.hasRemaining()) {
             // Space available in destination. Try and fill it.
             try {
                 written = inflater.inflate(dest.array(), dest.arrayOffset() + 
dest.position(), dest.remaining());
@@ -226,7 +242,7 @@ public class PerMessageDeflate implements Transformation {
             }
             dest.position(dest.position() + written);
 
-            if (inflater.needsInput() && !usedEomBytes) {
+            if (inflater.needsInput() && !eomBytesInserted) {
                 readBuffer.clear();
                 TransformationResult nextResult = next.getMoreData(opCode, 
fin, (rsv ^ RSV_BITMASK), readBuffer);
                 inflater.setInput(readBuffer.array(), 
readBuffer.arrayOffset(), readBuffer.position());
@@ -236,33 +252,78 @@ public class PerMessageDeflate implements Transformation {
                     } else if 
(TransformationResult.END_OF_FRAME.equals(nextResult) && readBuffer.position() 
== 0) {
                         if (fin) {
                             inflater.setInput(EOM_BYTES);
-                            usedEomBytes = true;
+                            eomBytesInserted = true;
                         } else {
-                            return TransformationResult.END_OF_FRAME;
+                            return endFrame(fin);
                         }
                     }
                 } else if (readBuffer.position() > 0) {
                     return TransformationResult.OVERFLOW;
-                } else if (fin) {
-                    inflater.setInput(EOM_BYTES);
-                    usedEomBytes = true;
-                }
-            } else if (written == 0) {
-                if (fin && (isServer && !clientContextTakeover || !isServer && 
!serverContextTakeover)) {
-                    try {
-                        inflater.reset();
-                    } catch (NullPointerException e) {
-                        throw new 
IOException(sm.getString("perMessageDeflate.alreadyClosed"), e);
+                } else if 
(TransformationResult.END_OF_FRAME.equals(nextResult)) {
+                    if (fin) {
+                        if (inflateEomBytes()) {
+                            return TransformationResult.OVERFLOW;
+                        }
                     }
+                    return endFrame(fin);
+                } else if (TransformationResult.UNDERFLOW.equals(nextResult)) {
+                    return nextResult;
                 }
-                return TransformationResult.END_OF_FRAME;
+            } else if (written == 0) {
+                return endFrame(fin);
+            }
+        }
+
+        if (eomBytesInserted) {
+            if (inflateEomBytes()) {
+                return TransformationResult.OVERFLOW;
             }
+            return endFrame(fin);
         }
 
         return TransformationResult.OVERFLOW;
     }
 
 
+    private boolean inflateEomBytes() throws IOException {
+        if (!eomBytesInserted) {
+            inflater.setInput(EOM_BYTES);
+            eomBytesInserted = true;
+        }
+
+        int written;
+        try {
+            written = inflater.inflate(eomOverflowBuffer, 0, 
eomOverflowBuffer.length);
+        } catch (DataFormatException e) {
+            throw new 
IOException(sm.getString("perMessageDeflate.deflateFailed"), e);
+        } catch (IllegalStateException | NullPointerException e) {
+            // As of Java 25, the JRE throws an ISE rather than an NPE
+            throw new 
IOException(sm.getString("perMessageDeflate.alreadyClosed"), e);
+        }
+
+        if (written > 0) {
+            eomOverflowWritten = true;
+            return true;
+        }
+
+        return false;
+    }
+
+
+    private TransformationResult endFrame(boolean fin) throws IOException {
+        eomBytesInserted = false;
+        eomOverflowWritten = false;
+        if (fin && (isServer && !clientContextTakeover || !isServer && 
!serverContextTakeover)) {
+            try {
+                inflater.reset();
+            } catch (NullPointerException e) {
+                throw new 
IOException(sm.getString("perMessageDeflate.alreadyClosed"), e);
+            }
+        }
+        return TransformationResult.END_OF_FRAME;
+    }
+
+
     @Override
     public boolean validateRsv(int rsv, byte opCode) {
         if (Util.isControl(opCode)) {
diff --git a/test/org/apache/tomcat/websocket/TestPerMessageDeflate.java 
b/test/org/apache/tomcat/websocket/TestPerMessageDeflate.java
index c74684b547..2798cecbee 100644
--- a/test/org/apache/tomcat/websocket/TestPerMessageDeflate.java
+++ b/test/org/apache/tomcat/websocket/TestPerMessageDeflate.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -101,6 +102,51 @@ public class TestPerMessageDeflate {
     }
 
 
+    @Test
+    public void testMessagePartThatOverfillsBuffer() throws IOException {
+
+        List<Parameter> parameters = Collections.emptyList();
+        List<List<Parameter>> preferences = new ArrayList<>();
+        preferences.add(parameters);
+
+        List<String> truncated = new ArrayList<>();
+
+        for (int size = 8192 + 1; size <= 8192 + 512; size++) {
+
+            // Compress `size` identical (highly compressible) bytes as one 
binary message.
+            PerMessageDeflate perMessageDeflateTx = 
PerMessageDeflate.build(preferences, true);
+            perMessageDeflateTx.setNext(new TesterTransformation());
+            byte[] data = new byte[size];
+            Arrays.fill(data, (byte) 0x80);
+            List<MessagePart> uncompressedParts = new ArrayList<>();
+            uncompressedParts.add(new MessagePart(true, 0, 
Constants.OPCODE_BINARY,
+                    ByteBuffer.wrap(data), null, null, -1));
+            MessagePart compressedPart = 
perMessageDeflateTx.sendMessagePart(uncompressedParts).get(0);
+
+            // Decompress the way WsFrameBase.processDataBinary does: fill an 
8192
+            // byte buffer, loop on OVERFLOW, stop on END_OF_FRAME.
+            PerMessageDeflate perMessageDeflateRx = 
PerMessageDeflate.build(preferences, true);
+            perMessageDeflateRx.setNext(new 
TesterTransformation(compressedPart.getPayload()));
+
+            int total = 0;
+            ByteBuffer received = ByteBuffer.allocate(8192);
+            TransformationResult tr;
+            do {
+                tr = 
perMessageDeflateRx.getMoreData(compressedPart.getOpCode(), 
compressedPart.isFin(),
+                        compressedPart.getRsv(), received);
+                total += received.position();
+                received.clear();
+            } while (tr == TransformationResult.OVERFLOW);
+
+            if (total != size) {
+                truncated.add("size=" + size + " recovered=" + total);
+            }
+        }
+
+        Assert.assertTrue("permessage-deflate dropped trailing bytes for: " + 
truncated, truncated.isEmpty());
+    }
+
+
     /*
      * https://bz.apache.org/bugzilla/show_bug.cgi?id=66681
      */


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

Reply via email to