A theory for decades of C vulnerabilities
The central argument of this book can be expressed in a single idea: A semantic invariant is a property that must remain true about a value, object, or relationship throughout the execution of a program if the program is to continue operating on the intended data and memory. C is full of such invariants. A variable called length may be intended to represent the number of bytes available in a buffer. A variable called count may represent the number of elements in an array. A pointer may be intended to identify the beginning of an allocated object. An offset may be intended to remain within that object. A pointer may be assumed to remain valid until a particular operation has completed. Two pointers may be assumed to refer to distinct regions of memory. None of these assumptions is merely a comment about the program. They are semantic facts on which the correctness of subsequent operations depends. The difficulty is that standard C generally does not make these relationships part of the types of the values that carry them. C can tell us that a value has type size_t. It cannot, in the ordinary type system, tell us whether that value represents a number of bytes, a number of elements, an allocation size, a buffer capacity, an offset, or the length of an input. C can tell us that a value has type char *. It cannot generally tell us the extent of the object accessible through that pointer, who owns the object, how long it remains alive, or whether a particular number of bytes may safely be accessed beginning at that address. The result is not simply that C contains "unsafe operations." The deeper problem is that the semantic relationships required to make those operations safe are frequently maintained as informal obligations in the programmer's reasoning rather than as invariants enforced by the language. This provides a common theoretical explanation for a remarkably broad family of security vulnerabilities. Integer overflow, buffer overflow, out-of-bounds access, use-after-free, and double free are not the same bug. They occur at different points in a program and involve different immediate mechanisms. But they can often be understood as different failure manifestations of broken semantic invariants. The vulnerability is the point at which the program's representation of reality ceases to correspond to reality. An invariant is a proposition that is expected to remain true throughout some region of program execution. Consider: size_t count; size_t size; char *p; The C type system tells us very little about the relationships between these values. But the programmer may be relying on a much richer set of propositions: count represents the number of elements requested size represents the number of bytes required for count elements p points to storage of at least size bytes an access at p + offset remains within that storage length does not exceed the remaining extent p remains valid until the access has completed These are semantic invariants. They are not necessarily visible in the declarations. The program may nevertheless depend upon every one of them. This distinction is fundamental. A type says what a value is permitted to be according to the language. A semantic invariant says what that value means in the particular program and what relationships must remain true for subsequent operations to be valid. For example: size_t length; does not mean: length is a valid length for the buffer p It means only that length has the C type size_t. The stronger proposition is something like: 0 100 bytes but an earlier calculation actually caused: p -> 60 bytes The pointer itself may be perfectly valid. The allocation may have succeeded. The program may have passed every local test up to this point. The failure occurs when the program subsequently assumes the wrong extent. For example: memcpy(p, source, 100); The semantic claim is: extent(p) >= 100 But reality is: extent(p) = 60 The copy is therefore not simply "a bad call to memcpy." It is the point at which a previously broken extent invariant becomes an observable memory violation. Heartbleed, CVE-2014-0160, is one of the most famous examples. The vulnerability involved a buffer over-read in OpenSSL's TLS and DTLS heartbeat handling. The attacker could cause the program to read beyond the logical bounds of the supplied heartbeat data and obtain information from process memory. The essential semantic failure was not that the machine could not read memory. The problem was that the program's description of the amount of memory belonging to the logical message did not match the amount of memory that was actually read. The machine was doing what the generated instructions told it to do. The problem was that the program's semantic model of the buffer was wrong. Buffer overflow and out-of-bounds access are closely related, but the semantic perspective helps distinguish them. An out-of-bounds access is fundamentally a failure of the invariant: The address being accessed must lie within the permitted extent of the intended object. Consider: int array[10]; array[index] = value; The programmer's intended invariant is: 0 live object After: p -> object whose lifetime has ended The bits may look unchanged. The invariant is not. That is precisely why lifetime is a semantic property rather than merely a representation property. A double free reveals a related but distinct invariant. Consider: free(p); free(p); The problem is not simply that the second call uses an invalid pointer. The deeper failure is that the program has lost track of authority over the lifetime of the allocation. A useful semantic invariant is: An allocation may be released exactly once by the entity responsible for ending its lifetime. C does not generally encode ownership in the type of p. The declaration: int *p; does not distinguish: owning pointer from: borrowed pointer or: shared alias or: pointer whose ownership has already been transferred Consequently, ownership is maintained through programming conventions. CVE-2006-2026 in libtiff is an early example of a double-free vulnerability that could result in denial of service and potentially arbitrary code execution. CVE-2025-5914 in libarchive provides a more recent example in which an integer-overflow condition could ultimately lead to a double-free condition and memory corruption. This is particularly revealing. An integer failure can eventually become an ownership and lifetime failure: integer invariant | v memory-state invariant | v ownership/lifetime invariant | v double free Again, the CVE taxonomy identifies multiple weaknesses. The semantic-invariant model sees a causal chain. This may be the most important observation when looking at real CVEs. CVE classifications tend to divide vulnerabilities into categories such as: CWE-190 Integer Overflow or Wraparound CWE-122 Heap-based Buffer Overflow CWE-787 Out-of-bounds Write CWE-416 Use After Free CWE-415 Double Free These classifications are useful because they identify the immediate failure mode. But they can conceal the relationship between them. CVE-2026-21486 is an unusually clear example. The vulnerability is associated with integer overflow, heap buffer overflow, use-after-free, and out-of-bounds write in the same code path. That is precisely what the semantic-invariant model predicts. The categories need not be independent. A single corrupted assumption can propagate: wrong integer | v wrong allocation | v wrong extent | v invalid pointer or range | v memory corruption | v lifetime corruption | v use-after-free The vulnerability database records the observable manifestations. The program's semantics reveal the chain connecting them. This is not an isolated property of small C programs. The same structure appears in some of the most important software written in C. OpenSSL contains cryptographic protocols and extensive binary parsing. glibc contains memory-management, string, networking, and resolver code. curl parses network protocols and maintains long-lived connection state. libarchive processes attacker-controlled archive formats. The Linux kernel contains enormous quantities of C code operating on memory, hardware, packets, filesystem structures, and kernel objects. These systems are particularly exposed to semantic-invariant failures because they repeatedly perform the same transformation: untrusted external information | v integer / length / offset | v pointer or allocation | v memory operation The external value begins as data. The program turns it into a claim about memory. The security problem arises when the program fails to preserve the truth of that claim. The following mapping is useful: CVE manifestation | v Immediate failure | v Broken semantic invariant Integer overflow: arithmetic result does not represent intended quantity | v meaning invariant Undersized allocation: allocated extent does not match logical object | v representation and extent invariant Buffer overflow: operation exceeds allocated region | v spatial extent invariant Out-of-bounds read or write: access lies outside permitted object range | v object and range invariant Use-after-free: access occurs after object lifetime ended | v temporal lifetime invariant Double free: lifetime is ended more than once | v ownership and lifetime invariant Stale pointer: pointer no longer identifies intended live object | v identity and lifetime invariant Invalid aliasing: references violate assumptions about permitted access | v aliasing invariant These are not interchangeable. They should not be collapsed into one undifferentiated category. But they can be understood as different ways in which the relationship between program representation and program meaning breaks down. The problem is not that C has no type system. Nor is it that C has no memory model. The problem is that C's ordinary types are not sufficiently expressive to represent the full set of semantic relationships required by memory-safe programming. Consider: ch
Comments
No comments yet. Start the discussion.