Pointer Provenance and Pointer Validity
This chapter explains pointer provenance — the idea that a pointer is not just a numeric address but also carries information about which object it was derived from. This is an expert-level topic that explains why certain "clever" pointer tricks are undefined behavior even when the addresses "look right."
Why This Matters
Treating pointers as integers is the root of many subtle bugs and optimizer surprises. Provenance is the formal reason why two pointers with the same bit pattern can still be different, and why integer-to-pointer round-trips are dangerous.
Prerequisites
c.core.20— pointer arithmetic.c.object.1— effective type.
Core Concept
A pointer is more than an address
Informally, a pointer value consists of:
- an address (the machine location);
- provenance (which object/array it is associated with).
Pointer arithmetic is only defined *within* a single array object (plus one past the end). The provenance records which array that is. Two pointers with identical addresses but different provenance are not interchangeable for arithmetic or dereference.
Why provenance exists
Consider:
int a[1], b[1];
int *p = a + 1; /* one-past-the-end of a */
int *q = b; /* points to b[0] */
On many systems, p and q might happen to have the same address, but p cannot legally be dereferenced or used for further arithmetic, while q can. Provenance captures this distinction even though the address bits are equal.
Integer round-trips lose provenance
Converting a pointer to uintptr_t and back to a pointer is not guaranteed to recover a usable pointer in all cases; the round-trip preserves the *address* but provenance is implementation-defined and may be lost. In practice, round-tripping through uintptr_t is widely supported, but using the result for anything other than the original object is dangerous.
Syntax
#include <stdint.h>
uintptr_t i = (uintptr_t)p; /* implementation-defined */
int *q = (int *)i; /* implementation-defined; provenance lost */
Examples
One-past-the-end vs. valid pointer
int a[5];
int *end = a + 5; /* valid to form, not to dereference */
int *bad = a + 6; /* UB: outside array even to form */
Integer round-trip (implementation-defined)
#include <stdint.h>
#include <stdio.h>
int main(void)
{
int x = 5;
uintptr_t addr = (uintptr_t)&x;
int *p = (int *)addr; /* usually works, but not strictly portable */
printf("%d\n", *p);
return 0;
}
On mainstream hosted systems this "works," but the standard does not fully guarantee the round-trip's usability. IMPLEMENTATION-DEFINED
How It Works
The C standard describes pointers in terms of the objects they point into. The compiler's optimizer tracks provenance to prove things like "this loop does not access outside the array." When you manufacture a pointer from an integer, the compiler may not know which object it points to, so it must be conservative — but the standard still restricts what such a pointer can do.
Variations
Pointer tagging
Some code stores extra bits in the low bits of aligned pointers. This is platform-specific and, if not done with extreme care, undefined behavior.
CHERI and capability pointers
On capability architectures (e.g., CHERI), pointers are not just addresses but capabilities with bounds and permissions. Provenance is literally part of the pointer representation. HARDWARE
Common Mistakes
- Assuming a pointer is just an address.
- Comparing or subtracting pointers from different arrays (UB).
- Storing pointers in
int/long(may truncate on some platforms). - Using integer round-tripped pointers for anything beyond the original object.
Undefined Behavior
- Forming a pointer outside the array bounds (beyond one-past-the-end).
VERIFIED
- Pointer arithmetic between different array objects.
VERIFIED - Dereferencing a pointer whose provenance is not a valid object.
VERIFIED
Portability
intptr_t/uintptr_tare optional in C (they exist only if the
implementation has an integer type wide enough for a pointer). VERIFIED
- Integer↔pointer conversions are implementation-defined.
VERIFIED
Under the Hood
On conventional flat-memory machines, provenance is a compile-time concept (the hardware pointer is just an address). On capability machines, it is a run-time concept. Optimizers such as GCC/Clang increasingly model provenance (e.g., via the "pointer is not an integer" view) and can miscompile code that assumes otherwise.
Practical Usage
- Treat pointers as opaque values associated with objects, not as numbers.
- Use
uintptr_tonly for the narrow purpose of storing a pointer as an
integer when the platform guarantees round-trip, and convert back to the *same* type.
- Never fabricate pointers from arbitrary integers except in platform-specific
embedded MMIO code.
Exercises
1. Explain why a + 6 for int a[5] is UB even if no dereference occurs. 2. Write a program that round-trips a pointer through uintptr_t and verify it on your platform; document why it is not strictly portable. 3. Explain the difference between two pointers with the same address but different provenance.
Deep Challenge
Research (or reason about) why pointer comparison between pointers from different arrays is unspecified in C17, and how provenance-based compiler models make such comparisons problematic. Write a short explanation with an example where an optimizer could exploit this.
Related Concepts
c.ptr.onepast— one-past-the-end.c.ptr.arithmetic— pointer arithmetic.c.obj.aliasing— aliasing.
References
- ISO/IEC 9899:2018 §6.3.2.3 (pointer conversions), §6.5.6 (additive
operators).
- WG14 papers on pointer provenance (e.g., N2362, N2676).
Verification
- One-past-the-end is valid to form but not dereference.
VERIFIED - Integer↔pointer conversion is implementation-defined.
VERIFIED intptr_t/uintptr_tare optional.VERIFIED- No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Pointer provenance
- [ ] Pointer validity
- [ ] Integer round-trips
- [ ] One-past-the-end provenance
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.obj.provenance | 0 | 7 |