# Google Guava: CompactHashMap/CompactHashSet/MapMakerInternalMap deserialization triggers uncontrolled memory allocation

Guava CompactHashMap/CompactHashSet/MapMakerInternalMap readObject trusts attacker-declared elementCount, so a 72-byte stream forces multi-GB allocation and JVM OutOfMemoryError.

- Payout: Research
- Severity: high
- Program: Guava
- Found by (vendor credit): Kaya Emre Arikan (https://github.com/kemrec)
- Found by: kemrec
- Disclosed: 2026-10-27
- CVE: CVE-2026-102554
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-xxph-c9ww-hj94
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-xxph-c9ww-hj94
- Tags: area:oss domain:oss impact:dos kind:research platform:ghsa sev:high tech:java technique:regression technique:source-review vuln:deserialization vuln:dos vuln:memory-corruption

**Target:** Java library deserialization (ObjectInputStream readObject)
**Root cause:** readObject reads an attacker-controlled elementCount/initialCapacity and only checks non-negativity; init() clamps to CompactHashing.MAX_SIZE (2^30-1) and allocArrays()/segment allocation eagerly sizes arrays to that full value on first insert, before stream content is consumed.
**Steps:**
1. Serialize a CompactHashSet with 1 element and with 2 elements; diff the byte streams to locate the elementCount field offset (63 in the 72-byte stream).
2. Patch that 4-byte field from `1` to `0x3FFFFFFF` (CompactHashing.MAX_SIZE), leaving the single real `"A"` element intact.
3. Feed the 72-byte stream to `new ObjectInputStream(...).readObject()` with guava-33.7.1-jre on the classpath.
4. Observe OutOfMemoryError: ~4.1 GB allocated on -Xmx8g; crash in 24 ms on -Xmx128m.
**How found:** Noticed CompactHashMap/CompactHashSet readObject/allocArrays pre-allocate from declared size, matching the shape of already-fixed CVE-2018-10237 (AtomicDoubleArray) and contradicting HashBiMap.readObject()'s explicit `init(16); // resist hostile attempts to allocate gratuitous heap` comment in the same codebase.
**Exploitation:** Attacker who controls bytes reaching any ObjectInputStream.readObject() names the Guava class directly in a hand-crafted stream; no victim factory usage needed. First put() triggers allocArrays() sizing table/keys/values arrays to ~1.07 billion entries, exhausting heap and crashing the JVM (DoS).
**Insight:** When auditing deserialization, grep for readObject implementations that size arrays from a stream-read count; compare sibling classes in the same library — a defensive comment or incremental-growth fix in one class (HashBiMap, AtomicDoubleArray) often marks the same bug unfixed in others.

## Proof of concept (copied exactly from the original report)

**72-byte malicious stream patched elementCount to MAX_SIZE**

```
1. Uses reflection only to legitimately obtain a `CompactHashSet` instance (it is package-private; a real attacker does not need this step, since they craft the serialized bytes directly rather than calling Guava's own serialization code).
2. Serializes one set with 1 element and one with 2 elements, diffs the two byte streams to locate the exact offset of the `elementCount` field (byte-precise, not guessed).
3. Patches that one 4-byte field in the 1-element stream from `1` to `0x3FFFFFFF` (`CompactHashing.MAX_SIZE`), leaving the rest of the 72-byte stream (one real `"A"` element) untouched.
4. Feeds the resulting 72-byte malicious stream into a plain `new ObjectInputStream(...).readObject()` call against the real, unmodified `guava-33.7.1-jre.jar`.
```

**Reproduction command for the PoC**

```
To reproduce: `mvn -q compile dependency:build-classpath -Dmdep.outputFile=cp.txt && java -Xmx128m -cp "target/classes:$(cat cp.txt)" poc.Poc`
```

