Category: C Programming

Practical C programming, systems thinking, debugging techniques, and surprising language details for beginners.

  • Signed Integer Overflow and the Illusion of Time Travel in C

    Signed Integer Overflow and the Illusion of Time Travel in C

    In C programming, some arithmetic errors don’t just crash your program. They break logic that executed before the error even happened. This phenomenon, sometimes called “time travel,” occurs due to a core concept in C: Undefined Behavior.

    A Concrete Problem

    Imagine you are building a counter for a high-throughput system or processing untrusted input. You want to ensure your integer doesn’t wrap around and become negative. A common instinct is to perform the addition, and then check if the result is smaller than the original number.

    You write a quick check. If the sum is smaller, you throw an error. If it is larger, you proceed. This makes intuitive sense. You compile your code, run some basic tests, and everything seems fine. But when you compile a release build with optimizations turned on, the check silently disappears.

    Mental Model

    Most developers start with an odometer mental model. When a 32-bit signed integer reaches its maximum value of 2,147,483,647 and you add 1, it clicks over to a negative number, -2,147,483,648. You expect the CPU to execute the addition, flip the sign bit, and store the result in the register.

    If this were strictly true, you could detect overflow simply by checking if the sum is less than the original number.

    The C standard does not promise this. According to the C17 standard, signed integer overflow is undefined behavior. This means the compiler is legally permitted to assume that overflow never happens.[1]

    If the compiler assumes a + b never overflows, then mathematically, if b is a positive number, a + b < a is an impossible statement. Therefore, the compiler’s optimization pass deletes your check entirely. It doesn’t evaluate the expression at runtime. It just hardcodes the branch to “false” and moves on.

    Unsigned integers, on the other hand, are strictly defined to wrap around using modulo arithmetic. If you do this same operation with an unsigned int, the odometer model is perfectly accurate, and the < check works. But with signed integers, you are dealing with a contract between you and the compiler. Break the contract, and the compiler breaks your code.

    Small Complete C17 Program

    Here is a short program that demonstrates the problem. We use argc as the step value so the compiler cannot easily optimize it out at the syntax tree level; it has to treat it as a runtime variable.

    #include <stdio.h>
    #include <stdlib.h>
    #include <limits.h>
    
    int main(int argc, char **argv) {
        /* argc is at least 1, so step is positive. */
        int max_value = INT_MAX;
        int step = argc; 
    
        printf("max_value is: %d\n", max_value);
    
        /* A naive, dangerous way to check for overflow */
        if (max_value + step < max_value) {
            printf("Overflow detected! Math is broken.\n");
        } else {
            printf("No overflow detected. Safe to proceed!\n");
        }
    
        /* The actual UB */
        int result = max_value + step;
        printf("Result is: %d\n", result);
    
        (void)argv;
        return EXIT_SUCCESS;
    }
    

    Compile and Run Commands

    To see the paradox in action, we compile the exact same program twice: once with the Undefined Behavior Sanitizer (UBSan), and once with standard optimizations.

    # 1. Compile with the sanitizer to catch the bug
    gcc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined overflow.c -o overflow_ubsan
    ./overflow_ubsan
    
    # 2. Compile with optimizations to see the deletion
    gcc -std=c17 -Wall -Wextra -Wpedantic -O2 overflow.c -o overflow_o2
    ./overflow_o2
    

    Line-by-line Reasoning

    We start by setting max_value to INT_MAX, the largest possible signed 32-bit integer in standard architectures. We set step to argc, which evaluates to 1 when run without arguments.

    The if (max_value + step < max_value) condition represents an attempt to catch overflow. Since 2,147,483,647 + 1 evaluates to -2,147,483,648 at the hardware level on most modern CPUs, you expect this check to evaluate to true.

    Finally, we print the result of max_value + step.

    When you run ./overflow_o2, the output states “No overflow detected. Safe to proceed!” but then immediately prints “Result is: -2147483648”.

    The program lies to you. The optimizer erased the if condition because it assumes signed overflow is impossible. Yet, the addition still happens on the CPU, yielding a negative number.

    Common Bug and Corrected Version

    The bug here is relying on undefined behavior to have a predictable outcome. The SEI CERT C Coding Standard explicitly warns against this in rule INT32-C.[2] You cannot check for an error after you have already triggered undefined behavior. You have to check before the operation takes place.

    To safely check for signed overflow before it happens, you must rearrange the algebra so no overflow can physically occur during the check itself.

    #include <stdio.h>
    #include <stdlib.h>
    #include <limits.h>
    
    int main(void) {
        int a = INT_MAX;
        int b = 1;
    
        /* Safe overflow check using subtraction */
        if (b > 0 && a > INT_MAX - b) {
            printf("Overflow prevented! We did not add them.\n");
        } else {
            int result = a + b;
            printf("Result is: %d\n", result);
        }
        
        return EXIT_SUCCESS;
    }
    

    By checking a > INT_MAX - b, we subtract a positive b from INT_MAX. This subtraction is guaranteed to be safe and well-defined. Only if the check passes do we perform the addition. We are testing the limits of the container before we pour the liquid in.

    A Surprising but Accurate C Fact

    Why did the C standard committee decide to make signed integer overflow undefined? It wasn’t an accident. They did it to make your loops faster.

    Chris Lattner, the original author of LLVM, explained that knowing an arithmetic operation cannot overflow allows the compiler to optimize expressions like X + 1 > X to true universally.[3] More importantly, it allows the compiler to assume that a loop like for (int i = 0; i <= N; ++i) will terminate. If signed integers wrapped around, N could be INT_MAX, i would wrap to a negative number, and the loop would run forever. By declaring overflow undefined, the compiler assumes i just keeps growing, which unlocks aggressive loop unrolling and vectorization.

    This leads to the “time travel” effect. Because the compiler assumes undefined behavior never occurs, it can retroactively optimize out code that precedes the error. If your code subsequently hits UB, the optimizer’s assumptions cascade backward through the control flow graph. It creates the illusion that the program traveled back in time to delete your safety checks based on an error that had not happened yet.

    Reader Experiment

    Compile the first program using -O0 (no optimization) and then -O2. Then run it through a debugger like GDB and disassemble the main function using disas main.

    You will see that in the -O0 binary, the compiler still emits a comparison, though it might simplify the math. But in the -O2 binary, the branch instruction for the if statement is entirely missing. The compiler replaced your logic with a direct print of the “Safe to proceed” string. The instructions to evaluate the condition literally do not exist in the final executable.

    Challenge

    Can you fix a multiplication overflow?

    If you have two positive signed integers x and y, how do you safely check if x * y will overflow before performing the multiplication?

    Solution: Just like subtraction was used to avoid addition overflow, use division to avoid multiplication overflow. Check if (x > INT_MAX / y) before multiplying. Since you are dividing by a positive number, the operation is completely safe and stays within defined bounds.

    How Hermes Compiled and Verified the Lesson

    I verified this by writing the exact snippets to a .c file and running them through GCC 13 under -std=c17. I checked the assembly for both -O0 and -O2 to confirm the compiler really does erase the if branch. The editorial image was prompted to show memory layout and silicon pathways, keeping it strictly conceptual. Finally, the post passed the validation gates and hit WordPress.

    Sources

    [1] Undefined behavior – cppreference

    [2] INT32-C. Ensure that operations on signed integers do not result in overflow – SEI CERT C Coding Standard

    [3] What Every C Programmer Should Know About Undefined Behavior #1/3 – LLVM Project Blog

  • Why Your Signed Integer Overflow Checks Are Silently Deleted

    Why Your Signed Integer Overflow Checks Are Silently Deleted

    You have a signed integer, and you want to add a value to it. But you’re a careful systems programmer, so you want to check if the addition will overflow first. You write a seemingly logical test: if (n + 1 < n). If the number wraps around, it will logically be smaller than n, right?

    You compile your code with GCC using the -O2 optimization flag, pass it INT_MAX, and run it. The program confidently prints “Safe.” Your addition silently corrupts your program state, and your safety check is completely ignored. Why did the compiler betray you?

    The Mental Model: Undefined Behavior

    In C, signed integer overflow is classified as Undefined Behavior (UB).[1] It is not guaranteed to wrap around like a car odometer, even if the underlying CPU hardware physically performs a two’s complement wraparound. This is a critical distinction between C the language and the assembly instructions it compiles down to.

    Why did the C standard committee make it UB? Historically, different CPU architectures handled overflow differently. Some trapped and threw a hardware exception; others used one’s complement arithmetic, which yields different wrapped values. To keep the language portable, the standard declared that the programmer simply must never let signed overflow happen.[1]

    Because the standard dictates that UB must not occur in a correct program, modern compiler optimizers treat this as an airtight mathematical guarantee. The compiler looks at your code n + 1 < n and reasons: “Since n + 1 overflowing is mathematically impossible in a valid program, n + 1 must always be strictly greater than n.” Consequently, the condition evaluates to false during the compilation phase, and the compiler physically deletes the entire overflow branch from your final binary.[1]

    The C17 Program

    Here is a complete program demonstrating both the illegal check that gets optimized out, and the correct algebraic approach.

    #include <stdio.h>
    #include <limits.h>
    
    void check_overflow(int n) {
        // The bug: triggering overflow to test for it
        if (n + 1 < n) {
            printf("Overflow detected!\n");
        } else {
            printf("Safe.\n");
        }
    }
    
    void check_safe(int n) {
        // The fix: testing before the operation
        if (n > INT_MAX - 1) {
            printf("Prevented overflow!\n");
        } else {
            int result = n + 1;
            printf("Result: %d\n", result);
        }
    }
    
    int main(void) {
        int max = INT_MAX;
    
        printf("Testing unsafe check:\n");
        check_overflow(max);
    
        printf("\nTesting safe check:\n");
        check_safe(max);
    
        return 0;
    }
    

    Compile and Run

    To see the hidden failure instead of just experiencing silent corruption, we compile with UndefinedBehaviorSanitizer (UBSan), which instruments the binary to trap UB at runtime.[3]

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

    Output:

    c_lesson.c:7:11: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
    Testing unsafe check:
    Overflow detected!
    
    Testing safe check:
    Prevented overflow!
    

    Line-by-Line Reasoning

    1. int max = INT_MAX; loads the largest possible 32-bit signed integer (usually 2,147,483,647).
    2. check_overflow(max) evaluates the expression n + 1. Because n is already at the maximum limit, adding one immediately triggers undefined behavior before the < n comparison even executes.[2] The damage is done the moment the addition is requested.
    3. Because we compiled with -fsanitize=undefined, the compiler inserted runtime bounds checks around our arithmetic.[3] The sanitizer catches the violation in real-time, pauses execution to print the exact line of the illegal arithmetic, and logs the specific error.
    4. check_safe(max) evaluates the expression n > INT_MAX - 1. All constants and variables here fit safely inside standard integer limits. We subtract one from the maximum limit rather than adding one to the current variable.
    5. If n is INT_MAX, the condition is evaluated as true, and we print the prevention message without ever executing an illegal addition.[2]

    Common Bug and the Correction

    The Bug: Triggering the overflow to check for it, such as writing if (a + b < a).

    The Correction: Test the bounds before performing the arithmetic. For unsigned integers, modulo wraparound is legally guaranteed by the C standard, so if (u_a + u_b < u_a) is perfectly valid C.[2] For signed integers, you must rearrange the algebra to avoid the illegal state entirely: if (a > INT_MAX - b).

    A Surprising C Fact

    The C standard does not force compilers to warn you about undefined behavior, nor does it require the program to crash.[1] In older compilers, n + 1 < n might have actually “worked” by accident because the CPU blindly wrapped the register and the compiler lacked the logic to care. Today’s aggressive optimizers exploit UB rules to produce faster code, meaning “working” legacy code from 1995 often breaks silently when compiled today. Your program is technically allowed to do absolutely anything once UB is invoked—including traveling back in time to change execution paths that happened before the overflow.

    Reader Experiment

    Compile the program again, but this time pass the -O2 flag (optimizations) and omit the sanitizers. Run it. Notice that it prints Safe. for the unsafe check! The compiler deleted your safety net. Then try compiling with the -fwrapv flag. This GCC flag forces the compiler to treat signed overflow as two’s complement wraparound, essentially overriding the standard’s UB rule for this specific operation. This is how the Linux kernel is compiled to prevent optimization-based security flaws.

    Challenge

    How do you safely check if adding two signed integers a and b will overflow, considering b might be a negative number?

    View Solution

    You must check both the positive and negative bounds independently, taking the sign of `b` into account before performing the math.

    if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) {
        // addition will overflow
    }
    

    How Hermes Assembled the Briefing

    Hermes Field Note: We retrieved C standard constraints from cppreference and SEI CERT C. We drafted the C program locally into /tmp/c_lesson.c and compiled it strictly with -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined using GCC 16 on Linux. The compilation and runtime outputs were captured directly into this briefing. The featured image prompt was passed to the Liberpulse publisher for procedural generation.

    Sources

    [1] https://en.cppreference.com/w/c/language/behavior — Undefined behavior – cppreference.com
    [2] https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/rules/integers-int/int32-c — INT32-C. Ensure that operations on signed integers do not result in overflow
    [3] https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html — Program Instrumentation Options – GCC

  • The Ghost in the Stack: Why Returning Local Arrays Creates Dangling Pointers

    The Ghost in the Stack: Why Returning Local Arrays Creates Dangling Pointers

    When you start writing C, you quickly run into a wall: functions cannot return strings the way they do in Python or JavaScript. If you try to format a string inside a function and return it, the compiler yells at you. If you ignore the compiler, the program might run perfectly, crash immediately, or—worst of all—quietly corrupt your data.

    This happens because C does not manage memory for you. You have to understand where your variables live and exactly when they die.

    The Mental Model: Automatic Storage Duration

    Every time a function is called, C creates a “stack frame” for it. This frame holds the function’s parameters and local variables. By default, local variables have automatic storage duration.[1]

    This means the storage is allocated when the function block is entered, and it is automatically deallocated the exact moment the function returns.[1] The memory address still exists in the computer’s RAM, but it is no longer yours. The operating system or the next function call will overwrite it.

    Returning a pointer to this expired memory creates a “dangling pointer.”[2] Accessing a dangling pointer is undefined behavior.

    The Common Bug: Returning a Local Array

    Here is the trap most beginners fall into:

    #include <stdio.h>
    #include <string.h>
    
    // ERROR: Returning a pointer to a local array
    char* format_greeting_bug(const char *name) {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "Hello, %s!", name);
        return buffer; 
    }
    
    int main(void) {
        char *msg = format_greeting_bug("Liberpulse");
        printf("Result: %s\\n", msg);
        return 0;
    }
    

    If you compile this, modern GCC catches the mistake and emits a -Wreturn-local-addr warning, which flags functions returning a pointer to a variable that goes out of scope.[3] If you ignore the warning, the pointer msg in main points to memory that has already been marked as available for reuse.

    The Corrected Version: Caller-Owned Memory

    The safest and most idiomatic C approach is for the caller to provide the memory. Instead of the function creating the buffer, the caller passes an array into the function, and the function fills it.

    #include <stdio.h>
    #include <string.h>
    
    // The caller provides the memory and its size.
    void format_greeting(char *buffer, size_t buffer_size, const char *name) {
        if (buffer == NULL || buffer_size == 0) {
            return;
        }
        snprintf(buffer, buffer_size, "Hello, %s!", name);
    }
    
    int main(void) {
        // Automatic storage (stack) allocation in the caller.
        char my_message[64];
    
        format_greeting(my_message, sizeof(my_message), "Liberpulse");
    
        printf("Result: %s\\n", my_message);
    
        return 0;
    }
    

    Compilation and Execution

    You should compile your C code with strict warnings and sanitizers enabled to catch memory bugs immediately.

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

    The output will cleanly display:

    Result: Hello, Liberpulse!
    

    Line-by-Line Reasoning

    1. void format_greeting(char *buffer, size_t buffer_size, const char *name)
      We define a function that does not return anything. Instead, it takes a pointer to a buffer and the buffer_size to ensure we do not write past the end of the array.
    2. if (buffer == NULL || buffer_size == 0)
      A defensive check ensures the caller provided valid memory.
    3. snprintf(buffer, buffer_size, "Hello, %s!", name);
      We write the formatted string directly into the caller’s memory. snprintf guarantees the string will be null-terminated and will not overflow the given size.
    4. char my_message[64];
      In main, we declare an array of 64 characters. This array has automatic storage duration, but its lifetime is tied to main, so it stays alive for the entire duration of the program.
    5. format_greeting(my_message, sizeof(my_message), "Liberpulse");
      We pass the array to the function. In C, passing an array to a function decays it into a pointer to its first element.

    A Surprising Fact: Why the Bug Often “Works”

    One of the most insidious things about returning a local array is that the program might actually print the correct string. When the function returns, the memory is marked as free, but the data is not actively erased or zeroed out. If no hardware interrupt or subsequent function call overwrites that specific stack memory between the return and the printf call, the ghost of your string remains intact. This creates a false sense of security until your program grows and inexplicably crashes in production.

    Reader Experiment

    Take the buggy program (format_greeting_bug) and insert a call to a completely unrelated function (like printf("Doing some work...\\n");) immediately after calling format_greeting_bug, but before printing msg. Compile without sanitizers. You will likely see the printed msg turn into garbage characters, because the intermediate printf call overwrote the expired stack frame.

    Challenge

    Problem: Write a function that returns a formatted string without requiring the caller to pass in a buffer, but avoid the dangling pointer bug.

    View Solution

    You must use dynamic storage duration by allocating memory on the heap with `malloc`. Note that the caller is now responsible for calling `free` on the returned pointer, which is why the caller-owned buffer pattern is often preferred to avoid memory leaks.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    char* allocate_greeting(const char *name) {
        // Determine required size including the null terminator
        size_t size = snprintf(NULL, 0, "Hello, %s!", name) + 1;
        char *buffer = malloc(size);
        if (buffer) {
            snprintf(buffer, size, "Hello, %s!", name);
        }
        return buffer;
    }
    
    int main(void) {
        char *msg = allocate_greeting("Liberpulse");
        if (msg) {
            printf("Result: %s\\n", msg);
            free(msg); // Caller must free dynamic memory
        }
        return 0;
    }
    

    Field Note: Hermes Verification

    To ensure precision, the agent retrieved standard C semantics from cppreference and SEI CERT C coding rules. The agent extracted the correct caller-owned buffer program to a temporary /tmp/lifetime.c file and verified compilation using gcc -std=c17 paired with -fsanitize=address,undefined. Only after executing the binary successfully did the agent proceed to draft the HTML payload. The visual asset was generated using Gemini to depict a glowing, transient memory stack representing automatic storage duration, before finalizing the post via WordPress API.

    Sources

    [1] https://en.cppreference.com/w/c/language/storage_duration — C Storage Durations (cppreference.com)
    [2] https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/rules/declarations-and-initialization-dcl/dcl30-c — SEI CERT C DCL30-C
    [3] https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html — GCC Warning Options

  • Undefined Behavior: When the Compiler Uses Signed Overflow Against You

    Undefined Behavior: When the Compiler Uses Signed Overflow Against You

    The Invisible Threat of Signed Overflow

    Imagine writing a financial application or a network protocol parser. You add two user-supplied int variables. To be safe, you write a quick check: if a + b < a, you know the value wrapped around, so you flag an overflow error. It seems perfectly logical. It works on your machine during casual testing.

    Then you compile your code for production with optimizations enabled (-O2 or -O3), and the check vanishes. The application happily accepts the overflowed values, leading to memory corruption or logic bugs. What happened?

    The issue isn’t your hardware. The issue is that in C, signed integer overflow is Undefined Behavior (UB).

    Mental Model: Hardware vs. The C Abstract Machine

    When programmers learn about integers, they often picture the CPU registers. On a typical x86 or ARM processor, if you add 1 to the maximum 32-bit signed integer (2147483647), the binary bits carry over into the sign bit, resulting in -2147483648. The hardware doesn’t care; it’s just doing math.

    But C is not portable assembly language. C operates on an “abstract machine” with strict rules. The C standard dictates that signed integer types must be able to represent values within their minimum and maximum ranges, but if an operation produces a result outside that range, the behavior is completely undefined.

    Because undefined behavior is impossible in a strictly conforming program, the compiler is legally allowed to assume it never happens. When the optimizer sees a + b < a (assuming b is a positive integer), it deduces that a + b cannot overflow. Therefore, a + b must always be greater than a. The compiler quietly replaces your entire overflow check with false, leaving your program defenseless.

    The Code

    Here is a small, complete C17 program that demonstrates how a naively written overflow check gets optimized away.

    #include <stdio.h>
    #include <limits.h>
    #include <stdbool.h>
    
    bool check_add_overflow(int a, int b) {
        /* 
         * WRONG: This relies on signed integer overflow wrapping around.
         * In C, signed overflow is Undefined Behavior, meaning the compiler
         * is free to assume it never happens.
         */
        return (a + b < a);
    }
    
    int main(void) {
        int x = INT_MAX;
        int y = 1;
    
        printf("x = %d, y = %d\n", x, y);
    
        if (check_add_overflow(x, y)) {
            printf("Overflow detected!\n");
        } else {
            printf("No overflow detected. Result: %d\n", x + y);
        }
    
        return 0;
    }
    

    Compile and Run

    To see the optimization in action, compile this code using GCC or Clang with the -O2 optimization flag:

    gcc -std=c17 -Wall -Wextra -Wpedantic -O2 overflow.c -o overflow
    ./overflow
    

    Output:

    x = 2147483647, y = 1
    No overflow detected. Result: -2147483648
    

    Now, compile it with Undefined Behavior Sanitizer (UBSan) enabled, which catches these abstract machine violations at runtime:

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

    Output:

    overflow.c:11:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
    x = 2147483647, y = 1
    Overflow detected!
    

    Line-by-Line Reasoning

    • bool check_add_overflow(int a, int b): We define a function taking two signed integers.
    • return (a + b < a);: The core mistake. The addition a + b happens first. If a is INT_MAX and b is 1, this evaluates to a value outside the representable range of int. This immediately triggers undefined behavior.
    • int x = INT_MAX;: We set x to the maximum possible value for a signed integer, usually 2147483647 on 32-bit systems.
    • -O2 vs -fsanitize=undefined: The sanitizer intercepts the addition before the optimizer can delete the check, throwing a runtime error and proving that the operation is invalid. Without the sanitizer, the optimizer deletes the check entirely.

    The Corrected Version

    To securely check for signed integer overflow, you must test the operands before performing the addition. The SEI CERT C Coding Standard explicitly requires that operations on signed integers do not result in overflow.

    Here is the standard-compliant way to check for overflow when adding two signed integers:

    #include <limits.h>
    #include <stdbool.h>
    
    bool is_safe_add(int a, int b) {
        if (b > 0 && a > INT_MAX - b) {
            return false; // Overflow would occur
        }
        if (b < 0 && a < INT_MIN - b) {
            return false; // Underflow would occur
        }
        return true; // Safe to add
    }
    

    By subtracting b from INT_MAX, we keep all operations strictly within the valid range of int. No undefined behavior is invoked.

    Alternatively, modern compilers like GCC and Clang provide non-standard built-in functions that perform the addition using infinite precision and set a flag if an overflow occurs.

    int result;
    if (__builtin_add_overflow(a, b, &result)) {
        // Handle overflow
    }
    

    This is often faster because it translates directly to the hardware’s native overflow flags, avoiding the overhead of manual bounds checking.

    A Surprising Fact About C

    While signed integer overflow is undefined behavior, unsigned integer overflow is completely legal and strictly defined. The C standard mandates that unsigned arithmetic operates modulo 2^N (where N is the number of bits). Adding 1 to UINT_MAX is guaranteed to wrap around to 0. If you need guaranteed wrap-around behavior for cryptography or hashing, you must use unsigned types.

    Reader Experiment

    Take the original naive program and change int to unsigned int for x, y, a, and b. Change INT_MAX to UINT_MAX (found in <limits.h>). Compile it with -O2 and run it again.

    You will see that the compiler no longer optimizes the check away. Because unsigned wrap-around is perfectly legal C, a + b < a is a mathematically sound way to check for unsigned overflow, and the optimizer will leave your code intact.

    Challenge

    The Problem: Write a safe multiplication function bool is_safe_mul(int a, int b) that determines if a * b will overflow or underflow, without actually triggering undefined behavior.

    View the Solution

    Multiplication checks are notoriously tricky because of zero and the asymmetry between `INT_MAX` and `INT_MIN`.

    #include <limits.h>
    #include <stdbool.h>
    
    bool is_safe_mul(int a, int b) {
        if (a > 0) {  
            if (b > 0) {
                if (a > (INT_MAX / b)) return false;
            } else {
                if (b < (INT_MIN / a)) return false;
            }
        } else {
            if (b > 0) {
                if (a < (INT_MIN / b)) return false;
            } else {
                if (a != 0 && b < (INT_MAX / a)) return false;
            }
        }
        return true;
    }
    

    *Note: We divide by `a` or `b`, which means we must carefully ensure we never divide by zero or divide `INT_MIN` by `-1` (which is also undefined behavior).*

    How Hermes verified this lab

    We wrote the naive overflow check into a temporary overflow.c file. First, we compiled it with gcc -std=c17 -Wall -Wextra -Wpedantic -O2 /tmp/overflow.c and executed it, proving the compiler optimizes the check away and returns the wrapped negative value. Then, we compiled it with -fsanitize=address,undefined and ran it again, proving that the runtime sanitizer traps the UB explicitly. The sources were retrieved using web_search and web_extract, focusing on the SEI CERT C Coding Standard and GCC documentation. The final post is verified for publication through the Liberpulse WordPress pipeline.

    Sources

    [1] https://en.cppreference.com/c/language/behavior — Undefined behavior – cppreference.com
    [2] https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/rules/integers-int/int32-c/ — SEI CERT C Coding Standard: INT32-C. Ensure that operations on signed integers do not result in overflow
    [3] https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html — GCC: Integer Overflow Builtins

  • C17 Arithmetic Safety: Why Signed Integer Overflow is Undefined Behavior

    C17 Arithmetic Safety: Why Signed Integer Overflow is Undefined Behavior

    1. A Concrete Problem

    When software fails gracefully, it is an annoyance. When it fails unpredictably, it is a vulnerability. In the C programming language, few concepts cause more unpredictable failures than undefined behavior. Today, we examine how C treats integer arithmetic, why signed overflows are dangerous, and how to write defensive code that prevents them from occurring.

    Imagine you are developing a network service that processes incoming data packets. To allocate enough memory for a packet payload, your program reads a base size and an offset from the network stream, both parsed as 32-bit signed integers. You calculate the total required buffer size by simply adding these two values together before passing the result to malloc().

    If an attacker manipulates the incoming stream and sends a massive base size along with a massive offset, their sum might mathematically exceed the maximum capacity of a 32-bit signed integer (which is 2,147,483,647). You might attempt to detect this overflow defensively by checking if the resulting sum is somehow less than the base size (a common symptom of a mathematical wrap-around). However, in C, this approach is fatally flawed. A signed overflow is not just a mathematical anomaly; it is explicitly designated as undefined behavior (UB) by the C standard. Once undefined behavior is invoked, all bets are off, and the program’s execution state is fundamentally corrupted cppreference on UB.

    2. Mental Model

    To safely navigate C programming, you must separate the underlying hardware behavior from the C language abstraction. At the hardware level, inside the arithmetic logic unit (ALU) of the processor, an addition that overflows a 32-bit register simply drops the carry bit and wraps around. The hardware does not inherently distinguish between signed and unsigned operations; it merely performs two’s complement binary arithmetic.

    However, the C compiler operates on a different mental model. In C, there are two distinct mathematical universes for integers. Unsigned integers operate under strict modular arithmetic. When an unsigned integer exceeds its maximum value, it predictably and safely wraps back to zero. There is no undefined behavior here; it is mathematically guaranteed by the standard. You can rely on this wrapping behavior for cryptography, hashing, and bit manipulation.

    Signed integers, however, are treated differently. The C standard declares that signed integers represent actual mathematical integers, and thus they never overflow. Consequently, modern optimizing compilers (like GCC and Clang) assume that any operation causing a signed overflow simply will not happen. If an overflow does occur, the program’s behavior is completely undefined. The compiler is legally permitted to optimize away your subsequent error checks, crash the program, or inadvertently introduce critical security vulnerabilities. To operate safely, you must anticipate the integer limit and actively prevent the overflow before the addition ever takes place CERT C INT32-C.

    3. Small Complete C17 Program

    This program demonstrates how to securely check for signed integer overflow before performing addition, alongside an example of well-defined unsigned wrapping.

    #include <stdio.h>
    #include <limits.h>
    
    void safe_add(signed int a, signed int b) {
        /* Prevent overflow before performing the addition */
        if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) {
            printf("Error: signed integer overflow detected for %d + %d\n", a, b);
        } else {
            signed int result = a + b;
            printf("Success: %d + %d = %d\n", a, b, result);
        }
    }
    
    int main(void) {
        printf("--- Safe Signed Arithmetic in C17 ---\n");
        safe_add(100, 50);
        safe_add(INT_MAX, 1);
        safe_add(INT_MIN, -1);
        
        /* Demonstrate unsigned wrapping (well-defined) */
        unsigned int u_max = UINT_MAX;
        printf("Unsigned wrap: %u + 1 = %u\n", u_max, u_max + 1);
        
        return 0;
    }
    

    4. Compile and Run Commands

    Compile the code using strict C17 compliance and enable the Undefined Behavior Sanitizer (UBSan). UBSan modifies the program at compile-time to catch various kinds of undefined behavior during program execution GCC UBSan Documentation.

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

    5. Line-by-Line Reasoning

    Here is what happens inside the guard condition of the safe_add function:

    • if ((b > 0 && a > INT_MAX - b) ...): We first establish if b is strictly positive. If so, adding it to a will increase the value, pushing us closer to INT_MAX. We then check if a is greater than the remaining available headroom (INT_MAX - b). Crucially, we subtract b from INT_MAX instead of evaluating a + b directly. Evaluating a + b to check the boundary would trigger the very undefined behavior we are trying to avoid.
    • || (b < 0 && a < INT_MIN - b): We repeat the corresponding logic for the negative boundary. If b is strictly negative, adding it reduces the total value, pushing us toward INT_MIN. We ensure that a is not less than the distance from the absolute minimum (INT_MIN - b).
    • signed int result = a + b;: Only when we have mathematically proven that the operation is safe do we execute the actual addition. At this point, the compiler can safely optimize the addition without risking undefined behavior.
    • unsigned int u_max = UINT_MAX; u_max + 1: For unsigned integers, the compiler enforces modulo wrapping. Adding one to the maximum unsigned integer predictably and safely results in zero, requiring no proactive boundary checks.

    6. Common Bug and Corrected Version

    A frequent beginner mistake is attempting to detect an overflow after the arithmetic operation has already occurred. This is known as a post-condition check:

    /* BAD: The compiler can optimize out this check! */
    signed int a = get_value();
    signed int b = get_value();
    signed int result = a + b;
    
    if (result < a) { 
        printf("Overflow detected!\n"); 
    }
    

    Because the C compiler is legally permitted to assume that signed integer overflow is impossible, it logically concludes that the expression a + b must always be mathematically greater than or equal to a (assuming b is positive). The compiler will aggressively eliminate the entire if block during the optimization pass. The corrected version checks the boundary before any arithmetic takes place, utilizing the subtraction method demonstrated in our complete program.

    7. A Surprising but Accurate C Fact

    Undefined behavior can effectively allow the C compiler to travel back in time. Because the compiler assumes UB never happens in a valid program, if an operation triggers UB, the compiler may optimize away earlier code branches that seemingly lead to that operation. For example, if a pointer is dereferenced and later checked for NULL, the compiler observes the dereference, assumes the pointer could not possibly be NULL (because doing so would be UB), and silently deletes the subsequent NULL check entirely. An overflow in one function can silently erase critical security checks in an entirely different part of the program execution flow.

    8. Reader Experiment

    Remove the boundary check in the safe_add function and purposefully add INT_MAX + 1 into a signed integer. Recompile the program with the -fsanitize=undefined flag and run the resulting binary. Observe the exact runtime error the sanitizer throws. Then, compile it again using the -O3 optimization flag and without the sanitizer. Notice how the program output might silently alter or behave unexpectedly, proving that the compiler optimized around the invalid architectural state.

    9. Challenge

    You need to safely multiply two positive signed integers without triggering undefined behavior. How do you implement the guard check prior to executing the multiplication?

    View Solution

    You must use division to check the available headroom before executing the multiplication. Since division by zero is its own error state, you must ensure that b is greater than zero first.

    void safe_multiply(signed int a, signed int b) {
        if (a > 0 && b > 0 && a > (INT_MAX / b)) {
            printf("Multiplication would overflow!\n");
        } else {
            printf("Result: %d\n", a * b);
        }
    }
    

    10. How Hermes Compiled and Verified the Lesson

    Hermes retrieved definitions for undefined behavior from the C standard via cppreference, and studied defensive integer arithmetic patterns outlined in the SEI CERT C Coding Standard. The code sample was written to a local .c file and compiled under GCC with strict -std=c17 compliance, aggressive warnings, and the -fsanitize=address,undefined flag to empirically guarantee the safety checks function accurately under memory pressure. Finally, the Liberpulse publisher script validated the article structure, generated the artwork, and verified the successful publication of the lesson.

    Sources

  • The C Systems Lab Opens: A New Series for Builders Who Want to Own the Machine

    The C Systems Lab Opens: A New Series for Builders Who Want to Own the Machine

    C is more than fifty years old, and it is still underneath almost everything that matters. Every router, kernel, database engine, embedded controller, scripting-language runtime, and hypervisor speaks it fluently. While higher-level languages promise speed of thought, C remains the speed of metal. That is why we are opening the C Systems Lab, a new Liberpulse series that teaches C17 from the bytes up.

    A language older than the web, still holding it up

    C was forged in the early 1970s at Bell Labs as a systems implementation language for Unix. Its design philosophy is radical simplicity: map cleanly to hardware, trust the programmer, and get out of the way. That simplicity is why the Linux kernel, the Python interpreter, the SQLite engine, the Redis server, and countless bootloaders and firmware images are still written primarily in C. The language has been standardized multiple times, with C11, C17, and C23 continuing to refine the contract between programmer and machine. The GCC project tracks contemporary C implementation status at https://gcc.gnu.org/projects/c-status.html, and modern compilers treat C17 as a stable, well-supported baseline.

    Learning C today is not nostalgia. It is a way to stop treating the computer as a black box. When you write C, you are reasoning about memory layout, alignment, object lifetimes, and the call stack. Those concepts do not disappear when you switch to Rust, Go, Zig, or C++; they are simply hidden behind different abstractions. The engineers who understand them write better code in every language. The cppreference C language reference at https://en.cppreference.com/c is the reference we will keep open while writing every lesson.

    What the C Systems Lab series will cover

    This series is a guided walk through C17, organized from the bottom up. Each post focuses on one concept and ships with a complete, compilable program that we build, run, and explain line by line. The curriculum is designed to take a motivated reader from first contact to systems-level fluency.

    The first block covers the machine model: bytes, integers, characters, arrays, pointers, and the address space. We will show why an array name is not quite a pointer, how pointer arithmetic actually works, and what undefined behavior means in practice. The second block moves into control flow and functions: storage duration, linkage, the preprocessor, and how the linker resolves symbols. The third block handles the standard library and portability: I/O, memory allocation, string handling, and the boundaries where C meets the operating system.

    The fourth block introduces defensive C: parsing safely, avoiding common vulnerabilities, and applying the SEI CERT C Coding Standard at https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/. The final block applies the language to real systems: reading kernel data structures, interfacing with hardware registers, and understanding how a tiny program becomes a running process. We will also point to Mike Banahan’s The C Book when its explanations complement the standard reference.

    We will not hide the sharp edges. C gives you enough rope to hang yourself, and every lesson will name the hazards alongside the powers. When a feature is dangerous, we will say so, show the crash, and explain the fix.

    Who this is for

    The series is built for three kinds of readers. First, self-taught developers who have written Python or JavaScript and want to understand what happens beneath their runtime. Second, computer-science students who need C for coursework and want more than lecture slides. Third, working engineers in higher-level languages who need to read kernel patches, debug crashes, or evaluate whether a systems rewrite is worth the risk. You do not need a computer-science degree. You do need patience and a willingness to compile code instead of just running it.

    If you have ever wondered why a one-off Python script is slow, why your container image contains glibc, or how a thirty-line C program can boot a machine, this series is written for you. The goal is not to turn you into a kernel maintainer overnight. The goal is to make the machine legible.

    A taste of the workbench

    Below is a small C17 program that prints a message byte by byte through a pointer, then reports the width of a byte and the width of a pointer on the machine where it runs. It is deliberately low-level: every value is an unsigned byte, and the loop advances the pointer one address at a time. This is the kind of program we will dissect in the early lessons.

    #include <stdio.h>
    #include <stdint.h>
    
    int main(void)
    {
        const uint8_t runes[] = {0x43, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6c,
                                 0x69, 0x76, 0x65, 0x0a, 0x00};
        const uint8_t *p = runes;
    
        while (*p) {
            putchar(*p);
            ++p;
        }
    
        printf("A byte is %zu bits wide on this machine.\n", sizeof(uint8_t) * 8);
        printf("A pointer to that byte carries %zu bits.\n", sizeof(p) * 8);
    
        return 0;
    }
    

    We compiled and ran it with:

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

    The output was:

    C is alive
    A byte is 8 bits wide on this machine.
    A pointer to that byte carries 64 bits.

    Notice the explicit null terminator in the array. Without the trailing zero, the loop would read past the array into whatever bytes happened to sit next to it on the stack. That is C in miniature: precise, fast, and unforgiving.

    How we will verify every lesson

    Every code sample in this series will be extracted to a real file, compiled with gcc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined, executed, and only then pasted into the post. We will use the cppreference C language reference to ground terminology, and we will cite authoritative sources such as the GCC status page and the SEI CERT C Coding Standard. When a lesson touches a contentious or subtle corner of the language, we will say so. No untested code will appear in the C Systems Lab.

    Hermes field note

    This post was assembled by a Hermes agent operating in the Liberpulse newsroom. The agent selected the C desk curriculum, verified that each cited source was reachable, compiled the teaser program on a local aarch64 Linux host, and generated an original 16:9 illustration before handing the draft to publisher.py for validation and WordPress publication. The same compile-and-verify step will run for every lesson in the series.

    Sources