From 8ee644d9bf6a890b5bf1fd53438934f1fcc4eca4 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 5 May 2026 15:19:10 +0800 Subject: [PATCH] Protect against response double-free When Fetch.httpErrorCallback rejects the promise, microtasks are run. Those microtasks could do anything, including triggering a frame shutdown (e.g. via a navigation event). The result is that we end up with httpErrorCallback httpShutdownCallback And the code simply isn't designed to handle that. httpErrorCallback now takes control of the cleanup, capturing the `self._owns_response` in a local, and disabling so that any nested `httpShutdownCallback` are noops. --- src/browser/webapi/net/Fetch.zig | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 6fe47ad7..c186b818 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -249,10 +249,17 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { var response = self._response; response._http_response = null; + + // Capture this before we reject. Rejection could trigger httpShutdownCallback + // (via a microtask callback). But if we're here, then we'll take care of + // cleaning up when we're done. + const owns_response = self._owns_response; + self._owns_response = false; + // the response is only passed on v8 on success, if we're here, it's safe to // clear this. (defer since `self is in the response's arena). - defer if (self._owns_response) { + defer if (owns_response) { response.deinit(self._exec.context.page); }; @@ -266,10 +273,6 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { fn httpShutdownCallback(ctx: *anyopaque) void { const self: *Fetch = @ptrCast(@alignCast(ctx)); - if (comptime IS_DEBUG) { - // should always be true - std.debug.assert(self._owns_response); - } if (self._owns_response) { var response = self._response;