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 <= length <= extent(p)
where `extent(p)` represents the amount of memory that may validly be accessed beginning at `p`.
That proposition is what makes:
memcpy(p, source, length);
safe.
C does not generally carry the proposition `length <= extent(p)` along with the values `length` and `p`.
The programmer must establish it.
Many memory vulnerabilities can be understood as a chain of dependent propositions.
A simplified example is:
size = count * element_size;
p = malloc(size);
q = p + offset;
memcpy(q, source, length);
At first sight, this is simply a sequence of ordinary C operations.
Semantically, however, it represents a chain:
count
|
v
required size
|
v
allocated extent
|
v
object identity
|
v
offset
|
v
remaining extent
|
v
copy length
|
v
valid memory access
Each relationship represents an invariant.
For example:
size = count * element_size
depends upon the invariant that `count` really represents the intended number of elements and that the multiplication produces the intended size without overflow.
Then:
p = malloc(size)
depends upon the invariant that the allocated size corresponds to the logical object the programmer intends to construct.
Then:
q = p + offset
depends upon `offset` being an appropriate position within that object.
Finally:
memcpy(q, source, length)
depends upon `length` not exceeding the remaining extent of the destination and upon the source and destination satisfying the requirements of the operation.
A failure near the beginning of the chain may not become visible until much later.
This is why a vulnerability can appear to be located at the `memcpy`, while the actual semantic failure occurred much earlier.
Integer overflow is often described as an arithmetic problem.
In security-critical C code, it is more useful to see it as a failure of semantic meaning.
Suppose:
size_t size = count * element_size;
The programmer intends:
size = the number of bytes required to store count elements
If the calculation does not represent that mathematical quantity, then the variable `size` no longer means what the programmer believes it means.
The semantic invariant has failed:
intended size = represented size
The vulnerability may not yet be a memory violation.
The program may still execute normally.
The failure appears later:
p = malloc(size);
Now the allocation is smaller than intended.
The program has crossed a semantic boundary:
mathematical quantity
|
v
represented integer
|
v
allocation size
The first incorrect value can therefore create a later memory-safety violation.
CVE-2019-19590 in radare2 is a useful example. The vulnerability involved an integer overflow in the calculation of a token allocation size, followed by a use-after-free involving the token buffer. The integer error therefore did not remain an isolated arithmetic defect. It propagated into the memory model of the program.
CVE-2022-37454 provides another example. The vulnerability in the Keccak XKCP SHA-3 implementation involved an integer overflow that resulted in a buffer overflow.
The CVE categories are different because they describe different observable failures. Semantically, however, they can form a single chain:
attacker-controlled quantity
|
v
integer calculation
|
v
intended allocation size
|
v
incorrect represented size
|
v
incorrect memory relationship
|
v
memory corruption
The integer overflow is one manifestation of a broken invariant. The eventual memory corruption is another.
Once an incorrect size reaches an allocation, the next invariant is spatial:
The memory region being accessed must be at least as large as the operation assumes it is.
Suppose the programmer believes:
p -> 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 <= index < 10
The C type of `index` does not express that fact.
It is merely an integer.
Nor does the type of `array` automatically turn `index` into a value whose range is statically constrained to the array's extent.
The operation is therefore safe only if the programmer or some earlier operation establishes the invariant.
CVE-2015-7547, for example, involved stack-based buffer overflows in glibc's resolver code when processing specially crafted DNS responses.
The important semantic question is:
Why did the program's assumptions about the size of attacker-controlled DNS data cease to correspond to the actual destination buffer?
The attacker wins when an external value causes an internal invariant about the extent of an object to become false.
The semantic-invariant model also explains something that is easy to overlook if memory safety is described only in terms of bounds.
A program can access:
the correct amount of memory
at:
the wrong location
This is a failure of object identity rather than merely extent.
A pointer is not semantically valid simply because its numerical representation happens to designate an address that can be accessed.
The program also needs the invariant:
This pointer identifies the object that the program believes it identifies.
This is one reason C's pointer semantics are considerably more subtle than the simplistic statement that "a pointer is just an address."
The C abstract machine has rules concerning objects, pointer values, object lifetime, and pointer arithmetic. Questions of pointer provenance have also been an ongoing subject of discussion within the C standardization community precisely because numerical address equality does not capture all of the semantic information associated with a pointer.
From the perspective of this book, the important observation is simple:
address
!=
object identity
The program must preserve both.
A pointer may have a plausible address while the semantic relationship between that pointer and the intended object has already been broken.
Spatial invariants answer:
Where may I access?
Lifetime invariants answer:
When may I access it?
Consider:
p = malloc(...);
/* use p */
free(p);
/* use p again */
The pointer value may still be held by the program after the object has been released.
The crucial invariant is:
if p is dereferenced,
the object identified by p must still be alive
After `free(p)`, that invariant no longer holds.
This produces a temporal memory-safety failure:
correct object
|
v
object lifetime ends
|
v
pointer remains available
|
v
pointer is used
|
v
use-after-free
CVE-2026-3805, for example, records a use-after-free in curl's SMB connection reuse code, where a data pointer could refer to memory that had already been freed.
The vulnerability is not fundamentally about the numerical value of the pointer.
The pointer can contain the same bits before and after the `free`.
What has changed is the semantic world to which those bits refer.
Before:
p -> 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:
char *p;
size_t length;
The programmer may intend:
p identifies the beginning of a live buffer
length is the number of bytes currently valid
length does not exceed the buffer's extent
p remains valid for the duration of the operation
the caller retains the appropriate ownership
The C declarations express almost none of those relationships.
They express only:
p is a pointer to char
length is a size_t
Everything else is convention.
This is the crucial gap.
In a memory-safe language, the compiler may be able to represent and enforce some of these relationships.
In C, the programmer frequently has to carry them mentally.
Consider:
void process(char *buffer, size_t length);
The caller and callee may both understand a contract such as:
buffer points to at least length bytes
buffer remains valid for the duration of process
the region may be modified
the caller retains ownership
But none of those properties is apparent from:
char *
size_t
The semantic contract exists.
It simply exists outside the type system.
This is why C programming can be understood as a continual exercise in maintaining informal proofs.
The programmer establishes an invariant in one part of the program and relies upon it somewhere else.
The compiler does not necessarily know that the invariant exists.
When the invariant fails, the failure may be separated from its cause by hundreds of lines of code, several function calls, or an entirely different subsystem.
This is particularly dangerous in security-sensitive software because an attacker can often control the values that participate in the invariant.
For example:
network packet
|
v
attacker-controlled length
|
v
integer conversion
|
v
allocation
|
v
pointer arithmetic
|
v
copy
Every step can appear locally reasonable.
The attacker does not need to make the program execute an obviously forbidden operation.
They need only cause a value to make a previously assumed invariant false.
This is a profound difference.
The attack is often not:
Make the program do something it was never designed to do.
It is:
Make the program believe that something false is true, and then let it perform an operation that is perfectly legitimate under that false assumption.
That is why these vulnerabilities are so persistent.
If the problem were simply that certain inputs cause an invalid memory access, exhaustive testing might theoretically solve it.
But the semantic-invariant problem is larger.
Suppose a program contains:
size = count * element_size;
Testing may demonstrate that the operation works for:
count = 10
count = 100
count = 1000
It does not establish the invariant:
size always represents the mathematically required number of bytes
Likewise, testing:
memcpy(p, source, length);
with valid lengths does not prove:
length <= extent(p)
for all possible inputs and execution paths.
Testing exercises examples.
Semantic invariants describe relationships that must hold for every execution satisfying the program's contract.
This is one reason static enforcement is so attractive.
The objective is not merely to test whether an invalid state occurs.
It is to make the invalid state difficult or impossible to represent.
This is the fundamental direction taken by modern memory-safe language design.
Instead of:
pointer + separate length + programmer convention
a language can provide an abstraction that means:
reference to a valid region of known extent
Instead of:
raw pointer + ownership convention
it can distinguish:
owned value
borrowed reference
shared reference
exclusive mutable reference
Instead of:
integer that happens to represent an element count
it can provide stronger abstractions that distinguish units and ranges.
Instead of requiring the programmer to remember:
this reference must not outlive that object
the language can encode lifetime relationships and have the compiler verify them.
The important shift is therefore:
C:
programmer establishes invariant
|
v
programmer preserves invariant
|
v
programmer proves invariant
versus:
memory-safe language:
language represents invariant
|
v
compiler preserves invariant
|
v
compiler rejects violations
No language can encode every semantic property of an arbitrary program.
But moving important classes of invariants into the language dramatically reduces the space in which these vulnerabilities can occur.
This argument should not be interpreted as claiming that every security vulnerability in a C program is a memory-safety vulnerability.
C programs can contain:
authentication errors
cryptographic mistakes
protocol-design errors
access-control failures
logic errors
race conditions
denial-of-service conditions
information leaks unrelated to memory corruption
Nor does every C memory vulnerability arise from exactly the same broken invariant.
The claim is more precise:
A very large and security-critical family of C vulnerabilities arises because C allows important semantic invariants concerning memory, extent, object identity, lifetime, ownership, and arithmetic relationships to remain implicit and dynamically unenforced.
This is why memory safety has become such a significant security concern.
The CISA Cybersecurity Advisory Committee has explicitly discussed memory-safety problems in terms of spatial and temporal categories and has argued that adopting memory-safe technologies can reduce the pool of vulnerabilities available for exploitation.
The distinction between spatial and temporal memory safety maps naturally onto the invariant model:
Spatial:
Where may this access occur?
How much memory may it access?
Temporal:
When may this access occur?
Does the object still exist?
Ownership and aliasing add further dimensions to the same underlying problem.
A CVE describes a vulnerability as it becomes observable from a security perspective.
It might say:
heap-based buffer overflow
or:
use-after-free
or:
integer overflow
or:
double free
These are useful descriptions of what went wrong.
But for understanding why C produces so many such vulnerabilities, we need to ask another question:
What semantic relationship did the program assume, and where did that relationship cease to be true?
For example:
# Heartbleed
The observable failure:
buffer over-read
The deeper question:
Why was the amount of memory read allowed to exceed
the amount actually belonging to the logical heartbeat?
CVE-2014-0160 records that buffer over-read.
# glibc resolver vulnerability
The observable failure:
stack buffer overflow
The deeper question:
Why did the program's assumptions about the size of
attacker-controlled DNS data cease to correspond to
the actual destination buffer?
CVE-2015-7547 records the resulting buffer overflows.
# radare2
The observable failures:
integer overflow
use-after-free
The deeper question:
Why was an arithmetic result allowed to become the
basis for a memory relationship without preserving
the intended size invariant?
CVE-2019-19590 provides a concrete instance of that chain.
# libarchive
The observable failure:
integer overflow
|
v
double free
|
v
memory corruption
The deeper question:
How did a corrupted numerical invariant propagate
into an invalid lifetime or ownership decision?
CVE-2025-5914 demonstrates such a progression.
# iccDEV
The observable failures span several categories:
integer overflow
heap buffer overflow
out-of-bounds write
use-after-free
CVE-2026-21486 is especially useful because the vulnerability record captures several stages of the same failure.
These examples demonstrate why the invariant perspective is valuable.
The CVE classifications are different.
The underlying semantic story can nevertheless be continuous.
The following model is perhaps the most concise representation of the argument:
SEMANTIC INVARIANT
|
|
+----------+----------+
| | |
Meaning Space Time
| | |
| | |
integer extent / lifetime /
quantity location ownership
| | |
v v v
overflow overflow use-after-free
/ OOB / double free
| | |
+----------+----------+
|
v
memory corruption
|
+----------+----------+
| | |
v v v
disclosure execution denial
The vulnerabilities are not all the same.
They are failure manifestations occurring when different semantic invariants are violated.
"Wrong memory" does not necessarily mean:
The programmer typed the wrong pointer.
It can mean:
the wrong amount of memory
the wrong location
the wrong object
the wrong lifetime
the wrong ownership
the wrong interpretation of a number
And these can arise from a chain of individually ordinary operations.
The ultimate failure occurs when:
The program's semantic description of memory diverges from the actual memory state of the machine.
That divergence is the common feature.
An integer may no longer represent the intended quantity.
A pointer may no longer identify the intended object.
A length may no longer describe the accessible extent.
An object may no longer be alive.
An owner may no longer have authority over an allocation.
Once the semantic invariant is broken, a later operation can turn that invisible discrepancy into a security vulnerability.
The criticism, then, should not be:
C is poorly typed.
That is too crude.
Nor should it be:
C has pointers.
Pointers are not intrinsically a defect.
Nor:
C permits manual memory management.
Manual memory management is sometimes necessary for systems programming.
The deeper criticism is:
C makes the programmer responsible for maintaining semantic relationships that the language's ordinary type and memory abstractions do not adequately represent.
That is a more precise statement.
C's `int *` is not merely a pointer.
In a real program it may implicitly mean:
a pointer
to an object
of a particular logical kind
within a particular allocation
at a particular offset
with a particular remaining extent
during a particular lifetime
under a particular ownership regime
and subject to particular aliasing assumptions
But its type is still:
int *
The missing information has not disappeared.
It has moved into:
programmer knowledge
naming conventions
documentation
comments
API contracts
assertions
code review
testing
static-analysis annotations
defensive programming
And ultimately, the programmer must ensure that these facts remain true.
This is where an engineering inconvenience becomes a security problem.
If an invariant is explicit and mechanically enforced, an attacker generally cannot violate it merely by supplying an unusual value.
If an invariant is implicit and maintained by programmer reasoning, an attacker may be able to search for an input that causes the reasoning to become false.
That produces a characteristic attack pattern:
attacker input
|
v
unexpected value
|
v
implicit assumption
|
v
invariant becomes false
|
v
ordinary C operation
|
v
invalid memory operation
|
v
security consequence
The attacker does not necessarily need control over the final memory operation.
They need control over the premise under which that operation is considered safe.
This is why integer values, lengths, offsets, and allocation sizes are so important in C security.
They are often the bridge between attacker-controlled data and memory semantics.
One possible way of reorganizing our understanding of C vulnerabilities is to stop treating the CWE categories as the deepest level of explanation.
Instead, consider the following hierarchy.
# Level 1: Semantic invariant
What must remain true?
length <= extent
offset within object
object is alive
allocation has one owner
count * element_size is representable
# Level 2: Invariant failure
What ceased to be true?
length > extent
offset outside object
object lifetime ended
two paths release same object
calculated size wrapped
# Level 3: Vulnerability manifestation
What does the program do as a result?
buffer overflow
out-of-bounds access
use-after-free
double free
integer overflow
# Level 4: Security consequence
What can an attacker obtain?
crash
information disclosure
memory corruption
control-flow corruption
privilege escalation
arbitrary code execution
denial of service
This gives us a causal model:
semantic assumption
|
v
broken invariant
|
v
C-level failure
|
v
memory corruption
|
v
security consequence
The CVE usually names Level 3.
The real engineering problem often begins at Level 1.
The history of C's security problems can therefore be understood as the history of semantic information that programmers were required to carry manually.
The machine knows addresses.
C gives programmers those addresses.
But safe programming requires knowing considerably more than an address.
It requires knowing:
what object is here
how large it is
where its boundaries are
how the pointer was derived
whether the object is still alive
who owns it
who else can access it
what the integer used as a length actually means
whether the arithmetic producing that length was valid
When these facts remain implicit, they become opportunities for divergence between what the program believes and what the machine actually contains.
That divergence is the fertile ground in which memory-safety vulnerabilities grow.
The individual CVEs are the visible manifestations.
The semantic invariant is the underlying concept.
The central proposition of this book can therefore be stated more formally:
A memory-safety vulnerability occurs when a program performs an operation under a semantic invariant that is false, or when a semantic invariant required for the validity of an operation has been lost between the point at which it was established and the point at which it is relied upon.
Under this formulation:
**Integer overflow** is a failure to preserve the invariant that a numerical value represents the intended mathematical quantity.
**Buffer overflow** is a failure to preserve the invariant that the accessible extent of an object is at least as large as the operation assumes.
**Out-of-bounds access** is a failure to preserve the invariant that an address lies within the permitted range of the intended object.
**Use-after-free** is a failure to preserve the invariant that the object identified by a pointer remains alive when the pointer is dereferenced.
**Double free** is a failure to preserve the invariant governing the unique lifetime and ownership of an allocation.
These are different vulnerabilities.
But they share a common structure:
SEMANTIC INVARIANT
|
v
programmer assumption
|
v
invariant is violated
|
v
program continues executing
|
v
invalid memory operation
|
v
security vulnerability
The most important property of the C language in this respect is therefore not simply that it permits pointers, arithmetic, manual allocation, or direct memory manipulation.
It is that the language frequently permits these operations without carrying forward enough semantic information to establish that the relationships on which their safety depends are still true.
That is why the same basic pattern can produce such apparently different CVEs.
The integer overflow is not the buffer overflow.
The buffer overflow is not the use-after-free.
The use-after-free is not the double free.
But all can be understood as points at which the program's semantic model of memory has diverged from the memory that actually exists.
And that is perhaps the most fundamental security lesson to draw from decades of C vulnerabilities:
Memory corruption begins not when the wrong byte is written, but when the program first loses the invariant that tells it which bytes are right.