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.
This commit is contained in:
Karl Seguin
2026-06-09 10:45:55 +08:00
parent f4774f1ac2
commit 72770cf438
2 changed files with 17 additions and 13 deletions

View File

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

View File

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