absl::Hash: fixed string pairs that collide for every seed (0 vs 8 bytes, and every 9..16-byte string ending in kMul hashes to 0)
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
Research direction
Start with absl/hash/internal/hash.h at lines 1168 and 1201-1207, then inspect hash.cc and the supplied verify/abseil-hash reproduction. Run make check and, if needed, make native to confirm the reported pairs against the library. Done means the maintainers have resolved whether the comment accurately describes the seed behavior and defined any required code or test changes.
Written by the indexing model from the issue text.
Description
Scope up front: Abseil states that absl::Hash makes no security claim, and this report does not say otherwise. It documents that several fixed pairs of short strings collide under absl::Hash<std::string_view> for every seed, including every one of the 32 per-table SwissTable seeds and the process seed, and that one code comment next to the affected path describes the second family as needing an "incredibly unlucky" seed when it in fact needs none. It is not a hash-flooding exploit and I have not built one.
Version: master at 73d2688300440c8af028eec865ee0dcd85e93025 (2026-09-17); absl/hash/internal/hash.h (sha256 65f71f11a2726a570d94610b494cde0592ae0a40d67d25f05559da1443d8e288) and hash.cc (f2b6084bffd569da77eb2822176c5d71b31bddcdde961cd8fad9cfb12db8857e) are, as far as I can tell, the files LTS 20260817.0 ships. Default open-source build (ABSL_OPTION_INLINE_HW_ACCEL_STRATEGY 0); the pairs are at most 16 bytes, so the LowLevelHash backend does not enter and the result is the same on the x86-64 scalar build, the -msse4.2 -maes build and, by source structure, the arm64 ARM-crypto build (that last one I have only compiled, not run).
The pairs
For len <= 8 the whole hash is Mix(state ^ v, kMul) (hash.h L1168), where v is the public packing of the bytes (Read4To8 / Read1To3, 0x57 for the empty string), state is the seed XOR the length mix D(len) (the 8-byte window of kStaticRandomData at offset len), and Mix(a, b) = hi64(a*b) ^ lo64(a*b). Seed and length are XORed into the same multiplicand before one fixed map, so any two (len, bytes) with equal v ^ D(len) collide for every seed:
pair 1: "" (0 bytes) vs a6e02637c07bd386 (8 bytes) m' = bytes8(D(0) ^ D(8) ^ 0x57)
pair 2: "a" (0x61) vs 44b53d572db1948b (8 bytes) m' = bytes8(D(1) ^ D(8) ^ 0x616161)
Every string of 0..8 bytes has such an 8-byte twin. For 9..16 bytes the hash is Mix(state ^ w0, kMul ^ w1) with w1 the last 8 bytes (L1207), so every string whose last 8 bytes are kMul = 0x79d5f9e0de1e8cf5 little-endian (f58c1edee0f9d579) hashes to 0 for every seed, a 2^64-way same-length multicollision:
pair 3: 6162636465666768f58c1edee0f9d579 vs 4142434445464748f58c1edee0f9d579 ("abcdefgh"/"ABCDEFGH" + kMul), both hash to 0
The comment above that code (L1201-L1205) says one half of the mix becoming zero happens for "exactly 1 in 2^64 values for each side ... unless the seed is also incredibly unlucky". That is right for the state ^ w0 side, but the kMul ^ w1 side does not involve the seed at all, so the zero is reached by one fixed choice of the last eight bytes under every seed.
Explicit values from the real library, hash_internal::HashWithSeed().hash(absl::Hash<std::string_view>{}, sv, seed):
seed 0: H("") = H(a6e02637c07bd386) = 2bda3ac53577c4b7 H("a") = H(44b53d572db1948b) = a65e8ab2c950c927
seed 0x4055c8: H("") = H(a6e02637c07bd386) = 06bb4292e0eae907 H("a") = H(44b53d572db1948b) = e3a0eef311f0006a
any seed: H(pair 3, either string) = 0000000000000000
(0x4055c8 was Seed(), the address of MixingHashState::kSeed, in one non-PIE Linux binary; in a PIE binary it is ASLR-slid. Either way it is one value the pairs already cover.)
Measurements
- Real library at
73d2688, scalar and-maesbuilds, throughHashWithSeed: all three pairs collide on the 32 SwissTable seeds{0, 64, ..., 1984}(exhaustive) and on 268,435,456 / 268,435,456 uniform 64-bit seeds, on both builds. - Real
flat_hash_set<std::string>andflat_hash_map<std::string, int>(CMakeadd_subdirectorybuild): 800 tables, whose seeds are exactly the 32 values above;hash_of(k)equalsHashWithSeed().hash(Hash<string>{}, k, seed)on 1200/1200 keys; the pairs collide inside every table, 2400/2400. - A single-file C re-implementation written from
hash.h/hash.ccmatches the real library on 325 recorded vectors (len 0..8192) on both builds and gives the same 32/32 and 2^28/2^28 counts.
The rate is exactly 1 by the algebra; the sampling only confirms that the code under test is the shipped absl::Hash.
Reproduction
The verify/abseil-hash/ directory of https://github.com/thomasahle/hash-collision-witnesses has the C program (MIT; no Abseil source text copied, the two constant tables are Abseil's under Apache-2.0), a README with the seed protocol and the expected output, and the two native checkers:
cd verify/abseil-hash
cc -O2 -std=c11 -o abseil_hash_verify abseil_hash_verify.c -lm
./abseil_hash_verify # 2^20 uniform seeds + the 32 table seeds, 0.2 s; make check expects 32 1048576 32 1048576 32 1048576
./abseil_hash_verify 28 # 2^28 seeds, about 45 s
make native # git fetch abseil-cpp at 73d2688, sha256-check hash.h/hash.cc, run the same pairs and vectors through the real library
make swiss # the same pairs inside real flat_hash_set/flat_hash_map tables
The C program aborts unless all 325 real-library vectors reproduce, the pair recipes re-derive the published hex from kStaticRandomData and kMul, and the recorded values at seeds 0 and 0x4055c8 reproduce; exit status is 0 only if every check passes.
What is and is not claimed
- Claimed: three fixed pairs (two cross-length at one 8-byte word, one same-length at two words) whose collision probability over any distribution of the seed is exactly 1, for
absl::Hash<std::string>/string_view/Cordbytes and for the default hasher of the Swiss tables. In the metric of the write-up this appears in,log2(L / epsilon)over the random seed, that is 0 bits atL = 1. - Not claimed: no DoS or flooding demonstration; no statement about the hash's speed or its avalanche behaviour, which is what
hash.hactually promises ("intended to strongly mix input bits with a target of passing an Avalanche Test"); and no claim that Abseil promised otherwise. TheSeed()comment says plainly that the seed "is not meant as a security feature right now", and the per-table seed is 5 bits, which by itself bounds every pair at 2^-5 regardless of the hash. Thehash.hsentence "One should assume that a hash algorithm is chosen randomly at the start of each process" is the one these pairs are indifferent to: for them, every choice is the same. - Scope: only byte strings (
std::stringand friends). Integer keys use a different fast path and are not covered by these pairs. Thelen > 32LowLevelHashpath has its own seed-independent families on the scalar and ARM builds, which I have not included here; the-maesx86 build behaves differently above 32 bytes. The non-default CRC32C configuration was not measured with these pairs.
I found no prior report of these pairs for the current absl::Hash; the multiply-fold cancellation is the same primitive that wyhash/rapidhash-style pairs use, and the length-mix cancellation looks new to me. Pointers to earlier reports are welcome and will be credited.
Two concrete things that might be worth doing on your side, if any: correcting the L1201 comment so that it does not suggest the kMul side needs an unlucky seed, and, if a per-process random seed is ever adopted as the comment leaves the door open to, noting that the len <= 8 and 9..16 paths would need the seed to enter differently for it to help against these families.
This will be part of a public write-up on fixed-pair collisions in fast hashes, with the verification package alongside. Please tell me if I have misread the code or its intended guarantee and I will correct the text.
Thomas Ahle
- Dominant language
- C++
- Stars
- 18.1k
- Forks
- 3.2k
- Avg merge
- 12h 35m
- Merged PRs (30d)
- 1
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from abseil/abseil-cpp
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
abseil/abseil-cpp#2168 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
abseil/abseil-cpp#2167 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
abseil/abseil-cpp#2153 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
abseil/abseil-cpp#2140 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
abseil/abseil-cpp#2027 · 2 reactions ·
All issues in abseil/abseil-cpp
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
Sensor initialization takes very long when `--initial-sim-time` is set to current UNIX timestamp Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
gazebosim/gz-sensors#662 · 1 comment ·
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
comp-datalake
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/ClickHouse#121222 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
LadybirdBrowser/ladybird#12123 ·