From 223611d89eefe36c2e6bee9ef5482aeb1eab260e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 18 Jun 2025 14:49:15 +0800 Subject: [PATCH 1/2] 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 { From ce98c336c997667c48e6ecbd9d61b9fefec2d5f3 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 18 Jun 2025 07:08:35 -0700 Subject: [PATCH 2/2] keep EventTargetTBase as the dom_event_target Mimic a dom_node by adding the refcnt field right after the vtable pointer. --- src/browser/dom/event_target.zig | 2 +- src/browser/netsurf.zig | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig index 109cd7af..d2a7aae0 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.target)) { + if (@intFromPtr(et) == @intFromPtr(&page.window.base)) { return .{ .Window = &page.window }; } return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))); diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 071871b8..414dd56d 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -775,7 +775,6 @@ pub const EventTargetTBase = extern struct { .add_event_listener = add_event_listener, .iter_event_listener = iter_event_listener, }, - 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 @@ -785,12 +784,9 @@ pub const EventTargetTBase = extern struct { // 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 = .{}, + refcnt: u32 = 0, - const DummyTarget = extern struct { - vtable: usize = undefined, - refcnt: u32 = 0, - }; + eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null }, 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)); @@ -800,11 +796,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, @ptrCast(&self.target)); + const e = c._dom_event_set_target(evt, et); if (e != c.DOM_NO_ERR) { return e; } - return c._dom_event_target_dispatch(@ptrCast(&self.target), &self.eti, evt, c.DOM_AT_TARGET, res); + return c._dom_event_target_dispatch(et, &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 {