Skip to main content
TCC can automatically generate memory and bound checks while allowing all C pointer operations. This feature can detect buffer overflows, invalid memory access, and other common memory errors at runtime, even when using non-patched libraries.
Bounds checking is activated with the -b command-line option. When enabled, TCC defines the __TCC_BCHECK__ macro.

How it works

The bounds checker instruments your code to track memory regions and validate all pointer accesses at runtime. It maintains a tree of valid memory regions and checks every pointer dereference against this tree.
# Compile with bounds checking
tcc -b -o program main.c

# Also enables debug info (-g is implied)
./program

Types of errors detected

TinyCC’s bounds checker can detect numerous memory safety violations:

Buffer overflow

char tab[10];
memset(tab, 0, 11);  // Error: writing 11 bytes to 10-byte buffer
Runtime error:
BCHECK: invalid pointer 0x7ffd1234abcd (size 11) is outside of the region
int tab[10];
int sum = 0;
for(int i = 0; i < 11; i++) {  // i goes to 10, out of bounds!
    sum += tab[i];
}
Runtime error:
BCHECK: invalid pointer 0x... (size 4) is outside of the region (0x...0x...)
int *tab = malloc(20 * sizeof(int));
for(int i = 0; i < 21; i++) {  // Accessing 21 elements, only 20 allocated
    sum += tab[i];
}
free(tab);
Detects heap buffer overflow.

Use-after-free

int *tab = malloc(20 * sizeof(int));
free(tab);
for(int i = 0; i < 20; i++) {
    sum += tab[i];  // Error: accessing freed memory
}
Runtime error:
BCHECK: freeing invalid region
int *tab = malloc(20 * sizeof(int));
free(tab);
free(tab);  // Error: double free
Detects multiple frees of the same pointer.

Environment variables

Control bounds checking behavior with environment variables:
export TCC_BOUNDS_WARN_POINTER_ADD=1
./program

# Warns when pointer arithmetic creates invalid pointers
# (even if they're not dereferenced)
TCC_BOUNDS_NEVER_FATAL should only be used for debugging. Running with invalid memory access can lead to crashes or data corruption.

Controlling bounds checking in code

You can enable/disable bounds checking at runtime using the API:
#ifdef __TCC_BCHECK__
extern void __bounds_checking(int x);
# define BOUNDS_CHECKING_OFF __bounds_checking(1)
# define BOUNDS_CHECKING_ON  __bounds_checking(-1)
#else
# define BOUNDS_CHECKING_OFF
# define BOUNDS_CHECKING_ON
#endif

void signal_handler(int sig) {
    // Disable checking in signal handler (required!)
    BOUNDS_CHECKING_OFF;
    
    // Handle signal
    fprintf(stderr, "Signal %d caught\n", sig);
    
    BOUNDS_CHECKING_ON;
}

void performance_critical_section() {
    // Temporarily disable checking for performance
    BOUNDS_CHECKING_OFF;
    
    // Fast unchecked code
    for (int i = 0; i < 1000000; i++) {
        process_data(i);
    }
    
    BOUNDS_CHECKING_ON;
}
The __bounds_checking(x) function adds x to a thread-local counter. When this counter is non-zero, bounds checking is disabled for that thread.

Platform support

Bounds checking is available on:
  • i386 (Linux and Windows)
  • x86_64 (Linux and Windows)
  • ARM
  • ARM64 (aarch64)
  • RISC-V 64
Bounds checking is not available on all platforms. Check your specific TCC build for support.

Performance and compatibility

Performance impact

  • Generated code is larger: Instrumentation adds checks before each pointer access
  • Slower execution: Runtime validation has overhead (typically 2-10x slower)
  • Memory overhead: Maintains tree of memory regions
Example overhead:
// Normal: ~0.1s for 1M iterations
// With -b: ~0.5-1.0s for 1M iterations

Compatibility notes

Important limitations:
  1. Shared libraries: The bounds checking code is not included in shared libraries. The main executable must always be compiled with -b.
  2. Signal handlers: Not compatible with bounds checking. The checker automatically disables checking in signal/sigaction handlers.
  3. fork() in multithreaded apps: Can cause issues. The bounds checking code fixes this for the child process, but be aware of potential problems.
  4. Locking: The checker uses internal locks. This is why signals and fork() have issues.

Checked code interoperability

Major advantages:
  • Pointer size unchanged: No modifications to pointer representation
  • Binary compatible: Checked and unchecked code can be mixed freely
  • Library compatible: Works with non-patched libraries
  • Supports all C pointer operations: Even obscure casts work correctly
When a pointer comes from unchecked code, it’s assumed to be valid.
// checked_main.c (compiled with -b)
#include <string.h>  // System library (unchecked)

int main() {
    char buf[10];
    
    // Calls unchecked system strcpy, but parameters are checked
    strcpy(buf, "hello");  // OK
    
    strcpy(buf, "this is way too long");  // Error detected!
    
    return 0;
}

Practical examples

Basic usage

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr = malloc(10 * sizeof(int));
    
    // This will be caught
    for (int i = 0; i <= 10; i++) {
        arr[i] = i;  // Error at i=10
    }
    
    free(arr);
    return 0;
}

Advanced usage with debugging

#include <stdio.h>
#include <stdlib.h>

#ifdef __TCC_BCHECK__
extern void __bounds_checking(int x);
#define BOUNDS_OFF __bounds_checking(1)
#define BOUNDS_ON  __bounds_checking(-1)
#else
#define BOUNDS_OFF
#define BOUNDS_ON
#endif

int main() {
    int *data = malloc(100 * sizeof(int));
    
    // Initialize with checking
    for (int i = 0; i < 100; i++) {
        data[i] = i;
    }
    
    // Verify bounds work
    printf("Accessing index 99: %d\n", data[99]);  // OK
    
    // This would fail:
    // printf("Accessing index 100: %d\n", data[100]);
    
    free(data);
    
    // This would also fail (use after free):
    // printf("After free: %d\n", data[0]);
    
    return 0;
}

Integration with backtrace

# Combine bounds checking with backtrace
tcc -b -bt -o program program.c

# When an error occurs, you get a full stack trace
./program
Use -bt[N] to display N callers in stack traces. When activated, __TCC_BACKTRACE__ is defined, and a function int tcc_backtrace(const char *fmt, ...) is available to manually trigger stack traces.

Implementation details

For those interested in how it works:

Memory region tracking

From lib/bcheck.c:
typedef struct tree_node Tree;
struct tree_node {
    Tree *left, *right;
    size_t start;           // Region start address
    size_t size;            // Region size
    unsigned char type;     // TCC_TYPE_MALLOC, etc.
    unsigned char is_invalid;  // True after free()
};
The checker maintains a splay tree of all valid memory regions:
  • Stack variables (local arrays)
  • Heap allocations (malloc, calloc, realloc)
  • Global/static variables
  • alloca() allocations

Checked functions

The following functions are automatically wrapped: Memory functions:
  • malloc, calloc, realloc, free
  • memalign (on supported platforms)
  • mmap, munmap (Unix)
String functions:
  • memcpy, memcmp, memmove, memset
  • strlen, strcpy, strncpy
  • strcmp, strncmp
  • strcat, strncat
  • strchr, strrchr
  • strdup
All checks are performed at runtime with minimal overhead, optimized using a splay tree data structure for fast lookups.

Best practices

  1. Development: Always use -b during development to catch memory errors early
  2. Testing: Run your test suite with bounds checking enabled
  3. Production: Consider disabling for performance (after thorough testing)
  4. Debugging: Combine with -g and -bt for maximum debug information
  5. Signal handlers: Always disable checking in signal handlers using __bounds_checking(1)
# Development build
tcc -b -g -bt -o myapp main.c

# Production build (no bounds checking)
tcc -O2 -o myapp main.c

Build docs developers (and LLMs) love