Skip to content

[desktop_multi_window]Linux: closing a child window can crash the app #488

Open

Description

@RelicOfTesla

Linux: closing a desktop_multi_window child window can crash the app

On Linux/X11, all three tested child-window closing paths trigger an
application crash:

  • windowManager.close() requested by the host window
  • windowManager.destroy() requested by the host window
  • Clicking the native title-bar X button

The three paths trigger the same class of failure during child-window cleanup.

Reproduce Steps

flutter create --platforms=linux desktop_multi_window_crash
cd desktop_multi_window_crash
  1. Replace pubspec.yaml with the content below.
  2. Replace lib/main.dart with the content below.
  3. Apply the Linux runner patch below.
  4. Run the application:
flutter pub get
flutter build linux --debug
GDK_SYNCHRONIZE=1 \
  build/linux/x64/debug/bundle/desktop_multi_window_crash

The host window provides three buttons. Each button creates one child window
with a different argument. Run one test at a time and close the previous child
window before starting another test.

  • Automatic close: the host requests windowManager.close() after three
    seconds, then the process crashes during cleanup.
  • Automatic destroy: the host requests windowManager.destroy() after
    three seconds, then the process crashes during cleanup.
  • Manual X close: no automatic operation is performed. Click the native
    X; the process then crashes during cleanup.

pubspec.yaml

name: desktop_multi_window_crash
description: Reproduction for a Linux child-window close crash.
publish_to: none
version: 1.0.0+1

environment:
  sdk: ^3.12.2

dependencies:
  flutter:
    sdk: flutter
  desktop_multi_window: 0.3.0
  window_manager: 0.5.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0

flutter:
  uses-material-design: true

lib/main.dart

The argument selects the child-window behavior. The child content updates
every 16 ms so that the Flutter view remains active while the native window is
being closed.

import 'dart:async';

import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';

const manualChildArgument = 'child-manual-close';
const automaticCloseChildArgument = 'child-auto-close';
const automaticDestroyChildArgument = 'child-auto-destroy';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final controller = await WindowController.fromCurrentEngine();

  final argument = controller.arguments;
  if (argument == manualChildArgument ||
      argument == automaticCloseChildArgument ||
      argument == automaticDestroyChildArgument) {
    await windowManager.ensureInitialized();
    final currentWindow = await WindowController.fromCurrentEngine();
    await currentWindow.setWindowMethodHandler((call) async {
      switch (call.method) {
        case 'window_close':
          await windowManager.close();
          return null;
        case 'window_destroy':
          await windowManager.destroy();
          return null;
        default:
          throw MissingPluginException('Not implemented: ${call.method}');
      }
    });
    runApp(ChildApp(mode: argument));
    return;
  }

  runApp(const HostApp());
}

class HostApp extends StatelessWidget {
  const HostApp({super.key});

  Future<void> _openChild(String argument) async {
    await Future<void>.delayed(const Duration(milliseconds: 500));
    final controller = await WindowController.create(
      WindowConfiguration(arguments: argument, hiddenAtLaunch: false),
    );
    await controller.show();
    if (argument == automaticCloseChildArgument) {
      await Future<void>.delayed(const Duration(seconds: 3));
      await controller.invokeMethod('window_close');
    } else if (argument == automaticDestroyChildArgument) {
      await Future<void>.delayed(const Duration(seconds: 3));
      await controller.invokeMethod('window_destroy');
    }
  }

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Scaffold(
          body: Center(
            child: Wrap(
              spacing: 12,
              children: [
                ElevatedButton(
                  onPressed: () => unawaited(_openChild(automaticCloseChildArgument)),
                  child: const Text('Automatic close'),
                ),
                ElevatedButton(
                  onPressed: () => unawaited(_openChild(automaticDestroyChildArgument)),
                  child: const Text('Automatic destroy'),
                ),
                ElevatedButton(
                  onPressed: () => unawaited(_openChild(manualChildArgument)),
                  child: const Text('Manual X close'),
                ),
              ],
            ),
          ),
        ),
      );
}

class ChildApp extends StatelessWidget {
  const ChildApp({required this.mode, super.key});

  final String mode;

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: ChildSession(mode: widget.mode),
      );
}

class ChildSession extends StatefulWidget {
  const ChildSession({required this.mode, super.key});

  final String mode;

  @override
  State<ChildSession> createState() => _ChildSessionState();
}

class _ChildSessionState extends State<ChildSession> {
  Timer? timer;
  int frame = 0;

  @override
  void initState() {
    super.initState();
    timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
      if (mounted) setState(() => frame++);
    });
  }

  @override
  void dispose() {
    timer?.cancel();
    debugPrint('Child session disposed');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ColoredBox(
          color: Colors.primaries[frame % Colors.primaries.length],
          child: SizedBox(
            width: 240,
            height: 120,
            child: Center(
              child: Text(switch (mode) {
                automaticCloseChildArgument => 'Automatic close test',
                automaticDestroyChildArgument => 'Automatic destroy test',
                _ => 'Manual X close test',
              }),
            ),
          ),
        ),
      ),
    );
  }
}

Linux runner patch

Register all generated plugins for each child Flutter engine:

diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc
@@
 #include "flutter/generated_plugin_registrant.h"
+#include "desktop_multi_window/desktop_multi_window_plugin.h"
@@
   fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+  desktop_multi_window_plugin_set_window_created_callback(
+      [](FlPluginRegistry* registry) {
+        // Register plugins for the child Flutter engine.
+        fl_register_plugins(registry);
+      });

This follows the plugin registration pattern documented by
desktop_multi_window.

Observed result

All three paths can produce the following error before the process crashes:

The implicit view cannot be removed.

Depending on GTK, GLX, and window-manager timing, the process may also report:

BadAccess (attempt to access private resource denied)

or invalid FlView/GtkWidget pointer warnings, followed by a segmentation
fault.

Expected behavior

Closing any child window should release that child window's resources and
leave the host application running. No implicit-view error, GTK/GLX error, or
segmentation fault should occur.

Version (please complete the following information):

  • Flutter Version: 3.44.6 stable
  • Dart Version: 3.12.2
  • OS: Ubuntu/Linux with X11
  • Plugin: desktop_multi_window: 0.3.0
  • Related plugin: window_manager: 0.5.2

Additional context

desktop_multi_window creates a child window with its own Flutter engine and
an implicit Flutter view. During child-window close, Flutter tries to remove
that implicit view, but the engine rejects the operation with
kInvalidArguments. Native GTK/GLX cleanup then continues with an invalid or
already-invalid view state. The final error depends on timing, so it may be a
GLX error or a segmentation fault.

The three paths have the same observed failure class. This reproduction does
not show a reliable safety difference between close(), destroy(), and the
native X button.

Activity

  1. changed the title [-]Linux: closing a `desktop_multi_window` child window can crash the app[/-] [+][desktop_multi_window]Linux: closing achild window can crash the app[/+] on Jul 17, 2026
  2. changed the title [-][desktop_multi_window]Linux: closing achild window can crash the app[/-] [+][desktop_multi_window]Linux: closing a child window can crash the app[/+] on Jul 17, 2026
  3. rhpuppi commented on Oct 14, 2026

    @rhpuppi

    I hit the same crash and found the root cause. It is in the Flutter Linux embedder, not in this plugin.

    My setup: Flutter 3.47.7 stable, desktop_multi_window 0.3.1, window_manager 0.5.2, Ubuntu 26.04, GNOME on Wayland. It crashes with both the NVIDIA proprietary driver and Mesa llvmpipe.

    What happens

    1. Each window created by desktop_multi_window has its own FlEngine. Closing a window disposes its FlView and FlEngine.
    2. fl_opengl_manager_dispose() then calls eglTerminate() on the EGLDisplay. That display is process-wide: it is shared with the other engines and with GDK.
    3. The next repaint of any remaining window fails with Gdk-WARNING: eglMakeCurrent failed, and the app segfaults in gdk_window_end_draw_frame().

    Proof: in gdb, making eglTerminate return without running it removes the crash completely.

    Closing the main window while a child is open crashes the same way. window_manager replaces the main window's delete-event handler, which would normally request app exit, so GTK destroys the window and disposes the main engine while the child engine is still alive.

    Reported upstream with a plugin-free reproduction: flutter/flutter#194399 (comment)

    Workaround until it is fixed: never destroy a Flutter window while another one is alive. In each window (window_manager is registered per engine):

    class _MyWindowState extends State<MyWindow> with WindowListener {
      @override
      void initState() {
        super.initState();
        windowManager.addListener(this);
        windowManager.setPreventClose(true);
      }
    
      @override
      void onWindowClose() {
        // Child window: hide it instead of destroying it, and show it again later.
        windowManager.hide();
        // Main window: end the process instead of destroying the window:
        // exit(0);
      }
    }

    This keeps every engine alive until the process exits, so eglTerminate is never called while windows are on screen. I verified it in gdb by closing the child and then the main window: no crash, and the process exits normally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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