From ee21138bdedcd02ca46a0f060d1a625d50a2311c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 5 Jun 2026 09:47:15 +0200 Subject: [PATCH] fix: drop orphaned deferred contexts on frame teardown A fetch() deferred behind a parser-blocking script keeps a DeferredContext in the DeferringLayer whose forward.ctx points at the Fetch struct, which lives in a page/session arena. If the transfer completes while deferred, it is deinited and unlinked from the frame owner, so abortTransfers -> abortOwner can't reach the now-orphaned context. It lingers in the active list, and when the page is torn down its Fetch arena is freed; a later flushFrame (e.g. the next page's parser-blocking script popping) replays the buffered header callback into the freed Fetch -> use-after-free. Add DeferringLayer.cancelFrame to drop these orphaned (terminal) contexts during Frame.abortTransfers. Non-terminal contexts still have a live transfer that cleans them up through its own callback path, so they are left alone. --- src/browser/Frame.zig | 5 ++++- src/network/layer/DeferringLayer.zig | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index c7f4ed92..eea0b8aa 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -895,7 +895,10 @@ pub fn abortTransfers(self: *Frame) void { for (self.child_frames.items) |child| { child.abortTransfers(); } - self._session.browser.http_client.abortOwner(&self._http_owner); + const http_client = &self._session.browser.http_client; + http_client.abortOwner(&self._http_owner); + // abortOwner misses deferred contexts whose transfer already completed. + http_client.deferring_layer.cancelFrame(self._frame_id); } pub fn documentIsLoaded(self: *Frame) void { diff --git a/src/network/layer/DeferringLayer.zig b/src/network/layer/DeferringLayer.zig index a09fc2fc..fd69b5c6 100644 --- a/src/network/layer/DeferringLayer.zig +++ b/src/network/layer/DeferringLayer.zig @@ -113,6 +113,25 @@ pub fn flushFrame(self: *DeferringLayer, frame_id: u32) void { } } +/// Drop orphaned deferred contexts for a frame that's going away. A `terminal` +/// context's transfer already completed while deferred, so it's been deinited +/// and unlinked from the owner — abortOwner can't reach it, yet it lingers in +/// `active` pointing at a forward target (the Fetch) whose arena page teardown +/// is about to free, and a later flushFrame would fire into it. Non-terminal +/// contexts still have a live transfer that cleans them up itself. +pub fn cancelFrame(self: *DeferringLayer, frame_id: u32) void { + var node = self.active.first; + while (node) |n| { + node = n.next; + const ctx: *DeferredContext = @fieldParentPtr("node", n); + if (ctx.frame_id != frame_id or !ctx.terminal) { + continue; + } + self.active.remove(n); + ctx.deinit(); + } +} + pub fn drainAll(self: *DeferringLayer) void { while (self.active.popFirst()) |node| { const ctx: *DeferredContext = @fieldParentPtr("node", node);