Whats the use of zero sized memblocks? - c

By the worry of my last days/weeks when I figured out that much of my code does break c99 rules, what is leading into undefined behaviour, I started explicitly reading the ISO/IEC 9899:TC3 draft paper.
Especially the Appendix "J.2 Undefined behaviour"
The most of it was logical to me why it could be hard to compile code that's breaking those rules, or at some cases I could at least think "well, I don't get it, what's the problem with that, but I 'shall' do it that way"
but there is one point...
"A non-null pointer returned by a call to the calloc, malloc, or realloc function with a zero requested size is used to access an object (7.20.3)."
(for all who haven't read ISO/IEC 9899:TC3 "J.2 Undefined behaviour", this section just explains which cases will run into undefined behaviour).
So there are so many Questions in my head about that case.
First of all:
Why should I want to allocate a memory-block of zero size?
And when I have got such a block, what can I do with it?
With the aim of avoiding undefined behaviours, probably I don't want to access the memory its pointing to...
So I did a bit more research.... looked for some different malloc() man pages.
and found out in the Linux malloc(3) man:
"If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free()."
Well the only thing this helped me with was: I got now additional questions to you and myself.
The case that function call with identical parameters under identical conditions may return different results isn't that hard to imagine, OK... but those different results mostly don't have to be tread that different.
Thats what lets me suggesting, a non-null pointer to a requested zero size block could just be a unwanted side-effect.
Does this mean
if ((void *ptr = malloc (0)) == NULL)
{
/*...*/
}
this isn't enough?
do I have to handle *alloc calls like this?
if (X <= 0)
{
if ((*ptr = malloc (X)) != NULL)
{
exit (*);
}
else
{
/*...*/
}
}
else
{
if ((*ptr = malloc (X)) == NULL)
{
/*...*/
}
}
But even if its expected, to get such an
"unique pointer value that can later be successfully passed to free()"
,
how to work with it?
I could change it around OK...
I'm even allowed to free it (BTW does it mean I HAVE to free it as I should do with every other allocated memory too, or is it just an >you are allowed to, to don't break your code flow
what would be the difference to just make any pointer like this?
void *X = (void *)"1234abc";
I hope any one can help me with that philosophy of science or is even better as interested as I'm, in it.

C does not support zero-sized objects, but the argument to malloc() is of type size_t, and there's no good way to prevent a program from calling malloc(0). It may not be the programmer's intent, but it's not necessarily literally malloc(0); it's more likely to be malloc(count), where count is the result of some computation.
As for the standard permitting two different behaviors, that's simply because existing implementations (at the time the original standard was being written) did different things, and the authors wanted to avoid breaking existing code. Such code was arguably already broken, or at least non-portable, but by permitting either behavior a program that made assumptions about how malloc(0) behaves could continue to work on the system for which it was written.
If you're looking for a coherent explanation, you're not going to find one. If C were being designed from scratch today, the behavior of malloc(0) almost certainly would have been nailed down, one way or the other. Either that, or the behavior would have been made undefined, but making it implementation-defined means that code doesn't have to check quite as carefully that it's not passing zero to malloc().
And in fact the committee's decision is documented in the C99 Rationale, section 7.20.3, pages 160-161.
It does mean that:
void *ptr = malloc(0);
free(ptr);
will work correctly; free() does nothing if its argument is a null pointer.
What can you do with the result of malloc(0)? Well, if malloc(1024) is successful, you can store 1024 bytes in the allocated space. You can store no bytes in the space allocated by malloc(0) -- which is exactly what you asked for.

Many memory allocation routines have a minimum-sized allocation block which they can support, and will expand any allocation request to that minimum size. While they could have had malloc check whether the size is zero and return null if it is, it was simpler and easier to have all malloc requests less than the minimum size (including those for exactly zero bytes) get padded out to the minimum size. Since the only code which would be likely to request a zero-byte malloc would be asking for a buffer whose size was determined at runtime, and since any such code would expect to free any buffer it had asked for without regard for its size, such behavior caused no difficulty.
Another thing to consider is that if realloc is called on a block multiple times, it may be desirable to keep on using the same memory block if it has adequate space available. If a block which has e.g. 256 bytes allocated to it gets sized to zero, it may be desirable to hold off for awhile on using the formerly-occupied 256 bytes for any purpose other than re-expanding that memory block. If calling realloc on a memory block with a requested size of zero returns a pointer to that same block, then the system will be able to recognize a subsequent request to re-expand that block and use the formerly-occupied space to satisfy it. If the zero-byte realloc returned a null pointer, a request to expand that block to a non-zero size would be indistinguishable from a request to allocate a new one.
Code which uses realloc (or anything malloc related, for that matter) will generally not have any control over memory allocation and recycling policy, but some implementations of the "malloc library" may benefit from having code which doesn't need something at the moment but will likely need it in future reallocate it to size zero rather than freeing it. Note that while the usage pattern which is best for one library may be sub-optimal in another, code which is uses "malloc library" functions in a standards-compliant fashion should work, even if not optimally, on any machine which implements them standards-compliant fashion. If one knows that for the next year or two a program is likely to be run on a machine with a particular allocator, it may be helpful to write the code in the way that's optimal for that allocator. The program might have in future have to run on a machine for which its allocation pattern is no longer optimal, but if that machine is faster and has more RAM (as will likely be the case), the suboptimal performance probably won't pattern.

Your code can be more explicit, as in:
if (X <= 0)
{
ptr = NULL;
}
else
{
ptr = malloc(X);
if (ptr == NULL)
{
/*...*/
}
}
and this can be distilled to:
ptr = (X <= 0) ? NULL : malloc(X);
if (ptr == NULL) { /* ... */}
Today, in C, if malloc cannot acquire the requested storage it returns NULL. Also, this code completely avoids the question and more importantly avoids a potential pitfall.

Related

Is it safe to assume that realloc()ing to a smaller size will always succeed? [duplicate]

This question already has answers here:
Can realloc fail (return NULL) when trimming?
(7 answers)
can realloc move pointer if new size smaller?
(6 answers)
Closed 2 years ago.
There is no reason for realloc()ing to a smaller size will fail. It's freeing up the remainder. I see no reason at all for it to fail. This being said, is it safe to assume that realloc()ing to a smaller size will never fail?
"There is no reason for realloc()ing to a smaller size will fail." is an assertion without evidence.
As the spec for the Standard C library does not require a reduction to never fail, robust code would not assume an error is not possible, even if unlikely.
In particular, C17dr spec has Future library directions which discusses reduction to 0.
Invoking realloc with a size argument equal to zero is an obsolescent feature.
I take this to imply now and in the future, the following code which reduces the allocation to 0 should be avoided.
void *p = malloc(42);
...
realloc(p, 0); // Obsolete
// and instead
free(p);
There is no reason for realloc()ing to a smaller size will fail.
Consider an implementation that grabs large blocks from the platform's underlying address space allocator and dices them into small pieces. A realloc that reduces the size of the allocation might require a new block to be allocated if the size requested is not within the range of supported sizes for the large block the block being reallocated came from.
In this case, the implementation will need to get a smaller block from a sub-allocator whose range of serviced sizes includes the size requested. That sub-allocator may not have any free blocks and when it requests a new large block to dice up, that can fail.
So the premise of this question is false.
Also, in general, it is a terrible idea to leap from "I cannot think of any reason this would fail" to "I can assume this will not fail". There are many stories of things that failed for reasons people could not foresee and some of them have horrible consequences.
From the linux manpages:
The realloc() function returns a pointer to the newly allocated memory, which is suitably aligned for any built-in type and may be different from ptr. [...] If realloc() fails, the original block is left untouched; it is not freed or moved.
It cannot be assumed that the block will not be moved because this is implementation-specific. For example, the block could be moved in the case of compactification.
For cases where the requested size is smaller than the original and non-zero, one could safely make a copy of the original pointer before calling realloc, and set the pointer back to that value in case the realloc returns null. If the realloc size is zero, things are a bit murky. Some implementations would treat realloc(ptr, 0); as equivalent to free(ptr); return 0;, which would return null after freeing the object, but others would treat it as equivalent to realloc(ptr,1);, which would only return null in cases where the original pointer would still be valid. Unfortunately, there's no general way of knowing which behavior an implementation would use, and thus no way to properly handle a null return from realloc(ptr, 0);.
TL;DR
No, you cannot assume that.
There is no reason for realloc()ing to a smaller size will fail. It's freeing up the remainder. I see no reason at all for it to fail.
chux covered details about this pretty well. So I answer in a more general way.
The type of reasoning you're using here is a quite dangerous one. Your basic reasoning is "I cannot see why X is true, therefore I assume X is false." Be very careful with reasoning that way.
First, let's skip the very obvious danger that even though you cannot see any reason for realloc failing in this case, that does not mean that you are correct.
Instead let's assume that you are correct. That there is provably no rational reason whatsoever to implement realloc in such a way that it would ever fail is the new size is equal or smaller than the original. Then it's still a faulty argument, because you cannot assume that the programmers who coded the implementation you're using had this knowledge. There was a provably optimal way of doing this, but the coders did not know that.
Also, the very fact that the C standard does NOT say that this is safe is a good indication (however, not a proof) that there are good reasons to not give that guarantee.
If the specifications does not say that it always succeeds during some circumstances, then you should always consider the risk of failure as non-zero. And in this case, the standard does not give any promises, so no you cannot assume that.
Also, in reality there is often the case where it would be relatively easy to implement things in a "good" way, but it will still be more complicated than the simplest way. And sometimes that simplicity is desirable. The easiest way I can think of to implement realloc is something like this:
void *realloc(void *ptr, size_t new_size)
{
void *ret = malloc(new_size);
if(ret) {
memcpy(ret, ptr, new_size);
free(ptr);
}
return ret;
}
One very valid reason of implementing it this way would be that you have a specific environment where you typically never would use realloc and you throw this in for the sole purpose of conforming to the standard. And whenever you do something for the sole purpose of conforming to a standard or specification, you would typically go for simplicity above all else.

C malloc/free corruption general questions

This question is similar to c malloc questions (mem corruption) but I ask it again because I want more specific information than what was provided.
So I have a program with one malloc, followed by some complex code, followed by one free. Somewhere in the complex code, memory gets corrupted by either a double-free or an out-of-bounds write (both in separate regions of memory from the original malloc). This causes the original free to fail. This behaviour is fairly deterministic.
My questions are several:
What are the minimal conditions for a memory corruption to affect a separate memory region like this.
Are there any proactive steps that can be taken to prevent this cross-corruption.
Is it defined behaviour wrt standards to use pointer arithmetic to jump back and forth between contiguously allocated memory.
/* 3 example */
void *m = malloc(sizeof(header_struct) + sizeof(body_struct));
body_struct *b = (body_struct*) (((header_struct*)m)+1);
header_struct *h = (header_struct*) (((header_struct*)b)-1);
Good questions.
Q1. The minimal conditions under the standard are anything that triggers Undefined Behaviour. Unfortunately, that's a rather long list and not actionable. In practice the list comes down to 4 common scenarios: object underflow, object overflow, dangling reference or wild store.
Object underflow happens when you write to bytes just before the allocated block. It is very common for those bytes to contain critical block links and the damage is usually severe.
Object overflow happens when you write to bytes just after the allocated block. There is usually a small a amount of padding at the end so a byte or two will usually do no serious damage. If you keep writing you will eventually hit something important, as per underflow.
Dangling reference means writing via a pointer that used to be valid. It could be a pointer to a local variable that has gone out of scope, or to a block that has been freed. These are nasty.
Wild store means writing to an address way outside an allocated block. That could be a small positive address (say pointer value of 0x20) and in a debug environment these areas can often be protected, or it can be random garbage because the pointer itself got damaged. These are less common, but very hard to find and fix.
Q2. A debug heap is your first level of protection. It will check the links and write special patterns into unused space and generally help to find and fix problems. if you are using a debug heap then free() usually triggers some diagnostic activities, but you can usually find some other call to do the same thing. On Windows that is HeapValidate().
You can do more by implementing your own heap with guards/sentinels (look that up) and your own heap checking functions. Beyond that (in C at least), you just have to get better at writing the code. You can add assertions so at least the code fails fast.
Then you can use external tools. One is valgrnd, but it is not always feasible. In one case we wrote a complete heap logging system to track every allocation, to find problems like these.
Q3. Your second example does not guarantee the correct alignment of body_struct in line 2. According to the C standard n1570 S7.22.3, memory returned by malloc() is suitable aligned to be used as a pointer to any object. The compiler will lay out the structs with this assumption.
However, that requirement does not extend to the members of an array of structs. It is implementation-defined whether the second member of an array of a struct like this is aligned or not.
struct s {
double d;
char c;
} ss[2];
With this in mind, your code is valid C, but may have implementation-defined or Undefined Behaviour, depending on alignment requirements. It is certainly not recommended.
(1) Any undefined behaviour can cause such memory corruption, including but not restricted to writing to any memory location that is not part of an object.
(2) Write your code carefully :-(
(3) The second assignment is not portable and can lead to all kinds of problems due to alignment problems. To make this correct and portable, you usually use a flexible array member. If you always allocate one header and one body, define a new struct
typedef struct {
header_struct header;
body_struct body;
} body_plus_header_struct;
If you allocate one header and a variable number of bodies, write
typedef struct {
header_struct header;
body_struct bodies [];
} body_plus_header_struct;
Here, body_plus_header_struct has a size that is guaranteed to be rounded up so that the address of the bodies array has the correct alignment. To allocate a struct for n bodies, allocate
body_plus_header_struct* p = malloc (sizeof (*p) + n * sizeof (p->bodies [0]));

Why does malloc(0) return a non-null address in Windows?

The code below returns an address when executed in Windows, though I was expecting it to return NULL.
int main()
{
char *ptr = NULL;
ptr = malloc(0);
printf("malloc returned = %u\n", ptr);
}
What could have prompted such an implementation of malloc? Is there any reason behind it?
Since, this is a 0 byte memory, I didn't experiment writing any data. But, can this memory be used for anything at all?
It's just the minimum size you're requesting. And since there are no zero-length blocks in the Win32 heap, you can:
void *p = malloc(0);
// ... do some stuff in between...
realloc(p, n);
Which should mostly result in reusing a block of the heap (if you're lucky and the new size is small). A minor opportunist optimization (or a slow-down, depending on the context and blood coffee-levels).
This is a simplified example. The actual situation could be a class that allocates a buffer when it's created and also allows to grow it. If the inputs are annoying to control, you could just let it do that zero-sized buffer allocation.
To answer the second part of your question no, the memory can't be used for anything, since "the accessible part of it" is 0 bytes starting from the address returned.
IIRC it is guaranteed though that if the implementation does return non-null, then it will be a different address from any other currently allocated memory block. So in theory you could use the address as a temporary unique ID (of pointer type), although obviously there are plenty of other ways of getting a unique ID. And even if you were going to use it for that, malloc(1) would work just as well as malloc(0) and be more portable.
That's answered within the comp.lang.c FAQ: http://c-faq.com/ansi/malloc0.html
The ANSI/ISO Standard says that it may do either; the behavior is implementation-defined (see question 11.33). Portable code must either take care not to call malloc(0), or be prepared for the possibility of a null return.
the standard says it can do either
http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html
It is NOT useless. Blocks that are returned from malloc are expected to be unique. Thus each block returned by malloc carries both the data within the block and a unique identifier for it, being its address. If the latter property is important, and the fact that it contains no data is not an issue, then it is useful.
However, as mentioned its a great way to write non-portable code. This is NOT the same thing as illegal code, however, and it is perfectly valid for an implementation to support it (as apparently windows does).

Is it safe to use realloc?

Some time ago a friend of mine told me not to use realloc because it's unsafe, but he couldn't tell me why, so I made some research on the subject and the nearest references to my doubt were:
First
Second
I want to know if I can continue to use realloc in my code or if it's unsafe is there any other way to reallocate memory?
It's perfectly safe to use realloc. It is the way to reallocate memory in a C program.
However you should always check the return value for an error condition. Don't fall into this common trap:
p = realloc(p, new_size); // don't do this!
If this fails, realloc returns NULL and you have lost access to p. Instead do this:
new_p = realloc(p, new_size);
if (new_p == NULL)
...handle error
p = new_p;
The first of the two linked article raises two complaints above and beyond the "check the call succeeded" points already raised here.
When this is done, the old contents are discarded and left in memory somewhere. For secure memory applications where it is important to erase all traces of data, this behavior is inappropriate.
This is a valid point if you happen to be storing sensitive data (e.g. private keys, unhashed(!) passwords etc.) and want to make it harder for exploits to recover the data or other processes on the system to steal the data.
Since it moves memory around, any old pointers to that memory become invalid and could cause the program to crash or otherwise misbehave.
This point seems like nonsense to me. Their proposed solution is no better, they malloc(), copy and then free() the original which has the same net effect - the address has changed. If you wanted to avoid moving the memory you might be able to use some platform specific calls to do that, if you arranged for there to be sufficient free address space near them. If you knew a priori how much address space to reserve then you'd probably not be thinking of calling realloc() in the first place though!
If you're gambling on realloc() never moving, always growing then you've probably got bigger problems to worry about anyway and switching to malloc() + copy + free() can't possibly solve that.
Besides the "check your return value properly point", the most interesting point from the second article is a warning about:
Do not realloc your buffer by 1 byte at a time.
they warn:
This is guaranteed to churn your memory heap
This is a potentially valid point, but it's not a criticism of realloc() itself; the same would happen if you used malloc()+copy+free(). The real fix is to grow buffers sensibly regardless of how you grow them or better yet allocate in correct sized chunks up front.
They also have a point about
Using realloc to return memory to the system.
They're correct here in that using any size other than 0 might not actually make a return. It probably makes things no worse, but this usage still seems like an example of premature "optimisation". The fix again is to use sensible sized allocations to begin with.
Sort answer: it's not unsafe, but it's not a magical solution to all your problems either.
realloc is safe in itself, but using it safely is a bit tricky -- to the point that I'd say roughly 85-90% of the code I've seen that uses it does not do so safely. The problem is that realloc returns NULL to indicate failure -- but when it does so, the pointer you supplied as input is still valid (provided you didn't resize its allocation to 0).
Therefore, you have to assign the return from realloc to the pointer you supplied as input if and only if realloc returned a non-null pointer. If it returns a null pointer, your previous pointer is valid, but the allocation has not be resized.
Also note that many people assume realloc can only fail and/or move the allocation when you enlarge the allocation. In reality, it can fail (though that's unlikely) or move the data to a different location (much more likely) even when you're reducing the allocation size.
Like everything in C, as long as you know what do you do, it's fine.
(Knowing what do you do includes checking for errors, don't use the old pointer,etc)

What are the chances that realloc should fail?

Does it fail when it runs out of free memory similar to malloc or could there be other reasons?
Any of the allocation functions (malloc, realloc, calloc, and on POSIX, posix_memalign) could fail for any of the following reasons, and possibly others:
You've used up your entire virtual address space, or at least the usable portion of it. On a 32-bit machine, there are only 4GB worth of addresses, and possibly 1GB or so is reserved for use by the OS kernel. Even if your machine has 16GB of physical memory, a single process cannot use more than it has addresses for.
You haven't used up your virtual address space, but you've fragmented it so badly that no contiguous range of addresses of the requested size are available. This could happen (on a 32-bit machine) if you successfully allocate 6 512MB blocks, free every other one, then try to allocate a 1GB block. Of course there are plenty of other examples with smaller memory sizes.
Your machine has run out of physical memory, either due to your own program having used it all, or other programs running on the machine having used it all. Some systems (Linux in the default configuration) will overcommit, meaning malloc won't fail in this situation, but instead the OS will later kill one or more programs when it figures out there's not really enough physical memory to go around. But on robust systems (including Linux with overcommit disabled), malloc will fail if there's no physical memory left.
Note that strictly speaking, the allocation functions are allowed to fail at any time for any reason. Minimizing failure is a quality-of-implementation issue. It's also possible that realloc could fail, even when reducing the size of an object; this could happen on implementations that strictly segregate allocations by size. Of course, in this case you could simply continue to use the old (larger) object.
You should think of realloc as working this way:
void *realloc(void *oldptr, size_t newsize)
{
size_t oldsize = __extract_size_of_malloc_block(oldptr);
void *newptr = malloc(newsize);
if (!newptr)
return 0;
if (oldsize > newsize)
oldsize = newsize;
memcpy(newptr, oldptr, oldsize);
free(oldptr);
return newptr;
}
An implementation may be able to do specific cases more efficiently than that, but an implementation that works exactly as shown is 100% correct. That means realloc(ptr, newsize) can fail anytime malloc(newsize) would have failed; in particular it can fail even if you are shrinking the allocation.
Now, on modern desktop systems there is a strong case for not trying to recover from malloc failures, but instead wrapping malloc in a function (usually called xmalloc) that terminates the program immediately if malloc fails; naturally the same argument applies to realloc. The case is:
Desktop systems often run in "overcommit" mode where the kernel will happily hand out more address space than can be backed by RAM+swap, assuming that the program isn't actually going to use all of it. If the program does try to use all of it, it will get forcibly terminated. On such systems, malloc will only fail if you exhaust the address space, which is unlikely on 32-bit systems and nigh-impossible on 64-bit systems.
Even if you're not in overcommit mode, the odds are that a desktop system has so much RAM and swap available that, long before you cause malloc to fail, the user will get fed up with their thrashing disk and forcibly terminate your program.
There is no practical way to test recovery from an allocation failure; even if you had a shim library that could control exactly which calls to malloc failed (such shims are at best difficult, at worst impossible, to create, depending on the OS) you would have to test order of 2N failure patterns, where N is the number of calls to malloc in your program.
Arguments 1 and 2 do not apply to embedded or mobile systems (yet!) but argument 3 is still valid there.
Argument 3 only applies to programs where allocation failures must be checked and propagated at every call site. If you are so lucky as to be using C++ as it is intended to be used (i.e. with exceptions) you can rely on the compiler to create the error recovery paths for you, so the testing burden is much reduced. And in any higher level language worth using nowadays you have both exceptions and a garbage collector, which means you couldn't worry about allocation failures even if you wanted to.
I'd say it mostly implementation specific. Some implementations may be very likely to fail. Some may have other parts of the program fail before realloc will. Always be defensive and check if it does fail.
And remember to free the old pointer that you tried to realloc.
ptr=realloc(ptr,10);
is ALWAYS a possible memory leak.
Always do it rather like this:
void *tmp=ptr;
if(ptr=realloc(ptr,10)==NULL){
free(tmp);
//handle error...
}
You have two questions.
The chances that malloc or realloc fail are negligible on most modern system. This only occurs when you run out of virtual memory. Your system will fail on accessing the memory and not on reserving it.
W.r.t failure realloc and malloc are almost equal. The only reason that realloc may fail additionally is that you give it a bad argument, that is memory that had not been allocated with malloc or realloc or that had previously been freed.
Edit: In view of R.'s comment. Yes, you may configure your system such that it will fail when you allocate. But first of all, AFAIK, this is not the default. It needs privileges to be configured in that way and as an application programmer this is nothing that you can count on. Second, even if you'd have a system that is configured in that way, this will only error out when your available swap space has been eaten up. Usually your machine will be unusable long before that: it will be doing mechanical computations on your harddisk (AKA swapping).
From zwol's answer:
Now, on modern desktop systems there is a strong case for not trying to recover from malloc failures, but instead wrapping malloc in a function (usually called xmalloc) that terminates the program immediately if malloc fails;
Naturally the same argument applies to realloc.
You can see that principle applied with Git 2.29 (Q4 2020): It was possible for xrealloc() to send a non-NULL pointer that has been freed, which has been fixed.
See commit 6479ea4 (02 Sep 2020) by Jeff King (peff).
(Merged by Junio C Hamano -- gitster -- in commit 56b891e, 03 Sep 2020)
xrealloc: do not reuse pointer freed by zero-length realloc()
Signed-off-by: Jeff King
This patch fixes a bug where xrealloc(ptr, 0) can double-free and corrupt the heap on some platforms (including at least glibc).
The C99 standard says of malloc (section 7.20.3):
If the size of the space requested is zero, the behavior is
implementation-defined: either a null pointer is returned, or the
behavior is as if the size were some nonzero value, except that the
returned pointer shall not be used to access an object.
So we might get NULL back, or we might get an actual pointer (but we're not allowed to look at its contents).
To simplify our code, our xmalloc() handles a NULL return by converting it into a single-byte allocation.
That way callers get consistent behavior. This was done way back in 4e7a2eccc2 ("?alloc: do not return NULL when asked for zero bytes", 2005-12-29, Git v1.1.0 -- merge).
We also gave xcalloc() and xrealloc() the same treatment. And according to C99, that is fine; the text above is in a paragraph that applies to all three.
But what happens to the memory we passed to realloc() in such a case? I.e., if we do:
ret = realloc(ptr, 0);
and "ptr" is non-NULL, but we get NULL back: is "ptr" still valid?
C99 doesn't cover this case specifically, but says (section 7.20.3.4):
The realloc function deallocates the old object pointed to by ptr and
returns a pointer to a new object that has the size specified by size.
So "ptr" is now deallocated, and we must only look at "ret".
And since "ret" is NULL, that means we have no allocated object at all. But that's not quite the whole story. It also says:
If memory for the new object cannot be allocated, the old object is
not deallocated and its value is unchanged.
[...]
The realloc function returns a pointer to the new object (which may
have the same value as a pointer to the old object), or a null pointer
if the new object could not be allocated.
So if we see a NULL return with a non-zero size, we can expect that the original object is still valid.
But with a non-zero size, it's ambiguous. The NULL return might mean a failure (in which case the object is valid), or it might mean that we successfully allocated nothing, and used NULL to represent that.
The glibc manpage for realloc() explicitly says:
[...]if size is equal to zero, and ptr is not NULL, then the call is
equivalent to free(ptr).
Likewise, this StackOverflow answer to "What does malloc(0) return?":
claims that C89 gave similar guidance (but I don't have a copy to verify it).
A comment on this answer to "What's the point of malloc(0)?" claims that Microsoft's CRT behaves the same.
But our current "retry with 1 byte" code passes the original pointer again.
So on glibc, we effectively free() the pointer and then try to realloc() it again, which is undefined behavior.
The simplest fix here is to just pass "ret" (which we know to be NULL) to the follow-up realloc().
But that means that a system which doesn't free the original pointer would leak it. It's not clear if any such systems exist, and that interpretation of the standard seems unlikely (I'd expect a system that doesn't deallocate to simply return the original pointer in this case).
But it's easy enough to err on the safe side, and just never pass a zero size to realloc() at all.

Resources