MixinNetwork / MixinNetwork/flutter-plugins

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

Open
#488 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

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.

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

Reproduce the crash with the supplied Flutter project and Linux runner patch, starting at linux/runner/my_application.cc and the desktop_multi_window child-engine registration callback. Trace child-window cleanup around the implicit Flutter view and compare the close(), destroy(), and native X paths. Done means all three paths close the child, release its resources, and leave the host running without implicit-view, GTK/GLX, or segmentation-fault errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, dart, linux
Domain
desktop
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.