the boundary every stateful tool gets wrong

Off-by-One Errors

The fencepost bug: a bound one too big or one too small, so the last element is dropped or one past the end is read. It is the boundary defect every stateful tool can produce -- shown here generic, then via a different stateful mechanism, across nine languages.

Off-by-one isn't one language's bug -- it's what every counter, cursor, and pointer drifts into.

01in the wild

In the wild

Generic Off-by-One

The plain fencepost: an inclusive bound or a len-1 where the language already gives you the right range.

example.js
// WRONG: length - 1 silently skips the last element
for (let i = 0; i < arr.length - 1; i++) send(arr[i]);

// RIGHT: iterate the whole range
for (const item of arr) send(item);
Subtracting 1 from the bound is the classic fencepost error. for...of removes the bound you can get wrong.
// observed
input: [a, b, c, d, e]
wrong: a b c d   (e is lost)
right: a b c d e
example.ts
// WRONG: <= on a half-open length reads one past the end
let total = 0;
for (let i = 0; i <= xs.length; i++) total += xs[i];  // xs[length] is undefined

// RIGHT: strict <, or reduce and never index at all
const total = xs.reduce((a, b) => a + b, 0);
An array of length n has no index n. <= reads xs[length] (undefined), turning the sum into NaN.
// observed
wrong: total === NaN
right: the exact sum
example.py
items = [1, 2, 3, 4, 5]

# WRONG: range() is already exclusive of the end
for i in range(len(items) - 1):
    process(items[i])

# RIGHT: iterate the object, not an index
for item in items:
    process(item)
range(n) already stops before n. Subtracting 1 on top of that drops the final element.
// observed
wrong: 1 2 3 4
right: 1 2 3 4 5
example.rb
# WRONG: .. is inclusive, so it reads one past the end
(0..arr.length).each { |i| puts arr[i] }   # last is arr[length] -> nil

# RIGHT: ... is exclusive (or just iterate the values)
(0...arr.length).each { |i| puts arr[i] }
arr.each { |v| puts v }
Ruby's .. range includes the end; ... excludes it. Indexing with the inclusive range reads one element too far (nil).
// observed
wrong: every value, then a blank line (nil)
right: every value, once
example.go
// WRONG: <= indexes one past the slice -> panic
for i := 0; i <= len(xs); i++ {
    fmt.Println(xs[i])
}

// RIGHT: use <, or range
for _, x := range xs { fmt.Println(x) }
len(xs) is one past the last valid index. <= panics with index out of range.
// observed
wrong: panic: index out of range [len]
right: every element
example.rs
// WRONG: 0..=n is inclusive -> indexes v.len()
for i in 0..=v.len() {
    println!("{}", v[i]);   // panics on the last i
}

// RIGHT: 0..n is half-open (or just iterate)
for x in &v { println!("{}", x); }
0..=v.len() includes v.len(), which is out of bounds. 0..v.len() is half-open; iterating avoids the index entirely.
// observed
wrong: panic: index out of bounds
right: every element
example.java
// WRONG: <= walks one past the end -> exception
for (int i = 0; i <= arr.length; i++) process(arr[i]);

// RIGHT: use < not <=
for (int i = 0; i < arr.length; i++) process(arr[i]);
<= reads index 5 of a length-5 array and throws ArrayIndexOutOfBoundsException.
// observed
wrong: ArrayIndexOutOfBoundsException: 5
right: 1 2 3 4 5
example.kt
// WRONG: 0..size is inclusive of size
for (i in 0..list.size) print(list[i])   // IndexOutOfBounds on size

// RIGHT: 0 until size is half-open
for (i in 0 until list.size) print(list[i])
Kotlin's .. is inclusive, so 0..size visits the invalid index size. until is the half-open form.
// observed
wrong: IndexOutOfBoundsException
right: every element
example.c
/* WRONG: <= reads one past the array -- undefined behavior */
for (int i = 0; i <= 5; i++) process(arr[i]);   /* arr[5] is garbage */

/* RIGHT */
for (int i = 0; i < 5; i++) process(arr[i]);
In C the off-by-one does not throw; it reads adjacent memory. Silent corruption is worse than a crash.
// observed
wrong: 1 2 3 4 5 <garbage>
right: 1 2 3 4 5

Off-by-One With a Stateful Tool

The same boundary bug, but produced by a different stateful mechanism in each language -- a counter, a cursor, an operator, a pointer.

example.js
// STATEFUL TOOL: the ++ operator mutates i inside the test
let i = 0;
while (i++ < arr.length) {     // i is advanced even on the final test
  send(arr[i]);                // skips arr[0], reads arr[length] (undefined)
}

// RIGHT: separate the test from the mutation
for (let i = 0; i < arr.length; i++) send(arr[i]);
The ++ operator mutates i as a side effect of the comparison, so the index and the bound disagree by one.
// observed
wrong: skips first, reads one past the end
right: every element, once
example.ts
// STATEFUL TOOL: a mutable cursor advanced by hand
let cursor = 0;
const out: string[] = [];
while (cursor <= items.length) {   // <= overshoots by one
  out.push(items[cursor]);         // last push is items[length] -> undefined
  cursor += 1;
}

// RIGHT: let the language own the bound
const out = items.map((x) => x);
A hand-mutated let cursor with <= reads one slot past the array; let-binding the iteration removes the cursor you can get wrong.
// observed
wrong: trailing undefined in out
right: a clean copy of items
example.py
# STATEFUL TOOL: a hand-mutated counter and a fixed bound drift apart
i = 0
total = 0
while i <= len(nums):        # <= runs one extra iteration
    total += nums[i]         # IndexError on the last pass
    i += 1

# RIGHT: iterate the object; no counter to mutate
total = sum(nums)
The mutable counter i and the <= bound are maintained separately, so they fall out of step by one.
// observed
wrong: IndexError: list index out of range
right: the exact sum
example.rb
# STATEFUL TOOL: a mutated offset added to the loop index
offset = 1
arr.each_with_index do |_, i|
  puts arr[i + offset]   # last read is arr[size] -> nil
end

# RIGHT: no offset; iterate the values directly
arr.each { |v| puts v }
Mutating an offset and adding it to the index pushes every access one past where you think you are (returns nil at the end).
// observed
wrong: shifted by one, trailing nil
right: each value in place
example.go
// STATEFUL TOOL: a manual index drives the loop past the slice
i := 0
for i <= len(xs) {           // <= indexes one past the slice
    fmt.Println(xs[i])       // panic: index out of range
    i++
}

// RIGHT: range owns the bound
for _, x := range xs { fmt.Println(x) }
Driving the loop with a mutable index and <= len(xs) reads one past the slice; range removes the manual bound.
// observed
wrong: panic: index out of range
right: every element
example.rs
// STATEFUL TOOL: a `mut` counter advanced by hand
let mut i = 0;
while i <= v.len() {          // <= goes one past the end
    println!("{}", v[i]);     // panics: index out of bounds
    i += 1;
}

// RIGHT: iterate; the bound is not yours to manage
for x in &v { println!("{}", x); }
A let mut index incremented by hand with <= indexes v.len(), which is one past the last valid index.
// observed
wrong: panic: index out of bounds
right: every element
example.java
// STATEFUL TOOL: manual for-loop control mutates the index
int n = list.size();
for (int i = 0; i <= n; i++) {   // <= reads index size -> exception
    System.out.println(list.get(i));
}

// RIGHT: half-open bound, no manual index
list.forEach(System.out::println);
Hand-rolled for-loop control with <= walks one past the last index; for-each hands the bound to the collection.
// observed
wrong: IndexOutOfBoundsException
right: every element
example.kt
// STATEFUL TOOL: a `var` index over an inclusive range
var i = 0
while (i <= list.size) {       // <= visits the invalid index size
    println(list[i])           // IndexOutOfBoundsException
    i += 1
}

// RIGHT: `until` is half-open; no var to mismanage
for (x in list) println(x)
A mutated var index with <= list.size reads one past the end. until is the half-open range that can't.
// observed
wrong: IndexOutOfBoundsException
right: every element
example.c
/* STATEFUL TOOL: a mutated pointer walks one element too far */
int *p = arr;
for (int i = 0; i <= 5; i++) {   /* <= and *p++ both overshoot */
    printf("%d ", *p++);          /* reads arr[5]: past the array */
}

/* RIGHT: stop before the end */
for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
The mutated pointer *p++ combined with <= reads one past the array -- undefined behavior, not a clean crash.
// observed
wrong: 1 2 3 4 5 <garbage>
right: 1 2 3 4 5
02weakness catalog

Mapped weaknesses (CWE)

On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.