Skip to content
  • Jeff King's avatar
    xrealloc: do not reuse pointer freed by zero-length realloc() · 6479ea4a
    Jeff King authored and Junio C Hamano's avatar Junio C Hamano committed
    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 4e7a2ecc (?alloc: do not return NULL when asked for zero
    bytes, 2005-12-29).
    
    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:
    
      https://stackoverflow.com/a/2135302
    
    claims that C89 gave similar guidance (but I don't have a copy to verify
    it). A comment on this answer:
    
      https://stackoverflow.com/a/2022410
    
    
    
    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.
    
    Signed-off-by: default avatarJeff King <peff@peff.net>
    Signed-off-by: default avatarJunio C Hamano <gitster@pobox.com>
    6479ea4a