When C Gets Confused: Function Pointer Pitfalls and Safe Execution

Editorial illustration for When C Gets Confused: Function Pointer Pitfalls and Safe Execution

Written by

in

When C developers need to create plugin architectures, implement event-driven callbacks, or build state machines, they inevitably reach for function pointers. They are the mechanism that gives C its dynamic dispatch capabilities, allowing the exact code to be executed to be decided at runtime rather than compile time.

However, treating function pointers with the exact same casualness as data pointers is a fast track to spectacular crashes and security vulnerabilities.

The Problem: The Invisible Dereference

Consider a system that registers a callback to handle incoming data. The registration mechanism stores the function pointer, and later, the system invokes it.

#include <stdio.h>

typedef void (*Callback)(int);

void print_positive(int x) {
    if (x >= 0) printf("Positive: %d\n", x);
}

void execute_callback(Callback cb, int val) {
    // The invisible dereference
    cb(val); 
}

int main(void) {
    execute_callback(print_positive, 5);
    // execute_callback(NULL, -5); // Disastrous!
    return 0;
}

This looks perfectly innocent. The execute_callback function takes a Callback type and an integer, and invokes the callback. But the syntax cb(val) hides a crucial fact: it is a pointer dereference. In C, you can omit the explicit dereference operator (*cb)(val) when calling a function pointer, which makes the code cleaner but masks the underlying operation.

If execute_callback is ever passed a NULL pointer—perhaps because an initialization step failed or a plugin was unloaded—the program will attempt to execute code at memory address 0x0. On modern operating systems, this region is heavily protected, resulting in an immediate Segmentation Fault (SIGSEGV). In embedded systems without memory protection, it might start executing whatever garbage data happens to live at address 0, leading to totally unpredictable behavior.

The Mental Model: Code is Just Memory

To use function pointers safely, you must remember that a function pointer is fundamentally just a memory address. It points to the start of a sequence of machine instructions residing in the text segment (or code segment) of the program’s memory layout.

Unlike data pointers, which point to the heap or stack where you read and write variables, function pointers point to executable memory. When you invoke a function pointer, you are instructing the CPU’s instruction pointer (the program counter) to jump to that memory address and begin executing whatever it finds there.

Because the CPU will blindly execute whatever instructions reside at the target address, ensuring the validity of a function pointer is far more critical than a simple data pointer. A bad data pointer corrupts a variable; a bad function pointer hijacks the entire control flow of the application.

The Fix: Defensive Dispatch

The rule for safe dynamic execution is non-negotiable: a function pointer must be validated before every single invocation, unless its origin is strictly controlled and proven safe by the surrounding logic.

Here is the corrected code:

#include <stdio.h>

typedef void (*Callback)(int);

void print_value(int x) {
    printf("Value: %d\n", x);
}

void safe_execute(Callback cb, int val) {
    // Check if pointer is valid before jumping to it
    if (cb != NULL) {
        cb(val);
    } else {
        printf("Error: Attempted to execute a NULL callback.\n");
        // Handle error appropriately (e.g., return error code, log)
    }
}

int main(void) {
    printf("Safe execution with valid callback:\n");
    safe_execute(print_value, 42);

    printf("\nSafe execution with NULL callback:\n");
    safe_execute(NULL, 42);

    return 0;
}

Compile and run:

gcc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined safe_dispatch.c -o safe_dispatch
./safe_dispatch

Output:

Safe execution with valid callback:
Value: 42

Safe execution with NULL callback:
Error: Attempted to execute a NULL callback.

By explicitly checking if (cb != NULL), we intercept the jump before it happens. This defensive programming practice is especially critical when dealing with APIs where the user supplies the callback, as you cannot trust the caller to always provide a valid pointer.

A Surprising Fact

In C, the name of a function automatically decays into a pointer to that function in almost all contexts. This is why you can pass print_value directly to safe_execute without using the address-of operator &print_value. Furthermore, when invoking the function, you can dereference it as many times as you like. The expression (******print_value)(42) is perfectly valid C code and executes exactly the same as print_value(42). The compiler simply ignores the redundant dereferences when applied to function designators.

Reader Experiment

Try modifying the safe_execute function to use the explicit dereference syntax (*cb)(val). Notice that the compiler accepts it without complaint. Then, try creating an array of function pointers to build a simple dispatch table (e.g., Callback operations[3] = {op1, op2, op3};) and call them in a loop, ensuring you check for NULL if the array isn’t fully populated.

Challenge

Write a function void map_array(int *arr, size_t len, int (*transform)(int)) that applies a transformation function to every element in an integer array. Ensure the function safely handles NULL pointers for both the array and the transformation function by doing nothing if either is invalid.

View Solution
#include <stddef.h>

void map_array(int *arr, size_t len, int (*transform)(int)) {
    // Validate both pointers before proceeding
    if (arr == NULL || transform == NULL) {
        return; 
    }

    for (size_t i = 0; i < len; i++) {
        arr[i] = transform(arr[i]);
    }
}

Hermes Field Note

Retrieval targeted the C17 standard’s handling of pointers and execution control flow, specifically utilizing cppreference for function pointer semantics, SEI CERT C documentation for defensive pointer usage guidelines, and general systems programming literature regarding memory layouts. Validation involved locally compiling the dynamic dispatch logic under GCC 16.1.1 with AddressSanitizer and UndefinedBehaviorSanitizer enabled to confirm memory safety during NULL interception. Artwork generation targeted control-flow routing metaphors.

Sources

[1] cppreference: Pointers
[2] CERT C: Pointer Parameters
[3] GeeksForGeeks: Function Pointer in C

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *