Skip to content

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 < size or 0 <= index <= size-1
  • Loop should use i < size NOT i <= 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 < size not <= 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)

Additional Resources