Skip to content

desktop_webview_window (Linux): use-after-free / SIGSEGV on every window close #497

Description

@sioakim

Environment

  • Package: desktop_webview_window 0.3.0 (pub.dev)
  • Platform: Linux (Ubuntu 24.04, GTK3, WebKitGTK 4.1, aarch64), Wayland session
  • Flutter 3.44.1

Bug: deterministic use-after-free on window close (SIGSEGV)

Every time a WebviewWindow on Linux is closed (both via the in-app "close"
call and via the window manager's own close button), the app segfaults.
Reproduced consistently while building a login flow that opens a webview,
waits for an OAuth redirect, then closes the window.

Backtrace (gdb, -batch -ex run -ex "bt full"):

Thread 1 "sentry_recovery" received signal SIGSEGV, Segmentation fault.
0x0000fffff47f1448 in g_type_check_instance_cast () from /lib/aarch64-linux-gnu/libgobject-2.0.so.0
#0  g_type_check_instance_cast () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0
#1  FL_METHOD_CHANNEL (ptr=0xeb41cf438ea94eb0) at linux/webview_window.cc:24  <fl_method_channel.h>
#2  WebviewWindow::WebviewWindow(...)::$_0::operator()(_GtkWidget*, void*) const
      at linux/webview_window.cc:109
#3  ... ::$_0::__invoke(...)
#4  g_closure_invoke ()
#5-8 (libgobject internals)
#9  g_signal_emit_valist ()
#10 g_signal_emit ()
#11 (libgtk-3 internals)
#12 g_object_run_dispose ()
#13 gtk_main_do_event ()
...
#19 main () at linux/runner/main.cc

ptr in FL_METHOD_CHANNEL() is garbage — clearly freed/reused memory, not
a valid pointer.

Root cause

In linux/webview_window.cc, the constructor connects a "destroy" signal
handler on window_ that captures this and, on the way out, invokes
on_close_callback_() before using window->method_channel_:

g_signal_connect(G_OBJECT(window_), "destroy",
                 G_CALLBACK(+[](GtkWidget *, gpointer arg) {
                   auto *window = static_cast<WebviewWindow *>(arg);
                   if (window->on_close_callback_) {
                     window->on_close_callback_();
                   }
                   auto *args = fl_value_new_map();
                   fl_value_set(args, fl_value_new_string("id"),
                                fl_value_new_int(window->window_id_));
                   fl_method_channel_invoke_method(
                       FL_METHOD_CHANNEL(window->method_channel_),
                       "onWindowClose", args, nullptr, nullptr, nullptr);
                 }),
                 this);

In linux/desktop_webview_window_plugin.cc, on_close_callback_ is set to:

auto webview = std::make_unique<WebviewWindow>(
    self->method_channel, window_id,
    [self, window_id]() {
      self->windows->erase(window_id);   // <-- destructs the unique_ptr,
                                          //     i.e. `delete this` for the
                                          //     WebviewWindow being erased
      g_object_unref(self);
    },
    title, width, height, title_bar_height);

self->windows is a std::map<int64_t, std::unique_ptr<WebviewWindow>>.
Calling .erase(window_id) synchronously destructs the WebviewWindow
(~WebviewWindow()this freed) while the "destroy" signal handler is
still executing
. The handler then keeps using the now-dangling window
pointer (window->window_id_, window->method_channel_) to fire
"onWindowClose" — a same-call-frame use-after-free, not a race. It
reproduces on essentially every window close.

Suggested fix

Capture what's needed into locals before invoking on_close_callback_(),
and use only those locals afterward:

g_signal_connect(G_OBJECT(window_), "destroy",
                 G_CALLBACK(+[](GtkWidget *, gpointer arg) {
                   auto *window = static_cast<WebviewWindow *>(arg);
                   auto *method_channel = window->method_channel_;
                   auto window_id = window->window_id_;
                   g_object_ref(method_channel);
                   if (window->on_close_callback_) {
                     window->on_close_callback_();  // may free `window`
                   }
                   auto *args = fl_value_new_map();
                   fl_value_set(args, fl_value_new_string("id"),
                                fl_value_new_int(window_id));
                   fl_method_channel_invoke_method(
                       FL_METHOD_CHANNEL(method_channel),
                       "onWindowClose", args, nullptr, nullptr, nullptr);
                   g_object_unref(method_channel);
                 }),
                 this);

As a defensive second layer, ~WebviewWindow() also never disconnects this
"destroy" handler (which captures this) before the object is destroyed —
worth adding g_signal_handler_find/g_signal_handler_disconnect for it
in the destructor too, in case of a deferred/re-entrant "destroy"
emission on an already-freed window.

I'm currently carrying both fixes as a vendored local patch to unblock a
Linux port. Happy to open a PR with this change if useful — let me know.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions