Search code examples
clinuxmacosreallocerrno

Different realloc behaviour in linux and osx


I have this simple C program:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main (int argc, char **argv) {
    int i = 0;
    int j = 0;
    size_t size = 4194304; /* 4 MiB */
    char *buffer = malloc(size);
    char *buffers[10] = {NULL};
    void *tmp_pointer = NULL;
    fprintf(stderr, "initial size == %zu\n", size);
    fprintf(stderr, "initial buffer == %p\n\n", buffer);
    srand(time(NULL));
    /* let's see when it fails ... */
    for (; i < 10; ++i) {
        /* some random writes */
        for (j = 0; j < 1000; ++j) {
            buffer[rand() % size] = (char)(rand());
        }
        /* some interleaving memory allocations */
        buffers[i] = malloc(1048576); /* 1 MiB */
        size *= 2;
        fprintf(stderr, "new size == %zu\n", size);
        tmp_pointer = realloc(buffer, size);
        if ((tmp_pointer == NULL) || (errno != 0)) {
            fprintf(stderr, "tmp_pointer == %p\n", tmp_pointer);
            fprintf(stderr, "errno == %d\n", errno);
            perror("realloc");
            return (1);
        } else {
            buffer = tmp_pointer;
        }
        fprintf(stderr, "new buffer == %p\n\n", buffer);
    }
    fprintf(stderr, "Trying to free the buffers.\n");
    free(buffer);
    if (errno != 0) {
        fprintf(stderr, "errno == %d\n", errno);
        perror("free(buffer)");
        return (2);
    }
    for (i = 0; i < 10; ++i) {
        free(buffers[i]);
        if (errno != 0) {
            fprintf(stderr, "i == %d\n", i);
            fprintf(stderr, "errno == %d\n", errno);
            perror("free(buffers)");
            return (3);
        }
    }
    fprintf(stderr, "Successfully freed.\n");
    return (0);
}

It just allocates a 4 MiB of memory and 10 times tries to double its size by reallocation. The simple calls to realloc are interleaved with another allocations of 1 MiB blocks and some random writes in order to minimize the heap allocator "tricks". On the Ubuntu linux machine with 16 GiB RAM, I have the following output:

./realloc_test
initial size == 4194304
initial buffer == 0x7f3604c81010

new size == 8388608
new buffer == 0x7f3604480010

new size == 16777216
new buffer == 0x7f360347f010

new size == 33554432
new buffer == 0x7f360147e010

new size == 67108864
new buffer == 0x7f35fd47d010

new size == 134217728
new buffer == 0x7f35f547c010

new size == 268435456
new buffer == 0x7f35e547b010

new size == 536870912
new buffer == 0x7f35c547a010

new size == 1073741824
new buffer == 0x7f3585479010

new size == 2147483648
new buffer == 0x7f3505478010

new size == 4294967296
new buffer == 0x7f3405477010

Trying to free the buffers.
Successfully freed.

So, all the reallocations up to the 4 GiB seemingly succeed. However, when I set the kernel virtual memory accounting mode to 2 (always check, never overcommit) by this command:

echo 2 > /proc/sys/vm/overcommit_memory

then the output changes to:

./realloc_test
initial size == 4194304
initial buffer == 0x7fade1fa7010

new size == 8388608
new buffer == 0x7fade17a6010

new size == 16777216
new buffer == 0x7fade07a5010

new size == 33554432
new buffer == 0x7fadde7a4010

new size == 67108864
new buffer == 0x7fadda7a3010

new size == 134217728
new buffer == 0x7fadd27a2010

new size == 268435456
new buffer == 0x7fadc27a1010

new size == 536870912
new buffer == 0x7fada27a0010

new size == 1073741824
new buffer == 0x7fad6279f010

new size == 2147483648
tmp_pointer == (nil)
errno == 12
realloc: Cannot allocate memory

The rellocation fails at 2 GiB. The computer's free memory at the time as reported by top has been around 5 GiB, so it is reasonable, because realloc must always allocate continuous block of memory. Now, let's see what happens when running the same program on Mac OS X Lion inside the VirtualBox on the very same machine, but with only 8 GiB of virtual RAM:

./realloc_test
initial size == 4194304
initial buffer == 0x101c00000

new size == 8388608
tmp_pointer == 0x102100000
errno == 22
realloc: Invalid argument

Here, this program has a problem with the very first realloc to 8 MiB. This is in my opinion very strange, because the virtual computer's free memory at the time as reported by top has been around 7 GiB.

The truth is, however, that the realloc in fact succeeded, because its return value is non-NULL (notice the tmp_pointer value just before the program termination). But the very same successful call to realloc has also set the errno to nonzero! Now, what is the correct way to handle this situation?

Should I just ignore the errno and check only the return value from the realloc? But then what about some following errno-based error handlers? This is probably not a good idea.

Should I set errno to zero when realloc returns a non-NULL pointer? This appears to be a solution. But ... I have looked here: http://austingroupbugs.net/view.php?id=374. I don't know how authoritative this resource is, but regarding realloc, it is very clear on this:

"... the standard is also explicit that errno cannot be inspected upon success unless documented, ..."

If I understand correctly, it says that: Yeah, when realloc returns NULL you can look at errno, but not otherwise! This said, am I allowed to reset errno to zero? Without ever looking at it? I find it very unclear to understand and decide what's bad and what's good to do.

I still can't understand why is realloc setting this errno in the first place. And what does its value of "Invalid argument" mean? It is not listed in the man pages, they mention only errno ENOMEM (usually number 12). Could something go wrong? Is something in this simple program causing this behaviour under Mac OS X? Probably yes, ... so, the two main questions are:

  1. What is wrong? and
  2. How to correct it? More precisely: How to improve this simple program so that the realloc on Mac OS X leaves the errno at zero when successful?

Solution

  • I think you are misunderstanding the purpose of errno - it is only defined in the case of a failure. To quote the POSIX standard:

    The value of errno should only be examined when it is indicated to be valid by a function's return value. No function in this volume of IEEE Std 1003.1-2001 shall set errno to zero.

    (Bold face by me). So you cannot expect errno to be 0 after a successful call. You should only check the return value -- then you can determine why it failed using errno but not the other way around (i.e., the value of errno does not indicate the presence of an error condition - that is not its purpose - in fact, it is not touched if there is no error).