0

Consider this code:

main() {
   float *ptr = NULL;
   while(true) {
      ptr = (float *)realloc(ptr, 0*sizeof(float));
      fprintf(stdout,"Errno: %d, Ptr value: %d\n",errno, (int)ptr);
   }
}

What is odd is that errno is never set (at least for me) but the call alternatively returns NULL and a pointer value. My thinking is that 0 allocations can return an error of a sort, but not one severe enough to set errno. Or the code with realloc is problematic. I am not sure.

I sort of would not care, but this is causing me a (0 byte) memory leak.

The 'Realloc Failure' question is not quite the same as it largely assumes that a NULL return from realloc() is an error. This is not the case in this situation. This is mostly about the different behavior of realloc() when a zero size is passed to it.

Jiminion
  • 5,080
  • 1
  • 31
  • 54
  • 2
    0*... = 0 - what is it suppose to do? – Ed Heal Jan 21 '20 at 20:27
  • 8
    NULL is not indicating a failure in this case. *If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free()* - from `man 3 realloc` – Eugene Sh. Jan 21 '20 at 20:29
  • 3
    I'd need to read the man page on `realloc`, but you're asking for 0 memory and it's giving you none, that's not an error condition, I wouldn't expect `errno` to get set. – yano Jan 21 '20 at 20:29
  • 1
    realloc is not failing - the code is asking for nothing to be allocated – Ed Heal Jan 21 '20 at 20:29
  • 2
    From [cppreference](https://en.cppreference.com/w/c/memory/realloc): "If new_size is zero, the behavior is implementation defined (null pointer may be returned (in which case the old memory block may or may not be freed), or some non-null pointer may be returned that may not be used to access storage)". You're allocating zero bytes so you're in "implementation-dependent" land. – Bob Jarvis - Слава Україні Jan 21 '20 at 20:30
  • @Ðаn No, I don't think so. Sometimes it returns a pointer, sometimes NULL. It seems to be dependent on the input ptr (whether it is NULL or not). – Jiminion Jan 21 '20 at 20:36
  • I understand the 'implementation defined' possibility. The issue I have is that it returns alternately a pointer and NULL. I guess Eugene Sh. is explaining the behavior best. I'd add that the unique pointer returned cannot itself be reallocated with 0 bytes. Or maybe it doesn, but it returns a NULL. – Jiminion Jan 21 '20 at 20:41
  • *"The issue I have"* - Does not seem like an issue to me :) – klutt Jan 21 '20 at 20:58
  • 2
    *"this is causing me a (0 byte) memory leak"* -- so no leak at all? – Marco Bonelli Jan 21 '20 at 21:12
  • @MarcoBonelli - Nope. It is still a leak. It must be the garbage surrounding a 0-byte pointer. (header or whatever). – Jiminion Jan 21 '20 at 21:13
  • @Jiminion then it's not 0 bytes. Pick one: either it leaks or it doesn't. If the chunk metadata is still left intact then it is not a 0 byte leak. – Marco Bonelli Jan 21 '20 at 21:15
  • I don't know how big the header is. The memory leak detector only knows how big the pointer allocation is, not the size of the whole shebang. – Jiminion Jan 21 '20 at 21:20
  • 1
    [n2464](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf) has a table that describes what happens with `realloc(.., 0)` on different implementations.Some set errno, some don't. – KamilCuk Jan 21 '20 at 21:39
  • regarding: `ptr = (float *)realloc(ptr, 0*sizeof(float));` This is telling `realloc()` to allocate 0 bytes, so naturally the returned value is NULL. Also, in C, do not cast the returned value. The type of the returned value is `void*` which can be assigned to any pointer – user3629249 Jan 22 '20 at 00:53

3 Answers3

3

Looking at:

https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#__libc_realloc

I can see what is going on. Based on the code, if the input pointer is not null, and the new size is 0, then it frees the old pointer and returns NULL.

If, however, the input pointer is NULL, then realloc just acts as a malloc and returns what malloc(0) produces. (In this case, it does not check if the size is 0 or not.)

So, there is no errno because there is no error. But realloc() returning a NULL is not necessarily an error, which is a nuance I did not recognize.

So, in this example, the first call (0 size, NULL ptr) returns an allocated pointer to a zero sized data area. The second call ( 0 size, non-NULL ptr) the routine frees the pointer and returns a NULL. And then the cycle repeats.

Jiminion
  • 5,080
  • 1
  • 31
  • 54
  • 1
    Good job at investigating the issue, but it is clearly stated in the man page. – Marco Bonelli Jan 21 '20 at 21:13
  • 2
    It says: "if size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr)." And then it says: "The free() function returns no value." And then it also says: "If size was equal to 0, either NULL or a pointer suitable to be passed to free() is returned." I agree the behavior is stated, but maybe not completely clear, at least not to me. – Jiminion Jan 21 '20 at 21:18
  • 2
    @MarcoBonelli: The Unix man page is not a proper source for questions about standard C. – Eric Postpischil Jan 21 '20 at 21:37
  • @EricPostpischil the behavior of requesting the allocation of zero bytes is implementation specific, so the man page is really the only meaningful thing to look at here. – Marco Bonelli Jan 21 '20 at 21:43
  • 1
    @MarcoBonelli: The Unix man page tells you what Unix has chosen for the implementation specification. It does not tell you what the C standard says. It is not a proper source for questions about standard C. – Eric Postpischil Jan 21 '20 at 22:10
  • 1
    @EricPostpischil and indeed I don't see OP mentioning the C standard anywhere in its question. – Marco Bonelli Jan 21 '20 at 22:12
  • 2
    @MarcoBonelli -- the c tag in use on this question does state: "This tag should be used with general questions concerning the C language, as defined in the ISO 9899 standard (the latest version, 9899:2018, unless otherwise specified...." – ad absurdum Jan 21 '20 at 22:24
  • @exnihilo the C tag doesn't in any way mean that the question is necessarily about the standard. It just means that the question is about C. The phrase "defined in ISO xxx yyy [...]" is merely identifying the language. – Marco Bonelli Jan 21 '20 at 22:27
  • 1
    @MarcoBonelli Not really, *"questions concerning the C language, **as defined in the ISO 9899 standard (the latest version, 9899:2018**, unless otherwise specified — also tag version-specific requests with c89, c99, c11, etc)"* , note that there **is** a Posix tag that can be used, if needed. It's unclear if the OP is targetting that environment. – Bob__ Jan 21 '20 at 22:36
  • @Bob__ as I already said, the behavior defined in the question is implementation defined. So referring to the standard does not make much sense here, since the standard says that it's implementation defined. And also the C tag does not automatically imply that the OP is seeking a standard-related answer. – Marco Bonelli Jan 21 '20 at 22:40
  • @MarcoBonelli Well, no problem, I agree to disagree ;). Glad to see that the OP has found an explanation. – Bob__ Jan 21 '20 at 22:44
  • @Bob__ whelp, happens ¯\\_(ツ)_/¯ – Marco Bonelli Jan 21 '20 at 22:46
  • 2
    @MarcoBonelli: Questions tagged with the C tag are for standard C unless otherwise stated. In any case, if you argue that the lack of requesting an answer about the C standard indicates the answer should not be about the C standard, then the lack of requesting an answer about Unix also indicates the answer should not be about Unix. In either case, the Unix man page is irrelevant. Certainly giving an answer that relies on Unix-specific behavior will mislead people who are writing code for other systems. – Eric Postpischil Jan 21 '20 at 22:46
  • 1
    @EricPostpischil ok, now that I can agree on. Perhaps a more correct answer would then be "look at the man page or source code for your specific implementation". – Marco Bonelli Jan 21 '20 at 22:48
1

realloc returns NULL, but does not set errno. How do I properly check for this error/odd behavior?

For realloc(), errno is not specified to be set by the C standard. Any setting of errno is an implementation defined behavior here.

C does specify:

If the size of the space requested is zero, the behavior is implementation-defined: either a null pointer is returned to indicate an error, 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. C17/18 § 7.22.3 1

If size is zero and memory for the new object is not allocated, it is implementation-defined whether the old object is deallocated. C17/18 § 7.22.3.5 3

Consider avoiding 3 implementation-defined behavior points. Use a helper function and call free() when the new size is 0.

// Return error status
bool realloc_float(float **ptr, size_t new_size) {
  // Size zero or too big ....
  if (new_size == 0 || new_size > SIZE_MAX/sizeof(float)) {
    free(*ptr);
    *ptr = NULL;
    return new_size > 0;  // fail on large new_size
  }
  float *newp = realloc(*ptr, sizeof(float) * new_size);
  if (newp == NULL) {
    free(*ptr);
    *ptr = NULL;
    return true; // failure
  }
  *ptr = newp;
  return false;
}

Usage

int main() {
   float *ptr = NULL;
   for (int i = 0; i < 10; i++) {
      size_t sz = rand()%4; 
      bool err = realloc_float(&ptr, sz);
      printf("Error: %d, Ptr value: %p, size %zu\n", err, (void*)ptr, sz);
   }
   free(ptr); 
}
Community
  • 1
  • 1
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
0

Just use an intermediate variable. For example

int main( void ) {
   float *ptr = NULL;
   while(true) {
      float *tmp = realloc(ptr, 0*sizeof(float));

      if ( !tmp ) 
      {
          // report an error
          break;
      }

      ptr = tmp;
      //...
   }
}

As for a zero-sized memory request then according to the C Standard (7.22.3 Memory management functions)

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.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
  • 1
    The behavior reported by OP is not an error. Zero bytes are being allocated so the behavior is "implementation-defined". – Bob Jarvis - Слава Україні Jan 21 '20 at 20:31
  • 1
    That's fixing the common misuse of `realloc`, but it's not really related to the OP's problem (which is a misunderstanding regarding how `realloc` behaves when zero bytes are requested). – ShadowRanger Jan 21 '20 at 20:32
  • @ShadowRanger: The behavior OP is observing is non-conforming (a bug in the malloc implementation). `realloc` is not allowed to return a null pointer unless the original pointer is still valid (the `realloc` failed). – R.. GitHub STOP HELPING ICE Jan 21 '20 at 20:43
  • @R..GitHubSTOPHELPINGICE: [According to cppreference](https://en.cppreference.com/w/c/memory/realloc): "If `new_size` is zero, the behavior is implementation defined (null pointer may be returned (in which case the old memory block may or may not be freed), or some non-null pointer may be returned that may not be used to access storage)." Is cppreference out of sync with the standard? Because otherwise, it sounds pretty clear that the implementation is free to choose a behavior that returns `NULL` even if it's passed a non-`NULL` `ptr`. – ShadowRanger Jan 21 '20 at 20:52
  • The problem is that realloc() can return a NULL and that is not necessarily an error. – Jiminion Jan 21 '20 at 21:01
  • 1
    @ShadowRanger: cppreference is a really low-quality source that shouldn't be used. The claim of "in which case the old memory block may or may not be freed" is unusable because you necessarily have memleaks or double-free and you can't tell which. C does not allow this. – R.. GitHub STOP HELPING ICE Jan 21 '20 at 21:42
  • 1
    See 7.22.3.5 The realloc function ¶3: "If memory for the new object cannot be allocated, the old object is not deallocated and its value is unchanged." and ¶4: "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." No allowance is made for returning a null pointer but deallocating the object. – R.. GitHub STOP HELPING ICE Jan 21 '20 at 21:42
  • @R..GitHubSTOPHELPINGICE: I've found cppreference fairly reliable (cplusplus.com is the flaky one). It's right in this case. You're missing 7.22.3's top level description of all allocator functions "¶1: ... 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." `realloc`'s specific docs don't explicitly override that clause, so it's free to return `NULL` on success when the requested size is `0`. – ShadowRanger Jan 21 '20 at 21:54
  • @ShadowRanger: Not missing that. This issue has been litigated to death and the dangerous behavior is non-conforming. It's allowable to do that in the case where the original pointer is null (in which case it must match `malloc(0)`) but it can't wrongly free an object. IOW if `realloc(nonnull,0)` returns a null pointer, `nonnull` must still be a valid pointer to an object that has not been freed. – R.. GitHub STOP HELPING ICE Jan 21 '20 at 21:58
  • 1
    Cite is outdated in C17/18. Just answered on that [here](https://stackoverflow.com/a/59850784/2410359) "If the size of the space requested is zero, the behavior is implementation-defined: either a null pointer is returned **to indicate an error**, ... – chux - Reinstate Monica Jan 22 '20 at 02:09
  • @ShadowRanger C17/18 has updated this part of the spec. – chux - Reinstate Monica Jan 22 '20 at 02:30