---
title: 'Undefined Behavior'
source: 'https://academia.sh/en/courses/c-memory/undefined-behavior'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Recognizes the four sources of undefined behavior (dangling pointer, freed memory, double free, out-of-bounds access) as contract violations'
    - "Explains why undefined behavior exists through the compiler's right to make assumptions"
    - 'Writes and runs a defined counterpart in place of each undefined source, verifying it by running it'
---

# Undefined Behavior

Reading a dangling pointer, using freed memory, freeing a block twice, and accessing an array out of bounds are all undefined behavior; none of them run — the lesson describes each one in prose and runs its defined counterpart alongside it.

The previous lesson gathered four regions on a single axis: the stack, static storage, the
string literal, and the heap — the common point was that who decides its lifetime is written
in the source. This lesson measures the moment that contract **breaks**. The contract used
to answer the question "who will free this block"; when the contract breaks, what appears is
not an error message but **undefined behavior** — and the most important property of
undefined behavior is that it cannot be measured.

This last sentence is not a warning, it is a definition that draws the lesson's boundary.
The four sections below describe four separate sources of undefined behavior — a dangling
pointer, freed memory, a double free, an out-of-bounds access — but none of them run the
source itself. The code running alongside each section is the **defined counterpart that
replaces that source**: a check line inserted before the contract breaks. This distinction
holds throughout the course — what is measured is always defined behavior — but it matters
especially in this lesson, because the name of the unmeasured side is also stated explicitly
in the prose.

## Why Undefined Behavior Exists

The C standard does not define the meaning of every expression. Some expressions are
**defined**: `int total = a + b;` always works by the same rule. Some are
**implementation-defined**: the compiler makes a choice and documents that choice. But some
expressions are **undefined**: the standard says nothing at all about what happens — the
compiler may behave however it likes, or not behave at all.

This gap is not an oversight, it is a design decision. The compiler optimizes by
**assuming that a program never enters undefined behavior**. For example it may assume that
a pointer always points to a valid block, because pointing to an invalid block is itself
undefined and the standard gives no guarantee in that case. This assumption lets the
compiler drop unnecessary checks, reorder operations, and cache a value instead of
recomputing it. If the assumption turns out wrong — that is, the program really does enter
undefined behavior — the generated code still compiles and still does **something**, but
what that something is is not the standard's concern.

The effect of this assumption changes with the compiler version, the optimization level, and
even the shape of the surrounding code; the same undefined expression may pass unnoticed in
one build and send the program down a completely different path in another. The standard
treats both outcomes as equally **valid**, because both fall under the word "undefined".
This is why the way to find undefined behavior is not to run the program and check whether
it produced an error — the absence of an error proves nothing, it only shows that nothing
was observed in that **particular** run, with that **particular** compiler.

Among the measurement core's four classes, this lesson's subject is **unowned** memory: once
the contract breaks, neither the language, nor the caller, nor the callee can reach that
block through a defined path any longer — the block may still sit there, but no party is
left to use it safely. That is why none of the four sources below answers the question
"who owns it" directly; all of them show the moment **after** the ownership contract has
already been broken.

This is why "looking like it works" carries no proof. The next section takes up, one by one,
the four sources this repository's measurement core deliberately never runs; each one first
explains in prose why the contract breaks, then runs a **defined** counterpart in its place.

## Reading a Dangling Pointer

Once a block is freed, the pointer that pointed to it **dangles**: the pointer itself still
holds the old address, but the memory at that address has already been returned to the
allocator. Dereferencing a dangling pointer (`*p`) — even just to read it — is undefined
behavior. The memory may appear to still hold the same content, because the allocator may
not yet have handed that block to another request; that is a **coincidence**, not a
guarantee. The same program may hand that address to a completely different block on the
next run, or after the next allocation request in the same run.

The lesson never runs this read. Instead it runs the defined pattern that **prevents** a
dangling pointer from forming: assigning `NULL` to the pointer the moment a block is freed,
and checking that before every use.

```c
// guard.c — the defined counterpart that prevents a dangling pointer from forming
#include <stdlib.h>

const char *dangling_counterpart(void) {
    int *block = malloc(sizeof *block);
    if (block != NULL) {
        *block = 9;
    }
    free(block);
    block = NULL;
    return (block == NULL) ? "pointer null, no dereference" : "accessed";
}
```

This counterpart's measurement is shown, together with the other three counterparts, in the
lesson's single closing code block that contains `main`.

The point to note is this: pulling `block` to `NULL` changes the pointer **itself**, not the
memory it pointed to — that memory already returned to the allocator with the `free` call.
The check only prevents that same pointer variable from being used again; if **another**
pointer holds the same address (for example a second variable pointing at the same block),
that second pointer still dangles and this pattern does not protect it. The pattern's scope
is limited exactly to the pointer variable it checks.

## Using Freed Memory

Using freed memory (use-after-free) is a little broader than reading a dangling pointer: it
also covers **writing** to the block. Writing `*p = value;` to the same block after a `free`
call may mean the allocator has already reopened that memory region for reuse — the write
may land on bytes that now belong to the allocator's own bookkeeping and corrupt the
allocator's internal consistency. This too is undefined and may not produce an error
message; the program may keep running "normally" for a while.

Its defined counterpart is to carry a **validity flag** tied to the block: the flag drops
when the block is freed, and every write checks that flag.

```c
// validity.c — the pattern that uses a flag to block writes after freeing
#include <stdlib.h>

const char *use_counterpart(void) {
    int *second = malloc(sizeof *second);
    int valid = (second != NULL);
    if (valid) {
        *second = 4;
    }
    free(second);
    second = NULL;
    valid = 0;
    if (valid) {
        *second = 7;
    }
    return valid ? "written" : "not written, flag dropped";
}
```

This counterpart's measurement is also shown in the same closing code block. Alongside the
flag, the pointer itself is also pulled to `NULL` right after `free`. This line is not
decoration: without it, a build with warnings enabled sees that `second` still appears in the
text after `free` and prints a warning — even though the branch never runs, because the
compiler cannot always prove that a branch will not run. Even the compiler itself keeps
tracking a freed pointer in its own view; the flag closes off the branch, pulling it to
`NULL` keeps the name from carrying a freed address, and together the two let the block
compile without warnings.

The flag's placement also matters: the flag must share the **same lifetime** as the pointer
it protects. If the flag is a local variable and the pointer passes from one function to
another, the callee cannot know whether the pointer is still valid unless the flag travels
with the pointer — which means the validity check itself carries **ownership separate** from
the block it checks; whose responsibility it is to update the flag is a question of its own.

## Double Free

Handing the same block to `free` twice is also undefined. Once the allocator frees a block,
it adds that address to its own ledger (the free list); a second `free` call tries to add
the same address to that ledger a second time and can corrupt the ledger's internal
structure. The result may show up on the allocator's next `malloc` call, or it may never
show up — neither is something the standard says anything about.

Its defined counterpart is to also mark that a block has been freed with a flag, and
**skip** the second free request using that flag.

```c
// single_free.c — the pattern that uses a flag to prevent a double free
#include <stdlib.h>

const char *single_free(void) {
    int *third = malloc(sizeof *third);
    int freed = 0;

    if (!freed) {
        free(third);
        freed = 1;
    }
    if (!freed) {
        free(third);
        freed = 1;
    }
    return freed ? "freed once" : "not freed";
}
```

The second `freed` check here **never runs** — it exists as a line of code, but the `freed`
flag is already true at that point, so the condition never holds. This is the concrete form
of the rule set up at the start of the lesson: a line that shows the undefined path may sit
in the source, as long as it is never executed. Its measurement is also in the closing code
block.

Seen from the ownership angle, a double free is an interesting case: the moment a block is
freed, its ownership has already passed to **nobody** — neither the language, nor the
caller, nor the callee owns it any longer. The second `free` call is a call into that gap;
the allocator does not recognize that address, or recognizes it wrongly, because it no
longer holds a **valid** record of that address. The single-free flag does not fill this
gap, it only prevents entering it a second time.

## Out-of-Bounds Access

Reaching outside an array's `[0, length)` range — `array[length]` or further — is undefined.
The memory may physically hold something there, but that byte is not part of the array; the
compiler may drop bounds checks by assuming an array access always stays within bounds. An
out-of-bounds read yields an arbitrary value, an out-of-bounds write may corrupt another
variable's data.

Its defined counterpart is to **compare the requested index against the known length**
before the access.

```c
// bounded.c — the pattern that compares an index against a length to keep access defined
const char *bounded_access(int requested) {
    int array[4] = {10, 20, 30, 40};
    int length = 4;
    if (requested >= 0 && requested < length) {
        (void)array[requested];
        return "accessed";
    }
    return "rejected, out of bounds";
}
```

The `requested < length` check does not exist in C itself — an array access (`array[i]`)
always silently permits it in the language, with no check at either run time or compile
time. The check is entirely a line **the programmer adds**; without it, an index past the
end of the array compiles and enters undefined behavior.

This shows a point that differs from the other three sources: a dangling pointer and freed
memory are a **timing** problem — a block is valid at one point, then invalid later.
Out-of-bounds access is independent of timing; even when the array was never freed and is
still fully valid, the wrong index still enters undefined behavior. The common point is that
in both cases the compiler adds **no** run-time check at all — C itself tracks neither the
timing nor the index.

## Running All Four Counterparts Together

None of the four is undefined; all four are patterns that **prevent** undefined behavior,
and all four run in a single `main`, in sequence.

```c
// measurement.c — all four defined counterparts measured together in one main
#include <stdio.h>

const char *dangling_counterpart(void);
const char *use_counterpart(void);
const char *single_free(void);
const char *bounded_access(int requested);

static void observe(const char *name, const char *result) {
    printf("%-28s%s\n", name, result);
}

int main(void) {
    printf("%-28s%s\n", "observation", "result");
    observe("dangling pointer", dangling_counterpart());
    observe("freed memory", use_counterpart());
    observe("double free attempt", single_free());
    observe("valid index", bounded_access(2));
    observe("out-of-bounds index", bounded_access(5));
    return 0;
}
```

```text
observation                 result
dangling pointer            pointer null, no dereference
freed memory                not written, flag dropped
double free attempt         freed once
valid index                 accessed
out-of-bounds index         rejected, out of bounds
```

**RO1.** The `block == NULL` check here **prevents** the dangling pointer, it does not
measure the dangling pointer itself. Undefined behavior has no measurable "result"; the only
thing that can be measured is proof that it was never entered — that is why the output line
says "no dereference", not "the value came out as such".

**RO2.** The flag itself is an ordinary `int`; C has no separate "validity" type. The
guarantee comes not from the type but from the **discipline of every write site checking the
flag** — the compiler does not enforce this discipline, a line that forgets to check
compiles silently.

**RO3.** The second `freed` check inside `single_free` never ran during this run — the
output gave a single "freed once" line, with no trace of a second free.

**RO4.** The last two lines are the same function called with two different `requested`
values on the same array: the valid index stays inside the array and is accessed, the
out-of-bounds index is rejected without being accessed — both are defined, neither ever
touches past the end of the array.

What all four lines share is that none of them relies on a guarantee from the language or a
runtime. `dangling_counterpart` pulls the pointer to `NULL` with its own hands,
`use_counterpart` drops the flag with its own hands, `single_free` skips the second free with
its own hands, `bounded_access` compares the bound with its own hands — because none of the
four has an **owning party** for that block, the only guarantee left is the check written in
the source. This does not mean that none of the previous lesson's four classes (language,
caller, callee, unowned) is in play here; on the contrary, these four examples are exactly
what makes the **unowned** class concrete: once a block loses its validity at some point,
neither the language, the caller, nor the callee protects it on its own anymore. If there is
protection, it is the line of code that put it there — not the compiler, not the runtime
library, and certainly not the type system. That is why this lesson's counterpart of the
unowned class does not **replace** the previous lesson's language/caller/callee classes; it
separately addresses a weak point common to all of them: the moment right after the contract
breaks.

## The Bounding Measurement

Undefined behavior is not an error message. The four counterparts above give exactly the
same output on every run — but that consistency comes from the counterparts being
**defined**, it does not show that the undefined operations involved are "actually
harmless". None of this lesson's code blocks dereferences a dangling pointer, writes to a
freed block, frees a block twice, or reaches past an array's end; all of them run the pattern
that **prevents** those things. A program "looking like it works" is not proof that it
contains no undefined behavior — the same source may behave differently when the compiler
version, the optimization level, or the next allocation request changes.

This also sets the lesson's measurement boundary: none of the programs run here shows what
undefined behavior "does", because showing such a thing would already contradict the
concept's own definition. The lesson can only measure two things — describing in prose where
the contract breaks, and running the defined path that replaces the break. The difference
resembles the difference between a lab experiment and a recipe: an experiment's result can be
observed, a recipe only guarantees its result if followed — what happens if it is not
followed is not the recipe's concern.

## Summary

- Undefined behavior is a gap where the compiler optimizes by **assuming it is never
  entered**; "looking like it works" does not show that it is defined.
- Reading a dangling pointer, using freed memory, freeing a block twice, and reaching past
  an array's bounds — all four are undefined, and **none of them ran** in this lesson.
- Every source has a defined counterpart: a validity flag, a single-free flag, and an
  index-length check — all lines the programmer adds, none enforced by the language.
- The memory this lesson measures is in the **unowned** class: once the contract breaks, no
  party obligated to free the block can reach it through a defined path any longer.

## Next Step

The contract breaking shows up not only in memory access but also on the path where a
function **returns an error** — something needs to be cleaned up there too, but this time
without entering undefined behavior. The next lesson measures that cleanup through the
callee class's function: return codes, error numbers, and single-exit-point cleanup
patterns.
