Home > Software design >  How come it's possible to malloc() an infinite amount of memory in C?
How come it's possible to malloc() an infinite amount of memory in C?

Time:09-29

I was trying to induce an ENOMEM error just to satisfy my own curiosity, so I decided to run the following code:

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

int main(void)
{
    double POS_INF = 1.0/0.0;
    char *s = malloc(POS_INF);
}

To my suprise, it compiled and ran successfully. Shouldn't there have been an ENOMEM error or some other type of error?

I decided to run valgrind to see what was going on. On the sixth line of the valgrind output, it showed the following:

Argument 'size' of function malloc has a fishy (possibly negative) value: -9223372036854775808

This output really confuses me. Does it think that the POS_INF variable, which is meant to be equal to positive infinity, is actually -9223372036854775808?

Any help on this would be highly appreciated.

CodePudding user response:

Function malloc() as defined in <stdlib.h> takes an argument of type size_t. Passing a double invokes an implicit conversion from double to size_t, which is only defined if double value is finite and the integral part in within the range of type size_t.

Converting Infinity is hence undefined. Note that computing 1.0/0.0 is not defined on all platforms, but does evaluate to a positive infinity on IEEE-754 conforming architectures such as most modern systems.

As a consequence, your code has undefined behavior. It seems converting a positive infinite value (if your system supports it) to the type unsigned long for which size_t is a typedef on your system produces the value 0x8000000000000000 (9223372036854775808 in decimal, reported as -9223372036854775808 if converted as a signed value), which malloc() reports as fishy... I tend to agree :). Other systems might behave differently, possibly terminating your program abruptly.

Malloc detects this argument value as fishy because programmers passing negative values to malloc() actually pass huge unsigned values as converted implicitly to unsigned type size_t. This explains the error message and the signed conversion. It must be a common enough mistake to warrant issuing a runtime warning to stderr, but it leaves the user clueless about something of interest only to the programmer.

Here is a modified version:

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

int main() {
    double POS_INF = 1.0/0.0;
    char *s = malloc(POS_INF);
    printf("(size_t)%g -> %zu\n", POS_INF, (size_t)POS_INF);
    printf("malloc(%zu) -> %p\n", (size_t)POS_INF, (void *)s);
    if (s == NULL) {
        printf("errno=%d %s (%s)\n", errno,
               errno == ENOMEM ? "(ENOMEM)" : "",
               strerror(errno));
    }
    free(s);
    return 0;
}

Running this program multiple times gives a good illustration of undefined behavior: the conversion gives different values in successive occurrences (but produces a value small enough for malloc() to allocate):

chqrlie$ make 220929-malloc
clang -O3 -Weverything -Wno-padded -o 220929-malloc 220929-malloc.c
220929-malloc.c:8:22: warning: implicit conversion turns floating-point number into integer: 'double' to 'unsigned long'
      [-Wfloat-conversion]
    char *s = malloc(POS_INF);
              ~~~~~~ ^~~~~~~
1 warning generated.
chqrlie$ ./220929-malloc
(size_t)inf -> 4194304
malloc(3520768) -> 0x7fbc69400350

CodePudding user response:

How come it's possible to malloc() an infinite amount of memory in C?

It is not possible to malloc an infinite amount of memory in C.

Firstly, remember that programming in general (and C in particular) is not pure mathematics. The concept of "infinity" does not necessarily even exist.

It's true, IEEE-754 has a distinct and meaningful value for "infinity". (It even has two: positive and negative.) And, it's also true, the expression 1.0/0.0 will generate this value.

However, malloc does not take a double. So the concept, "malloc an infinite amount of memory" is quiet meaningless. malloc actually takes an argument of type size_t, so the only values you can (attempt to) malloc are values that can be expressed as type size_t.

Now, C supports implicit conversions, including from double to size_t, so the compiler will attempt to do that. But I don't think the conversion of double to size_t is perfectly defined (or can be). I can't remember, but I'm pretty sure the rule is that if the truncated double value can't be properly represented as a value of type size_t, the result is undefined. So malloc(1024.0) is fine, and malloc(1024.1) is probably also fine, but malloc(1e100) is not fine, and malloc(1.0/0.0) is right out.

And then, finally, you expressed surprise that malloc didn't fail, but in fact, maybe it did: since your program didn't check, you have no way of knowing!

The last line of your test program was

char *s = malloc(POS_INF);

So you assign malloc's result to a pointer variable s, but then throw it away and exit. Since you don't do anything with s, it wouldn't be too wrong for the compiler to throw away the whole line — that is, to not call malloc at all — since you obviously didn't care about the return value, and would have no way of knowing if the compiler cheated and didn't call it.

Suppose you add the lines

if(s == NULL) {
    perror("malloc");
    exit(1);
}

at the end of the program. Now you're actually in a position to determine whether malloc failed.

After that there are three possibilities:

  1. malloc fails, because the converted size_t value (whatver it is) asks for more memory than is available.
  2. malloc succeeds, because the converted size_t value (whatver it is) asks for memory that is available.
  3. malloc seems to succeed, because although the converted size_t value asks for more memory than is available, you're on a system that overcommits memory, and only fails when you try to use it.

So to really see what's going on, you might want to change the code to

size_t sz = POS_INF;
char *s = malloc(sz);
if(s == NULL) {
    printf("can't malloc %zu bytes: %s\n", sz, strerror(errno));
    exit(1);
}

so that you can actually see what size_t value you got. And to be really sure, you can follow that up with

s[0] = 1;
s[sz/2] = 2;
s[sz-1] = 3;

to see if you actually got memory you can use.

CodePudding user response:

First thing, in case memory allocation functions (malloc() and family) fails to allocate the memory, they return a null pointer. They don't create some fault or error on their own. You need to check for the validity (non-null pointer in this case) after the call to a(ny) library function as a best practice.

That said, malloc() accepts an argument of type size_t. Passing a double is not allowed.

  • Related