User Safety: safe

Summary

A developer encountered unexpected behavior when compiling and running C code on macOS using clang. Instead of printing the file size and the file content as expected, the program printed the file size followed by a fragmented, partial representation of the file contents. The developer incorrectly attributed this to a compiler or OS issue rather than a fundamental memory safety violation in the source code.

Root Cause

The primary issue is a buffer overflow caused by a missing null terminator (\0).

  • Unterminated String: The program uses fread to read the file contents into a buffer allocated via malloc.
  • Lack of Null Termination: fread does not append a null terminator to the buffer. Unlike some high-level language string functions, fread simply fills the requested number of bytes.
  • Undefined Behavior in fprintf: The format specifier %s tells fprintf to read memory sequentially starting from the pointer address until it encounters a null byte (\0).
  • Memory Leakage: Because there was no null terminator at the end of the file content, fprintf continued reading past the end of the allocated buffer into adjacent memory addresses (the “garbage” or sensitive data found in the heap) until it happened to encounter a zero byte.

Why This Happens in Real Systems

This is a classic case of Undefined Behavior (UB). In a production environment, this manifests as:

  • Information Leakage: A process might accidentally print sensitive data (like pointers, passwords, or fragments of other files) stored in adjacent memory.
  • Segmentation Faults: If the fprintf function traverses memory into a protected segment that the process does not have permission to read, the OS will kill the process immediately.
  • Heisenbugs: The program may appear to work perfectly on one machine (where a zero byte happens to exist near the buffer) but crash or leak data on another (where memory is laid out differently).

Real-World Impact

  • Security Vulnerabilities: This is a textbook Heartbleed-style vulnerability, where an attacker can read arbitrary parts of a process’s memory by triggering a read past a buffer boundary.
  • Data Corruption: If the program were writing instead of reading, it would result in a Heap Buffer Overflow, potentially overwriting critical metadata and causing a crash.
  • Non-deterministic Failures: Debugging becomes a nightmare because the output changes based on the state of the heap at the time of execution.

Example or Code

#include 
#include 
#include 

typedef int32_t i32;

int main(int argc, char **argv) {
    FILE *file = fopen("Makefile", "rb");
    if (file == NULL) return 1;

    fseek(file, SEEK_END, SEEK_SET);
    i32 filesize = ftell(file);
    fseek(file, SEEK_SET, SEEK_SET);

    fprintf(stderr, "%d\n", filesize);

    // Allocate space for contents + 1 for the null terminator
    char *text = malloc(filesize + 1);
    if (text == NULL) {
        fclose(file);
        return 1;
    }

    // Read the data
    size_t bytesRead = fread(text, 1, filesize, file);

    // CRITICAL FIX: Manually null-terminate the string
    text[bytesRead] = '\0';

    fprintf(stderr, "%s", text);

    fclose(file);
    free(text);
    return 0;
}

How Senior Engineers Fix It

Senior engineers focus on defensive programming and invariant enforcement:

  • Explicit Termination: Always explicitly null-terminate buffers after performing bulk I/O operations.
  • Return Value Validation: Always check the return value of fread to ensure the number of bytes read matches the expected size.
  • Tooling Integration: Use AddressSanitizer (ASan) during development. Running the original code with -fsanitize=address would have immediately flagged the “read of size X at [address] inside buffer” error.
  • Static Analysis: Employ linters and static analyzers that detect unchecked return values and potential buffer overflows before the code reaches production.

Why Juniors Miss It

  • Assumption of “Magic” Functions: Juniors often assume that functions like fread or scanf automatically handle string termination like higher-level languages (Python, JavaScript).
  • The “It Works on My Machine” Fallacy: When the program prints some content instead of crashing, a junior may assume the code is correct but the “compiler is broken.”
  • Lack of Memory Model Knowledge: Juniors often view variables as abstract values rather than specific byte ranges in a physical memory layout. They don’t realize that %s is essentially a pointer-walking instruction that doesn’t know where your buffer ends.

Leave a Comment