title: “Hash-Flooding DoS Attacks and Defense in Rust” meta_description: “Learn about HashDoS attacks, SipHash, RandomState, and how to protect HashMap from algorithmic complexity attacks.” keywords: [“hashdos”, “hash flooding”, “siphash rust”, “randomstate”, “dos attack prevention”, “algorithmic complexity attack”]
🔐 Reinventing from Scratch: Building HashMap
Chapter 6 — “Hasher and DoS Resistance”
“A predictable hash function is an attacker’s best friend.”
🧭 6.1. The Hash-Flooding Attack
Imagine an attacker who can send data to your server:
// Your innocent HashMap
let mut cache: HashMap<String, Data> = HashMap::new();
// Attacker sends these keys (carefully chosen):
for key in attacker_keys {
cache.insert(key, data);
}
What if all those keys hash to the same bucket?
💥 6.2. Algorithmic Complexity Attack
Normal Case (Good Distribution)
Buckets: [X, _, X, _, X, _, X, _]
Lookups: O(1) average
Attack Case (Forced Collisions)
Buckets: [XXXXXXXXXX, _, _, _, _, _, _, _]
All 10 keys in bucket 0!
Lookups: O(n) — must check all 10 entries
Impact:
- 1,000 malicious keys → 1,000x slower
- 10,000 keys → 10,000x slower
- Server grinds to a halt → Denial of Service
🎯 6.3. How Attackers Find Collisions
Step 1: Reverse-Engineer the Hash Function
If you use a deterministic, public hash function:
// If everyone uses the same FNV hash...
fn fnv_hash(s: &str) -> u64 {
let mut hash = 0xcbf29ce484222325u64;
for byte in s.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
Attacker can:
- Download your code
- Find the hash function
- Generate keys that collide
Step 2: Generate Colliding Keys
# Attacker's script
def find_collisions(target_hash, count):
collisions = []
for i in range(1000000):
key = f"attack_{i}"
if fnv_hash(key) % 16 == target_hash:
collisions.append(key)
if len(collisions) >= count:
break
return collisions
# Generate 10,000 keys that all hash to bucket 0
attack_keys = find_collisions(0, 10000)
Step 3: Send to Victim
# Attacker floods your API
for key in attack_keys:
POST /api/data {"key": key, "value": "X"}
Your server’s HashMap now has 10,000 entries in one bucket → O(n) operations.
🛡️ 6.4. Defense: Randomized Hashing
Solution: Add a secret random seed to the hash function!
struct RandomState {
k0: u64, // Random seed 1
k1: u64, // Random seed 2
}
impl RandomState {
fn new() -> Self {
Self {
k0: rand::random(),
k1: rand::random(),
}
}
}
Now the hash depends on the seed:
hash("attack_0") with seed_A = 42
hash("attack_0") with seed_B = 9831 // Different!
Attacker can’t predict which bucket keys will land in!
🔒 6.5. SipHash: Cryptographic Hashing
Rust uses SipHash by default (via RandomState):
What Makes SipHash Special?
- Keyed hash function — Requires a secret key
- Cryptographically strong — Can’t reverse-engineer easily
- Fast enough — Not as fast as FNV, but secure
The Algorithm (Simplified)
pub struct SipHasher {
k0: u64, // Secret key part 1
k1: u64, // Secret key part 2
// ... internal state ...
}
impl SipHasher {
fn new_with_keys(k0: u64, k1: u64) -> Self {
// Initialize internal state with keys
// Process data through multiple compression rounds
// Output is cryptographically strong
}
}
Key point: Without knowing k0 and k1, attackers can’t predict collisions.
⚖️ 6.6. Performance Tradeoff
| Hash Function | Speed | Security | When to Use |
|---|---|---|---|
| FNV | Very fast | None | Trusted input only |
| FxHash | Very fast | None | Internal maps, no external input |
| SipHash | Moderate | High | Untrusted input (default) |
| SHA256 | Slow | Very high | Cryptographic needs |
Rust’s default (RandomState → SipHash):
- Fast enough for most uses
- Secure against DoS
- Good default choice
🎲 6.7. Implementing Custom BuildHasher
You can provide your own:
use std::hash::BuildHasherDefault;
use rustc_hash::FxHasher; // Fast but NOT DoS-resistant
type FastHashMap<K, V> = MyHashMap<K, V, BuildHasherDefault<FxHasher>>;
let mut map = FastHashMap::default();
When is this OK?
- Internal data structures
- No untrusted input
- Performance critical and you trust your data
🧪 6.8. Testing DoS Resistance
#[test]
fn test_different_seeds_produce_different_hashes() {
let map1 = MyHashMap::<String, i32>::new();
let map2 = MyHashMap::<String, i32>::new();
let key = "test_key";
let hash1 = map1.hash_key(&key);
let hash2 = map2.hash_key(&key);
// With RandomState, these should differ!
assert_ne!(hash1, hash2);
}
#[test]
fn test_collision_attack_resistance() {
let mut map = MyHashMap::new();
// Try to create collisions (won't work with random seed!)
for i in 0..10000 {
map.insert(format!("attack_{}", i), i);
}
// Measure max probe distance
// Should be reasonable (< 20) even with 10k entries
// If using predictable hash, would be > 1000
}
🌐 6.9. Real-World DoS Attacks
Case Study: Python 2.x (Pre-2012)
Python’s dict used predictable hashing:
# Attacker could generate keys that all collide
hash("Aa") == hash("BB") # True in Python 2!
Result: Websites crashed when sent ~100KB of colliding JSON keys.
Fix: Python 3 added randomized hashing (like Rust).
Case Study: Ruby (2011)
Similar issue — predictable hash function led to DoS attacks on Rails apps.
Lesson: Always use randomized hashing for untrusted input!
🧠 6.10. Chapter Takeaways
| Concept | Purpose |
|---|---|
| HashDoS | Algorithmic complexity attack via collisions |
| RandomState | Generates random seeds for SipHash |
| SipHash | Cryptographically strong hash function |
| Seed | Secret that makes hash unpredictable |
| BuildHasher | Abstraction for hash function choice |
🚀 6.11. What’s Next
We now have:
- Secure hashing (RandomState + SipHash)
- Insertion and deletion
- Safe iteration
Next: Chapter 7 — Panic Safety and Invariants
Learn how to maintain HashMap invariants even when code panics, ensuring no memory leaks or corruption.
💡 Exercises
- Implement your own BuildHasher using FNV hash
- Benchmark SipHash vs FxHash on small keys
- Try to generate colliding keys for FNV (it’s possible!)
- Measure probe distances with predictable vs random hash
- Research other DoS-resistant hash functions (AHash, HighwayHash)
Next: Chapter 7 — Panic Safety and Invariants ⚠️