From: Markus Koschany <apo@debian.org> Date: Sat, 20 Mar 2021 17:48:26 +0200 Subject: CVE-2020-11612 Origin: https://github.com/netty/netty/commit/1543218d3e7afcb33a90b728b14370395a3deca0 --- .../handler/codec/compression/JZlibDecoder.java | 60 +++++++++++++++++-- .../handler/codec/compression/JdkZlibDecoder.java | 70 +++++++++++++++++++--- .../handler/codec/compression/ZlibDecoder.java | 65 ++++++++++++++++++++ .../netty/handler/codec/compression/JZlibTest.java | 4 +- .../handler/codec/compression/JdkZlibTest.java | 4 +- .../handler/codec/compression/ZlibCrossTest1.java | 4 +- .../handler/codec/compression/ZlibCrossTest2.java | 4 +- .../netty/handler/codec/compression/ZlibTest.java | 57 +++++++++++++++++- 8 files changed, 247 insertions(+), 21 deletions(-) diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java index 5d23bb8..ab01e56 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.compression; import com.jcraft.jzlib.Inflater; import com.jcraft.jzlib.JZlib; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import java.util.List; @@ -34,7 +35,21 @@ public class JZlibDecoder extends ZlibDecoder { * @throws DecompressionException if failed to initialize zlib */ public JZlibDecoder() { - this(ZlibWrapper.ZLIB); + this(ZlibWrapper.ZLIB, 0); + } + + /** + * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}) + * and specified maximum buffer allocation. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + * + * @throws DecompressionException if failed to initialize zlib + */ + public JZlibDecoder(int maxAllocation) { + this(ZlibWrapper.ZLIB, maxAllocation); } /** @@ -43,6 +58,21 @@ public class JZlibDecoder extends ZlibDecoder { * @throws DecompressionException if failed to initialize zlib */ public JZlibDecoder(ZlibWrapper wrapper) { + this(wrapper, 0); + } + + /** + * Creates a new instance with the specified wrapper and maximum buffer allocation. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + * + * @throws DecompressionException if failed to initialize zlib + */ + public JZlibDecoder(ZlibWrapper wrapper, int maxAllocation) { + super(maxAllocation); + if (wrapper == null) { throw new NullPointerException("wrapper"); } @@ -53,7 +83,7 @@ public class JZlibDecoder extends ZlibDecoder { } } - /** + /** * Creates a new instance with the specified preset dictionary. The wrapper * is always {@link ZlibWrapper#ZLIB} because it is the only format that * supports the preset dictionary. @@ -61,6 +91,23 @@ public class JZlibDecoder extends ZlibDecoder { * @throws DecompressionException if failed to initialize zlib */ public JZlibDecoder(byte[] dictionary) { + this(dictionary, 0); + } + + /** + * Creates a new instance with the specified preset dictionary and maximum buffer allocation. + * The wrapper is always {@link ZlibWrapper#ZLIB} because it is the only format that + * supports the preset dictionary. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + * + * @throws DecompressionException if failed to initialize zlib + */ + public JZlibDecoder(byte[] dictionary, int maxAllocation) { + super(maxAllocation); + if (dictionary == null) { throw new NullPointerException("dictionary"); } @@ -110,11 +157,11 @@ public class JZlibDecoder extends ZlibDecoder { final int oldNextInIndex = z.next_in_index; // Configure output. - ByteBuf decompressed = ctx.alloc().heapBuffer(inputLength << 1); + ByteBuf decompressed = prepareDecompressBuffer(ctx, null, inputLength << 1); try { loop: for (;;) { - decompressed.ensureWritable(z.avail_in << 1); + decompressed = prepareDecompressBuffer(ctx, decompressed, z.avail_in << 1); z.avail_out = decompressed.writableBytes(); z.next_out = decompressed.array(); z.next_out_index = decompressed.arrayOffset() + decompressed.writerIndex(); @@ -170,4 +217,9 @@ public class JZlibDecoder extends ZlibDecoder { z.next_out = null; } } + + @Override + protected void decompressionBufferExhausted(ByteBuf buffer) { + finished = true; + } } diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java index c90cc4b..6665d86 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.compression; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import java.util.List; @@ -64,7 +65,19 @@ public class JdkZlibDecoder extends ZlibDecoder { * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}). */ public JdkZlibDecoder() { - this(ZlibWrapper.ZLIB, null, false); + this(ZlibWrapper.ZLIB, null, false, 0); + } + + /** + * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}) + * and the specified maximum buffer allocation. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public JdkZlibDecoder(int maxAllocation) { + this(ZlibWrapper.ZLIB, null, false, maxAllocation); } /** @@ -73,7 +86,20 @@ public class JdkZlibDecoder extends ZlibDecoder { * supports the preset dictionary. */ public JdkZlibDecoder(byte[] dictionary) { - this(ZlibWrapper.ZLIB, dictionary, false); + this(ZlibWrapper.ZLIB, dictionary, false, 0); + } + + /** + * Creates a new instance with the specified preset dictionary and maximum buffer allocation. + * The wrapper is always {@link ZlibWrapper#ZLIB} because it is the only format that + * supports the preset dictionary. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public JdkZlibDecoder(byte[] dictionary, int maxAllocation) { + this(ZlibWrapper.ZLIB, dictionary, false, maxAllocation); } /** @@ -82,18 +108,41 @@ public class JdkZlibDecoder extends ZlibDecoder { * supported atm. */ public JdkZlibDecoder(ZlibWrapper wrapper) { - this(wrapper, null, false); + this(wrapper, null, false, 0); + } + + /** + * Creates a new instance with the specified wrapper and maximum buffer allocation. + * Be aware that only {@link ZlibWrapper#GZIP}, {@link ZlibWrapper#ZLIB} and {@link ZlibWrapper#NONE} are + * supported atm. + * + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public JdkZlibDecoder(ZlibWrapper wrapper, int maxAllocation) { + this(wrapper, null, false, maxAllocation); } public JdkZlibDecoder(ZlibWrapper wrapper, boolean decompressConcatenated) { - this(wrapper, null, decompressConcatenated); + this(wrapper, null, decompressConcatenated, 0); + } + + public JdkZlibDecoder(ZlibWrapper wrapper, boolean decompressConcatenated, int maxAllocation) { + this(wrapper, null, decompressConcatenated, maxAllocation); } public JdkZlibDecoder(boolean decompressConcatenated) { - this(ZlibWrapper.GZIP, null, decompressConcatenated); + this(ZlibWrapper.GZIP, null, decompressConcatenated, 0); } - private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary, boolean decompressConcatenated) { + public JdkZlibDecoder(boolean decompressConcatenated, int maxAllocation) { + this(ZlibWrapper.GZIP, null, decompressConcatenated, maxAllocation); + } + + private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary, boolean decompressConcatenated, int maxAllocation) { + super(maxAllocation); + if (wrapper == null) { throw new NullPointerException("wrapper"); } @@ -177,7 +226,7 @@ public class JdkZlibDecoder extends ZlibDecoder { inflater.setInput(array); } - ByteBuf decompressed = ctx.alloc().heapBuffer(inflater.getRemaining() << 1); + ByteBuf decompressed = prepareDecompressBuffer(ctx, null, inflater.getRemaining() << 1); try { boolean readFooter = false; while (!inflater.needsInput()) { @@ -208,7 +257,7 @@ public class JdkZlibDecoder extends ZlibDecoder { } break; } else { - decompressed.ensureWritable(inflater.getRemaining() << 1); + decompressed = prepareDecompressBuffer(ctx, decompressed, inflater.getRemaining() << 1); } } @@ -238,6 +287,11 @@ public class JdkZlibDecoder extends ZlibDecoder { } } + @Override + protected void decompressionBufferExhausted(ByteBuf buffer) { + finished = true; + } + @Override protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { super.handlerRemoved0(ctx); diff --git a/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java index d01bc6b..26fd3e7 100644 --- a/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java +++ b/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.compression; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; /** @@ -23,9 +25,72 @@ import io.netty.handler.codec.ByteToMessageDecoder; */ public abstract class ZlibDecoder extends ByteToMessageDecoder { + /** + * Maximum allowed size of the decompression buffer. + */ + protected final int maxAllocation; + + /** + * Same as {@link #ZlibDecoder(int)} with maxAllocation = 0. + */ + public ZlibDecoder() { + this(0); + } + + /** + * Construct a new ZlibDecoder. + * @param maxAllocation + * Maximum size of the decompression buffer. Must be >= 0. + * If zero, maximum size is decided by the {@link ByteBufAllocator}. + */ + public ZlibDecoder(int maxAllocation) { + if (maxAllocation < 0) { + throw new IllegalArgumentException("maxAllocation must be >= 0"); + } + this.maxAllocation = maxAllocation; + } + /** * Returns {@code true} if and only if the end of the compressed stream * has been reached. */ public abstract boolean isClosed(); + + /** + * Allocate or expand the decompression buffer, without exceeding the maximum allocation. + * Calls {@link #decompressionBufferExhausted(ByteBuf)} if the buffer is full and cannot be expanded further. + */ + protected ByteBuf prepareDecompressBuffer(ChannelHandlerContext ctx, ByteBuf buffer, int preferredSize) { + if (buffer == null) { + if (maxAllocation == 0) { + return ctx.alloc().heapBuffer(preferredSize); + } + + return ctx.alloc().heapBuffer(Math.min(preferredSize, maxAllocation), maxAllocation); + } + + // this always expands the buffer if possible, even if the expansion is less than preferredSize + // we throw the exception only if the buffer could not be expanded at all + // this means that one final attempt to deserialize will always be made with the buffer at maxAllocation + if (buffer.ensureWritable(preferredSize, true) == 1) { + // buffer must be consumed so subclasses don't add it to output + // we therefore duplicate it when calling decompressionBufferExhausted() to guarantee non-interference + // but wait until after to consume it so the subclass can tell how much output is really in the buffer + decompressionBufferExhausted(buffer.duplicate()); + buffer.skipBytes(buffer.readableBytes()); + throw new DecompressionException("Decompression buffer has reached maximum size: " + buffer.maxCapacity()); + } + + return buffer; + } + + /** + * Called when the decompression buffer cannot be expanded further. + * Default implementation is a no-op, but subclasses can override in case they want to + * do something before the {@link DecompressionException} is thrown, such as log the + * data that was decompressed so far. + */ + protected void decompressionBufferExhausted(ByteBuf buffer) { + } + } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java index 28f3919..015559e 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java @@ -23,7 +23,7 @@ public class JZlibTest extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JZlibDecoder(wrapper, maxAllocation); } } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java index 54a48a9..5ff19f1 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java @@ -38,8 +38,8 @@ public class JdkZlibTest extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JdkZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JdkZlibDecoder(wrapper, maxAllocation); } @Test(expected = DecompressionException.class) diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java index 9e16e1a..3c31274 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java @@ -23,7 +23,7 @@ public class ZlibCrossTest1 extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JZlibDecoder(wrapper, maxAllocation); } } diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java index 8717019..00c6e18 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java @@ -25,8 +25,8 @@ public class ZlibCrossTest2 extends ZlibTest { } @Override - protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { - return new JdkZlibDecoder(wrapper); + protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) { + return new JdkZlibDecoder(wrapper, maxAllocation); } @Test(expected = DecompressionException.class) diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java index 7c25ec4..9d79c81 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java @@ -15,7 +15,9 @@ */ package io.netty.handler.codec.compression; +import io.netty.buffer.AbstractByteBufAllocator; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; @@ -88,8 +90,12 @@ public abstract class ZlibTest { rand.nextBytes(BYTES_LARGE); } + protected ZlibDecoder createDecoder(ZlibWrapper wrapper) { + return createDecoder(wrapper, 0); + } + protected abstract ZlibEncoder createEncoder(ZlibWrapper wrapper); - protected abstract ZlibDecoder createDecoder(ZlibWrapper wrapper); + protected abstract ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation); @Test public void testGZIP2() throws Exception { @@ -345,6 +351,25 @@ public abstract class ZlibTest { testCompressLarge(ZlibWrapper.GZIP, ZlibWrapper.ZLIB_OR_NONE); } + @Test + public void testMaxAllocation() throws Exception { + int maxAllocation = 1024; + ZlibDecoder decoder = createDecoder(ZlibWrapper.ZLIB, maxAllocation); + EmbeddedChannel chDecoder = new EmbeddedChannel(decoder); + TestByteBufAllocator alloc = new TestByteBufAllocator(chDecoder.alloc()); + chDecoder.config().setAllocator(alloc); + + try { + chDecoder.writeInbound(Unpooled.wrappedBuffer(deflate(BYTES_LARGE))); + fail("decompressed size > maxAllocation, so should have thrown exception"); + } catch (DecompressionException e) { + assertTrue(e.getMessage().startsWith("Decompression buffer has reached maximum size")); + assertEquals(maxAllocation, alloc.getMaxAllocation()); + assertTrue(decoder.isClosed()); + assertFalse(chDecoder.finish()); + } + } + private static byte[] gzip(byte[] bytes) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); GZIPOutputStream stream = new GZIPOutputStream(out); @@ -360,4 +385,34 @@ public abstract class ZlibTest { stream.close(); return out.toByteArray(); } + + private static final class TestByteBufAllocator extends AbstractByteBufAllocator { + private ByteBufAllocator wrapped; + private int maxAllocation; + + TestByteBufAllocator(ByteBufAllocator wrapped) { + this.wrapped = wrapped; + } + + public int getMaxAllocation() { + return maxAllocation; + } + + @Override + public boolean isDirectBufferPooled() { + return wrapped.isDirectBufferPooled(); + } + + @Override + protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { + maxAllocation = Math.max(maxAllocation, maxCapacity); + return wrapped.heapBuffer(initialCapacity, maxCapacity); + } + + @Override + protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { + maxAllocation = Math.max(maxAllocation, maxCapacity); + return wrapped.directBuffer(initialCapacity, maxCapacity); + } + } }