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

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-mime4j.git

commit 4383a37b469fd2861c3b3143d10ae628f103ae7b
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Nov 4 14:56:55 2026 +0100

    [FIX] stop() inside a multipart: read nothing more and end the multipart
    
    stop() truncates the root buffer, but only its read() and readLine()
    checked the truncation. BufferedLineReaderInputStream#fillBuffer, used
    directly by MimeBoundaryInputStream, kept reading from the caller's
    stream. The boundary stream then found the next delimiter in the refill,
    its part ended without eof nor last part, and MimeEntity opened the next
    part at the same position: new body parts after stop(), and with some
    reads an endless loop (DefaultMessageBuilder#parseHeader never returned).
    
    - BufferedLineReaderInputStream#fillBuffer returns -1 once truncated.
    - MimeBoundaryInputStream treats a truncated buffer as the end of the
      stream: eof is set, nothing is skipped, so the multipart ends with
      T_END_MULTIPART.
    - MimeEntity does not report a part cut by stop() as a premature end,
      which strict parsing turned into an error.
    
    Found by twake-fuzz, scenarios 30-stop-contract and
    31-parse-header-matches-message.
---
 .../mime4j/io/BufferedLineReaderInputStream.java   |  4 ++
 .../james/mime4j/io/MimeBoundaryInputStream.java   | 16 ++++++
 .../org/apache/james/mime4j/stream/MimeEntity.java |  7 ++-
 .../james/mime4j/parser/MimeStreamParserTest.java  | 54 +++++++++++++++++++
 .../DefaultMessageBuilderParseHeaderTest.java      | 60 ++++++++++++++++++++++
 5 files changed, 140 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java
 
b/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java
index c03b5867..1c360129 100644
--- 
a/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java
+++ 
b/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java
@@ -99,6 +99,10 @@ public class BufferedLineReaderInputStream extends 
LineReaderInputStream {
     }
 
     public int fillBuffer() throws IOException {
+        if (this.truncated) {
+            // stop(): nothing more is read from the source
+            return -1;
+        }
         // compact the buffer if necessary
         if (this.bufpos > 0) { // could swtich to (this.buffer.length / 2) but 
needs a 4*boundary capacity, then (instead of 2).
             int len = bufferLen();
diff --git 
a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java 
b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java
index e95aa44c..609a5d9c 100644
--- a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java
+++ b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java
@@ -145,6 +145,12 @@ public class MimeBoundaryInputStream extends 
LineReaderInputStream {
         if (completed) {
             return false;
         }
+        if (!buffer.readAllowed()) {
+            // The buffer was truncated by stop(): the part ends here, without 
its delimiter
+            eof = true;
+            completed = true;
+            return false;
+        }
         if (endOfStream() && !hasData()) {
             skipBoundary();
             verifyEndOfStream();
@@ -239,6 +245,11 @@ public class MimeBoundaryInputStream extends 
LineReaderInputStream {
         if (eof) {
             return -1;
         }
+        if (!buffer.readAllowed()) {
+            // Truncated by stop(): the buffered bytes are gone and no more 
are read
+            eof = true;
+            return -1;
+        }
         int bytesRead;
         if (!hasData()) {
             int before = buffer.pos();
@@ -385,6 +396,11 @@ public class MimeBoundaryInputStream extends 
LineReaderInputStream {
     private void skipBoundary() throws IOException {
         if (!completed) {
             completed = true;
+            if (!buffer.readAllowed()) {
+                // Truncated by stop(): no delimiter left to skip
+                eof = true;
+                return;
+            }
             buffer.skip(boundaryLen);
             boolean checkForLastPart = true;
             for (;;) {
diff --git a/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java 
b/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java
index f418b732..28f384ce 100644
--- a/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java
+++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java
@@ -77,6 +77,7 @@ class MimeEntity implements EntityStateMachine {
 
     private RecursionMode recursionMode;
     private MimeBoundaryInputStream currentMimePartStream;
+    private boolean stopped;
     private LineReaderInputStreamAdaptor dataStream;
     /** The limited view of {@link #dataStream}, shared by every reader of the 
current content. */
     private InputStream limitedContentStream;
@@ -168,6 +169,7 @@ class MimeEntity implements EntityStateMachine {
     }
 
     public void stopSoft() {
+        this.stopped = true;
         if(currentMimePartStream != null) {
             try {
                 currentMimePartStream.close();
@@ -355,7 +357,10 @@ class MimeEntity implements EntityStateMachine {
             // Removed the T_IN_BODYPART state (always use T_PREAMBLE)
             advanceToBoundary();
             if (currentMimePartStream.eof() && 
!currentMimePartStream.isLastPart()) {
-                monitor(Event.MIME_BODY_PREMATURE_END);
+                if (!stopped) {
+                    // A part cut short by stop() is not a malformed message
+                    monitor(Event.MIME_BODY_PREMATURE_END);
+                }
             } else {
                 if (!currentMimePartStream.isLastPart()) {
                     clearMimePartStream();
diff --git 
a/core/src/test/java/org/apache/james/mime4j/parser/MimeStreamParserTest.java 
b/core/src/test/java/org/apache/james/mime4j/parser/MimeStreamParserTest.java
index 74e2c36e..5a74a9f8 100644
--- 
a/core/src/test/java/org/apache/james/mime4j/parser/MimeStreamParserTest.java
+++ 
b/core/src/test/java/org/apache/james/mime4j/parser/MimeStreamParserTest.java
@@ -31,6 +31,7 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.LinkedList;
 
 public class MimeStreamParserTest {
@@ -485,4 +486,57 @@ public class MimeStreamParserTest {
         return ContentUtil.decode(byteSequence);
     }
 
+
+    @Test(timeout = 10000)
+    public void stopAtTheEndOfTheHeaderShouldNotReadMoreFromAChunkedSource() 
throws Exception {
+        final byte[] msg = ("From: [email protected]\nContent-Type: 
multipart/mixed; boundary=b1\n\n"
+            + "--b1\nX-Part: one\n\nfirst body\n--b1\nX-Part: two\n\nsecond 
body\n--b1--\n")
+            .getBytes(StandardCharsets.US_ASCII);
+        final int[] delivered = {0};
+        InputStream source = new InputStream() {
+            // returns at most 8 bytes per read, like a socket
+            @Override
+            public int read() {
+                return delivered[0] < msg.length ? msg[delivered[0]++] & 0xff 
: -1;
+            }
+
+            @Override
+            public int read(byte[] b, int off, int len) {
+                if (delivered[0] >= msg.length) {
+                    return -1;
+                }
+                int n = Math.min(Math.min(len, 8), msg.length - delivered[0]);
+                System.arraycopy(msg, delivered[0], b, off, n);
+                delivered[0] += n;
+                return n;
+            }
+        };
+        final MimeStreamParser parser = new MimeStreamParser();
+        final int[] bodyParts = {0};
+        final int[] endMultipart = {0};
+        final int[] atStop = {-1};
+        parser.setContentHandler(new AbstractContentHandler() {
+            @Override
+            public void endHeader() {
+                atStop[0] = delivered[0];
+                parser.stop();
+            }
+
+            @Override
+            public void startBodyPart() {
+                bodyParts[0]++;
+            }
+
+            @Override
+            public void endMultipart() {
+                endMultipart[0]++;
+            }
+        });
+
+        parser.parse(source);
+
+        Assert.assertEquals(0, bodyParts[0]);
+        Assert.assertEquals(1, endMultipart[0]);
+        Assert.assertEquals(atStop[0], delivered[0]);
+    }
 }
diff --git 
a/dom/src/test/java/org/apache/james/mime4j/message/DefaultMessageBuilderParseHeaderTest.java
 
b/dom/src/test/java/org/apache/james/mime4j/message/DefaultMessageBuilderParseHeaderTest.java
index 56b9662f..f6b2bd9a 100644
--- 
a/dom/src/test/java/org/apache/james/mime4j/message/DefaultMessageBuilderParseHeaderTest.java
+++ 
b/dom/src/test/java/org/apache/james/mime4j/message/DefaultMessageBuilderParseHeaderTest.java
@@ -22,9 +22,11 @@ package org.apache.james.mime4j.message;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 
 import org.apache.james.mime4j.dom.Header;
+import org.apache.james.mime4j.stream.MimeConfig;
 import org.junit.Test;
 
 public class DefaultMessageBuilderParseHeaderTest {
@@ -49,4 +51,62 @@ public class DefaultMessageBuilderParseHeaderTest {
         assertThat(header.getField("Subject").getBody()).isEqualTo("test");
         assertThat(header.getFields()).hasSize(2);
     }
+
+    private static String twoPartMessage(int firstBodyLines) {
+        StringBuilder body = new StringBuilder();
+        for (int i = 0; i < firstBodyLines; i++) {
+            
body.append("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n");
+        }
+        return "From: [email protected]\nContent-Type: multipart/mixed; 
boundary=b1\n\n"
+            + "--b1\nX-Part: one\n\n" + body + "--b1\nX-Part: two\n\nsecond 
body\n--b1--\n";
+    }
+
+    /** Returns at most 8 bytes per read, like a socket. */
+    private static InputStream chunked(final byte[] msg) {
+        return new InputStream() {
+            private int delivered;
+
+            @Override
+            public int read() {
+                return delivered < msg.length ? msg[delivered++] & 0xff : -1;
+            }
+
+            @Override
+            public int read(byte[] b, int off, int len) {
+                if (delivered >= msg.length) {
+                    return -1;
+                }
+                int n = Math.min(Math.min(len, 8), msg.length - delivered);
+                System.arraycopy(msg, delivered, b, off, n);
+                delivered += n;
+                return n;
+            }
+        };
+    }
+
+    @Test(timeout = 10000)
+    public void 
parseHeaderShouldReturnOnAMultipartOfSeveralPartsLargerThanTheBuffer() throws 
Exception {
+        Header header = new DefaultMessageBuilder()
+            .parseHeader(new 
ByteArrayInputStream(twoPartMessage(50).getBytes(StandardCharsets.US_ASCII)));
+
+        assertThat(header.getFields()).hasSize(2);
+    }
+
+    @Test(timeout = 10000)
+    public void parseHeaderShouldReturnOnAMultipartReadInChunks() throws 
Exception {
+        Header header = new DefaultMessageBuilder()
+            
.parseHeader(chunked(twoPartMessage(1).getBytes(StandardCharsets.US_ASCII)));
+
+        assertThat(header.getFields()).hasSize(2);
+    }
+
+    @Test(timeout = 10000)
+    public void strictParseHeaderShouldReturnOnAMultipartReadInChunks() throws 
Exception {
+        DefaultMessageBuilder builder = new DefaultMessageBuilder();
+        builder.setMimeEntityConfig(MimeConfig.STRICT);
+
+        Header header = 
builder.parseHeader(chunked(twoPartMessage(50).getBytes(StandardCharsets.US_ASCII)));
+
+        assertThat(header.getFields()).hasSize(2);
+    }
 }


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

Reply via email to