Off-by-one Error

Description

Off-by-one Error is a logic error that occurs when a program miscalculates a boundary by exactly one unit. This typically happens when iterating through arrays (using <= instead of <), allocating buffers (forgetting the NULL terminator), or calculating string lengths. Off-by-one errors cause programs to read or write one byte beyond allocated buffer boundaries. While writing a single byte past a buffer may seem minor, it can corrupt critical data structures, overwrite heap metadata, or enable more severe exploitation through careful manipulation of that single byte.

Risk

Off-by-one errors are deceptively dangerous. While they only affect a single byte, that byte can corrupt adjacent stack canaries, heap chunk metadata, or critical control data. In heap allocations, a single-byte overflow can corrupt the size field of adjacent chunks, enabling heap exploitation techniques. On the stack, overwriting one byte of a saved frame pointer can redirect execution. Modern exploitation techniques have demonstrated that even single-byte overwrites can lead to full code execution. These vulnerabilities are common because they're easy to introduce and difficult to spot during code review.

Solution

Carefully review all loop boundaries and buffer size calculations. When allocating strings, always add 1 for the NULL terminator: malloc(strlen(s) + 1). Use < instead of <= for array iteration unless you specifically need to include the last element. Use strncpy and similar functions with size - 1 to leave room for NULL termination. Enable compiler warnings and use static analysis to detect off-by-one conditions. Test edge cases with maximum-length inputs. Consider memory-safe languages that perform bounds checking automatically.

Common Consequences

ImpactDetails
IntegrityScope: Memory Corruption

Single-byte overwrite can corrupt adjacent data, heap metadata, or stack canaries.
Access ControlScope: Code Execution

Heap metadata corruption or frame pointer overwrite can enable arbitrary code execution.
AvailabilityScope: Denial of Service

Memory corruption typically causes crashes and system instability.

Example Code + Solution Code

Vulnerable Code

#include <string.h>
#include <stdlib.h>

// VULNERABLE: Off-by-one in loop (fence post error)
void process_items(int *items, int count) {
    // Should be i < count, not i <= count
    for (int i = 0; i <= count; i++) {
        items[i] = 0;  // Writes one past end when i == count
    }
}

// VULNERABLE: Off-by-one in string allocation
char *copy_string(const char *src) {
    size_t len = strlen(src);

    // Forgets NULL terminator!
    char *dst = malloc(len);  // Should be len + 1

    strcpy(dst, src);  // Writes NULL one byte past allocation
    return dst;
}

// VULNERABLE: Off-by-one with strncpy
void copy_filename(char *dest, const char *src, size_t dest_size) {
    strncpy(dest, src, dest_size);  // May not NULL terminate!

    // If src is dest_size or longer, dest is not NULL terminated
    // Should be: strncpy(dest, src, dest_size - 1);
    //            dest[dest_size - 1] = '\0';
}

// VULNERABLE: MAX_NUM_WIDGETS off-by-one
#define MAX_WIDGETS 10
Widget *widgets[MAX_WIDGETS];

void init_widgets(int count) {
    if (count > MAX_WIDGETS) return;  // Should be >=

    for (int i = 0; i < count; i++) {
        widgets[i] = create_widget();
    }
    widgets[count] = NULL;  // Off-by-one if count == MAX_WIDGETS
}

Fixed Code

#include <string.h>
#include <stdlib.h>
#include <stdint.h>

// SAFE: Correct loop boundary
void process_items_safe(int *items, size_t count) {
    // Use < instead of <=
    for (size_t i = 0; i < count; i++) {
        items[i] = 0;
    }
}

// SAFE: Account for NULL terminator
char *copy_string_safe(const char *src) {
    size_t len = strlen(src);

    // Add 1 for NULL terminator
    char *dst = malloc(len + 1);
    if (!dst) return NULL;

    memcpy(dst, src, len);
    dst[len] = '\0';

    return dst;
}

// SAFE: Proper strncpy usage
void copy_filename_safe(char *dest, const char *src, size_t dest_size) {
    if (dest_size == 0) return;

    // Copy at most dest_size - 1 characters
    strncpy(dest, src, dest_size - 1);

    // Always NULL terminate
    dest[dest_size - 1] = '\0';
}

// SAFE: Correct bounds check
#define MAX_WIDGETS 10
Widget *widgets[MAX_WIDGETS + 1];  // Extra slot for NULL

void init_widgets_safe(size_t count) {
    // Correct comparison
    if (count >= MAX_WIDGETS) {
        count = MAX_WIDGETS;
    }

    for (size_t i = 0; i < count; i++) {
        widgets[i] = create_widget();
    }
    widgets[count] = NULL;  // Now within bounds
}

Exploited in the Wild

Novell iManager Stack Overflow (Novell, 2010)

CVE-2010-1929 and CVE-2010-1930 were off-by-one errors in Novell iManager that led to stack-based buffer overflow. Authenticated users could craft POST requests with overly long class names, enabling return address and SEH overwrite for arbitrary code execution.

OpenBSD ftp (OpenBSD, 2001)

A famous off-by-one error in OpenBSD's ftp client that could be exploited by malicious FTP servers, demonstrating that even security-focused projects can contain these subtle bugs.

Multiple Enterprise Products (IBM, QNAP, 2025)

Recent CWE-193 vulnerabilities affecting IBM Storage Defender, IBM API Connect, QNAP QTS/QuTS hero, and IBM QRadar SIEM with public exploits available.


Tools to test/exploit

  • AddressSanitizer — detects off-by-one reads and writes at runtime.

  • Valgrind — memory error detector that catches single-byte overflows.

  • AFL++ — fuzzer effective at triggering boundary conditions.


CVE Examples


References

  1. MITRE. "CWE-193: Off-by-one Error." https://cwe.mitre.org/data/definitions/193.html

  2. CERT. "STR31-C. Guarantee that storage for strings has sufficient space for character data and the null terminator." https://wiki.sei.cmu.edu/confluence/display/c/STR31-C