April 11, 20254 min

C Gives You Control, But at What Cost?

m
mayo

With C, there's no runtime, no GC. Just raw speed and control.

char* msg = malloc(100);
strcpy(msg, "hello");
free(msg);
printf("%s", msg); // ❌ Use after free

Common Pitfalls

Problem Code Risk
Use-after-free printf("%s", msg); Undefined behavior
Double free free(msg); free(msg); Heap corruption
Buffer overflow char buf[4]; strcpy(buf, "long"); Memory corruption
Memory leak malloc(...) with no free Slow crashes

Manual Memory Model

You must:

  • Allocate memory
  • Track ownership
  • Free it manually
  • Avoid accessing freed or invalid memory

Real-World Consequences

Heartbleed (OpenSSL)

// Simplified version of the bug
char* buffer = malloc(payload_length);
memcpy(buffer, payload, payload_length); // No bounds check!
// Attacker could read past buffer end

Impact: 500,000+ servers exposed private keys and passwords.

CVE-2021-44228 (Log4Shell equivalent in C)

char* user_input = get_user_data();
sprintf(log_buffer, "User: %s", user_input); // Buffer overflow possible

The Problem: No automatic bounds checking means attackers can:

  • Crash your program
  • Execute arbitrary code
  • Steal sensitive data

Memory Safety Statistics

Security vulnerabilities by category:

  • 70% of Microsoft security bugs: memory safety issues
  • 65% of Chrome vulnerabilities: memory corruption
  • ~50% of Android security patches: memory-related

The Developer Burden

Every Allocation Needs Tracking

typedef struct {
    char* data;
    size_t size;
} Buffer;

Buffer* create_buffer(size_t size) {
    Buffer* buf = malloc(sizeof(Buffer));
    if (!buf) return NULL;
    
    buf->data = malloc(size);
    if (!buf->data) {
        free(buf);  // Must remember to cleanup!
        return NULL;
    }
    
    buf->size = size;
    return buf;
}

void destroy_buffer(Buffer* buf) {
    if (buf) {
        free(buf->data);  // Must free in correct order
        free(buf);
    }
}

Mental overhead: Every function must consider:

  • Who owns this pointer?
  • When should it be freed?
  • Is it still valid?

Debugging Memory Issues

$ valgrind ./my_program
==12345== Invalid read of size 4
==12345==    at 0x40084B: main (test.c:10)
==12345==  Address 0x5204044 is 0 bytes after a block of size 4 alloc'd
==12345==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)

The problem: Bugs found at runtime, not compile time.

Performance vs Safety Trade-off

C Performance Characteristics

// Zero overhead - direct memory access
int sum = 0;
for (int i = 0; i < 1000000; i++) {
    sum += array[i];  // No bounds checking
}

Speed: ✅ Maximum performance
Safety: ❌ One mistake = security vulnerability

Memory Layout Control

// Precise control over memory layout
struct Point {
    float x, y, z;     // Exactly 12 bytes
} __attribute__((packed));

Point* points = malloc(1000 * sizeof(Point)); // Predictable allocation

Control: ✅ Complete memory layout control
Risk: ❌ Manual lifetime management

Tools Help, But Aren't Enough

Static Analysis

// clang-static-analyzer can catch some issues
char* ptr = malloc(10);
free(ptr);
*ptr = 'x';  // ⚠️ Warning: use after free

Runtime Detection

// AddressSanitizer (ASan) catches bugs at runtime
$ gcc -fsanitize=address program.c
$ ./a.out
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free

The Limitation

  • Static tools: Miss complex cases, false positives
  • Runtime tools: Only catch bugs that execute during testing
  • Code review: Human error, time-consuming

Why C Persists Despite Risks

Systems Programming Requirements

  • Operating systems: Need direct hardware access
  • Embedded systems: Memory constraints, no room for runtime
  • Performance-critical code: Every nanosecond matters

Legacy and Ecosystem

  • Massive codebases: Decades of C code in production
  • Library ecosystem: Most system libraries written in C
  • Developer knowledge: Generations of C programmers

The Fundamental Problem

C gives you two bad choices:

Option 1: Manual Memory Management

char* data = malloc(size);
// ... complex logic ...
if (error) {
    free(data);  // Must remember cleanup in ALL paths
    return -1;
}
// ... more logic ...
free(data);  // Easy to forget or double-free

Option 2: Garbage Collection

  • Add GC library like Boehm GC
  • Lose performance predictability
  • Still possible to have memory leaks

Key Takeaways

Predictable performance - no GC pauses
Complete control over memory layout
Minimal runtime overhead
Unsafe by default - one mistake = vulnerability
High mental burden for developers
Most security bugs stem from memory issues
Tools catch bugs after they're written, not before


The Challenge: We want C's performance without its danger.

The Question: What if the compiler could prevent memory bugs at compile time?

➡️ Next: "Rust's Ownership: Memory Safety Without Garbage Collection"