openlang

Assigning realloc back to the only pointer leaks on failure

*buffer = realloc(*buffer, n) turns up in a lot of growth helpers, and it is wrong in a specific way. If the allocation fails, the only owned pointer is overwritten with null. The C11 draft says the old object stays allocated and unchanged in that case. The helper has just lost the one route to storage it still has to release.

The helper I have in mind takes char **buffer and size_t *capacity from the caller. *buffer owns a valid allocation, and the requested size is strictly positive and larger than the current capacity.

Put the result in a temporary pointer first. On failure, return an allocation error and leave both output slots as they were. On success, write the returned pointer and the new capacity.

int grow(char **buffer, size_t *capacity, size_t n)
{
	char *p = realloc(*buffer, n);
	if (p == NULL)
		return -1;
	*buffer = p;
	*capacity = n;
	return 0;
}

Nothing here is clever, and that is the point. The order of the two writes does not matter for correctness, since neither can fail, but writing the pointer first keeps the slots consistent for anyone stepping through it.

Callers then get one simple promise. After a failed call the buffer is the same one they had, so they can report the error, free it, or retry with a more modest size, and nothing has to be repaired first.

One more rule belongs in the documentation. A successful realloc deallocates the old object, and by 6.2.4 every pointer a caller saved into it becomes indeterminate, even when the returned address happens to be the same. My draft returns plain success or failure and tells callers to rebuild their aliases after success. I think that covers it, but I am not fully sure the alias rule is clear enough as written.

后续讨论 8

顺着彼此的依据读下去
  1. 固定链接

    Rebuilding aliases works for C callers. From a Lua script I'd rather there were no aliases to rebuild. Keep the allocation in the C host and give the script a userdata that stands for the buffer. A growth request then either fails, with the old buffer still there for another try, or succeeds and the same userdata now reaches the new block. If the wrapper never hands out raw addresses, no script has to remember which saved pointer just expired.

  2. 固定链接

    Forth's RESIZE can't have this bug. It leaves the new address and an ior on the stack, and on failure the address it returns is the old one and the old region is untouched. So after a failure you hold the pointer you started with plus a nonzero code. No assignment to get wrong, just two cells and whatever you do with them.

    Forth is looser on the alias part. The standard only says the new address may or may not equal the old one. Nothing like C's rule that the old object is gone even when the address matches, so your doc line is stricter than anything Forth asks for.

  3. 固定链接

    Lua has already made this choice for you, which I find kind of lovely. Its allocator function gets the old pointer and both sizes, and the manual asks it to behave like realloc, returning NULL only when it can't fulfil the request. The one in the reference manual is barely more than that.

    static void *l_alloc(void *ud, void *ptr, size_t osize,
                         size_t nsize) {
      (void)ud; (void)osize;
      if (nsize == 0) {
        free(ptr);
        return NULL;
      }
      return realloc(ptr, nsize);
    }

    When the allocator returns NULL, Lua raises a memory error and the old block is still where it was. So the null case is the interpreter's problem, not the script's. A wrapper that hands scripts a userdata instead of addresses gets the same comfort.

  4. 固定链接

    Your precondition says the size is strictly positive. Keep it that way in the code, not only the docs. C23 made realloc with a size of zero undefined behavior outright, where C11 left it to the implementation whether you get null or a pointer you must not use. Plenty of older code assumes a zero size frees the block, so grep your callers for it before trusting the precondition.

  5. 固定链接

    The helper exists now, with the alias rule as drafted. For the failure path I wrapped realloc so that a chosen call returns a null pointer.

    static int calls, fail_at = 3;
    
    static void *test_realloc(void *p, size_t n)
    {
    	if (++calls == fail_at)
    		return NULL;
    	return realloc(p, n);
    }

    Whichever growth is set to fail, the helper has to hand back *buffer and *capacity unchanged and the contents up to the old capacity intact, and the single free at the end must release the block once. The wrapper turns each of those into a plain assertion.

    The alias rule is harder to test. Save a char *end into the buffer, grow it from 40 bytes to 48, write through end. In a plain build that write can go through and the test pass, because the allocator grows the block in place, the address comes back equal, and the stale pointer keeps working. By 6.2.4 it is indeterminate all the same, but nothing on a typical platform says so. Under AddressSanitizer the same write is reported as a heap use after free, because there realloc returns a new block.

    So the alias test runs only under the sanitizer, and the documentation states plainly that an equal address proves nothing about the old pointers. The sanitizer build is slower, but it is the only build here where the rule can fail visibly.

  6. 固定链接

    fail_at = 3 tests one failure, the third growth. Make it a loop. Count the reallocs a full run makes, then rerun the test once per index with calls reset, and check the same assertions every time. A fixed index tests the path you already had in mind. Looping over Zig's FailingAllocator the same way is how I found a leak at the very first allocation, a spot I'd never have picked by hand.

  7. 固定链接

    The upper bound comes out of the same wrapper. Run once with fail_at set to 0. Nothing ever matches, since calls is already 1 at the first comparison, and calls at the end is how many reallocs a full run makes. Then loop fail_at from 1 up to that number and reset calls before each run.

同一个问题的其他角度

  1. What survives a catalog reload?

    A slice is an address and a length. Neither field keeps the backing allocation alive, and the Zig reference is plain that poi…

    回复 15最近活动 zigswiftrustcppodin
  2. The dangling pointer warning went away, the pointer did not

    Passing a name into a C++ library through a wrapper, and the first version was one line, CString::new(name).unwrap().as_ptr()…

    回复 3最近活动 rustcpp
  3. Same sink call, a copy on one line and a move on the next

    Nim 2 decides copy or move per call site. Not per proc. Same argument, same proc. The first call copies, because a is still n…

    回复 2最近活动 nim