Add double-free detection to ArenaPool (in Debug Mode)

Double-freeing should eventually cause a segfault (on ArenaPool.deinit, if not
sooner), but having an explicit check allows us to log the responsible owner.
This commit is contained in:
Karl Seguin
2026-01-28 12:53:30 +08:00
parent d02d974cd0
commit 946f02b7a2

View File

@@ -174,7 +174,10 @@ call_arena: Allocator,
arena_pool: *ArenaPool, arena_pool: *ArenaPool,
// In Debug, we use this to see if anything fails to release an arena back to // In Debug, we use this to see if anything fails to release an arena back to
// the pool. // the pool.
_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, []const u8) else void), _arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
owner: []const u8,
count: usize,
}) else void),
window: *Window, window: *Window,
document: *Document, document: *Document,
@@ -236,7 +239,9 @@ pub fn deinit(self: *Page) void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
var it = self._arena_pool_leak_track.valueIterator(); var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| { while (it.next()) |value_ptr| {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); if (value_ptr.count > 0) {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
}
} }
} }
@@ -247,7 +252,9 @@ fn reset(self: *Page, comptime initializing: bool) !void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
var it = self._arena_pool_leak_track.valueIterator(); var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| { while (it.next()) |value_ptr| {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); if (value_ptr.count > 0) {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
}
} }
self._arena_pool_leak_track.clearRetainingCapacity(); self._arena_pool_leak_track.clearRetainingCapacity();
} }
@@ -370,14 +377,23 @@ const GetArenaOpts = struct {
pub fn getArena(self: *Page, comptime opts: GetArenaOpts) !Allocator { pub fn getArena(self: *Page, comptime opts: GetArenaOpts) !Allocator {
const allocator = try self.arena_pool.acquire(); const allocator = try self.arena_pool.acquire();
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
try self._arena_pool_leak_track.put(self.arena, @intFromPtr(allocator.ptr), opts.debug); const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr));
if (gop.found_existing) {
std.debug.assert(gop.value_ptr.count == 0);
}
gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 };
} }
return allocator; return allocator;
} }
pub fn releaseArena(self: *Page, allocator: Allocator) void { pub fn releaseArena(self: *Page, allocator: Allocator) void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
_ = self._arena_pool_leak_track.remove(@intFromPtr(allocator.ptr)); const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
if (found.count != 1) {
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count });
return;
}
found.count = 0;
} }
return self.arena_pool.release(allocator); return self.arena_pool.release(allocator);
} }