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.
This commit is contained in:
Adrià Arrufat
2026-06-05 09:47:15 +02:00
parent 0e12790397
commit ee21138bde
2 changed files with 23 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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);