11

Does it fail when it runs out of free memory similar to malloc or could there be other reasons?

Earlz
  • 62,085
  • 98
  • 303
  • 499
user963241
  • 6,758
  • 19
  • 65
  • 93

5 Answers5

13

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.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
5

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:

  1. 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.
  2. 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.
  3. 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.

zwol
  • 135,547
  • 38
  • 252
  • 361
1

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...
}
Earlz
  • 62,085
  • 98
  • 303
  • 499
  • I believe the only thing the standard says about it is that a null pointer is returned “[i]f the space cannot be allocated”, nothing more specific about the reasons. – Mormegil Feb 07 '11 at 19:36
  • Throwing away the old data when you can't make room for new data is probably not the right behavior for most applications... – R.. GitHub STOP HELPING ICE Feb 07 '11 at 19:40
  • @R.. Well, that's true. I was just warning of the common memory leak – Earlz Feb 07 '11 at 19:42
  • Missing parentheses: `if(ptr=realloc(ptr,10)==NULL){` -> `if ((ptr = realloc(ptr, 10)) == NULL) {` – chqrlie Sep 06 '20 at 18:37
0

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).

Jens Gustedt
  • 76,821
  • 6
  • 102
  • 177
  • Failure on *accessing* versus *allocating* is not a "modern" behavior. It's a lazy behavior. Commit accounting is hard, and early in Linux development, everyone was too lazy to get it right. Robust unix systems have done proper commit accounting for decades, and these days Linux can be configured for proper accounting too. – R.. GitHub STOP HELPING ICE Feb 07 '11 at 19:39
  • I would argue that it's not the application developer's responsibility to worry about the possibility that overcommit is enabled. There's no good workaround for the possibility that your program might crash when accessing memory it's already "successfully" allocated. You can trap `SIGSEGV`, but what do you do if you catch it? I suppose you could remap a `MAP_SHARED` page from a dummy file over top of it and then return from the signal handler, and have the caller detect that this happened... – R.. GitHub STOP HELPING ICE Feb 08 '11 at 19:32
  • @R.: After allocation, you could temporarily trap `SIGSEGV` and `SIGBUS` and loop over the pages to access them. By that you could at least confine the error, and then fail gracefully. The initial overhead for an allocation would be noticeable, but the amortized cost if all that memory is really used would be tolerable, I think. – Jens Gustedt Feb 08 '11 at 21:11
  • I think you have to do something like I described, because otherwise even if you catch the signal, there's no way to return from the signal handler. You have to change things so that it doesn't fault again after returning... – R.. GitHub STOP HELPING ICE Feb 08 '11 at 22:11
  • I wasn't paying attention to Linux during its early development, but I *was* sysadmining a bunch of SunOS 4 and Solaris 2.x (x <=4) machines in the late nineties, and I vividly remember memory overcommit being *hyped as a feature* -- your gigantic static Fortran arrays, only a tiny part of which is actually used on typical runs of the program, will not bring the computer to its knees with paging! (Sun Microsystems is not responsible for what happens if you have not configured enough RAM and/or swap to cover your problem size.) – zwol Feb 09 '11 at 00:43
  • @Zack: For what it's worth, overcommit has nothing to do with preventing the system from getting bogged down paging. The only difference it will make is whether you're able to run the program at all. – R.. GitHub STOP HELPING ICE Oct 14 '12 at 22:36
  • @R..: That may well be true on a modern system, but the 1995-era systems that were hyping overcommit as a feature did not separate *reserving* pages from *clearing and mapping* pages. So if your aforementioned Fortran program had giant static arrays that fit into swap but not RAM, and overcommit was disabled, it really would bog down paging just to load the program. – zwol Oct 15 '12 at 12:48
0

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.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • See also https://github.com/git/git/commit/c4bbd9bb8fd307c3c8be16121391416a5685ffe1 – VonC Sep 11 '22 at 16:49