This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new 81ad0e72aa Fix BZ 70126. permessage-deflate may drop bytes of inflated
frame
81ad0e72aa is described below
commit 81ad0e72aa5bd9a07dbd9e64ad13e486afb41507
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 b3c94192a6..b4258500ba 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 45080ade46..9d4307a824 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]