CWE-193: Off-by-one Error
Overview
Off-by-one errors occur when loop bounds, array indices, or buffer size calculations are incorrect by exactly one position, often due to < vs <=, or forgetting null terminators. This causes buffer overflows, reading/writing past array bounds, or incorrect loop iterations.
Risk
Medium-High: Off-by-one errors cause buffer overflows (writing one byte past buffer), out-of-bounds reads (information leakage), null terminator overwrite, incorrect string handling, and logic errors. Classic mistake in C string operations and array iterations.
Remediation Steps
Core principle: Allocate and check bounds carefully to avoid off-by-one errors (including terminators).
Locate Off-by-One Errors in Your Code
When reviewing security scan results:
- Find loop bound errors: Identify loops using
<=instead of<(or vice versa) - Check array indexing: Look for accesses at index
array[size](should be< size) - Identify buffer calculations: Find allocations missing +1 for null terminator
- Review boundary conditions: Check edge cases like empty arrays, size 1, maximum size
- Trace string operations: Look for strncpy, malloc with strlen, manual null termination
Common off-by-one patterns:
// Loop: <= should be <
for (int i = 0; i <= size; i++) // Accesses array[size] - OUT OF BOUNDS!
array[i] = 0;
// Missing +1 for null terminator
char *buf = malloc(strlen(str)); // Should be strlen(str) + 1
// Wrong bounds check
if (index <= 10) // For array[10], valid indices are 0-9
buffer[index] = value; // buffer[10] is OUT OF BOUNDS!
// strncpy without null termination
char buf[10];
strncpy(buf, input, sizeof(buf)); // If input >= 10 chars, not null-terminated!
Understanding array bounds:
- Array
int array[10]has indices 0-9 (size 10, but max index is 9) - Valid indices:
0 <= index < sizeor0 <= index <= size-1 - Loop should use
i < sizeNOTi <= size
Use Correct Loop Bounds (Primary Defense)
#include <stdio.h>
// VULNERABLE - loop goes one past end
void init_array_bad(int *array, int size) {
// BUG: i <= size means i goes from 0 to size (inclusive)
// For array[10], this accesses array[10] which is OUT OF BOUNDS
for (int i = 0; i <= size; i++) {
array[i] = 0; // Writes past end when i == size
}
}
// SECURE - correct loop bound
void init_array_good(int *array, int size) {
// CORRECT: i < size means i goes from 0 to size-1
// For array[10], this accesses array[0] through array[9]
for (int i = 0; i < size; i++) {
array[i] = 0;
}
}
// VULNERABLE - wrong bounds check
void set_element_bad(int *array, int size, int index, int value) {
// BUG: index <= size allows index == size (out of bounds)
if (index <= size) {
array[index] = value; // array[size] is out of bounds!
}
}
// SECURE - correct bounds check
void set_element_good(int *array, int size, int index, int value) {
// CORRECT: index must be < size (0 to size-1)
if (index >= 0 && index < size) {
array[index] = value;
}
}
// VULNERABLE - reverse loop with unsigned
void process_backwards_bad(unsigned int count) {
// BUG: i >= 0 is always true for unsigned - infinite loop!
for (unsigned int i = count - 1; i >= 0; i--) {
process(data[i]);
}
}
// SECURE - correct reverse loop
void process_backwards_good(unsigned int count) {
if (count == 0) return;
// CORRECT: decrement in condition, loop body uses i
for (unsigned int i = count; i-- > 0; ) {
process(data[i]);
}
// Alternative: use signed loop variable
for (int i = count - 1; i >= 0; i--) {
process(data[i]);
}
}
Why this works: Using < instead of <= ensures loop variables stay within valid array bounds (0 to size-1). Understanding array indexing prevents accessing one element past the end.
Loop bound rules:
- Forward:
for (i = 0; i < size; i++)- accesses 0 to size-1 - Reverse:
for (i = size-1; i >= 0; i--)- use signed int - Reverse unsigned:
for (i = size; i-- > 0; )- decrement in condition
Always Account for Null Terminator
#include <string.h>
#include <stdlib.h>
// VULNERABLE - forgot +1 for null terminator
void copy_string_bad(const char *src) {
// BUG: strlen returns length WITHOUT null terminator
char *dest = malloc(strlen(src)); // Missing +1!
// strcpy writes strlen(src) + 1 bytes (includes \0)
strcpy(dest, src); // Writes null terminator OUT OF BOUNDS!
}
// SECURE - include +1 for null terminator
void copy_string_good(const char *src) {
size_t len = strlen(src);
// CORRECT: allocate length + 1 for null terminator
char *dest = malloc(len + 1);
if (dest == NULL) return;
strcpy(dest, src);
// Or more explicit:
memcpy(dest, src, len);
dest[len] = '\0'; // Manually add null terminator
}
// VULNERABLE - strncpy without ensuring null termination
void copy_name_bad(const char *input) {
char buffer[10];
// BUG: If input is >= 10 characters, strncpy does NOT add \0
strncpy(buffer, input, sizeof(buffer));
// buffer may NOT be null-terminated!
printf("%s\n", buffer); // May read past buffer
}
// SECURE - reserve space for null terminator
void copy_name_good(const char *input) {
char buffer[10];
// CORRECT: use sizeof(buffer) - 1 to reserve space for \0
strncpy(buffer, input, sizeof(buffer) - 1);
// ALWAYS explicitly null-terminate
buffer[sizeof(buffer) - 1] = '\0';
printf("%s\n", buffer);
}
// VULNERABLE - wrong buffer size for concatenation
void concat_strings_bad(const char *s1, const char *s2) {
// BUG: needs strlen(s1) + strlen(s2) + 1 for null
char *result = malloc(strlen(s1) + strlen(s2)); // Missing +1
strcpy(result, s1);
strcat(result, s2); // Null terminator written out of bounds!
}
// SECURE - account for null terminator
void concat_strings_good(const char *s1, const char *s2) {
size_t len1 = strlen(s1);
size_t len2 = strlen(s2);
// CORRECT: total length + 1 for null terminator
char *result = malloc(len1 + len2 + 1);
if (result == NULL) return;
strcpy(result, s1);
strcat(result, s2);
}
Use Safe APIs and Containers
#include <vector>
#include <string>
#include <array>
#include <algorithm>
class SafeArrayOperations {
public:
// Use std::vector - no off-by-one possible
void initArray(std::vector<int>& array) {
// Range-based loop - automatic bounds
for (int& value : array) {
value = 0;
}
// Or use algorithm
std::fill(array.begin(), array.end(), 0);
}
// Use std::string - automatic memory management
std::string copyString(const std::string& src) {
return src; // No null terminator issues
}
// Bounds-checked access
void setElement(std::vector<int>& array, size_t index, int value) {
// at() throws out_of_range exception if index >= size
try {
array.at(index) = value;
} catch (const std::out_of_range& e) {
std::cerr << "Index out of bounds: " << index << std::endl;
}
}
// Use std::array for fixed-size arrays
void processFixedArray() {
std::array<int, 10> arr;
// size() returns actual size
for (size_t i = 0; i < arr.size(); i++) {
arr[i] = 0; // Correct bounds
}
}
};
// Java example - automatic bounds checking
public class SafeArrayOps {
public static void initArray(int[] array) {
// Java arrays know their length
for (int i = 0; i < array.length; i++) {
array[i] = 0;
}
// Or use Arrays utility
java.util.Arrays.fill(array, 0);
}
public static String copyString(String src) {
// String is immutable - no buffer issues
return new String(src);
}
public static void setElement(int[] array, int index, int value) {
// Java throws ArrayIndexOutOfBoundsException automatically
if (index >= 0 && index < array.length) {
array[index] = value;
}
}
}
Add Assertions and Validation
#include <assert.h>
#include <stdio.h>
void safe_array_access(int *array, int array_size, int index) {
// Development: assert catches errors early
assert(array != NULL);
assert(array_size > 0);
assert(index >= 0 && index < array_size);
// Production: validate and handle error
if (index < 0 || index >= array_size) {
fprintf(stderr, "Index %d out of bounds [0, %d)\n",
index, array_size);
return;
}
array[index] = 42;
}
void safe_loop_with_assertion(int *array, int size) {
assert(size >= 0);
for (int i = 0; i < size; i++) {
// Assert we're in bounds
assert(i >= 0 && i < size);
array[i] = i;
}
}
// Use size_t to prevent negative indices
void use_size_t(int *array, size_t size) {
// size_t is unsigned - can't be negative
for (size_t i = 0; i < size; i++) {
array[i] = 0;
}
}
Test with Edge Cases and Sanitizers
#include <stdio.h>
#include <assert.h>
#include <string.h>
void test_off_by_one_fixes() {
// Test 1: Empty array
int empty[1] = {0};
init_array_good(empty, 0); // Should handle size 0
printf("✓ Test 1: Empty array\n");
// Test 2: Size 1 array
int single[1] = {99};
init_array_good(single, 1);
assert(single[0] == 0);
printf("✓ Test 2: Size 1 array\n");
// Test 3: Exact boundary
int array[10];
init_array_good(array, 10);
// Verify array[9] was set (last valid index)
assert(array[9] == 0);
// array[10] should NOT have been touched
printf("✓ Test 3: Boundary access\n");
// Test 4: String with exact fit
char buffer[10];
const char *exact = "123456789"; // 9 chars + \0 = 10 bytes
copy_name_good(exact);
assert(buffer[9] == '\0');
printf("✓ Test 4: Exact fit string\n");
// Test 5: String too long
const char *too_long = "1234567890ABC";
copy_name_good(too_long);
assert(buffer[9] == '\0'); // Should be null-terminated
assert(strlen(buffer) == 9); // Should be truncated
printf("✓ Test 5: Truncated string\n");
printf("All off-by-one tests passed!\n");
}
// Compile with AddressSanitizer to catch OOB access
// gcc -fsanitize=address -g -o test test.c
// ./test
// Valgrind can also detect off-by-one errors
// valgrind --leak-check=full ./test
Common Vulnerable Patterns
Loop Boundary Error (<= Instead of <)
#include <string.h>
#include <stdlib.h>
// Dangerous: loop goes one past array end
void init_array(int *array, int size) {
for (int i = 0; i <= size; i++) { // BUG: should be i < size
array[i] = 0; // Writes past end when i == size
}
}
Missing +1 for Null Terminator in Allocation
#include <string.h>
#include <stdlib.h>
// Dangerous: forgot +1 for null terminator
void copy_string(const char *src) {
char *dest = malloc(strlen(src)); // Missing +1
strcpy(dest, src); // Writes null terminator out of bounds!
}
strncpy Without Null Termination
#include <string.h>
#include <stdio.h>
// Dangerous: strncpy doesn't guarantee null termination
void copy_name(const char *input) {
char buffer[10];
strncpy(buffer, input, sizeof(buffer)); // If input is 10+ chars
// buffer is NOT null-terminated!
printf("%s\n", buffer); // May read past buffer
}
Off-by-One in Bounds Check
// Dangerous: off-by-one in bounds check
void set_char(char *buffer, int index, char value) {
if (index <= 10) { // Buffer is char[10], indices 0-9
buffer[index] = value; // buffer[10] is out of bounds!
}
}
## Secure Patterns
### Correct Loop Boundaries (C)
```c
#include <string.h>
#include <stdlib.h>
void init_array_safe(int *array, int size) {
// Correct: i < size means i goes from 0 to size-1
for (int i = 0; i < size; i++) {
array[i] = 0;
}
}
Why this works: Using i < size ensures the loop variable ranges from 0 to size-1, which are the only valid indices for an array of the given size. The < operator prevents accessing the element at position size, which would be out of bounds.
Null Terminator Allocation (C)
#include <string.h>
#include <stdlib.h>
void copy_string_safe(const char *src) {
size_t len = strlen(src);
char *dest = malloc(len + 1); // +1 for null terminator
if (dest != NULL) {
strcpy(dest, src);
// Or safer:
memcpy(dest, src, len);
dest[len] = '\0';
}
}
Why this works: Adding 1 to strlen(src) accounts for the null terminator that strlen doesn't count. This ensures the allocated buffer has space for all characters plus the terminating \0.
Explicit Null Termination with strncpy (C)
#include <string.h>
#include <stdio.h>
void copy_name_safe(const char *input) {
char buffer[10];
// Reserve space for null terminator
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // Ensure termination
printf("%s\n", buffer);
}
Why this works: Using sizeof(buffer) - 1 reserves the last byte for the null terminator. Explicitly setting buffer[sizeof(buffer) - 1] = '\0' guarantees null termination even when strncpy doesn't add one (which happens when the source is longer than the limit).
Validated Array Index Access (C)
void set_char_safe(char *buffer, int buffer_size, int index, char value) {
// Correct: index must be < buffer_size (0 to buffer_size-1)
if (index >= 0 && index < buffer_size) {
buffer[index] = value;
}
}
Why this works: Checking index < buffer_size (not <=) ensures the index is within the valid range 0 to buffer_size-1. An array of size N has indices 0 through N-1, so index N is out of bounds.
Bounds-Checked Containers (C++)
#include <vector>
#include <string>
#include <algorithm>
class SafeArray {
public:
void initArray(std::vector<int>& array) {
// Range-based loop - no off-by-one possible
for (int& value : array) {
value = 0;
}
// Or use algorithm
std::fill(array.begin(), array.end(), 0);
}
std::string copyString(const std::string& src) {
// std::string handles memory automatically
return src;
}
void setChar(std::vector<char>& buffer, size_t index, char value) {
// Use at() for bounds checking
try {
buffer.at(index) = value;
} catch (const std::out_of_range& e) {
// Handle error
}
}
};
Why this works: C++ containers like std::vector and std::string handle memory allocation and bounds automatically. The at() method performs runtime bounds checking and throws an exception on out-of-bounds access, preventing memory corruption.
Automatic Bounds Enforcement (Java)
import java.util.Arrays;
public class SafeArrayOps {
public static void initArray(int[] array) {
// Java arrays know their length
for (int i = 0; i < array.length; i++) {
array[i] = 0;
}
// Or use Arrays utility
Arrays.fill(array, 0);
}
public static String copyString(String src) {
// String is immutable, no buffer issues
return new String(src);
}
public static void setChar(char[] buffer, int index, char value) {
// Java automatically throws ArrayIndexOutOfBoundsException
if (index >= 0 && index < buffer.length) {
buffer[index] = value;
}
}
public static byte[] allocateForString(String str) {
// Correct: getBytes() includes all bytes
byte[] data = str.getBytes();
// If manually allocating:
byte[] buffer = new byte[str.length() + 1]; // +1 if needed
return buffer;
}
}
Why this works: Java arrays have built-in length tracking and automatic bounds checking. Any out-of-bounds access throws ArrayIndexOutOfBoundsException, preventing memory corruption. String and array utilities handle sizing correctly.
Safe Indexing with range() (Python)
def init_array(array: list[int]) -> None:
"""Initialize array - Python handles bounds."""
for i in range(len(array)): # range(n) goes 0 to n-1
array[i] = 0
# Or more Pythonic:
array[:] = [0] * len(array)
def copy_string(src: str) -> str:
"""Copy string - Python strings are immutable."""
return src
def safe_slice(data: list, start: int, length: int) -> list:
"""Get slice with proper bounds."""
end = start + length
# Python slicing handles out-of-bounds gracefully
return data[start:end]
Why this works: Python's range(n) generates indices from 0 to n-1, preventing off-by-one errors. Python automatically raises IndexError on out-of-bounds access, and slicing operations handle out-of-bounds indices gracefully by returning partial results rather than crashing.
Security Checklist
- All loops use
< sizenot<= size - All malloc includes +1 for null terminator
- All strncpy uses size-1, manual null termination
- All array accesses validate
index < size - Bounds checks use
<not<= - Reverse loops handle unsigned correctly
- Tests cover edge cases (empty, size 1, exact fit)