Skip to content

Commit

Permalink
Fix hash collision handling in DefaultHeaders iterator remove (#11028)
Browse files Browse the repository at this point in the history
Motivation:
If two different headers end up in the same hash bucket, and you are iterating the header that is not the first in the bucket, and you use the iterator to remove the first element returned from the iterator, then you would get a NullPointerException.

Modification:
Change the DefaultHeaders iterator remove method, to re-iterate the hash bucket and unlink the entry once found, if we don't have any existing iteration starting point.

Also made DefaultHeaders.remove0 package private to avoid a synthetic method indirection.

Result:
Removing from iterators from DefaultHeaders is now robust towards hash collisions.
  • Loading branch information
chrisvest committed Feb 19, 2021
1 parent bd8a337 commit 28d4154
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 3 deletions.
16 changes: 13 additions & 3 deletions codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java
Original file line number Diff line number Diff line change
Expand Up @@ -1012,12 +1012,22 @@ private V remove0(int h, int i, K name) {
return value;
}

private HeaderEntry<K, V> remove0(HeaderEntry<K, V> entry, HeaderEntry<K, V> previous) {
HeaderEntry<K, V> remove0(HeaderEntry<K, V> entry, HeaderEntry<K, V> previous) {
int i = index(entry.hash);
HeaderEntry<K, V> e = entries[i];
if (e == entry) {
HeaderEntry<K, V> firstEntry = entries[i];
if (firstEntry == entry) {
entries[i] = entry.next;
previous = entries[i];
} else if (previous == null) {
// If we don't have any existing starting point, then start from the beginning.
previous = firstEntry;
HeaderEntry<K, V> next = firstEntry.next;
while (next != null && next != entry) {
previous = next;
next = next.next;
}
assert next != null: "Entry not found in its hash bucket: " + entry;
previous.next = entry.next;
} else {
previous.next = entry.next;
}
Expand Down
54 changes: 54 additions & 0 deletions codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package io.netty.handler.codec;

import io.netty.util.AsciiString;
import io.netty.util.HashingStrategy;
import org.junit.Test;

import java.util.ArrayList;
Expand All @@ -26,6 +27,9 @@

import static io.netty.util.AsciiString.of;
import static java.util.Arrays.asList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
Expand All @@ -48,6 +52,10 @@ private static final class TestDefaultHeaders extends
TestDefaultHeaders(ValueConverter<CharSequence> converter) {
super(converter);
}

TestDefaultHeaders(HashingStrategy<CharSequence> nameHashingStrategy) {
super(nameHashingStrategy, CharSequenceValueConverter.INSTANCE);
}
}

private static TestDefaultHeaders newInstance() {
Expand Down Expand Up @@ -759,4 +767,50 @@ public void testGetBooleanTrueValue() {
assertTrue(headers.getBoolean("name2", false));
assertTrue(headers.getBoolean("name3", false));
}

@Test
public void handlingOfHeaderNameHashCollisions() {
TestDefaultHeaders headers = new TestDefaultHeaders(new HashingStrategy<CharSequence>() {
@Override
public int hashCode(CharSequence obj) {
return 0; // Degenerate hashing strategy to enforce collisions.
}

@Override
public boolean equals(CharSequence a, CharSequence b) {
return a.equals(b);
}
});

headers.add("Cookie", "a=b; c=d; e=f");
headers.add("other", "text/plain"); // Add another header which will be saved in the same entries[index]

simulateCookieSplitting(headers);
List<CharSequence> cookies = headers.getAll("Cookie");

assertThat(cookies, hasSize(3));
assertThat(cookies, containsInAnyOrder((CharSequence) "a=b", "c=d", "e=f"));
}

/**
* Split up cookies into individual cookie crumb headers.
*/
static void simulateCookieSplitting(TestDefaultHeaders headers) {
Iterator<CharSequence> cookieItr = headers.valueIterator("Cookie");
if (!cookieItr.hasNext()) {
return;
}
// We want to avoid "concurrent modifications" of the headers while we are iterating. So we insert crumbs
// into an intermediate collection and insert them after the split process concludes.
List<CharSequence> cookiesToAdd = new ArrayList<CharSequence>();
while (cookieItr.hasNext()) {
//noinspection DynamicRegexReplaceableByCompiledPattern
String[] cookies = cookieItr.next().toString().split("; ");
cookiesToAdd.addAll(asList(cookies));
cookieItr.remove();
}
for (CharSequence crumb : cookiesToAdd) {
headers.add("Cookie", crumb);
}
}
}

0 comments on commit 28d4154

Please sign in to comment.