Repository: netty/netty
Issue: HTTP/2 HPACK decoder oversized header handling causes client/server Dynamic Table inconsistencies
Labels: defect

### Problem Description
When a client sends oversized headers that causes the decoder to exceed the MAX_HEADER_LIST_SIZE setting, the decoder stops processing headers, and throws a HeaderListSizeException which causes a 431, and a RST_STREAM. If the incoming request had additional headers after the header that caused the HeaderListSizeException to be thrown that would have been incrementally indexed, they are ignored. This results in subsequent requests from the same client using what the client thinks are indexed headers to be rejected with GO_AWAY COMPRESSION_ERROR as the server side decoder dynamic table does not contain the indexed headers.

### Expected behavior
When oversized headers are received, netty will continue processing headers, and updating the dynamic table until all headers are received. Once header processing is complete the HeaderListSizeException should be thrown. To prevent malicious users from sending an infinite number of header frames, and tying up resources, there should be a capped MAX_HEADER_LIST_SIZE overflow that netty will accept then it should issue a GO_AWAY to the client and close the connection.

### Netty version
4.1.6 and later

### Repro Case
See attached maven project zip

### Other things worth noting
While this case occurs when oversized headers are received, any exception that occurs during header processing that is not a connection error (GO_AWAY) can potentially cause dynamic table corruption, and should result in a GO_AWAY


I'm planning on working on a pull request to fix this, but figured this is worth some discussion, so I'm opening the issue first.
[netty-decoder-debug.zip](https://github.com/netty/netty/files/702509/netty-decoder-debug.zip)


Issue comments:
@Scottmitch @nmittler can you take a look ?
I've been helping @exell-christopher with debugging this.

Our current understanding is that there really is no safe way to recover from an exception thrown in the Decoder except for tearing down the connection as the index tables in client and server can have gotten out of synch with no way to safely bring them back in sync.
This is even called out in the RFC. Makes sense since HPACK is stateful.

https://tools.ietf.org/html/rfc7540#section-10.5.1
> The header block MUST be processed to ensure a consistent
   connection state, unless the connection is closed.

Introducing a "limit for how much we allow exceeding another limit" is starting to get intense.  To make things interesting we enforce the limit at 2 levels (during accumulating the header blocks, and during the decompression). So implementing the "continue to process and bit bucket" approach will not be limited in scope to `hpack.Decoder`.  A simpler alternative is just to GO_AWAY/close the connection if the peer violates the settings we advertise.
For our use case, sending a go-away as the behavior for any overflow is not desirable.  The client in our case is a proxy and the inbound connection is heavily multiplexed. With what you are suggesting, one request that exceeds the limit by even one byte causes all other requests to be dropped. Having a reasonable overflow amount allows us to continue servicing the other requests and reject just the one that was oversized, but not particularly malicious.
@exell-christopher - Yes I understand the implications. Just requires a bit more effort to implement, and the memory upper bound would not be set by `SETTINGS_MAX_HEADER_LIST_SIZE` but some other implementation dependent limit. This is not necessarily a bad thing but just needs to be communicated clearly (via javadocs, docs, etc...).

Just curious in your scenario what do you anticipate your "malicious threshold" to be vs. `SETTINGS_MAX_HEADER_LIST_SIZE`?
I would likely use MAX_HEADER_LIST_SIZE + (MAX_HEADER_LIST_SIZE * .25)  I would expect that the .25 would be user configurable
I'll take a stab at this...there is some other related cruft that can be cleaned up too.
Thanks. Happy to submit a PR as well.
@exell-christopher - Already in progress :) I'll ping you for review when I have a PR ready.
Some information about summarize the header length,
In [rfc7540](https://tools.ietf.org/html/rfc7540#section-6.5.2)
>    SETTINGS_MAX_HEADER_LIST_SIZE (0x6):  This advisory setting informs a
      peer of the maximum size of header list that the sender is
      prepared to accept, in octets.  The value is based on the
      uncompressed size of header fields, including the length of the
      name and value in octets plus an overhead of 32 octets for each
      header field.

I think that means when we sum up all headers, we should plus 32 byte for each header.
Maybe it could be implemented at the same time! [Decoder.jara#L404](https://github.com/netty/netty/blob/4.1/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Decoder.java#L404)
Sorry, there is a better reference [rfc7541](https://tools.ietf.org/html/rfc7541#section-4.1)
>   The size of an entry is the sum of its name's length in octets (as
   defined in Section 5.2), its value's length in octets, and 32.
@Scottmitch Let me know if there's anything you need on this. 
@exell-christopher - All good for now thanks. I will ping you once the PR is pushed (should be soon).
@chhsiao90 - We can consider this as a followup PR if you want. The `SETTINGS_MAX_HEADER_LIST_SIZE` is not required to be strictly enforced. We account for the `32` bits of overhead on the `encoding` size, but not on the decode size. So we are currently following the "be liberal in what you accept and conservative in what you send" principle which I'm not yet convinced is a problem in this case.
@Scottmitch 
Sure, I will rebase once the PR is merged.

About the 32 bytes overhead,
I found that encoder will calculate the header size by add 32 bytes for each header. [Encoder](https://github.com/netty/netty/blob/4.1/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Encoder.java#L122)
But the decoder didn't add the 32 bytes in current 4.1 branch.
Or maybe I missed that the 32 bytes is added in somewhere when encoding.
> I found that encoder will calculate the header size by add 32 ... But the decoder didn't add the 32 bytes

Yip this reflects the current state. This is what I described in https://github.com/netty/netty/issues/6209#issuecomment-273365577.
OK, sorry, I missed read that.... :(
So, the 32 bytes overhead could implement or not depends on need!
Thanks for your clarify!

Pull request review comments:

Patch that resolved the issue:
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Decoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Decoder.java
index 509069fb87a..aea195cca00 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Decoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Decoder.java
@@ -51,10 +51,12 @@
 import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
 
 public final class Decoder {
-    private static final Http2Exception DECODE_DECOMPRESSION_EXCEPTION = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - decompression failure"), Decoder.class, "decode(...)");
     private static final Http2Exception DECODE_ULE_128_DECOMPRESSION_EXCEPTION = unknownStackTrace(
             connectionError(COMPRESSION_ERROR, "HPACK - decompression failure"), Decoder.class, "decodeULE128(...)");
+    private static final Http2Exception DECODE_ULE_128_TO_LONG_DECOMPRESSION_EXCEPTION = unknownStackTrace(
+            connectionError(COMPRESSION_ERROR, "HPACK - long overflow"), Decoder.class, "decodeULE128(...)");
+    private static final Http2Exception DECODE_ULE_128_TO_INT_DECOMPRESSION_EXCEPTION = unknownStackTrace(
+            connectionError(COMPRESSION_ERROR, "HPACK - int overflow"), Decoder.class, "decodeULE128ToInt(...)");
     private static final Http2Exception DECODE_ILLEGAL_INDEX_VALUE = unknownStackTrace(
             connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), Decoder.class, "decode(...)");
     private static final Http2Exception INDEX_HEADER_ILLEGAL_INDEX_VALUE = unknownStackTrace(
@@ -184,7 +186,7 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
                     break;
 
                 case READ_MAX_DYNAMIC_TABLE_SIZE:
-                    setDynamicTableSize(decodeULE128(in, index));
+                    setDynamicTableSize(decodeULE128(in, (long) index));
                     state = READ_HEADER_REPRESENTATION;
                     break;
 
@@ -346,7 +348,7 @@ HeaderField getHeaderField(int index) {
         return dynamicTable.getEntry(index + 1);
     }
 
-    private void setDynamicTableSize(int dynamicTableSize) throws Http2Exception {
+    private void setDynamicTableSize(long dynamicTableSize) throws Http2Exception {
         if (dynamicTableSize > maxDynamicTableSize) {
             throw INVALID_MAX_DYNAMIC_TABLE_SIZE;
         }
@@ -422,26 +424,53 @@ private static IllegalArgumentException notEnoughDataException(ByteBuf in) {
         return new IllegalArgumentException("decode only works with an entire header block! " + in);
     }
 
-    // Unsigned Little Endian Base 128 Variable-Length Integer Encoding
-    private static int decodeULE128(ByteBuf in, int result) throws Http2Exception {
+    /**
+     * Unsigned Little Endian Base 128 Variable-Length Integer Encoding
+     * <p>
+     * Visible for testing only!
+     */
+    static int decodeULE128(ByteBuf in, int result) throws Http2Exception {
+        final int readerIndex = in.readerIndex();
+        final long v = decodeULE128(in, (long) result);
+        if (v > Integer.MAX_VALUE) {
+            // the maximum value that can be represented by a signed 32 bit number is:
+            // [0x1,0x7f] + 0x7f + (0x7f << 7) + (0x7f << 14) + (0x7f << 21) + (0x6 << 28)
+            // OR
+            // 0x0 + 0x7f + (0x7f << 7) + (0x7f << 14) + (0x7f << 21) + (0x7 << 28)
+            // we should reset the readerIndex if we overflowed the int type.
+            in.readerIndex(readerIndex);
+            throw DECODE_ULE_128_TO_INT_DECOMPRESSION_EXCEPTION;
+        }
+        return (int) v;
+    }
+
+    /**
+     * Unsigned Little Endian Base 128 Variable-Length Integer Encoding
+     * <p>
+     * Visible for testing only!
+     */
+    static long decodeULE128(ByteBuf in, long result) throws Http2Exception {
         assert result <= 0x7f && result >= 0;
+        final boolean resultStartedAtZero = result == 0;
         final int writerIndex = in.writerIndex();
-        for (int readerIndex = in.readerIndex(), shift = 0;
-             readerIndex < writerIndex; ++readerIndex, shift += 7) {
+        for (int readerIndex = in.readerIndex(), shift = 0; readerIndex < writerIndex; ++readerIndex, shift += 7) {
             byte b = in.getByte(readerIndex);
-            if (shift == 28 && ((b & 0x80) != 0 || b > 6)) {
-                // the maximum value that can be represented by a signed 32 bit number is:
-                // 0x7f + 0x7f + (0x7f << 7) + (0x7f << 14) + (0x7f << 21) + (0x6 << 28)
+            if (shift == 56 && ((b & 0x80) != 0 || b == 0x7F && !resultStartedAtZero)) {
+                // the maximum value that can be represented by a signed 64 bit number is:
+                // [0x01L, 0x7fL] + 0x7fL + (0x7fL << 7) + (0x7fL << 14) + (0x7fL << 21) + (0x7fL << 28) + (0x7fL << 35)
+                // + (0x7fL << 42) + (0x7fL << 49) + (0x7eL << 56)
+                // OR
+                // 0x0L + 0x7fL + (0x7fL << 7) + (0x7fL << 14) + (0x7fL << 21) + (0x7fL << 28) + (0x7fL << 35) +
+                // (0x7fL << 42) + (0x7fL << 49) + (0x7fL << 56)
                 // this means any more shifts will result in overflow so we should break out and throw an error.
-                in.readerIndex(readerIndex + 1);
-                break;
+                throw DECODE_ULE_128_TO_LONG_DECOMPRESSION_EXCEPTION;
             }
 
             if ((b & 0x80) == 0) {
                 in.readerIndex(readerIndex + 1);
-                return result + ((b & 0x7F) << shift);
+                return result + ((b & 0x7FL) << shift);
             }
-            result += (b & 0x7F) << shift;
+            result += (b & 0x7FL) << shift;
         }
 
         throw DECODE_ULE_128_DECOMPRESSION_EXCEPTION;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Encoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Encoder.java
index 8c201590959..805f09530f9 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Encoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/Encoder.java
@@ -204,7 +204,7 @@ public void setMaxHeaderTableSize(ByteBuf out, long maxHeaderTableSize) throws H
         this.maxHeaderTableSize = maxHeaderTableSize;
         ensureCapacity(0);
         // Casting to integer is safe as we verified the maxHeaderTableSize is a valid unsigned int.
-        encodeInteger(out, 0x20, 5, (int) maxHeaderTableSize);
+        encodeInteger(out, 0x20, 5, maxHeaderTableSize);
     }
 
     /**
@@ -227,20 +227,27 @@ public long getMaxHeaderListSize() {
     }
 
     /**
-     * Encode integer according to Section 5.1.
+     * Encode integer according to <a href="https://tools.ietf.org/html/rfc7541#section-5.1">Section 5.1</a>.
      */
     private static void encodeInteger(ByteBuf out, int mask, int n, int i) {
+        encodeInteger(out, mask, n, (long) i);
+    }
+
+    /**
+     * Encode integer according to <a href="https://tools.ietf.org/html/rfc7541#section-5.1">Section 5.1</a>.
+     */
+    private static void encodeInteger(ByteBuf out, int mask, int n, long i) {
         assert n >= 0 && n <= 8 : "N: " + n;
         int nbits = 0xFF >>> (8 - n);
         if (i < nbits) {
-            out.writeByte(mask | i);
+            out.writeByte((int) (mask | i));
         } else {
             out.writeByte(mask | nbits);
-            int length = i - nbits;
+            long length = i - nbits;
             for (; (length & ~0x7F) != 0; length >>>= 7) {
-                out.writeByte((length & 0x7F) | 0x80);
+                out.writeByte((int) ((length & 0x7F) | 0x80));
             }
-            out.writeByte(length);
+            out.writeByte((int) length);
         }
     }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/HuffmanEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/HuffmanEncoder.java
index 136f6c67c71..aad58198130 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/HuffmanEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/internal/hpack/HuffmanEncoder.java
@@ -118,7 +118,7 @@ private void encodeSlowPath(ByteBuf out, CharSequence data) {
      * @param data the string literal to be Huffman encoded
      * @return the number of bytes required to Huffman encode <code>data</code>
      */
-    public int getEncodedLength(CharSequence data) {
+    int getEncodedLength(CharSequence data) {
         if (data instanceof AsciiString) {
             AsciiString string = (AsciiString) data;
             try {
diff --git a/microbench/src/main/java/io/netty/microbench/http2/internal/hpack/DecoderULE128Benchmark.java b/microbench/src/main/java/io/netty/microbench/http2/internal/hpack/DecoderULE128Benchmark.java
new file mode 100644
index 00000000000..a13c978f246
--- /dev/null
+++ b/microbench/src/main/java/io/netty/microbench/http2/internal/hpack/DecoderULE128Benchmark.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2017 The Netty Project
+ *
+ * The Netty Project 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 io.netty.microbench.http2.internal.hpack;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http2.Http2Error;
+import io.netty.handler.codec.http2.Http2Exception;
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.TimeUnit;
+
+@Threads(1)
+@State(Scope.Benchmark)
+@Fork(1)
+@Warmup(iterations = 5)
+@Measurement(iterations = 10)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+public class DecoderULE128Benchmark extends AbstractMicrobenchmark {
+    private static final Http2Exception DECODE_ULE_128_TO_LONG_DECOMPRESSION_EXCEPTION =
+            new Http2Exception(Http2Error.COMPRESSION_ERROR);
+    private static final Http2Exception DECODE_ULE_128_TO_INT_DECOMPRESSION_EXCEPTION =
+            new Http2Exception(Http2Error.COMPRESSION_ERROR);
+    private static final Http2Exception DECODE_ULE_128_DECOMPRESSION_EXCEPTION =
+            new Http2Exception(Http2Error.COMPRESSION_ERROR);
+
+    private ByteBuf longMaxBuf;
+    private ByteBuf intMaxBuf;
+
+    @Setup
+    public void setup() {
+        byte[] longMax = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
+                          (byte) 0xFF, (byte) 0x7F};
+        longMaxBuf = Unpooled.wrappedBuffer(longMax);
+        byte[] intMax = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x07};
+        intMaxBuf = Unpooled.wrappedBuffer(intMax);
+    }
+
+    @Benchmark
+    @BenchmarkMode(Mode.AverageTime)
+    public long decodeMaxLong() throws Http2Exception {
+        long v = decodeULE128(longMaxBuf, 0L);
+        longMaxBuf.readerIndex(0);
+        return v;
+    }
+
+    @Benchmark
+    @BenchmarkMode(Mode.AverageTime)
+    public long decodeMaxIntWithLong() throws Http2Exception {
+        long v = decodeULE128(intMaxBuf, 0L);
+        intMaxBuf.readerIndex(0);
+        return v;
+    }
+
+    @Benchmark
+    @BenchmarkMode(Mode.AverageTime)
+    public int decodeMaxInt() throws Http2Exception {
+        int v = decodeULE128(intMaxBuf, 0);
+        intMaxBuf.readerIndex(0);
+        return v;
+    }
+
+    @Benchmark
+    @BenchmarkMode(Mode.AverageTime)
+    public int decodeMaxIntUsingLong() throws Http2Exception {
+        int v = decodeULE128UsingLong(intMaxBuf, 0);
+        intMaxBuf.readerIndex(0);
+        return v;
+    }
+
+    static int decodeULE128UsingLong(ByteBuf in, int result) throws Http2Exception {
+        final int readerIndex = in.readerIndex();
+        final long v = decodeULE128(in, (long) result);
+        if (v > Integer.MAX_VALUE) {
+            in.readerIndex(readerIndex);
+            throw DECODE_ULE_128_TO_INT_DECOMPRESSION_EXCEPTION;
+        }
+        return (int) v;
+    }
+
+    static long decodeULE128(ByteBuf in, long result) throws Http2Exception {
+        assert result <= 0x7f && result >= 0;
+        final boolean resultStartedAtZero = result == 0;
+        final int writerIndex = in.writerIndex();
+        for (int readerIndex = in.readerIndex(), shift = 0; readerIndex < writerIndex; ++readerIndex, shift += 7) {
+            byte b = in.getByte(readerIndex);
+            if (shift == 56 && ((b & 0x80) != 0 || b == 0x7F && !resultStartedAtZero)) {
+                // the maximum value that can be represented by a signed 64 bit number is:
+                // [0x01L, 0x7fL] + 0x7fL + (0x7fL << 7) + (0x7fL << 14) + (0x7fL << 21) + (0x7fL << 28) + (0x7fL << 35)
+                // + (0x7fL << 42) + (0x7fL << 49) + (0x7eL << 56)
+                // OR
+                // 0x0L + 0x7fL + (0x7fL << 7) + (0x7fL << 14) + (0x7fL << 21) + (0x7fL << 28) + (0x7fL << 35) +
+                // (0x7fL << 42) + (0x7fL << 49) + (0x7fL << 56)
+                // this means any more shifts will result longMaxBuf overflow so we should break out and throw an error.
+                throw DECODE_ULE_128_TO_LONG_DECOMPRESSION_EXCEPTION;
+            }
+
+            if ((b & 0x80) == 0) {
+                in.readerIndex(readerIndex + 1);
+                return result + ((b & 0x7FL) << shift);
+            }
+            result += (b & 0x7FL) << shift;
+        }
+
+        throw DECODE_ULE_128_DECOMPRESSION_EXCEPTION;
+    }
+
+    static int decodeULE128(ByteBuf in, int result) throws Http2Exception {
+        assert result <= 0x7f && result >= 0;
+        final boolean resultStartedAtZero = result == 0;
+        final int writerIndex = in.writerIndex();
+        for (int readerIndex = in.readerIndex(), shift = 0; readerIndex < writerIndex; ++readerIndex, shift += 7) {
+            byte b = in.getByte(readerIndex);
+            if (shift == 28 && ((b & 0x80) != 0 || !resultStartedAtZero && b > 6 || resultStartedAtZero && b > 7)) {
+                // the maximum value that can be represented by a signed 32 bit number is:
+                // [0x1,0x7f] + 0x7f + (0x7f << 7) + (0x7f << 14) + (0x7f << 21) + (0x6 << 28)
+                // OR
+                // 0x0 + 0x7f + (0x7f << 7) + (0x7f << 14) + (0x7f << 21) + (0x7 << 28)
+                // this means any more shifts will result longMaxBuf overflow so we should break out and throw an error.
+                throw DECODE_ULE_128_TO_INT_DECOMPRESSION_EXCEPTION;
+            }
+
+            if ((b & 0x80) == 0) {
+                in.readerIndex(readerIndex + 1);
+                return result + ((b & 0x7F) << shift);
+            }
+            result += (b & 0x7F) << shift;
+        }
+
+        throw DECODE_ULE_128_DECOMPRESSION_EXCEPTION;
+    }
+}


Test patch:
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/internal/hpack/DecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/internal/hpack/DecoderTest.java
index db282044026..76c070d91e4 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/internal/hpack/DecoderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/internal/hpack/DecoderTest.java
@@ -38,7 +38,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import static io.netty.handler.codec.http2.Http2TestUtil.newTestDecoder;
+import static io.netty.handler.codec.http2.internal.hpack.Decoder.decodeULE128;
 import static io.netty.util.AsciiString.EMPTY_STRING;
 import static io.netty.util.AsciiString.of;
 import static java.lang.Integer.MAX_VALUE;
@@ -50,10 +50,6 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 public class DecoderTest {
-
-    private static final int MAX_HEADER_LIST_SIZE = 8192;
-    private static final int MAX_HEADER_TABLE_SIZE = 4096;
-
     private Decoder decoder;
     private Http2Headers mockHeaders;
 
@@ -77,6 +73,109 @@ public void setUp() throws Http2Exception {
         mockHeaders = mock(Http2Headers.class);
     }
 
+    @Test
+    public void testDecodeULE128IntMax() throws Http2Exception {
+        byte[] input = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x07};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        try {
+            assertEquals(Integer.MAX_VALUE, decodeULE128(in, 0));
+        } finally {
+            in.release();
+        }
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void testDecodeULE128IntOverflow1() throws Http2Exception {
+        byte[] input = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x07};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        final int readerIndex = in.readerIndex();
+        try {
+            decodeULE128(in, 1);
+        } finally {
+            assertEquals(readerIndex, in.readerIndex());
+            in.release();
+        }
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void testDecodeULE128IntOverflow2() throws Http2Exception {
+        byte[] input = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x08};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        final int readerIndex = in.readerIndex();
+        try {
+            decodeULE128(in, 0);
+        } finally {
+            assertEquals(readerIndex, in.readerIndex());
+            in.release();
+        }
+    }
+
+    @Test
+    public void testDecodeULE128LongMax() throws Http2Exception {
+        byte[] input = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
+                        (byte) 0xFF, (byte) 0x7F};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        try {
+            assertEquals(Long.MAX_VALUE, decodeULE128(in, 0L));
+        } finally {
+            in.release();
+        }
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void testDecodeULE128LongOverflow1() throws Http2Exception {
+        byte[] input = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
+                        (byte) 0xFF, (byte) 0xFF};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        final int readerIndex = in.readerIndex();
+        try {
+            decodeULE128(in, 0L);
+        } finally {
+            assertEquals(readerIndex, in.readerIndex());
+            in.release();
+        }
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void testDecodeULE128LongOverflow2() throws Http2Exception {
+        byte[] input = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
+                        (byte) 0xFF, (byte) 0x7F};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        final int readerIndex = in.readerIndex();
+        try {
+            decodeULE128(in, 1L);
+        } finally {
+            assertEquals(readerIndex, in.readerIndex());
+            in.release();
+        }
+    }
+
+    @Test
+    public void testSetTableSizeWithMaxUnsigned32BitValueSucceeds() throws Http2Exception {
+        byte[] input = {(byte) 0x3F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0E};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        try {
+            final long expectedHeaderSize = 4026531870L; // based on the input above
+            decoder.setMaxHeaderTableSize(expectedHeaderSize);
+            decoder.decode(0, in, mockHeaders);
+            assertEquals(expectedHeaderSize, decoder.getMaxHeaderTableSize());
+        } finally {
+            in.release();
+        }
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void testSetTableSizeOverLimitFails() throws Http2Exception {
+        byte[] input = {(byte) 0x3F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0E};
+        ByteBuf in = Unpooled.wrappedBuffer(input);
+        try {
+            decoder.setMaxHeaderTableSize(4026531870L - 1); // based on the input above ... 1 less than is above.
+            decoder.decode(0, in, mockHeaders);
+        } finally {
+            in.release();
+        }
+    }
+
     @Test
     public void testLiteralHuffmanEncodedWithEmptyNameAndValue() throws Http2Exception {
         byte[] input = {0, (byte) 0x80, 0};
@@ -123,7 +222,7 @@ public void testLiteralHuffmanEncodedWithPaddingNotCorrespondingToMSBThrows() th
     }
 
     @Test(expected = Http2Exception.class)
-    public void testIncompleteIndex() throws Http2Exception, Http2Exception {
+    public void testIncompleteIndex() throws Http2Exception {
         byte[] compressed = Hex.decodeHex("FFF0".toCharArray());
         ByteBuf in = Unpooled.wrappedBuffer(compressed);
         try {
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/internal/hpack/EncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/internal/hpack/EncoderTest.java
new file mode 100644
index 00000000000..6e72b41628d
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/internal/hpack/EncoderTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 The Netty Project
+ *
+ * The Netty Project 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 io.netty.handler.codec.http2.internal.hpack;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http2.Http2Exception;
+import io.netty.handler.codec.http2.Http2Headers;
+import org.junit.Before;
+import org.junit.Test;
+
+import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_TABLE_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+public class EncoderTest {
+    private Decoder decoder;
+    private Encoder encoder;
+    private Http2Headers mockHeaders;
+
+    @Before
+    public void setUp() throws Http2Exception {
+        encoder = new Encoder();
+        decoder = new Decoder();
+        mockHeaders = mock(Http2Headers.class);
+    }
+
+    @Test
+    public void testSetMaxHeaderTableSizeToMaxValue() throws Http2Exception {
+        ByteBuf buf = Unpooled.buffer();
+        encoder.setMaxHeaderTableSize(buf, MAX_HEADER_TABLE_SIZE);
+        decoder.setMaxHeaderTableSize(MAX_HEADER_TABLE_SIZE);
+        decoder.decode(0, buf, mockHeaders);
+        assertEquals(MAX_HEADER_TABLE_SIZE, decoder.getMaxHeaderTableSize());
+        buf.release();
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void testSetMaxHeaderTableSizeOverflow() throws Http2Exception {
+        ByteBuf buf = Unpooled.buffer();
+        try {
+            encoder.setMaxHeaderTableSize(buf, MAX_HEADER_TABLE_SIZE + 1);
+        } finally {
+            buf.release();
+        }
+    }
+}