Issues with C library routine - realloc()

All of us are pretty familiar with this seemingly harmless routine packaged mostly as a part of C library. At the first glance, this looks pretty familiar and easy to include in our existing code. This is mainly due to the fact that we assume the library routines to work perfectly fine every time we call them. Even if there is an issue with library routines, we always try to find bugs in our own code.

But, at times these library routines have weird interfaces and a lot of care needs to taken while using them. Have a look at the sample code below which uses realloc() to change the size of a memory block.

pbuf = (char*)realloc(pbuf, sNew);
if (pbuf != NULL)
/* ..... */

      
There is a subtle yet dangerous bug in the above code snippet. Can you find it ?

Here is the answer: when the call to realloc() fails, pbuf is assigned NULL and thus destroying the only pointer to the previously allocated block of memory. In other words, the code is leading to lost memory blocks. This is one very common trap that programmer falls to while using realloc(). The issue lies in the interface of the function. The return value is a mixture of result and error condition. Many of the library routines suffers from the same limitation, but let us focus on realloc() in this article. There is an easy way to rectify the above mistake -

flag myRealloc(void **ppbuf, size_t sNew)
{
char **temp = (char **)ppbuf;
char *pNew;

pNew = (char*)realloc(*temp, sNew);
if (pNew != NULL)
*temp = pNew;
return (pNew != NULL);
}

if (myRealloc(&pbuf, sNew))
/* ... */

    
The above implementation only returns the status in the return value. If the call to realloc() fails, it leaves the original pointer un-changed. So, that was one kind of dangerous behavior of realloc() routine that we need to take care while using it. If you ever get hold of your C manuals and look up for the full description of realloc(), you may find something like this:

void *realloc(void *pv, size_t size);

realloc changes the size of a previously allocated memory block. The contents of the block are preserved up to the lesser of the new and old block sizes.

  • If the new size of the block is smaller than the old size, realloc releases the unwanted memory at the tail end of the block and pv is returned unchanged


  • If the new size is larger than the old size, the expanded block may be allocated at a new address and the contents of the original block copied to the new location. A pointer to the expanded block is returned, and the extended part of the block is left uninitialized.


  • If you attempt to expand a block and realloc cannot satisfy the request, NULL is returned. realloc will always succeed when you shrink a block.


  • If pv is NULL, then realloc behaves as though you called malloc(size) and returns a pointer to a newly allocated block, or NULL if the request cannot be satisfied.


  • If the new size is 0 and pv is not NULL, then realloc behaves as though you called free(pv) and NULL is always returned.


  • If pv is NULL and size is 0, the result is undefined.


Amazingly complicated, isnt it ?? realloc() is a prime example of bad implementation. It is complete memory manager in itself, then why you ever need to have malloc() and free().

That should give you an idea how not to design your function interfaces. With such interfaces, you cant expect programmers to use your function safely. With so many details, even experienced programmers may falter. How much funny it would sound if you tell people that realloc can also free the memory block.

An important point to note while using library routines is to guard it against invalid arguments. But, in case of realloc, if you pass a NULL pointer by mistake, that is legal. If you pass a 0 as size by mistake, that's legal too. Its not possible to put assert statements to guard realloc against invalid inputs. No matter what input you give, realloc() will handle them. At one end it frees blocks; on the other hand it allocates them - totally extreme behavior.

Thus, we can clearly see that we need to take utmost care while using library routines in our code. Also, this bad implementation teaches us a lesson how not to over-design your function interfaces. It would have been better if we had two different routines instead of one single routine e.g.

flag growMemory(void **pbuf, size_t sizeLarge);
flag shrinkMemory(void *pbuf, size_t sizeSmall);

This way we eliminate the bulky and confusing values to the input parameters and thus the need to provide a lengthy description to the callers. Having a function that only does what it is intended to do, saves us and other from a lot of trouble. In short we should ponder and think over our design strategies.
comments powered by Disqus