Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Is it UB to read value of heap pointer after freeing?
I've read in a guide that "the use of indeterminate memory for anything, including apparently harmless comparison or arithmetic, can have undefined behavior if the value can be a trap representation for the type."
The included quote from C11, section 6.2.4 §2, says:
The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime.
The reason I'm mentioning the quote here is partially because I don't have a copy of the C standard.
Is reading the value of a pointer returned by a heap allocation function after its deallocation truly an instance of undefined behavior? (For example, debuggers may read and output the address to various I/O destinations: printer, screen, disk, network, etc.)
Consider the following program:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
void *pointer = malloc(1);
if (pointer == NULL) return 1;
free(pointer);
printf("%p\n", pointer); /* Reading the address. Undefined behavior? */
return 0;
}
Does the specification for this differ across ANSI C and other versions of ISO C?
3 answers
You are accessing this answer with a direct link, so it's being shown above all other answers regardless of its score. You can return to the normal view.
It may or may not be undefined behavior:
- Depending on if the ABI of the given target system has trap representations for pointers and the value we ended up now happens to be a trap representation.
- Otherwise it is unspecified behavior and will work fine, but print garbage.
(C23 renamed trap values "non-value representations".)
Be aware that there is a lot of misinformation and confusion over this out on the Internet, so you must dismiss anyone who cannot give you direct quotes from the C standard. There are lots of people subjectively claiming "it is undefined behavior because I say so"...
This is a recurring "language lawyer" question.
Contrary to popular belief, it is not UB to read an indeterminate value, it is unspecified behavior. With some special exceptions, details and C standard sources are given here.
The pointer variable was initialized so it is not indeterminate for that reason. Thus it doesn't matter that it has automatic storage duration or if its address was taken etc. It became indeterminate after the free call.
The pointer value in itself may be a trap representation for that given pointer type, in which case the program invokes undefined behavior. (Which is highly unlikely in practice since nothing happened that changed the pointer lvalue itself.)
Otherwise, the pointer becomes an indeterminate value which is unspecified. These kind of "wobbly values" have been up for debate a couple of times in the C committee and the conclusion were pretty much that the value need not be consistent if read several times, or the compiler might give the same garbage value every time - we don't know and we can't assume anything about it.
Unfortunately, some bad compilers chose to be so-called "low quality implementations" in this case. That is, the compiler chose to crash and break programs needlessly when it doesn't have to. Example: clang 15 miscompiles code accessing indeterminate values. From clang 15 for x86, the compiler chose to become bats*** crazy and just cut program generation halfways, giving the programmer a half program to execute with seg fault as guaranteed outcome, which is obviously useless. That compiler remains just as broken as of today.
However, the specific example of the question (which is far less nasty than the one I cooked up in the SO post) actually runs even in clang v22 x86, printing a garbage value. x86_64 does at least to my knowledge not have any trap representations for void* or other object pointers.
EDIT
To clarify to everyone why accessing the pointer variable itself while it is indeterminate cannot reasonably be UB, lets consider this:
int* p=NULL;
{
int var=1;
if(something)
p=&var;
}
if(p == NULL)
....
After the inner scope ends, p might point to a variable no longer existing - the value of p is indeterminate. Now if this was UB, then we couldn't do the final if(p == NULL) check or the program might crash on that line. That's not sensible, it would break the whole language.
Even more obvious:
int* p = malloc(n);
free(p);
p = NULL;
If using p while the value is indeterminate, you wouldn't even be able to write bog standard code like the above, because the assignment involves a value computation of the left operand p before it is written to (C23 6.5.17.1).
Summary: if accessing the pointer while it's value is indeterminate, that pointer variable would turn into some kind of explosive mine that might crash the program if we ever try to reuse it again. We would never be able to verify or re-use pointer variables.
1 comment thread
The following users marked this post as Works for me:
| User | Comment | Date |
|---|---|---|
| Intel A80486DX2-66 |
Thread: Works for me With the essential clarification provided by alx in their comment |
Aug 23, 2026 at 01:49 |
This is not a C issue, but really about dynamically allocated memory in general.
Think about it. When you allocate a chunk of dynamic memory, the heap manager finds an available memory region of the right size (or tells you it can't give you the requested memory), marks it as in-use, and returns you a pointer to it. When you release the chunk, the heap manager marks it as unused.
What happens to the memory after that is not your business. You have no guarantee that the memory hasn't been allocated by another thread, implicitly by the run-time library, or whether the address is even still valid in your address space. It could cause a memory fault if you try to read from it.
Is reading the address returned by a heap allocation function after its deallocation truly an instance of undefined behavior?
Yes! That's the point of deallocating memory. You are saying that you're done with it. That means something else is free to use it, and may have by the time you try dereferencing the pointer again. Depending on the OS, it may have been marked as invalid memory for you and trigger some kind of trap when you try to access it.
Again, think about it. How else do you expect it to work? If you still wanted to access the memory, then you wouldn't deallocate it yet. Once you do deallocate it, it's not yours anymore.
EDIT 3: So the C standard example is now apparently being misinterpreted. Guess I’ll have to explain it.
EDIT 2: So I’ve been asked to provide sources for my answer. Fair enough. To recap, my answer is:
- A pointer to an object becomes indeterminate when that object’s lifetime ends.
- Accessing that indeterminate value is UB.
My source is the current ISO C standard, ISO/IEC 9899:2023, 6.2.4.p2. I’ve inserted bold highlighting, and my notes in parentheses:
(...) If an object is referred to outside of its lifetime, the behavior is undefined. If a pointer value is used in an evaluation after the object the pointer points to (or just past) reaches the end of its lifetime, the behavior is undefined. (← that’s point 2) The representation of a pointer object becomes indeterminate when the object the pointer points to (or just past) reaches the end of its lifetime. (← that’s point 1)
There is also a second section later on that makes the same point in a more complex and roundabout way, in 6.5.3.6.p19-20. It shows a block of code with a fake loop construct using goto, where the pointer is set to an object that exists within the “loop”, then used in a comparison later. It says (in p19) that this is well-defined, because the object is still alive when the comparison happens. Then p20 says (again, the bold is from me):
If an iteration statement were used instead of an explicit
gotoand a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time aroundpwould have indeterminate representation, which would result in undefined behavior.
I don’t claim to be a language lawyer, but I see no other interpretation to that, other than the answer to the topic question is unequivocally “yes”. Not “maybe”. Not “on some platforms”. Just “yes, it’s UB”. Always.
<<<<<<<< (BEGIN EDIT 3) >>>>>>>>
Let’s look at the actual code in the example:
struct s { int i; };
int f (void)
{
struct s *p = 0, *q;
int j = 0;
again:
q = p, p = &((struct s){ j++ });
if (j < 2) goto again;
return p == q && q->i == 1;
}
And this is the actual text:
(paragraph 19) EXAMPLE 9 Each compound literal creates only a single object in a given scope:
The function
f()always returns the value 1.(paragraph 20) If an iteration statement were used instead of an explicit
gotoand a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time aroundpwould have indeterminate representation, which would result in undefined behavior.
It’s paragraph 20 that is most relevant to us. It’s describing a hypothetical modification to the example that might look like this:
struct s { int i; };
int f (void)
{
struct s *p = 0, *q;
int j = 0;
do
{
q = p, p = &((struct s){ j++ });
} while (j < 2);
return p == q && q->i == 1;
}
On the entry to the loop the first time around, p is 0, q is 🤷🏼, and j is 0. That should be clear, I hope.
At the end of the loop the first time around, p points to the unnamed object, q is 0, and j is 1. Step through the code carefully to convince yourself that this is true.
Now here’s where the divergence happens. in the goto version, execution just jumps back up to the label… but nothing else happens. So p still points to the unnamed object created the first time through the loop, q is still 0, and j is still 1, and we continue from there. No problems.
But in the structured loop version, before jumping back to the start of the loop, the unnamed object p points to ends its lifetime. Per 6.2.4.p2… and these are the literal words of the standard: “The representation of a pointer object becomes indeterminate when the object the pointer points to (or just past) reaches the end of its lifetime.” The object p points to has reached the end of its lifetime, so p becomes indeterminate. Not *p. p becomes indeterminate.
So now, on entry next time around p would have indeterminate representation… that is the literal text of the standard… q is 0, and j is 1. And then there are 3 expressions within the loop:
q = pp = &((struct s){ j++ })j < 2
(Technically there is a 4th and a 5th expression. One is the evaluation of the result of q = p… which is effectively evaluating q after the assignment. There is also the evaluation of the result of p = &((struct s){ j++ }), both as the result of the assignment, and due to the comma operator. But in both cases, if the assignment itself is kosher, then result of the assignment is also kosher (because both assignments are assignments to the same type, so no conversions are happening). So we’ll just skip worrying about those for brevity.)
Given that j is 1, there is no possibility that either the evaluation of j in 2, or the entire expression 3, can either be UB.
That leaves 2 possibilities:
q = pp = <address of a struct>
Now p is indeterminate, but even so, the assignment in 2 cannot be UB, because it is an assignment expression, and the left-hand operand of an assignment expression does not get evaluated (until after assignment). This should be obvious. If the lhs of an assignment were evaluated, then int i; i = 0; would be UB on any system where int has trap representations, and the uninitialized int gets set to one of those trap representation. But you don’t even need intuition to figure this out. The literal text of the standard describing how assignment works (6.5.17.2p2) says that the value in the left hand side gets replaced… not evaluated: replaced. (By contrast, the following section on compound assignment does literally specify that the left-hand side gets evaluated (once). So they obviously didn’t just “forget” to mention it.)
So the only possible source for UB “on entry next time around” is expression 1: q = p. And it can’t be the q causing the issue, because 0 is a perfectly cromulent value for a pointer (it sets the pointer value to the null value)… and even if it weren’t, well, the lhs of an assignment isn’t evaluated until after the assignment. So q can’t be the problem.
The only possible source for the UB has to be p. Not *p. Just p. p has indeterminate value. The simple act of evaluating that indeterminate value in order to put it in q is what triggers the UB.
This jibes with the text of 6.2.4.p2: “If a pointer value is used in an evaluation after the object the pointer points to (or just past) reaches the end of its lifetime, the behavior is undefined.”
It also matches the text describing the example: “If an iteration statement were used instead of an explicit goto and a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time around p would have indeterminate representation, which would result in undefined behavior.”
And it matches the case in the OP question exactly: There is an object which has been destroyed, and a dangling pointer to it… and MERELY reading the value of that pointer… NOT dereferencing it, just evaluating it… is UB.
There is literal and explicit text in the C standard saying that a pointer to an object becomes indeterminate when the object lifetime ends, and that evaluating that pointer—getting its value in any context—is, literally and explicitly, undefined behaviour. And it is backed up with an example that reinforces that.
There is not a single word in the standard saying that it is safe to access a pointer value after the object it points to has gone out of scope. There is not a single example showing that this is okay.
Q.E.D. ∎
<<<<<<<< (END EDIT 3) >>>>>>>>
I have also been asked for a pre-C++26 source. Again, fair enough; C++26 is technically not the current standard (yet!).
But I don’t have the C++23 standard, so here is something from the C++17 standard, which is from about a decade ago. This is from [dcl.init], paragraph 12:
If no initializer is specified for an object, the object is default-initialized. When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced (8.18). [ Note: Objects with static or thread storage duration are zero-initialized, see 6.6.2. — end note ] If an indeterminate value is produced by an evaluation, the behavior is undefined except in the following cases:
(The “following cases” are exemptions for the “raw storage” types—unsigned char and std::byte—that are too complex to go into here, and don’t really matter.)
There may have been some confusion because in a section dedicated specifically to “NullablePointer requirements” ([nullablepointer.requirements]) it only says it MAY cause undefined behaviour:
paragraph 1: A
NullablePointertype is a pointer-like type that supports null values. A typePmeets the requirements ofNullablePointerif: (...)paragraph 2: A value-initialized object of type
Pproduces the null value of the type. The null value shall be equivalent only to itself. A default-initialized object of typePmay have an indeterminate value. [ Note: Operations involving indeterminate values may cause undefined behavior. — end note ]
But NullablePointer is not specifically about raw pointers; it is a generic requirement. std::unique_ptr is a NullablePointer, and it does not get default-initialized to an indeterminate value, and thus does not cause undefined behaviour. A smart pointer type could conceivably get default-initialized to an indeterminate value, and thus (potentially) cause UB… but I don’t think any standard library ones do.
Also, it does only say that operations on indeterminate values may cause undefined behaviour. The reason why is simply because not all operations on indeterminate values cause undefined behaviour. Specifically, as stated in [dcl.init]p12, replacing an indeterminate value is fine (the section “8.18” mentioned in parentheses is the section on assignment operations). However, again, as [dcl.init]p12 very clearly states, any operation that produces an indeterminate value… such as reading a variable that has indeterminate value… is UB. So that “may” is not saying that reading indeterminate values is sometimes not UB… it is saying what it literally says: that some operations on indeterminate values are not UB: Overwriting an indeterminate value is not UB; any operation that produces an indeterminate value (that is: any load) is UB.
EDIT: Talked with a C++ expert, and I was misunderstanding EB.
tl;dr Reading a pointer value after free is definitely UB.
RATIONALE:
- After free, a pointer value becomes indeterminate.
- Any read of an indeterminate value is undefined behaviour.
My misconception about erroneous behaviour was that some reads of indeterminate values became EB rather than UB. That was wrong. What C++26 did was create a new type of value—an erroneous value—and said that some formerly indeterminate values were now erroneous values. Reading an erroneous value is EB… but reading any indeterminate value remains UB.
For example:
- Before C++26,
int a;leavesawith an indeterminate value. Doing anything with an indeterminate value, likeint b = a;, is UB. - C++26 made the value of
ainint a;an erroneous value instead. So now doing anything with it, likeint b = a;, is EB, not UB.
But a pointer after free is still an indeterminate value, so reading it is still UB.
ORIGINAL ANSWER:
I am not a C expert, but I believe the answer is that it definitely used to be undefined behaviour (UB)… but may now only be implementation-defined behaviour (IB), or something similar. To be clear, I am not sure that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.
However… I believe what you are actually doing in that code might be unequivocally elevating it to UB.
In case you aren’t aware, the difference is:
- IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) incorrect (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean the whole program is invalid.
- UB, undefined behaviour, means that “anything can happen”… literally anything… and no-one is obliged to even attempt to tell you what that might include, because any instance of UB means the whole program is invalid.
I think your reasoning about debugging is correct. De-referencing a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.
The technical wording of it boils down to that the value in a pointer becomes indeterminate… not necessarily invalid… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.
But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) used to be UB, but I think that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.
Again there’s the however…
You have passed a pointer with an indeterminate value to printf(). Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value may be invalid, and I see no exemption for indeterminate pointer values in printf(), so… kaboom goes the program. Maybe.
So in summary:
- A pointer value becomes indeterminate when it no longer points to a valid object, such as after de-allocation.
- Reading or copying an indeterminate value (of any kind, not just pointers) was definitely UB at some point, but that may have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
- A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
- Passing an invalid value to C library functions is UB… but passing an indeterminate value…?
Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.
But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.

1 comment thread