38 The rules of pointers — alignment and provenance
What to know first
Looking back
Chapter 4 taught the alignment rule that “a four-byte load goes at a multiple of four.” But a pointer is merely a variable holding a number — why can it not hold any number at all?
A. Holding it is often not blocked at all — the accident happens when it is followed. Dereferencing through an int* means “read four bytes at that address through int’s eye” (chapter 36), and if that address breaks int’s alignment (a multiple of four) then chapter 4′s consequences follow — slowness, or on some machines a bus fault on the spot. In the standard’s eye, the cast to a type with stricter alignment is already outside the contract. A pointer’s type is both an eye and a contract.
The need for this chapter, and its context
By the end of this chapter
char* alone), and a practical feel for the provenance (the tag of origin) foreshadowed in chapter 15. The latter section is a rare place where this book treats something not yet in the standard’s text — why such a concept became necessary, how far it has been settled, what changes if it lands, and that machines enforcing it in hardware already exist. The most “contract-like” of this part’s safety rules.The questions this chapter answers
- Is it not common for code that breaks these rules to “run fine on my computer”?
- If it is all settled, why not simply put it in the standard?
- So is this rule really preparation for special machines like CHERI?
38.1 Alignment and casts — the privilege of char*#
Pointer casts (looking through a changed type) are grammatically free but narrow by contract. Reduced to a practical sentence — a cast to a type demanding stricter alignment is dangerous whenever the address does not satisfy that alignment. Converting an unaligned char* into an int* (alignment 4) and following it is the typical violation; converting back a pointer that originally came from a properly aligned int object is fully defined. The opposite direction is safe — and here lies a privilege C intended: a pointer to a character type (char*, signed char* and unsigned char*) may look at any object byte by byte. With alignment 1 it can point anywhere, and the standard explicitly permits that “the representation of any object may be read through the eye of bytes” (chapter 14′s strict aliasing has this exception carved into it too). The official channel for the perspective of chapter 3, “inside a slot it is only bits”, is unsigned char*.
The alignment requirement of each type can be asked with alignof (a C23 keyword):
examples-en/ch37/align.c
#include <stdio.h>
int main(void)
{
printf("alignof(char) = %zu\n", alignof(char));
printf("alignof(int) = %zu\n", alignof(int));
printf("alignof(double) = %zu\n", alignof(double));
return 0;
}
Output
alignof(char) = 1
alignof(int) = 4
alignof(double) = 8
38.1.1 The two privileges side by side#
Here we tidy up the two passages announced in chapter 36. They do different work.
void * | the char * family | |
|---|---|---|
| what it forgets | it forgets the type pointed at | it forgets nothing — it sees the representation as it is |
| dereference | no (it does not know the size) | yes — one byte at a time |
| arithmetic | no (by the standard) | yes — in bytes |
| aliasing | not applicable | it may read the representation of any object (the excepted passage) |
| round trip | object pointer ↔ void * returns the original value | reading bytes and recovering a pointer are different matters |
Table 38.1 — The privileges of void * and the character pointers
This is why memcpy takes void * in its prototype and copies bytes inside — it forgets the type on the way in and looks at bytes on the way through. That the standard requires void * and the character-type pointers to share a representation is for exactly this combination (chapter 36′s word-addressed machines).
A common misconception. “Since char * can pick an object apart, a pointer may be built out of the bytes”
Two things are mixed up here. Reading is permitted — the representation of any object may be read byte by byte through unsigned char *. But assembling those bytes back into a pointer and making it point at another object is a separate matter, because the provenance attached to that pointer belongs to the original object — which is exactly the subject of the next section.
The practical conclusion is simple. To move a representation, move the value with memcpy; if a pointer is needed, obtain it again from the original object. An address assembled out of bytes mostly works on x86-64, but on a machine where pointers carry permissions, such as CHERI (capability hardware enhanced RISC instructions), it is stopped on the spot (chapter 36).
38.2 Provenance — the same number, a different origin#
Here chapter 15′s foreshadowing turns into practical instinct. In the naive picture a pointer is only a number, so if the number is right it should be followable however it was made — but the contract of modern C is not like that. A pointer has an origin (provenance): an invisible tag saying which object it derived from. Reduced to three practical rules.
- Pointer arithmetic stays inside the object it was born in. Taking a pointer that started from one object’s address and pushing it (chapter 39) onto another
object — even if the number really does coincide with that object — is outside the contract.
- One past the end is permitted. Pointing at the slot “after” the end of an array (one-past-the-end) is legal (as long as you do not follow it). It is so useful in a loop’s ending condition that the standard carved out this place specially (in action in chapter 39).
- To treat it as a number, use the official channel. Converting a pointer to an integer to store or compute with it (such as chapter 4′s tagged pointers) has a road provided by the contract: going through the dedicated type
uintptr_t.
Q. Is it not common for code that breaks these rules to “run fine on my computer”?
A. It is common — and that is why these rules are dangerous. What collects the price of a violation is not the machine but the compiler’s optimisation (chapter 14): it rearranges and caches on the premise that “a pointer does not point outside its origin”, so violating code silently behaves differently when the optimisation level or the compiler version changes. That “it ran just now” is not evidence of keeping the contract — that the criterion is chapter 15′s “is it correct on the abstract machine” — is this chapter’s conclusion, and chapter 54 shows the whole of this subject.
38.2.1 A word that is not in the standard yet#
One thing must be said honestly here. Provenance is not a term of the C23 standard text. Search C23 from cover to cover and there is no clause defining that word. This book still spends a section on it for three reasons.
- Compilers already optimise on this concept. That it is absent from the standard does not mean it may be kept or ignored at will.
- The committee has finished writing it down, in a separate technical specification (ISO/IEC TS 6010, 2025). Only the folding into the main text remains.
- What that specification does is not to pile on new rules but to fill the gaps between rules that already existed and make them fit together.
The third is the heart of this section. Even when the concept enters the standard, there is no new syntax to learn. The rules you must keep stay as they are — but for the first time the standard will explain, in one word, why.
38.2.2 Where was the gap — two neighbouring variables#
Look at just one place where the explanation stalls without this concept. Two variables laid side by side at file scope.
int y = 2, x = 1;
int *p = &x + 1; /* the slot "one past" x — making it is legal */
int *q = &y;
if (p == q) /* this can be true */
*p = 11; /* but what about this line? */The question splits in two. Can p == q be true? It can. If the two variables really are placed next to each other the addresses coincide, and which way the comparison then goes is not fixed by the standard (unspecified). Then, inside that true branch, may y be modified through *p? No. p was born from x, and the one-past-the-end slot is legal to point at, not to follow.
This is where the naive picture collapses. “Two pointers that answered equal cannot be used interchangeably” is a self-contradiction in the pointer = number picture. And it is the real compilers that stand on the contradictory side — with optimisation on, GCC and Clang alike move and delete code on the premise that “p and q came from different objects, so they cannot overlap.” The code above therefore leaves y at 11 or at 2 depending on the compiler and the optimisation level. It matters, too, that this is separate from chapter 14′s strict aliasing — -fno-strict-aliasing does not make this premise go away.
The standard’s text was silent about the situation. As a result three parties have been talking past each other for twenty years. The text reads as “a pointer is a value”, the compiler optimises as “different origin, different thing”, and the programmer says “it ran on my machine.” With no referee, one cannot even decide whether the compiler produced a bug or the program broke the contract.
38.2.3 Where the word was born — DR260#
The word first appears in an official document in 2004, in the committee’s response to Defect Report 260. The gist reduces to two sentences.
“ An implementation may track the origins of a bit pattern. Pointers with different origins may be treated as distinct even when they are bitwise identical. ”
Permission was granted. But that permission never reached the standard’s text. There was no definition, and no boundary of what was allowed. In that state the compilers went ahead and built provenance-based alias analysis on the response.
What happens when an undefined concept becomes the ground of optimisation — that is the lesson of this story. Nobody can settle which idiom is legal. Half of the old low-level code sinks into a grey zone.
38.2.4 Writing it down — from a questionnaire to a technical specification#
It is worth noting that the work began in an unusual way. What came first was not drafting clauses but asking. In 2013 researchers at Cambridge circulated a survey of forty-two questions on pointers and object representation, gathering what working C programmers and implementers actually take to be legal, and the analysis went to the committee as documents.1 Only then were the open issues distilled into twenty questions (Q1–Q20) and put to the committee as “ought this to be legal?”2 Measuring what reality does before changing the standard.
A model was built on top of that. Its name is PNVI-ae-udi; the name is not worth memorising, and in practice it is three lines.
- Every object (allocation) carries one invisible tag of origin. A pointer value is the pair «origin + address».
- Casting a pointer to an integer exposes that object’s address (address exposed).
- Casting an integer back to a pointer looks among the objects that have been exposed for one holding that address and revives its origin. If none was ever exposed, there is no origin to revive — such a pointer cannot be followed.
In a single line: only an object whose address was once handed out as an integer can be recovered from that integer. Why a uintptr_t round trip is the official channel, and why an address assembled out of bytes is not, both fall out of that one line — the two rules are not separate decrees but consequences of one model. That is what “tidying up” means here.
The result of the work is ISO/IEC TS 6010 (2025), “a provenance-aware memory object model for C”.3 It came out not as the standard’s text but as a separate Technical Specification, and it defines itself in those terms — not a complete specification of the language, but a document that constrains and clarifies the memory object model implicit in ISO/IEC 9899:2018. A conforming implementation must behave “as if these indicated differences to ISO/IEC 9899:2018 had been integrated into ISO/IEC 9899”.
It also marks its own boundary: this specification does not address subobject provenance. How far a pointer to a structure member may walk is still an open question.
Q. If it is all settled, why not simply put it in the standard?
A. Because changing the text without implementation experience is risky. The specification records the circumstance itself: WG14 and the corresponding C++ subgroup both approved the overall direction, subject to implementation experience. Around the same time the proposal was discussed with the Clang/LLVM and GCC communities at EuroLLVM and the GNU Tools Cauldron.
Publishing separately first gives compiler writers time to implement it and to measure how much real code it breaks. Chapter 1′s answer to “why is standardisation this slow” is here in full — because what is nailed down once lasts thirty years.
The numbers and names are not for memorising. One thing is worth keeping: pointer provenance is not anyone’s taste but a concept hardened into a document through the standardisation process, and a candidate for the next revision’s text.
38.2.5 What changes if it lands#
To answer the most important part first — nothing happens to most code. No new keyword, no new syntax, no reason to rebuild. What changes is the number of greys. The idioms now sunk in the grey zone separate into black and white.
| idiom | today (by the text) | with the provenance model |
|---|---|---|
uintptr_t round trip | guaranteed, but on hazy grounds | legal — the cast to an integer is itself the “exposure” |
copying a pointer representation with memcpy | grey | legal — the origin travels along |
| tagged pointers (chapter 4) | grey | legal if the round-trip channel is respected |
| XOR linked lists | grey | illegal — a value mixing two origins has none |
| a pointer pushed outside its object | illegal (via the array clause) | illegal — with the reason unified as “origin” |
| an address assembled from bytes or I/O | grey | illegal — an origin never exposed cannot be revived |
Table 38.2 — Idioms that separate once the provenance model lands
What to read in the table is not the individual verdicts but the fact that verdicts became possible. Today those cells could only say “probably”. The other changes are these.
- The compiler gains a ground. Today’s provenance-based optimisation leans on a single twenty-year-old defect response. With the model in the text, which rearrangement is legal can be decided mechanically — meaning there is finally a referee between a compiler’s bug and a program’s violation.
- Tools become precise. A sanitiser (chapter 18) catches “a bad address” today but is poor at “an address with the wrong origin”. Once the rules are written down, a diagnostic can say “the origin was lost here”, and formal verification tools gain a basis for proving C programs correct.
- Teaching and documentation change. Explanations that ended at “that is UB (undefined behaviour)” become “that pointer was not born from this object”. Which is exactly the shape of the explanation this book is giving now.
- This is not C’s problem alone. Rust settled the same problem first at library level — APIs such as
with_addr, which swaps only the address,expose_provenance, which makes exposure explicit, andwith_exposed_provenance, which takes it back; they were stabilized in release 1.84 in January 2025.4 The same discussion runs through LLVM’s intermediate representation. That several languages converge on the same conclusion says this is not C’s sophistry but the nature of the machinery called a compiler.
A common misconception. “It is not in the standard yet, so it need not be minded for now”
38.2.6 Machines that carry the origin in hardware#
None of this is confined to paper. Machines already exist in which the origin rides on the pointer and the hardware enforces it.
In practice. Capability-based architectures
CHERI. The architecture met in chapter 36 as a story about size. Here we look at it again from the angle of origin. A CHERI pointer is «address + bounds + permissions + one validity tag bit». The last item is decisive — the tag is not kept alongside the data but out of band, and ordinary instructions cannot set it. Manufacture an address with integer arithmetic and store it there, and the hardware clears the tag. A value that has lost its tag is no longer a pointer but merely a number, and it is stopped the moment it is followed.
Arm Morello. A real prototype board, shipped in early 2022.5 Memory was extended to carry tag bits as well — that is, supporting this concept takes not only the CPU core but the whole machine.
CHERIoT. The same idea pushed to the very small end, aimed at 32-bit RISC-V-class devices for the internet of things; it began as an instruction-set extension designed at Microsoft and has reached real boards. Evidence that this is not a server-only story.
The idea is not new. Tagged machines are the older lineage, in fact. The 128-bit tagged pointers of IBM’s System/38 and AS/400 seen in chapter 36, and before them the tagged words of the Burroughs B5000 line, put the same thought into hardware. It merely happened that C was born on the untagged branch.
Platform note. An easily confused neighbour
To sum up, there are two referees. On x86-64 the price of breaking provenance is collected by the compiler (so it is quiet and late); on a capability machine it is collected by the hardware, on the spot (so it is loud and immediate). Either way it is collected.
Q. So is this rule really preparation for special machines like CHERI?
A. No — the order is the other way round. The provenance rules did not arise because of CHERI; they became necessary because the optimising compiler on ordinary x86-64 already runs on that premise. CHERI merely moved the rule into hardware. Code that keeps the rule therefore gets a bonus: almost nothing to change when it moves to the new machines. Code resting on “it ran fine on x86-64”, by contrast, breaks once when the optimisation level changes and again when the machine does.
We have the rules of pointers too. Now we take these tools to contiguous memory — the array. The credit of line[100], carried since chapter 26, is settled in the next chapter.
Notes
- WG14 N2013, “C memory object and value semantics: the space of de facto and ISO standards”, and N2014, “What is C in practice? (Cerberus survey v2): Analysis of Responses”, 2016. ↩
- WG14 N2219 and N2263, “Clarifying Pointer Provenance (Q1-Q20)”, Memarian, Gomes and Sewell, 2018. ↩
- ISO/IEC TS 6010:2025, Programming languages — C — A provenance-aware memory object model for C, published May 2025. The working draft is public as WG14 N3231. ↩
- Rust 1.84.0 release notes, 2025-01-09: the “strict provenance” and “exposed provenance” API set. ↩
- Arm Morello — a prototype SoC and development board implementing a CHERI-extended ARMv8-A processor, shipped January 2022; a joint effort of Arm and the CTSRD/CHERI project at the University of Cambridge. ↩