OneBug All write-ups

Java ·

Java binary search overflow: the bug that hid for 9 years

Java's binary search threw ArrayIndexOutOfBoundsException on huge arrays for nine years: low + high silently overflows to a negative int before the divide.

Test yourself before reading - the write-up below gives it away.

Spot the bug Java
1
// Index of key in a sorted array, or -1.
2
static int indexOf(int[] a, int key) {
3
    int low = 0, high = a.length - 1;
4
    while (low <= high) {
5
        int mid = (low + high) / 2;
6
        if      (a[mid] < key) low  = mid + 1;
7
        else if (a[mid] > key) high = mid - 1;
8
        else                   return mid;
9
    }
10
    return -1;
11
}
Powered by OneBug

A textbook binary search, the kind you write on a whiteboard in an interview and never think about again. Sorted array, two pointers closing in on the middle, return the index or -1. Nothing about it looks unusual, which is exactly why it shipped in a standard library for a decade before anyone fed it the array that breaks it.

// Index of key in a sorted array, or -1.
static int indexOf(int[] a, int key) {
    int low = 0, high = a.length - 1;
    while (low <= high) {
        int mid = (low + high) / 2;
        if      (a[mid] < key) low  = mid + 1;
        else if (a[mid] > key) high = mid - 1;
        else                   return mid;
    }
    return -1;
}

Why does binary search throw ArrayIndexOutOfBoundsException on large arrays?

The midpoint is the bug: int mid = (low + high) / 2;. On a large enough array, low + high exceeds Integer.MAX_VALUE and overflows to a negative int before the division runs, so mid comes out negative and a[mid] throws ArrayIndexOutOfBoundsException. Compute the midpoint as int mid = low + (high - low) / 2; instead, and the sum can never overflow.

low + high overflows int before the divide

The loop condition low <= high is the usual off-by-one suspect (< vs <=), and the mid + 1 / mid - 1 updates on the following lines are the other classic miscount. Both are fine here. The fault is one line up, in how mid gets computed in the first place.

int mid = (low + high) / 2; computes low + high as an int, and only then divides by 2. Java int is a 32-bit two's complement type per the JLS, with a hard ceiling of Integer.MAX_VALUE (2,147,483,647). If the array is big enough that low + high can exceed that ceiling, the addition overflows and wraps around to a negative number. Divide a negative number by 2 and you still get a negative number, so mid goes negative, and a[mid] throws ArrayIndexOutOfBoundsException.

That takes an array bigger than roughly 2^30 elements, about 1.07 billion. For an int[], that's an array pushing 4 GB, which is why almost nobody hits this in practice, and why almost every test suite in existence passes clean. The bug does not require exotic input, just a boring one: an array large enough that nobody bothered writing that test case.

Why it bites in real code

The trap is not specific to binary search. It is any place two int bounds get averaged to find a midpoint: mergesort's merge step, pagination math like (offset + limit) / 2, range-splitting logic in a scheduler, anything computing a middle value from two sizes that could plausibly both be large. Wherever you see (a + b) / 2 on integers whose upper bound you have not personally verified, the same overflow is sitting there.

The fix

int mid = low + (high - low) / 2;

high - low is never negative and never larger than high itself, so it cannot overflow, and adding it back to low lands exactly where (low + high) / 2 was supposed to. The JDK's own fix, used internally in Arrays.binarySearch today, takes a different route to the same result: int mid = (low + high) >>> 1;. The unsigned right shift operates on the raw bit pattern, so even when low + high wraps into negative territory as a 32-bit value, shifting it right unsigned still produces the correct midpoint, right up until the array size approaches 2^32.

War stories

This is Joshua Bloch's bug, and it is real, documented history rather than a hypothetical. In his 2006 Google Research blog post, "Extra, Extra - Read All About It: Nearly All Binary Searches and Mergesorts are Broken," Bloch traced this exact overflow through java.util.Arrays.binarySearch and the JDK's merge sort, where it had lived for about nine years before anyone noticed. The lineage goes back further still, to the binary search implementation printed in Jon Bentley's Programming Pearls, where the same flaw sat unchallenged for roughly two decades. A textbook algorithm, reviewed and taught for years, with a one-line arithmetic bug that only a billion-element array would ever trigger.

Related bugs

Same family, different disguise: an equality check that looks correct for small values and quietly breaks past a boundary, in the Integer cache bug. Browse the rest of the bug write-ups.