From 946f02b7a2c9588915110c70c8c4fbfb05b98df6 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 28 Jan 2026 12:53:30 +0800 Subject: [PATCH 1/2] 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. --- src/browser/Page.zig | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 31803987..e82184a8 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -174,7 +174,10 @@ call_arena: Allocator, arena_pool: *ArenaPool, // In Debug, we use this to see if anything fails to release an arena back to // 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, document: *Document, @@ -236,7 +239,9 @@ pub fn deinit(self: *Page) void { if (comptime IS_DEBUG) { var it = self._arena_pool_leak_track.valueIterator(); 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) { var it = self._arena_pool_leak_track.valueIterator(); 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(); } @@ -370,14 +377,23 @@ const GetArenaOpts = struct { pub fn getArena(self: *Page, comptime opts: GetArenaOpts) !Allocator { const allocator = try self.arena_pool.acquire(); 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; } pub fn releaseArena(self: *Page, allocator: Allocator) void { 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); } From 5d56fea2d3849742cbbde2c4afb06947e5dada03 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 28 Jan 2026 14:25:37 +0800 Subject: [PATCH 2/2] check for leak after context is removed, as that can cause finalizers to run --- src/browser/Page.zig | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index e82184a8..1853958b 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -249,19 +249,20 @@ pub fn deinit(self: *Page) void { } fn reset(self: *Page, comptime initializing: bool) !void { - if (comptime IS_DEBUG) { - var it = self._arena_pool_leak_track.valueIterator(); - while (it.next()) |value_ptr| { - if (value_ptr.count > 0) { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); - } - } - self._arena_pool_leak_track.clearRetainingCapacity(); - } - if (comptime initializing == false) { self._session.executor.removeContext(); + // removing a context can trigger finalizers, so we can only check for + // a leak after the above. + if (comptime IS_DEBUG) { + var it = self._arena_pool_leak_track.valueIterator(); + while (it.next()) |value_ptr| { + log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); + } + self._arena_pool_leak_track.clearRetainingCapacity(); + } + + // We force a garbage collection between page navigations to keep v8 // memory usage as low as possible. self._session.browser.env.memoryPressureNotification(.moderate);