From 223611d89eefe36c2e6bee9ef5482aeb1eab260e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 18 Jun 2025 14:49:15 +0800 Subject: [PATCH] Fix crash when event.currentTarget is used with EventTargetTBase When EventTargetTBase is used, we pass the container as the target to libdom. This is not safe, as libdom is expecting an event_target. We see, for example that when _dom_event_get_current_target is called, the refcnt is increased. This works if the current_target is a valid event_target, but if it's a Zig instance (like the Window) ... we're just altering some bits of the window instance. This attempts to add a dummy target to EventTargetTBase which can acts as a real event_targt in place of the Zig instance. --- src/browser/dom/event_target.zig | 2 +- src/browser/html/window.zig | 15 +++++++++++++++ src/browser/netsurf.zig | 19 +++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig index d2a7aae0..109cd7af 100644 --- a/src/browser/dom/event_target.zig +++ b/src/browser/dom/event_target.zig @@ -39,7 +39,7 @@ pub const EventTarget = struct { // Ideally, we'd remove this duality. Failing that, we'll need to embed // data into the *parser.EventTarget should we need this for other types. // For now, for the Window, which is a singleton, we can do this: - if (@intFromPtr(et) == @intFromPtr(&page.window.base)) { + if (@intFromPtr(et) == @intFromPtr(&page.window.base.target)) { return .{ .Window = &page.window }; } return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))); diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 74fa28b3..f18b0cd2 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -387,4 +387,19 @@ test "Browser.HTML.Window" { .{ "window.setTimeout(() => {longCall = true}, 5001);", null }, .{ "longCall;", "false" }, }, .{}); + + // window event target + try runner.testCases(&.{ + .{ + \\ let called = false; + \\ window.addEventListener("ready", (e) => { + \\ called = (e.currentTarget == window); + \\ }, {capture: false, once: false}); + \\ const evt = new Event("ready", { bubbles: true, cancelable: false }); + \\ window.dispatchEvent(evt); + \\ called; + , + "true", + }, + }, .{}); } diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 03db92e2..071871b8 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -777,6 +777,21 @@ pub const EventTargetTBase = extern struct { }, eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null }, + // When we dispatch the event, we need to provide a target. In reality, the + // target is the container of this EventTargetTBase. But we can't pass that + // to _dom_event_target_dispatch, because it expects a dom_event_target. + // If you try to pass an non-event_target, you'll get weird behavior. For + // example, libdom might dom_node_ref that memory. Say we passed a *Window + // as the target, what happens if libdom calls dom_node_ref(window)? If + // you're lucky, you'll crash. If you're unlucky, you'll increment a random + // part of the window structure. + target: DummyTarget = .{}, + + const DummyTarget = extern struct { + vtable: usize = undefined, + refcnt: u32 = 0, + }; + pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception { const self = @as(*Self, @ptrCast(et)); return c._dom_event_target_add_event_listener(&self.eti, t, l, capture); @@ -785,11 +800,11 @@ pub const EventTargetTBase = extern struct { pub fn dispatch_event(et: [*c]c.dom_event_target, evt: ?*c.struct_dom_event, res: [*c]bool) callconv(.C) c.dom_exception { const self = @as(*Self, @ptrCast(et)); // Set the event target to the target dispatched. - const e = c._dom_event_set_target(evt, et); + const e = c._dom_event_set_target(evt, @ptrCast(&self.target)); if (e != c.DOM_NO_ERR) { return e; } - return c._dom_event_target_dispatch(et, &self.eti, evt, c.DOM_AT_TARGET, res); + return c._dom_event_target_dispatch(@ptrCast(&self.target), &self.eti, evt, c.DOM_AT_TARGET, res); } pub fn remove_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {