Bar.init calls Foo.deinit and thus results in double free
How could I prevent the double free here? is there a standard way used in the zig std to handle such cases? Removing errdefer in Bar.init is not an option because what if Foo.init succeeds but afterwards an error get’s thrown in otherFunctionThatCouldFail in Bar.init. I would still like to be able to cleanup Foo in that case.
I know I could add a bool isInitialized or make the ptr nullable or something to add an check whether it should be freed or not but I was curious if there were some commen patterns to handle these cases.
The errdefer for Foo.deinit (that is within Bar.init) only gets called after Foo.init returns without failure, when Foo.init returns with a failure, the line that creates the errdefer for Foo.deinit only was reached within Foo.init, but the errdefer in Bar.init wasn’t reached yet and by the time it is reached (in the no-error case) the errdefer in Foo.init has fallen out of scope and is forgotten.
For situations where there is a potential double free, for example something like:
var duped = allocator.dupe(u8, foo);
errdefer allocator.free(duped);
// If this succeeds, then `duped` will be freed when cleaning up `something`
// so we need to avoid freeing `duped` if this succeeds
try something.takeOwnershipOf(duped);
// If this function fails, we will double free the memory of `duped`
try someFunctionThatCanFail();
We can fix the bug by limiting the scope of the errdefer by sticking it in a block:
{
var duped = allocator.dupe(u8, foo);
errdefer allocator.free(duped);
try something.takeOwnershipOf(duped);
}
// The errdefer is no longer active since it's block-scoped.
try someFunctionThatCanFail();
Truly one of my favorite features of Zig. I’d also like a way for tests to automatically trigger every possible error, and not just error.OutOfMemory, since they can all leave memory in an invalid state, and it’s not possible to trigger every error simply by providing erroneous inputs to functions.
There’s another limitation to checkAllAllocationFailures, which is that since it works on the allocator level, you can’t use it with logic which accepts an OutOfMemory as recoverable. That’s admittedly minor and there are ways to work around it, the one time I ran into this I used builtin.is_test to return the OOM during testing and supply a default the rest of the time.
The only blocker on writing a ‘failurizer’ in userspace is the limitations on declarations in type reflection, as far as I’ve been able to work out. We can get the error set off functions, but there doesn’t seem to be a way to generate wrapped functions using comptime. It would have to be a compiler hack which added logic to the AST, and while that might even be possible right now, it’s beyond my level of understanding of Zig internals to attempt.
I think you actually can test these, read @squeek502 blogpost above and this from the checkAllAllocationFailures documentation:
Expects that test_fn has a deterministic number of memory allocations:
If an allocation was made to fail during a run of test_fn, but test_fn didn’t return error.OutOfMemory, then error.SwallowedOutOfMemoryError is returned from checkAllAllocationFailures. You may want to ignore this depending on whether or not the code you’re testing includes some strategies for recovering from error.OutOfMemory.