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.

Types of errors detected

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

Buffer overflow

Runtime error:
Runtime error:
Detects heap buffer overflow.

Use-after-free

Runtime error:
Detects multiple frees of the same pointer.

Environment variables

Control bounds checking behavior with environment variables:
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:
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:

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.

Practical examples

Basic usage

Advanced usage with debugging

Integration with backtrace

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:
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)