December 1, 2019·5 min read·engineeringcallocationstandards

Zero-size realloc exposes an ownership ambiguity

N2464 proposes undefined behavior for zero-size reallocations after earlier wording left callers uncertain about what a null result means for the old allocation.

A null result from realloc usually leads a caller to preserve the old pointer. With a zero size, that policy can be wrong because some implementations have already freed the allocation. Robert C. Seacord's N2464 proposal, published in the WG14 log on December 1, proposes classifying zero-size reallocations as undefined behavior. The problem is the ownership information conveyed by the return value.

C makes this distinction consequential because the caller controls the allocation's lifetime. I like that explicit responsibility when the interface describes it clearly. An operation that can both return null and release the old object complicates a common failure-handling pattern at exactly the point where the caller needs an unambiguous ownership decision.

A null result has two possible meanings

Nick Stoughton's earlier N2438 clarification request, submitted on behalf of the Austin Group, identifies the remaining ambiguity after DR400's resolution entered C17. The general allocation wording associates a null pointer with an error. The realloc wording separately permits implementation-defined deallocation when a zero-size request does not allocate a new object.

The resulting question is whether realloc can return null after successfully deallocating the old object without treating that result as an error. This is more than a disagreement about whether zero-byte objects are useful. It affects what a caller may do with the pointer it already holds.

Consider the familiar temporary-pointer pattern for a request known to be nonzero. A failed allocation leaves the existing pointer available to its owner. The temporary variable prevents that pointer from being overwritten before the result has been examined:

#include <stdlib.h>

int resize_nonzero(void **buffer, size_t bytes)
{
    if (bytes == 0)
        return -1;

    void *replacement = realloc(*buffer, bytes);
    if (replacement == NULL)
        return -1;

    *buffer = replacement;
    return 0;
}

This helper requires its argument to point to a valid pointer variable. The stored pointer must be null or eligible for reallocation under the library's rules. Checking zero states a deliberate precondition before calling realloc. The helper's return convention is an application choice, while preservation of the allocation on a failed nonzero request comes from the realloc contract.

Allowing zero through the same logic would change the ownership question. A null return could no longer be interpreted using only that failure branch across implementations with different zero-size behavior. Retaining the old pointer variable would not establish that the old allocation remained live.

The disagreement is in existing implementations

N2464 collects differences among implementations and describes the earlier attempt to accommodate them in C17. It reports that some zero-size calls with a non-null pointer return null after freeing storage. Other behavior can return a pointer for a zero-size allocation, subject to restrictions on access.

N2438 notes that glibc returns null without setting errno for the deallocation case it discusses. It also describes POSIX wording that distinguishes allocation failure through ENOMEM. A program that interprets only the pointer result can therefore miss a distinction that another interface specification tries to express through an error indicator.

That is a difficult foundation for a reusable allocation helper. Its caller wants to know whether it retains an object, owns a replacement, or has released ownership entirely. Those are different states even when more than one can involve a null pointer result.

The draft's proposed response is to stop requiring ISO C to reconcile this case. N2464 says discussion at Ithaca and on the mailing list suggested undefined behavior for a size of zero. That proposal would also leave POSIX free to specify its own behavior for the case.

A proposal does not change today's library

N2464 is proposed wording for C2x. It does not announce that all existing libraries have changed their zero-size implementation. A reader has to distinguish the proposed language rule from the behavior of a particular deployed runtime.

Its rationale is also distinct from a claim that no implementation can support zero-size requests. An implementation can define an extension or operate under another specification. The issue for ISO C is whether retaining a portable contract for this operation is useful when the possible outcomes already complicate ownership.

I prefer an allocation wrapper to make zero a separate policy decision. The interface can release storage explicitly when the requested logical size becomes empty, without asking a return value to distinguish release from allocation failure.

One such application policy is to clear the owner's pointer after an explicit free. The code can keep the reallocation branch limited to nonzero sizes. This policy also gives an empty request a successful result without invoking the disputed library operation:

#include <stdlib.h>

int resize_bytes(void **buffer, size_t bytes)
{
    if (bytes == 0) {
        free(*buffer);
        *buffer = NULL;
        return 0;
    }

    void *replacement = realloc(*buffer, bytes);
    if (replacement == NULL)
        return -1;

    *buffer = replacement;
    return 0;
}

This helper has the same pointer-validity requirements as the earlier example. It accepts an address of a void pointer variable, not an arbitrary pointer-to-pointer converted to that type. Its zero case means release ownership by application policy. A caller that needs to distinguish an empty logical buffer from released storage needs a different state representation.

A successful nonzero reallocation also requires the caller to update any assumptions about the old storage. The replacement may occupy a different location, and preserving an alias elsewhere does not make that alias designate the replacement object. Centralizing the owner update in a helper only handles the pointer variable supplied to that helper.

Requested bytes must express the intended size

A zero request can also arise accidentally in size computation. The distinction between an intended empty allocation and a computed zero belongs before the library call. The C11 draft's allocation specification describes requests in terms of their resulting size, not the programmer's earlier intent.

For example, a caller multiplying an element count by an element size has to ensure that the result represents the desired byte count. A resize helper taking bytes cannot recover the intended count from a wrapped arithmetic result. The ownership policy for an intentional zero therefore does not validate the calculation that produced the argument.

N2464 addresses one operation in that larger allocation contract. Its practical significance is the separation it encourages between releasing an object and attempting to replace one. An explicit zero branch can give those actions separate meanings, while the nonzero branch retains the failure behavior on which temporary-pointer reallocation code depends.