MixinNetwork / MixinNetwork/flutter-plugins

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

Open
#497 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C
Stars
512
Forks
292
Avg merge
15h 8m
Merged PRs (30d)
7

Description

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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with linux/webview_window.cc and linux/desktop_webview_window_plugin.cc, tracing the destroy signal handler and the close callback's window removal. Reproduce the Linux close flow, then verify that both in-app and window-manager closes complete without a SIGSEGV and still deliver onWindowClose.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, linux
Domain
desktop
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.