# Unchecked bitmap index scaling

Scaled index (multiply, shift, accumulate) reaching a bit set/clear or trie bound without a check on the scaled value

_Area: Memory corruption and native code. Seen in 3 disclosed reports; latest 2026-10-07. Updated 2026-11-02._

## How it works
A bitmap, bitset or trie is indexed by a value derived from input (an address, a size, a column name, a level). The code assumes that value is already in range, or that a later guard will catch it. When the index is scaled (multiplied, shifted, accumulated) before the bit operation, the scaling can push the index past the end of the bitmap, or wrap it back into range. The bit clear/set then writes outside the allocation or to the wrong bit. The broken assumption is: "the input is bounded, so the computed index is bounded." Scaling and integer width break that link.

## Where it shows up
- A multiply or shift on an input-derived value immediately before a bit set/clear, e.g. `start_addr * 4` indexing a dirty-page bitmap [E1](https://bugbountylibrary.com/r/blog%3Azerodayinitiative-com-blog-2024-5-9-cve-2024-21115-an-oracle-virtualbox-lpe-used-to-win-pwn2own.md).
- `1 << n` where `n` comes from a user-controlled size or level; JS shift counts are taken mod 32, so thresholds at 2^30/2^31 silently flip [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-v56q-mh7h-f735.md).
- Accumulators (base-26, base-N) in a fixed-width integer with only a post-loop upper-bound check and no overflow detection [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md).
- Downstream code that treats `err == nil` from a parser as proof the value is in-domain [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md).

## How to review
Find every bit set/clear and trace its index back to input. The invariant that must hold: **before the bit operation, the scaled index is proven `< bitmap_length` (and `>= 0`) by a check on the scaled value itself, not on the pre-scaled input.** Check the arithmetic width: does the multiply/shift/accumulate fit the type, or can it wrap into range? For shifts, confirm the count is bounded below the type width. For parsers, confirm a range check exists on the final value, not just an error return. When one axis of a coordinate/range check is hardened, audit the sibling axis for the same class [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md).

## How finders spotted it
- Read the VGA dirty-page code and noticed the `*4` on `start_addr` before the bit clear, then reasoned a large `start_addr` clears bits past the heap bitmap [E1](https://bugbountylibrary.com/r/blog%3Azerodayinitiative-com-blog-2024-5-9-cve-2024-21115-an-oracle-virtualbox-lpe-used-to-win-pwn2own.md).
- Analyzed the 32-wide trie (SHIFT=5) and noticed the level-raising loop's `1 << (newLevel + SHIFT)` relies on JS shift counts mod 32, going negative at 31 and wrapping to 1 at 32, keeping the comparison true forever [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-v56q-mh7h-f735.md).
- Noticed the row side of a coordinate gate was hardened but the column side was not; read `ColumnNameToNumber`, saw only a post-loop MaxColumns check, and probed a 14-letter name to get `(0, nil)` [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md).

## Variants seen
- Out-of-bounds bit clear via scaled address past a heap bitmap [E1](https://bugbountylibrary.com/r/blog%3Azerodayinitiative-com-blog-2024-5-9-cve-2024-21115-an-oracle-virtualbox-lpe-used-to-win-pwn2own.md).
- Signed shift wrap in a trie level loop causing non-termination/OOM [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-v56q-mh7h-f735.md).
- int64 accumulation overflow wrapping a coordinate back in-domain, passing a guard, then a negative slice index [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md).

## Impact and severity
E1 escalated from a single-bit heap clear to arbitrary read/write and LPE by chaining critical-section corruption, a bounds-check race, scratch-region corruption and function-pointer hijack [E1](https://bugbountylibrary.com/r/blog%3Azerodayinitiative-com-blog-2024-5-9-cve-2024-21115-an-oracle-virtualbox-lpe-used-to-win-pwn2own.md). E2 gave an unrecoverable DoS from one request [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-v56q-mh7h-f735.md). E3 caused a panic on attacker-supplied files [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md). Impact rises when the corrupted bit lands in a security-relevant structure (a lock handle) or when the wrapped value passes a guard into a memory access.

## Seen in real software
Open-source packages and projects, and products, where this pattern was found. If the target runs one, compare the version it runs with the fix.
- **github.com/xuri/excelize/v2 (go)** >= 2.0.0, < 2.11.1-0.20260910071107-696050fbf14e, fixed in 2.11.1-0.20260910071107-696050fbf14e: Excelize ColumnNameToNumber: int64 overflow yields an out-of-domain coordinate with nil error, causing negative slice in (CVE-2026-107217). Report: https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md. Fix: https://github.com/qax-os/excelize/commit/696050fbf14e74e96a58eef2b16aaf72f381a6a8
- **immutable (npm)** >= 5.0.0-beta.1, < 5.1.8, fixed in 5.1.8: Immutable.js `List` 32-bit trie overflow → unrecoverable DoS (CVE-2026-59879). Report: https://bugbountylibrary.com/r/ghsa%3AGHSA-v56q-mh7h-f735.md. Fix: https://github.com/immutable-js/immutable-js/commit/a1a1ee412dcaa380ab325196283d06594ffe4b84

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Azerodayinitiative-com-blog-2024-5-9-cve-2024-21115-an-oracle-virtualbox-lpe-used-to-win-pwn2own.md
- E2: https://bugbountylibrary.com/r/ghsa%3AGHSA-v56q-mh7h-f735.md
- E3: https://bugbountylibrary.com/r/ghsa%3AGHSA-c85p-xxjj-2r75.md
