Off-by-One Errors in C Buffer Indexing Explained

An off-by-one error (OBOE) is a common logic flaw that occurs when a program iterates through a sequence or accesses a collection using an incorrect boundary condition, targeting an index that is one position ahead or behind the intended target. In the C programming language, these errors frequently manifest during buffer manipulation due to zero-based indexing, faulty loop comparisons, and improper accounting for string null terminators. This article explains the mechanics behind off-by-one errors in C, demonstrates how they appear in code, and details the security and stability risks they introduce.

The Mechanism of Zero-Based Indexing

In C, arrays and memory buffers use zero-based indexing. When an array is declared with a specific size, the valid indices range from 0 to size - 1:

int buffer[5]; // Valid indices: 0, 1, 2, 3, 4

Accessing buffer[5] does not trigger a compile-time error in standard C, but it references memory directly outside the allocated bounds of the array. An off-by-one error occurs when a developer incorrectly assumes the upper bound is equal to the size of the array rather than size - 1.

Common Manifestations in C

1. Incorrect Loop Boundary Conditions

The most frequent source of an off-by-one error is using a less-than-or-equal-to operator (<=) instead of a strict less-than operator (<) when iterating through an array:

#define SIZE 10
int buffer[SIZE];

// Error: Loop runs 11 times (i = 0 through 10)
for (int i = 0; i <= SIZE; i++) {
    buffer[i] = 0; // buffer[10] is out of bounds
}

In the final iteration, i equals 10, causing the program to write beyond the allocated space of buffer. The correct loop boundary should strictly check i < SIZE.

2. Neglecting the Null Terminator in Strings

In C, strings are null-terminated byte sequences ending with '\0'. A buffer storing a string of length \(N\) must have a capacity of at least \(N + 1\). Developers often calculate buffer size using strlen(), which returns the character count excluding the null terminator, leading to an off-by-one allocation or write:

char source[] = "example"; // Length is 7, requires 8 bytes including '\0'
char destination[7];       // Insufficient space

// Error: Strncpy does not null-terminate if the bound is reached,
// or writing '\0' at index 7 overflows destination.
strncpy(destination, source, sizeof(destination));
destination[7] = '\0'; // Write off-by-one error

3. Fencepost Counting Errors

Fencepost errors occur when calculating sizes or intervals. For example, calculating the number of elements between index a and index b as b - a rather than (b - a) + 1 can lead to allocating one element too few. When the program later attempts to populate the entire range inclusive of both ends, the final write falls outside the buffer.

Consequences and Impact

Off-by-one errors in C lead to defined types of undefined behavior:

  • Stack Smashing and Memory Corruption: Overwriting an adjacent byte on the stack can corrupt adjacent local variables, frame pointers, or function return addresses.
  • Denial of Service (Crashes): Reading or writing to an invalid page boundary often triggers a segmentation fault (SIGSEGV), causing the application to terminate immediately.
  • Security Vulnerabilities: Even a single-byte overflow (often called a "poison byte" vulnerability) can allow an attacker to alter execution flow, bypass security checks, or execute arbitrary code by overwriting least-significant bytes of memory pointers.

Prevention

Mitigating off-by-one errors requires disciplined boundary verification:

  • Use standard idioms for array traversals: for (size_t i = 0; i < size; ++i).
  • Always allocate strlen(str) + 1 bytes for strings that require a null terminator.
  • Compile with compiler warnings enabled (-Wall, -Wextra) and utilize AddressSanitizer (-fsanitize=address) during development to catch boundary violations at runtime.