From 72770cf438c27eafa48f333ada1009ab7d571237 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 9 Jun 2026 10:45:55 +0800 Subject: [PATCH] webapi, gc: Give FormData its own arena Rather than relying on the frame_arena, use a distinct arena for FormData. This generally results in tighter memory usage, but more importantly it ensures that if FormData outlives the frame, we don't get a UAF. This can happen if the FormData is refernced in v8 and finalized late (e.g. after the frame would appear to still be needed). Also, in Frame.submitForm use the explicit acquireRef and releaseRef. This FormData can [in theory] be passed to JS, via the `formdata` event that we fire. --- src/browser/Frame.zig | 4 ++-- src/browser/webapi/net/FormData.zig | 26 +++++++++++++++----------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 3e8fd8b2..61667086 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -4302,8 +4302,8 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For // The submitter can be an input box (if enter was entered on the box) // I don't think this is technically correct, but FormData handles it ok const form_data = try FormData.init(form, submitter_, &self.js.execution); - // FormData.init acquires file's references. So we must release them once done. - defer form_data.deinit(self._page); + form_data.acquireRef(); + defer form_data.releaseRef(self._page); const arena = try self._session.getArena(.medium, "submitForm"); errdefer self._session.releaseArena(arena); diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index cafbff3d..5f8b6986 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -72,14 +72,18 @@ pub const Entry = struct { }; pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormData { - const form = form_ orelse { - return try exec._factory.create(FormData{ - ._rc = .{}, - ._arena = exec.arena, - ._entries = .empty, - }); + const arena = try exec.getArena(.small, "FormData"); + errdefer exec.releaseArena(arena); + + const form_data = try arena.create(FormData); + form_data.* = .{ + ._rc = .{}, + ._arena = arena, + ._entries = .empty, }; + const form = form_ orelse return form_data; + const frame = switch (exec.js.global) { .frame => |f| f, .worker => lp.assert(false, "FormData worker form", .{}), @@ -93,12 +97,10 @@ pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormD form._constructing_entry_list = true; defer form._constructing_entry_list = false; - const form_data = try exec._factory.create(FormData{ - ._rc = .{}, - ._arena = exec.arena, - ._entries = try collectForm(frame.arena, form, submitter, frame), - }); + form_data._entries = try collectForm(arena, form, submitter, frame); + // Hold a reference on each entry's File for the FormData's lifetime; released + // in deinit. for (form_data._entries.items) |entry| { switch (entry.value) { .file => |file| file.acquireRef(), @@ -123,6 +125,8 @@ pub fn deinit(self: *FormData, page: *Page) void { else => {}, } } + // Frees the entry list and this FormData itself; do not touch self afterwards. + page.releaseArena(self._arena); } pub fn releaseRef(self: *FormData, page: *Page) void {