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.
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.
// 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);input: [a, b, c, d, e] wrong: a b c d (e is lost) right: a b c d e
// 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);wrong: total === NaN right: the exact sum
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)wrong: 1 2 3 4 right: 1 2 3 4 5
# 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 }wrong: every value, then a blank line (nil) right: every value, once
// 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) }wrong: panic: index out of range [len] right: every element
// 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); }wrong: panic: index out of bounds right: every element
// 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]);wrong: ArrayIndexOutOfBoundsException: 5 right: 1 2 3 4 5
// 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])wrong: IndexOutOfBoundsException right: every element
/* 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]);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.
// 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]);wrong: skips first, reads one past the end right: every element, once
// 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);wrong: trailing undefined in out right: a clean copy of items
# 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)wrong: IndexError: list index out of range right: the exact sum
# 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 }wrong: shifted by one, trailing nil right: each value in place
// 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) }wrong: panic: index out of range right: every element
// 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); }wrong: panic: index out of bounds right: every element
// 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);wrong: IndexOutOfBoundsException right: every element
// 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)wrong: IndexOutOfBoundsException right: every element
/* 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]);wrong: 1 2 3 4 5 <garbage> right: 1 2 3 4 5
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.