Growing a Vector Without Losing It
A storage service keeps a growable byte buffer:
typedef struct { char *data; size_t len, cap; } buf_t;
int buf_append(buf_t *b, const char *s, size_t n) {
if (b->len + n > b->cap) {
b->cap = (b->len + n) * 2;
b->data = realloc(b->data, b->cap);
if (!b->data) return -1;
}
memcpy(b->data + b->len, s, n);
b->len += n;
return 0;
}
A fuzz test reports a leak when the allocator is limited to 4 MiB. A second bug appears when a caller does buf_append(&b, b.data + 2, 5): after enough growth, the copied bytes are wrong. The buffer starts as {0} (all members zero), and n can be up to 1 MiB. The team must keep the API and return -1 on failure without changing b->len or losing the old allocation. The build target is 64-bit Linux with a 64-bit size_t.
1. Explain both reported bugs in the current function. What does `realloc` guarantee on failure, and what happens to the caller's source pointer when growth succeeds?
Read the worked answer
The first bug is the direct assignment b->data = realloc(b->data, b->cap). If realloc fails, it returns NULL and leaves the original block allocated and unchanged. Assigning NULL to b->data loses the only pointer to that original block, so it can never be freed. The function also has already changed b->cap before knowing whether the allocation succeeded, leaving the buffer in an inconsistent state. To make it worse, b->len + n and (b->len + n) * 2 can overflow size_t; a wrapped small value can cause an undersized allocation followed by an out-of-bounds memcpy.
The second bug is aliasing. The caller passes b.data + 2, which points inside the buffer's own allocation. If realloc moves the block, the old address becomes invalid, and s still points to the old location. The subsequent memcpy reads freed memory. Even if realloc does not move the block, memcpy requires non-overlapping regions; appending from a region that overlaps the destination is undefined behavior. The function must account for a source pointer into the same buffer before it reallocates, or the API must forbid aliasing explicitly.
2. Rewrite `buf_append` safely. Include overflow checks, a growth policy, and correct failure behavior. Preserve the old allocation and `b->len` on failure.
Read the worked answer
A safe version is:
int buf_append(buf_t *b, const char *s, size_t n) {
if (n > SIZE_MAX - b->len) return -1;
size_t need = b->len + n;
if (need > b->cap) {
size_t newcap = b->cap ? b->cap : 16;
while (newcap < need) {
if (newcap > SIZE_MAX / 2) { newcap = need; break; }
newcap *= 2;
}
char *tmp = realloc(b->data, newcap);
if (!tmp) return -1;
b->data = tmp;
b->cap = newcap;
}
memcpy(b->data + b->len, s, n);
b->len = need;
return 0;
}
The overflow check n > SIZE_MAX - b->len prevents b->len + n from wrapping. The growth loop doubles capacity but caps the multiplication before it can overflow; if doubling would overflow, it jumps directly to need. The realloc result goes into a temporary. Only after success are b->data and b->cap updated. On failure the function returns -1 with the old pointer, capacity, and length intact. realloc(NULL, size) is valid and behaves like malloc, so the zero-initialized starting state works. This rewrite does not yet solve aliasing; that requires an additional rule or a separate offset-based API.
3. The fuzz test insists that `buf_append(&b, b.data + 2, 5)` must work. What must change, and why is a simple `memmove` alone not sufficient?
Read the worked answer
memmove alone is not sufficient because the danger occurs before the copy: realloc may move the allocation and invalidate s. Even if no reallocation happens, memmove would handle overlap, but the function cannot rely on that. The function must detect that s points into the current buffer before reallocating and convert it to an offset. After a successful realloc, it can reconstruct the pointer as b->data + offset. If no reallocation occurs, it can use memmove for the overlapping copy.
A practical implementation computes an offset only when the source lies within [b->data, b->data + b->len). Strictly portable C does not define relational comparisons between pointers to unrelated objects, so the cleanest design is an internal helper that takes an offset, or a documented requirement that callers pass an offset for self-appends. If the project accepts the usual platform assumption, it can compare converted uintptr_t values, but that is not a pure ISO C guarantee. The key point is that the source must be adjusted after reallocation and the copy must use memmove, because the destination begins at b->data + b->len and can overlap a source that starts earlier in the same buffer. Also update b->len only after the copy succeeds.