Skip to content
VLSI Mentor

C Part I · Module 9 · Memory and Object Lifetime

Dangling and Invalid Pointers

Chapter 7.4 taught you the one pointer value that is deliberately not an address, and then stopped — because the interesting question, which pointers are not usable, could not be answered yet. Two of the three answers live in this module: a pointer to an automatic object whose block has exited, from Chapter 9.2, and a pointer to a block that has been freed, from Chapter 9.10. The third is a pointer that was never given a value at all. All three have now been taught, so this is the chapter that puts them in one list, states exactly what the standard says about such a pointer — which is stronger and stranger than most people expect — and explains the property the three of them share: the place where your program breaks has almost nothing to do with the place where you made the mistake.

Advanced22 min readCPart IFundamentalsMemory

Part I · Module 9 · Chapter 9.11 · Dangling and Invalid Pointers

1. The Three Sources

Every invalid pointer in C arrives through one of three doors, and you have now walked through each of them.

#SourceThe object it addressedTaught in
1The pointer was never initialisedthere never was one2.3, and here
2The pointer addresses an automatic object whose block has exitedended when the block did9.2
3The pointer addresses a block that has been freedended at the free9.10
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    /* Source 1 — never initialised */
    int *a;

    /* Source 2 — a returned local */
    int *b;
    { int local = 7; b = &local; }          /* the block ends here */

    /* Source 3 — a freed block */
    int *c = malloc(sizeof *c);
    free(c);                                /* the lifetime ends here */

A pointer from source 2 or 3 is usually called a dangling pointer — it once addressed a real object, and the object went away underneath it. A pointer from source 1 never addressed anything. For the purposes of this chapter the distinction is historical: all three are invalid, and the rules for all three are identical.

Notice what is not on the list. A null pointer is not invalid — Chapter 7.4 established it as a well-defined value with a well-defined meaning. A pointer to an object with static storage duration is never invalid, because that lifetime never ends (Chapter 9.3). And having many pointers to one object is not a problem at all (Chapter 7.1).

2. What "Valid" Means, Precisely

A pointer value is valid when it is one of exactly three things:

  • the address of an object whose lifetime has not ended,
  • a null pointer, or
  • one past the end of an array object.

Anything else is invalid, and the standard calls its value indeterminate.

The third case is easy to overlook and it is load-bearing, because it is what makes ordinary loops legal:

Azvya Education Pvt. Ltd.VLSI Mentor
one past the end is a legal value to form and to compare
#include <stdio.h>

int main(void)
{
    int a[4] = {10, 20, 30, 40};
    int *one_past = a + 4;                  /* legal to form, legal to compare */

    printf("elements: ");
    for (int *p = a; p != one_past; p++) {
        printf("%d ", *p);
    }
    printf("\n");
    printf("one_past == a + 4 ? %d\n", one_past == a + 4);

    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
elements: 10 20 30 40 
one_past == a + 4 ? 1

That pointer may be formed, compared, and assigned. It may not be dereferenced — there is no object there. Chapter 7.8 §6 introduced this as the boundary that makes pointer-walking loops work; it is restated here because it is the one case where "valid value" and "safe to dereference" come apart.

3. The Rule Is Stronger Than You Think

Here is the part that almost every treatment of this topic states too weakly. You will be told: do not dereference a dangling pointer. That advice is correct and incomplete.

When an object's lifetime ends, the value held by every pointer to it becomes indeterminate. Indeterminate is a property of the value, not of the act of dereferencing. So reading that pointer variable at all is undefined — copying it, comparing it, passing it to a function, or printing it. Not only dereferencing it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int *p = malloc(sizeof *p);
    free(p);

    int *q = p;                  /* undefined — this READS p */
    if (p == NULL) { }           /* undefined — this READS p */
    printf("%p", (void *)p);     /* undefined — this READS p */

Every one of those three lines looks harmless. None dereferences anything. All three are undefined, because all three evaluate p, and p no longer holds a determinate value.

Whether a given implementation does anything surprising with them is a separate question, and mostly it does not. But the rule explains two things that would otherwise look like pedantry.

It explains why p = NULL; after a free is more than hygiene. It replaces an indeterminate value with a determinate one. After the assignment, p == NULL is a legal test again, q = p is legal, and free(p) is a defined no-op. One assignment moves the variable from "cannot be touched" back to "fully usable".

It explains the shape of Chapter 9.10 §3's demonstration. That program converted the address to a uintptr_t before the free, and compared integers afterwards. It had to: reading the pointer variable after the free, even just to print it, would have put undefined behaviour into a program whose whole purpose was not to have any.

4. Why the Symptom Is Far From the Cause

This is c.ts's second question for this chapter, and it is the one that costs real debugging time.

Five stages separating the cause of an invalid pointer from its symptom. One, the mistake: a pointer outlives its object, from any of three sources. Two, nothing: the program runs on with no diagnostic at all. Three, it travels: the pointer is copied, stored and passed, so the bad value now exists in several places. Four, the stale use: it is read or written, reaching storage that has another owner. Five, the symptom: a crash or wrong data, appearing in unrelated code.1 · Themistake2 · Nothing3 · Ittravels4 · The staleuse5 · ThesymptomObjectlifetime endsone of threesourcesThe programruns onno diagnostic atallCopied andpassed onnow in severalplacesRead orwrittenstorage withanother ownerA crash orwrong datain unrelatedcode12
Figure 1 — the distance between the mistake and the symptom. Stage 2 is where the debugging difficulty is created: the program continues with no diagnostic, so the mistake is now in the past and nothing links to it. By stage 3 the bad value may exist in several variables, so there is no single place to look. By stage 5 the failing code is code that did nothing wrong — it asked for memory and got memory that something else was still writing to. Every stage after the first is time and distance between you and the line you need to change.

Three properties of that sequence are what make the bug expensive.

Stage 1 produces no event. There is no moment at which the program notices. Chapter 9.10 §2 explained why for the freed-block case — free cannot touch your variable — and the same is true of the other two sources: a block exiting does not visit the pointers that referred to its locals, and a declaration without an initialiser does not mark the variable.

Stages 1 and 4 can be arbitrarily far apart. The pointer can be stored in an array, passed down three calls, and used an hour later. Nothing in the stale use points back at the mistake; the stack trace you get describes stage 5.

Stage 5 happens in innocent code. The function that crashes is the function that was handed the reused storage. Its code is correct. This is why "the bug is in the logging module" is so often wrong — the logging module is simply the next thing that allocated.

The debugging rule that follows: when a crash makes no sense where it is, stop reading the crash site. Ask instead which object owned that storage and when its lifetime ended. That is a question about Chapter 9.8's ownership, and it is answerable; "why does this correct function crash" is not.

5. A Fourth Kind, and Why It Is Different

The three sources above are all lifetime failures: a real object existed and its lifetime ended, or never began. There is one more way to hold an unusable pointer, and it is worth separating because the cure is different.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int a[4];
    int *p = a + 7;              /* undefined — outside the array */
    int *q = a - 1;              /* undefined — before the array */

Both are undefined at the point the pointer is computed, not when it is dereferenced. Chapter 7.8 §5 established the rule: pointer arithmetic is defined only within an array object and up to one past its end. A result outside that range is not a pointer to anything, and there was never an object there to have a lifetime.

Call this a provenance failure rather than a lifetime failure: the problem is not that the object ended, it is that the pointer never had a valid origin. The cure is correspondingly different — bounds, not lifetimes. Carry the length with the pointer (Chapter 7.7), check indices against it (Chapter 6.4), and the question cannot arise.

6. Cases That Look Like New Sources and Are Not

Four situations come up often enough to be worth placing explicitly. None of them is a fourth lifetime source; each is one of the three, wearing different clothes.

A pointer into a block that realloc moved. Chapter 9.7 §5 measured the move. The old block was released as part of the operation, so every pointer into it — including interior ones like &buf[3] — is source 3. The cure is source 3's cure: have one handle, and recompute anything derived from it after the resize.

An interior pointer into a freed block. free(base) ends the lifetime of the whole block, so base + 2 is just as invalid as base. Setting base = NULL does nothing for it — which is the limitation Chapter 9.10 §7 flagged, and the strongest argument for keeping derived pointers in a scope narrower than the block's.

A pointer to a local, stashed in a file-scope variable. The pointer's own lifetime is now long; the object's is not. Source 2, exactly as in Chapter 9.2, and the longer-lived the hiding place, the longer the gap in Figure 1's stage 2.

A pointer read back out of a block you then freed. If a block held a pointer to a second block, freeing the first does not free the second — but reading the stored pointer out of the first block after freeing it is source 3 applied to the container. Read what you need before you release the thing holding it.

7. What the Tools Actually Catch

Each cell below was measured individually, at the curriculum's standard warning level with optimisation on, and then with a static analyser.

Shape-Wall -Wextra -Wpedantic -O2clang --analyze
Source 1 — plain local, dereferencedcaught · -Wuninitializedcaught
Source 1 — an array element of pointersmissedcaught
Source 2 — return &x;caught · -Wreturn-stack-addresscaught
Source 2 — via a local pointer variablemissedcaught
Source 2 — a local array, decaying on returncaught · -Wreturn-stack-addresscaught
Source 2 — written through an out-parametermissedcaught
Source 3 — read through the freed pointermissedcaught
Source 3 — double free, one line latermissedmissed
3 of 87 of 8

Read the pattern down the column rather than cell by cell, because the pattern is the lesson.

The compiler is good when the mistake is next to the declaration. int *p; then *p — caught. return &x; — caught. Both are visible in one glance, and the compiler sees what you would see.

Its hit rate collapses as the mistake moves away. Put the same address in a local pointer first, or write it through an out-parameter, or store it in an array element, and the warning disappears. Nothing got more dangerous; the dataflow just got longer than a single statement.

It is zero for source 3. Not one of the freed-block shapes produced a warning. That is the structural reason source 3 is the one that reaches production: it is the only source with no compile-time net under it at all.

The analyser is far better and still not complete. It caught seven, including every shape the compiler missed. Its one miss is instructive: it reported the use-after-free and stopped there, so the double free on the following line was never examined. An analyser traces a path and abandons it at the first defect, so fixing only what it reports and re-running is a loop you have to repeat, not a single pass.

Run a static analyser on any C that allocates. On this evidence it more than doubles what the warning flags find, it costs one command, and it needs no test input. Chapter 13.10 covers what the run-time tools add on top, including the provenance that makes a source-3 report actionable.

8. Prevention, By Source

The three sources need three different cures, and trying to cover all three with one habit is how people end up believing p = NULL is a complete answer.

Source 1 — never initialised. Give every pointer a value where you declare it. If you do not have a real one yet, use NULL:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int *p = NULL;               /* determinate, testable, safe to free */

Chapter 6.2's rule, and this is the case where it is cheapest: NULL is always available and always meaningful.

Source 2 — the returned local. Never return, or store anywhere outliving the block, the address of an automatic object. Chapter 9.2 gave the three correct alternatives and they are still the only three: give the object static duration, let the caller supply the storage, or allocate it. Convention B from Chapter 9.8 is the usual answer.

Source 3 — the freed block. This one needs all of:

  • one owner — Chapter 9.8, the only measure that addresses the cause;
  • one free, in one place — Chapter 9.8 §8's cleanup label;
  • a pointer scope no larger than the block's lifetime — if the variable is gone, it cannot be misused;
  • p = NULL immediately after — which restores a determinate value to that variable;
  • no long-lived derived pointers — because the previous habit does not reach them.

9. What You Should Be Able to Say Now

  1. There are three sources of an invalid pointer: never initialised, an automatic object whose block exited (9.2), and a freed block (9.10).
  2. A pointer value is valid only if it addresses a live object, is null, or is one past the end of an array.
  3. One past the end may be formed and compared — verified — but never dereferenced.
  4. An invalid pointer's value is indeterminate, so reading the variable is undefined — copying, comparing, printing, not only dereferencing.
  5. That rule is why p = NULL; restores a determinate value, and why 9.10 §3 had to convert to uintptr_t before the free.
  6. Cause and symptom are far apart because stage 1 produces no event, the value travels, and the crash lands in innocent code.
  7. Out-of-range pointer arithmetic is a fourth, different failure — provenance, not lifetime — cured by bounds, not by lifetimes.
  8. A realloc that moved, an interior pointer, a stashed local, and a pointer read out of a freed container are instances of the three, not new sources.
  9. Measured: standard warning flags caught 3 of 8 shapes, a static analyser 7 of 8 — and the compiler caught none of the freed-block shapes.
  10. p = NULL is necessary and never sufficient: it fixes one variable, not the design.

10. Exercises

Exercise 1 — name the source. For each, say which of the three sources it is, or that the pointer is valid.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *a; *a = 1;
int *b = NULL;
int *c = malloc(4); free(c); printf("%p", (void *)c);
static int s; int *d = &s; /* ... much later ... */
int *e; { int t = 1; e = &t; }
int f[4]; int *g = f + 4;
int f2[4]; int *h = f2 + 4; int v = *h;

Exercise 2 — the stronger rule. Three of these four lines are undefined even though none dereferences anything. Identify them and say what the fourth one is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
free(p); int *q = p;
free(p); if (p == NULL) { }
free(p); p = NULL;
free(p); printf("%p", (void *)p);

Exercise 3 — count the stages. Take the §8 callout's four-line example and walk it through Figure 1's five stages, assuming b is dereferenced two hundred lines later. At which stage would a NULL assignment to a have helped, and why does it not help here?

Exercise 4 — classify the cases. For each, say which of the three sources it actually is.

  • a pointer to buf[3] after buf was grown with realloc and moved
  • a pointer to a local, stored in a file-scope variable
  • base + 2 after free(base)
  • a pointer read out of a block after that block was freed

Exercise 5 — explain the detection ranking. Using §7's table, explain in three sentences why the compiler catches source 1 and source 2 but never source 3. Your answer should be about what a compiler can see, not about effort.

Exercise 6 — repair by source. Each has one invalid-pointer bug. Name the source and give the minimal correct fix.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *first_even(int *a, size_t n) { int *r; for (size_t i=0;i<n;i++) if (a[i]%2==0) r=&a[i]; return r; }
int *counter(void) { int c = 0; return &c; }
void done(int *p) { free(p); p = NULL; }

Note which of the three fixes changes the function's interface rather than its body, and say why that is unavoidable.

Exercise 7 — the debugging rule. A crash report points at a string-formatting function that has not changed in two years. Write the three questions you would ask next, in order, using §4's rule.

Exercise 8 — design it away. Rewrite done from Exercise 6 so that the caller's pointer is genuinely retired, and say which chapter's mechanism you needed. Then say why the version you wrote is still not a substitute for one documented owner.

11. Summary

  • Three sources: never initialised · an automatic object whose block exited · a freed block.
  • Valid means: a live object, null, or one past the end — the last may be formed and compared, never dereferenced.
  • An invalid pointer's value is indeterminate, so even reading the variable is undefined.
  • p = NULL; restores a determinate value — and reaches exactly one variable.
  • Cause and symptom are far apart: no event at stage 1, the value travels, the crash lands on innocent code.
  • When a crash makes no sense where it is, ask who owned that storage and when its lifetime ended.
  • Out-of-range arithmetic is a provenance failure — cured by bounds, not lifetimes.
  • Measured: warning flags 3 of 8, static analyser 7 of 8, compiler 0 of 2 on freed-block shapes.
  • Run a static analyser on C that allocates. One command, no test input, more than double the coverage.

Source 1 has been on every list in this chapter and has never really been explained. A pointer declared without an initialiser is invalid because it holds an indeterminate value — and that phrase has been doing a lot of unexamined work. It is not a synonym for "a random number". The standard's definition has a second half that most C programmers have never read, and it has a consequence that will probably surprise you: a program can read the same uninitialised variable twice, in a row, with nothing in between, and the language does not require the two reads to agree. The final chapter of this module is about what indeterminate actually means, why reading one is undefined rather than merely unpredictable, and why that distinction decides whether a whole class of real optimisations is allowed to break your code.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.