Skip to content

Commit

Permalink
CASSANDRA-11452: Stronger resistence to hash collision attacks
Browse files Browse the repository at this point in the history
Thanks to further discussions with the Cassandra and TinyLFU folks,
this is a fairly strong guard against the collision attack. This also
has very low overhead and impact to existing hit rates. The previous
commit was fairly weak in comparison.

We concluded that some randomness was desirable in either the admission
or eviction policy. A few of the better options were simulated and this
is my favorite balance. It might change when Roy presents his feedback
from pondering it on his long flight home.

The attack requires that the victim be pinned to an artifically high
frequency due to a hash collision, so that no entries are admitted.
When a warm candidate is rejected by the frequency filter, we give it
a 1% chance of being accepted anyway. This kicks out the attacker's
victim and protects well against her having multiple collisions in her
arsenal. In small and large traces it is rarely triggered due to normal
usage, and does not admit too frequency to hurt the hit rate.

The other attack vector is the sketch's hash functions. This is already
protected with a random seed. We now protect against the weak 32-bit
hash codes used by Java.
  • Loading branch information
ben-manes committed Apr 17, 2016
1 parent 9372ffc commit d690f5c
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public final class Specifications {
public static final TypeName WRITE_QUEUE = ParameterizedTypeName.get(
WRITE_QUEUE_TYPE, ClassName.get(Runnable.class));

public static final TypeName FREQUENCY_SKETCH = ClassName.get(PACKAGE_NAME, "FrequencySketch");
public static final TypeName FREQUENCY_SKETCH = ParameterizedTypeName.get(
ClassName.get("com.github.benmanes.caffeine.cache", "FrequencySketch"), kTypeVar);

private Specifications() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ public class FrequencySketchBenchmark {

int index = 0;
Integer[] ints;
FrequencySketch sketch;
FrequencySketch<Integer> sketch;

@Setup
public void setup() {
ints = new Integer[SIZE];
sketch = new FrequencySketch();
sketch = new FrequencySketch<>();
sketch.ensureCapacity(ITEMS);

NumberGenerator generator = new ScrambledZipfianGenerator(ITEMS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
Expand Down Expand Up @@ -359,7 +360,7 @@ protected boolean isWeighted() {
return (weigher != Weigher.singletonWeigher());
}

protected FrequencySketch frequencySketch() {
protected FrequencySketch<K> frequencySketch() {
throw new UnsupportedOperationException();
}

Expand Down Expand Up @@ -578,22 +579,9 @@ void evictFromMain(int candidates) {
continue;
}

// Evict the victim on a potential hash collision attack
int victimHash = victimKey.hashCode();
int candidateHash = candidateKey.hashCode();
if (victimHash == candidateHash) {
candidates--;
Node<K, V> evict = victim;
victim = victim.getNextInAccessOrder();
evictEntry(evict, RemovalCause.SIZE, 0L);
continue;
}

// Evict the entry with the lowest frequency
candidates--;
int victimFreq = frequencySketch().frequency(victimHash);
int candidateFreq = frequencySketch().frequency(candidateHash);
if (candidateFreq > victimFreq) {
if (admit(candidateKey, victimKey)) {
Node<K, V> evict = victim;
victim = victim.getNextInAccessOrder();
evictEntry(evict, RemovalCause.SIZE, 0L);
Expand All @@ -606,6 +594,31 @@ void evictFromMain(int candidates) {
}
}

/**
* Determines if the candidate should be accepted into the main space, as determined by its
* frequency relative to the victim. A small amount of randomness is used to protect against hash
* collision attacks, where the victim's frequency is artificially raised so that no new entries
* are admitted.
*
* @param candidateKey the key for the entry being proposed for long term retention
* @param victimKey the key for the entry chosen by the eviction policy for replacement
* @return if the candidate should be admitted and the victim ejected
*/
boolean admit(K candidateKey, K victimKey) {
int victimFreq = frequencySketch().frequency(victimKey);
int candidateFreq = frequencySketch().frequency(candidateKey);
if (candidateFreq > victimFreq) {
return true;
} else if (candidateFreq <= 5) {
// The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack
// exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm
// candidate reduces the number of random acceptances to minimize the impact on the hit rate.
return false;
}
int random = ThreadLocalRandom.current().nextInt();
return ((random & 127) == 0);
}

/** Expires entries that have expired in the access and write queues. */
@GuardedBy("evictionLock")
void expireEntries() {
Expand Down Expand Up @@ -987,7 +1000,7 @@ void onAccess(Node<K, V> node) {
if (key == null) {
return;
}
frequencySketch().increment(key.hashCode());
frequencySketch().increment(key);
if (node.inEden()) {
reorder(accessOrderEdenDeque(), node);
} else if (node.inMainProbation()) {
Expand Down Expand Up @@ -1106,7 +1119,7 @@ public void run() {

K key = node.getKey();
if (key != null) {
frequencySketch().increment(key.hashCode());
frequencySketch().increment(key);
}
}

Expand Down Expand Up @@ -2070,7 +2083,7 @@ public Set<Entry<K, V>> entrySet() {
@SuppressWarnings("GuardedByChecker")
Map<K, V> evictionOrder(int limit, Function<V, V> transformer, boolean ascending) {
Comparator<Node<K, V>> comparator = Comparator.comparingInt(node ->
frequencySketch().frequency(node.getKey().hashCode()));
frequencySketch().frequency(node.getKey()));
comparator = ascending ? comparator : comparator.reversed();
PeekingIterator<Node<K, V>> eden;
PeekingIterator<Node<K, V>> main;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.concurrent.ThreadLocalRandom;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;

/**
Expand All @@ -28,7 +29,7 @@
* @author ben.manes@gmail.com (Ben Manes)
*/
@NotThreadSafe
final class FrequencySketch {
final class FrequencySketch<E> {

/*
* This class maintains a 4-bit CountMinSketch [1] with periodic aging to provide the popularity
Expand Down Expand Up @@ -114,16 +115,16 @@ public boolean isNotInitialized() {
/**
* Returns the estimated number of occurrences of an element, up to the maximum (15).
*
* @param hashCode the hash code of the element to count occurrences of
* @param e the element to count occurrences of
* @return the estimated number of occurrences of the element; possibly zero but never negative
*/
@Nonnegative
public int frequency(int hashCode) {
public int frequency(@Nonnull E e) {
if (isNotInitialized()) {
return 0;
}

int hash = spread(hashCode);
int hash = spread(e.hashCode());
int start = (hash & 3) << 2;
int frequency = Integer.MAX_VALUE;
for (int i = 0; i < 4; i++) {
Expand All @@ -139,14 +140,14 @@ public int frequency(int hashCode) {
* of all elements will be periodically down sampled when the observed events exceeds a threshold.
* This process provides a frequency aging to allow expired long term entries to fade away.
*
* @param hashCode the hash code of the element to count occurrences of
* @param e the element to add
*/
public void increment(int hashCode) {
public void increment(@Nonnull E e) {
if (isNotInitialized()) {
return;
}

int hash = spread(hashCode);
int hash = spread(e.hashCode());
int start = (hash & 3) << 2;

// Loop unrolling improves throughput by 5m ops/s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class FrequencySketchTest {
final int item = ThreadLocalRandom.current().nextInt();
final Integer item = ThreadLocalRandom.current().nextInt();

@Test
public void construc() {
FrequencySketch sketch = new FrequencySketch();
FrequencySketch<Integer> sketch = new FrequencySketch<>();
assertThat(sketch.table, is(nullValue()));
}

@Test(dataProvider = "sketch", expectedExceptions = IllegalArgumentException.class)
public void ensureCapacity_negative(FrequencySketch sketch) {
public void ensureCapacity_negative(FrequencySketch<Integer> sketch) {
sketch.ensureCapacity(-1);
}

@Test(dataProvider = "sketch")
public void ensureCapacity_smaller(FrequencySketch sketch) {
public void ensureCapacity_smaller(FrequencySketch<Integer> sketch) {
int size = sketch.table.length;
sketch.ensureCapacity(size / 2);
assertThat(sketch.table.length, is(size));
Expand All @@ -53,7 +53,7 @@ public void ensureCapacity_smaller(FrequencySketch sketch) {
}

@Test(dataProvider = "sketch")
public void ensureCapacity_larger(FrequencySketch sketch) {
public void ensureCapacity_larger(FrequencySketch<Integer> sketch) {
int size = sketch.table.length;
sketch.ensureCapacity(size * 2);
assertThat(sketch.table.length, is(size * 2));
Expand All @@ -62,21 +62,21 @@ public void ensureCapacity_larger(FrequencySketch sketch) {
}

@Test(dataProvider = "sketch")
public void increment_once(FrequencySketch sketch) {
public void increment_once(FrequencySketch<Integer> sketch) {
sketch.increment(item);
assertThat(sketch.frequency(item), is(1));
}

@Test(dataProvider = "sketch")
public void increment_max(FrequencySketch sketch) {
public void increment_max(FrequencySketch<Integer> sketch) {
for (int i = 0; i < 20; i++) {
sketch.increment(item);
}
assertThat(sketch.frequency(item), is(15));
}

@Test(dataProvider = "sketch")
public void increment_distinct(FrequencySketch sketch) {
public void increment_distinct(FrequencySketch<Integer> sketch) {
sketch.increment(item);
sketch.increment(item + 1);
assertThat(sketch.frequency(item), is(1));
Expand All @@ -87,7 +87,7 @@ public void increment_distinct(FrequencySketch sketch) {
@Test
public void reset() {
boolean reset = false;
FrequencySketch sketch = new FrequencySketch();
FrequencySketch<Integer> sketch = new FrequencySketch<>();
sketch.ensureCapacity(64);

for (int i = 1; i < 20 * sketch.table.length; i++) {
Expand All @@ -103,20 +103,20 @@ public void reset() {

@Test
public void heavyHitters() {
FrequencySketch sketch = makeSketch(512);
FrequencySketch<Double> sketch = makeSketch(512);
for (int i = 100; i < 100_000; i++) {
sketch.increment(Double.hashCode(i));
sketch.increment((double) i);
}
for (int i = 0; i < 10; i += 2) {
for (int j = 0; j < i; j++) {
sketch.increment(Double.hashCode(i));
sketch.increment((double) i);
}
}

// A perfect popularity count yields an array [0, 0, 2, 0, 4, 0, 6, 0, 8, 0]
int[] popularity = new int[10];
for (int i = 0; i < 10; i++) {
popularity[i] = sketch.frequency(Double.hashCode(i));
popularity[i] = sketch.frequency((double) i);
}
for (int i = 0; i < popularity.length; i++) {
if ((i == 0) || (i == 1) || (i == 3) || (i == 5) || (i == 7) || (i == 9)) {
Expand All @@ -136,8 +136,8 @@ public Object[][] providesSketch() {
return new Object[][] {{ makeSketch(512) }};
}

private static FrequencySketch makeSketch(long maximumSize) {
FrequencySketch sketch = new FrequencySketch();
private static <E> FrequencySketch<E> makeSketch(long maximumSize) {
FrequencySketch<E> sketch = new FrequencySketch<>();
sketch.ensureCapacity(maximumSize);
ensureRandomSeed(sketch);
return sketch;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public static void ensureRandomSeed(Cache<?, ?> cache) {
}

/** Force the random seed to a predictable value. */
public static void ensureRandomSeed(FrequencySketch sketch) {
public static void ensureRandomSeed(FrequencySketch<?> sketch) {
try {
Field field = FrequencySketch.class.getDeclaredField("randomSeed");
field.setAccessible(true);
Expand Down
Loading

0 comments on commit d690f5c

Please sign in to comment.