From a1228d375d4ae318f12714358d2d0d817d131c7a Mon Sep 17 00:00:00 2001 From: pushnanashi2 Date: Wed, 9 Sep 2026 20:58:40 +0900 Subject: [PATCH] Fix truncated byte range responses in DefaultServlet Generated-by: OpenAI Codex --- .../catalina/servlets/DefaultServlet.java | 23 ++- .../servlets/TestDefaultServletRangeCopy.java | 194 ++++++++++++++++++ webapps/docs/changelog.xml | 5 + 3 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletRangeCopy.java diff --git a/java/org/apache/catalina/servlets/DefaultServlet.java b/java/org/apache/catalina/servlets/DefaultServlet.java index 986124013f5b..6f2afb6b4874 100644 --- a/java/org/apache/catalina/servlets/DefaultServlet.java +++ b/java/org/apache/catalina/servlets/DefaultServlet.java @@ -19,6 +19,7 @@ import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -2779,23 +2780,23 @@ protected IOException copyNoThrow(InputStream istream, ServletOutputStream ostre } IOException exception = null; - long bytesToRead = end - start + 1; + long rangeLength = end - start + 1; + long bytesToRead = rangeLength; byte[] buffer = new byte[input]; - int len = buffer.length; - while ((bytesToRead > 0) && (len >= buffer.length)) { + while (bytesToRead > 0) { try { - len = istream.read(buffer); - if (bytesToRead >= len) { - ostream.write(buffer, 0, len); - bytesToRead -= len; - } else { - ostream.write(buffer, 0, (int) bytesToRead); - bytesToRead = 0; + int len = istream.read(buffer, 0, (int) Math.min(buffer.length, bytesToRead)); + if (len == -1) { + exception = new EOFException(sm.getString("defaultServlet.wrongByteCountForRange", + Long.valueOf(rangeLength - bytesToRead), Long.valueOf(rangeLength))); + break; } + ostream.write(buffer, 0, len); + bytesToRead -= len; } catch (IOException ioe) { exception = ioe; - len = -1; + break; } } diff --git a/test/org/apache/catalina/servlets/TestDefaultServletRangeCopy.java b/test/org/apache/catalina/servlets/TestDefaultServletRangeCopy.java new file mode 100644 index 000000000000..0346696b02ab --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletRangeCopy.java @@ -0,0 +1,194 @@ +/* + * 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.catalina.servlets; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; + +import org.junit.Assert; +import org.junit.Test; + +public class TestDefaultServletRangeCopy { + + @Test + public void testCopyRangeContinuesAfterShortRead() throws IOException { + byte[] source = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }; + TesterServletOutputStream output = new TesterServletOutputStream(); + + IOException exception = copy(new ShortReadingInputStream(source), output, 0, 7); + + Assert.assertNull(exception); + Assert.assertArrayEquals(source, output.toByteArray()); + } + + + @Test + public void testCopyRangeReturnsEofExceptionAfterShortRead() throws IOException { + byte[] source = new byte[] { 0, 1, 2 }; + TesterServletOutputStream output = new TesterServletOutputStream(); + + IOException exception = copy(new ShortReadingInputStream(source), output, 0, 7); + + Assert.assertTrue(exception instanceof EOFException); + Assert.assertArrayEquals(source, output.toByteArray()); + } + + + @Test + public void testCopyRangeReturnsEofExceptionForEmptyStream() throws IOException { + TesterServletOutputStream output = new TesterServletOutputStream(); + + IOException exception = copy(new ByteArrayInputStream(new byte[0]), output, 0, 7); + + Assert.assertTrue(exception instanceof EOFException); + Assert.assertEquals(0, output.size()); + } + + + @Test + public void testCopyRangeAtBufferBoundaries() throws IOException { + int bufferSize = new DefaultServlet().input; + int[] sourceLengths = new int[] { bufferSize - 1, bufferSize, bufferSize + 1 }; + + for (int sourceLength : sourceLengths) { + byte[] source = new byte[sourceLength]; + for (int i = 0; i < source.length; i++) { + source[i] = (byte) i; + } + TesterServletOutputStream output = new TesterServletOutputStream(); + + IOException exception = copy(new ByteArrayInputStream(source), output, 0, sourceLength - 1); + + Assert.assertNull(exception); + Assert.assertArrayEquals(source, output.toByteArray()); + } + } + + + @Test + public void testCopyRangeWithNonZeroStart() throws IOException { + byte[] source = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; + TesterServletOutputStream output = new TesterServletOutputStream(); + + IOException exception = copy(new ShortReadingInputStream(source), output, 2, 9); + + Assert.assertNull(exception); + Assert.assertArrayEquals(Arrays.copyOfRange(source, 2, 10), output.toByteArray()); + } + + + @Test + public void testCopyRangeReturnsInputIOException() throws IOException { + IOException expected = new IOException(); + InputStream input = new InputStream() { + + @Override + public int read() throws IOException { + throw expected; + } + + @Override + public int read(byte[] target, int offset, int length) throws IOException { + throw expected; + } + }; + + IOException actual = copy(input, new TesterServletOutputStream(), 0, 7); + + Assert.assertSame(expected, actual); + } + + + private static IOException copy(InputStream input, ServletOutputStream output, long start, long end) + throws IOException { + DefaultServlet servlet = new DefaultServlet(); + try (InputStream bufferedInput = new BufferedInputStream(input, servlet.input)) { + return servlet.copyNoThrow(bufferedInput, output, start, end); + } + } + + + private static class ShortReadingInputStream extends InputStream { + + private static final int MAX_READ_SIZE = 3; + + private final byte[] bytes; + private int position; + + private ShortReadingInputStream(byte[] bytes) { + this.bytes = bytes; + } + + @Override + public int read() { + return position < bytes.length ? bytes[position++] & 0xFF : -1; + } + + @Override + public int read(byte[] target, int offset, int length) { + if (position == bytes.length) { + return -1; + } + int count = Math.min(Math.min(length, MAX_READ_SIZE), bytes.length - position); + System.arraycopy(bytes, position, target, offset, count); + position += count; + return count; + } + } + + + private static class TesterServletOutputStream extends ServletOutputStream { + + private final ByteArrayOutputStream output = new ByteArrayOutputStream(); + + @Override + public void write(int value) { + output.write(value); + } + + @Override + public void write(byte[] bytes, int offset, int length) { + output.write(bytes, offset, length); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + // NO-OP + } + + private int size() { + return output.size(); + } + + private byte[] toByteArray() { + return output.toByteArray(); + } + } +} diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 06201d01214e..4fd442901495 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -241,6 +241,11 @@ interface must implement this method. (markt) + + Fix byte range responses generated by DefaultServlet so a + short resource stream read does not truncate the response. Treat a + premature end of stream as an I/O error. (aoto-tech) +