Skip to content

[Linux] Use-after-free of FlTaskRunner on app shutdown: timeout source outlives the task runner #193742

Closed

Description

@Mk-strg

Steps to reproduce

  1. Run a Flutter Linux desktop app that uses the window_manager plugin (observed with FluffyChat, Flathub Flatpak im.fluffychat.Fluffychat).
  2. Use the app normally.
  3. Close the main window.

The app crashes with SIGSEGV very often (not every time) during shutdown.

Expected results

The app exits cleanly.

Actual results

On exit the following is logged, followed by a segfault:

embedder.cc (2603): 'FlutterEngineRemoveView' returned 'kInvalidArguments'. Remove view info was invalid. The implicit view cannot be removed.

The crash happens on the main thread while g_application_run() is still iterating the main loop: a GLib timeout source fires and its callback in libflutter_linux_gtk.so casts its user_data to FlTaskRunner, but that object has already been finalized.

Note that user_data of the timeout source and type_instance in the failed cast are the same address (0x55c78106db20).

fl_task_runner_tasks_did_change_locked() schedules
g_timeout_add(..., fl_task_runner_on_expired_timeout, self) without holding a reference to self. If the FlTaskRunner is disposed/finalized during engine shutdown while this timeout is still pending, the source is not removed and fl_task_runner_on_expired_timeout() runs on freed memory (FL_TASK_RUNNER(data) → g_type_check_instance_cast → SIGSEGV).

Possible fixes: remove timeout_source_id in fl_task_runner_dispose(), or let the timeout source hold a strong or weak reference to the task runner.

This is the same class of bug as #192873 (idle source in FlView firing after the view was destroyed). window_manager seems to make the race much more likely by changing how the view is torn down (see leanflutter/window_manager#585). Possibly also related: #164564.

Code sample

Code sample
no minimal sample yet. Reproduced with FluffyChat (https://github.com/krille-chan/fluffychat), Linux Flatpak build. According to leanflutter/window_manager#585, just adding `window_manager` as a dependency is enough to get the `FlutterEngineRemoveView` error on exit.

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs Backtrace (gdb via flatpak-coredumpctl, GNOME 50 SDK + debug symbols) ``` Signal: 11 (SEGV)

#0 g_type_check_instance_cast (type_instance=0x55c78106db20, iface_type=0x55c7813e8560 [FlTaskRunner]) at ../gobject/gtype.c:3949
#1 0x00007f6fe94f5a7d in ??? () at /app/fluffychat/lib/libflutter_linux_gtk.so
#2 0x00007f6fe8106901 in g_timeout_dispatch (source=0x55c782de0dc0, callback=, user_data=) at ../glib/gmain.c:5324
#3 0x00007f6fe810252b in g_main_dispatch (context=context@entry=0x55c780ee79e0) at ../glib/gmain.c:3591
dispatch = 0x7f6fe81068e0 <g_timeout_dispatch>
user_data = 0x55c78106db20
callback = 0x7f6fe94f5a30
source = 0x55c782de0dc0
#4 0x00007f6fe8105957 in g_main_context_dispatch_unlocked (context=0x55c780ee79e0) at ../glib/gmain.c:4451
#5 g_main_context_iterate_unlocked (context=context@entry=0x55c780ee79e0, block=block@entry=0, dispatch=dispatch@entry=1, self=) at ../glib/gmain.c:4516
#6 0x00007f6fe81061b3 in g_main_context_iteration (context=context@entry=0x55c780ee79e0, may_block=may_block@entry=0) at ../glib/gmain.c:4582
#7 0x00007f6fe82feaaa in g_application_run (application=0x55c780ee6050 [MyApplication], argc=, argv=) at ../gio/gapplication.c:2773
#8 0x000055c75085d4ae in main ()


All other threads are idle (tokio workers from vodozemac, glycin image loader threads).


</details>


### Flutter Doctor output

<details open><summary>Doctor output</summary>

Not built locally. The app is the official FluffyChat Flatpak `<FluffyChat-Version>` (Flutter version: see FluffyChat release notes), runtime `org.gnome.Platform//50`, GLib 2.88.3, Fedora `<Version>`, `<GNOME/KDE>` on Wayland.

</details>

Activity

  1. suryaKommana2662 commented on Oct 5, 2026

    @suryaKommana2662
    Contributor

    @Mk-strg Thanks for the detailed report. Could you provide a minimal Flutter Linux application that reproduces the issue? In particular, a minimal example using window_manager that demonstrates the crash during shutdown would help isolate the lifecycle issue from FluffyChat.

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    and removed
    in triagePresently being triaged by the triage team
    on Oct 5, 2026
  3. Mk-strg commented on Oct 5, 2026

    @Mk-strg
    Author

    @Mk-strg Thanks for the detailed report. Could you provide a minimal Flutter Linux application that reproduces the issue? In particular, a minimal example using window_manager that demonstrates the crash during shutdown would help isolate the lifecycle issue from FluffyChat.

    Hi thanks for reviewing this issue. I was able to reproduce this with a minimal app that only depends on window_manager (0.5.2), built natively on the host with Flutter 3.47.6 (current stable). No Flatpak, no FluffyChat.

    Steps to reproduce

    flutter create --platforms=linux shutdown_repro
    cd shutdown_repro
    flutter pub add window_manager
    # replace lib/main.dart with the code below, add repro.sh
    flutter build linux --release --dart-define=AUTO_CLOSE=true
    chmod +x repro.sh
    ./repro.sh

    The app keeps the engine busy (continuous CircularProgressIndicator animation plus a 5 ms periodic platform call) and closes itself after a random delay of 1.5–3 s via windowManager.close(), which emits the same delete-event as clicking the close button. repro.sh starts the app repeatedly and counts crashes by exit code.

    lib/main.dart
    import 'dart:async';
    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'package:window_manager/window_manager.dart';
    
    const autoClose = bool.fromEnvironment('AUTO_CLOSE');
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await windowManager.ensureInitialized();
    
      const options = WindowOptions(size: Size(800, 600), title: 'shutdown repro');
      windowManager.waitUntilReadyToShow(options, () async {
        await windowManager.show();
        await windowManager.focus();
      });
    
      runApp(const ReproApp());
    }
    
    class ReproApp extends StatefulWidget {
      const ReproApp({super.key});
    
      @override
      State<ReproApp> createState() => _ReproAppState();
    }
    
    class _ReproAppState extends State<ReproApp> {
      Timer? _timer;
      bool _busy = false;
      int _ticks = 0;
    
      @override
      void initState() {
        super.initState();
    
        // Keep the platform task runner busy so engine work is likely
        // pending when the window is closed.
        _timer = Timer.periodic(const Duration(milliseconds: 5), (_) async {
          if (_busy) return;
          _busy = true;
          try {
            await windowManager.isFocused();
            if (mounted) setState(() => _ticks++);
          } catch (_) {
            // Ignore errors while the app is shutting down.
          } finally {
            _busy = false;
          }
        });
    
        if (autoClose) {
          // Close at a random moment (1.5–3 s) to hit different points of the
          // frame/task cycle. windowManager.close() emits the same delete-event
          // as clicking the close button.
          final delayMs = 1500 + Random().nextInt(1500);
          Future.delayed(Duration(milliseconds: delayMs), () => windowManager.close());
        }
      }
    
      @override
      void dispose() {
        _timer?.cancel();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  // Continuous animation, like FluffyChat's typing indicator.
                  const CircularProgressIndicator(),
                  const SizedBox(height: 16),
                  Text('ticks: $_ticks'),
                ],
              ),
            ),
          ),
        );
      }
    }
    repro.sh
    #!/bin/bash
    BIN=build/linux/x64/release/bundle/shutdown_repro
    RUNS=${1:-50}
    segv=0
    abrt=0
    other=0
    
    for i in $(seq 1 "$RUNS"); do
      "$BIN" >/dev/null 2>>stderr.log
      rc=$?
      case $rc in
        0)   ;;
        139) segv=$((segv + 1)) ;;
        134) abrt=$((abrt + 1)) ;;
        *)   other=$((other + 1)) ;;
      esac
      echo "run $i: exit code $rc"
    done
    
    echo
    echo "SIGSEGV (139): $segv/$RUNS"
    echo "SIGABRT (134): $abrt/$RUNS"
    echo "other non-zero: $other/$RUNS"
    echo "stderr of all runs saved in stderr.log"

    Results

    3 crashes with SIGSEGV (exit code 139) in 82 runs (~3.7%), 0 SIGABRT.

    The following message is printed on every exit (82/82), including the clean ones:

    embedder.cc (2615): 'FlutterEngineRemoveView' returned 'kInvalidArguments'. Remove view info was invalid. The implicit view cannot be removed.
    

    So window_manager sends the engine through an invalid view-removal path on every shutdown, and the crash happens only when a task-runner timeout is still pending at that moment.

    Backtrace of a crash from the minimal app

    Signal: 11 (SEGV)
    #0  g_pointer_bit_lock_and_get (libglib-2.0.so.0 + 0x4f9d)
    #1  g_datalist_id_dup_data (libglib-2.0.so.0 + 0x205e9)
    #2  g_weak_ref_get (libgobject-2.0.so.0 + 0x134b3)
    #3  libflutter_linux_gtk.so + 0x519430
    #4  libflutter_linux_gtk.so + 0x5195c4
    #5  g_timeout_dispatch (libglib-2.0.so.0 + 0x47022)
    #6  g_main_context_dispatch_unlocked (libglib-2.0.so.0 + 0x43f24)
    #7  g_main_context_iterate_unlocked (libglib-2.0.so.0 + 0x48038)
    #8  g_main_context_iteration (libglib-2.0.so.0 + 0x481e3)
    #9  g_application_run (libgio-2.0.so.0 + 0xbd0ba)
    #10 main (shutdown_repro + 0x76e)
    

    All other threads are idle (Mesa util_queue workers, GDBus, dconf, glycin).

    This matches the original report: a GLib timeout source created by the embedder fires during shutdown after its owner has been freed. In current stable, FlTaskRunner stores the engine in a GWeakRef. In this release build the crash shows up at g_weak_ref_get() on the freed task runner, rather than in the FL_TASK_RUNNER() cast seen with the older Flutter version used by FluffyChat (g_type_check_instance_cast(..., [FlTaskRunner])). Symbols for libflutter_linux_gtk.so should confirm that frames #3–#4 are fl_task_runner_on_expired_timeout.

    In FluffyChat, the crash is most frequent when the window is closed while an animation is running (e.g. the typing indicator), which fits the "pending engine task during teardown" theory.

    Environment

    Flutter 3.47.6 • channel stable • https://github.com/flutter/flutter.git
    Framework • revision 5fc346839b • 2026-09-30 15:02:49 -0700
    Engine • hash b8c8d3d8d5d0095127057f8a29ca8cc53da2167c (revision 692136cb65) • 2026-09-30 00:56:59.000Z
    Tools • Dart 3.13.5 • DevTools 2.60.0
    

    Fedora 44, KDE Plasma (kde-gtk-config 6.7.5), Wayland, Mesa 26.2.3, GTK 3.24.52, GLib 2.88.3, window_manager 0.5.2

  4. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Oct 5, 2026
  5. suryaKommana2662 commented on Oct 6, 2026

    @suryaKommana2662
    Contributor

    @Mk-strg Thanks for providing the minimal reproduction. I was able to reproduce the crash locally using the provided reproduction setup. On my local Linux environment, running the reproduction script 5 times resulted in:

    SIGSEGV (139): 3/5 runs
    SIGABRT (134): 2/5 runs
    Other non-zero exits: 0/5
    

    The issue has sufficient information for further investigation, including a minimal reproduction, crash logs, and a backtrace. Routing this to the team-linux for further investigation.

    Flutter Doctor Output

    [✓] Flutter (Channel stable, 3.47.6, on Ubuntu 26.04 LTS 6.6.143-09589-gf9e106250941,
        locale C.UTF-8) [69ms]
        • Flutter version 3.47.6 on channel stable at /home/suryak2662/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 5fc346839b (5 days ago), 2026-09-30 15:02:49 -0700
        • Engine revision 692136cb65
        • Dart version 3.13.5
        • DevTools version 2.60.0
        • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop,
          enable-windows-desktop, enable-android, enable-ios, cli-animations,
          enable-native-assets, enable-record-use, enable-swift-package-manager,
          omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
        [4.4s]
        • Android SDK at /home/suryak2662/Android/Sdk
        • Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
        • Platform android-36, build-tools 36.0.0
        ! Multiple adb binaries found. This can cause conflicts and device detection
          issues:
            - /home/suryak2662/Android/Sdk/platform-tools/adb
            - /usr/lib/android-sdk/platform-tools/adb
        • Java binary at: /opt/android-studio/jbr/bin/java
          This JDK is specified in your Flutter configuration.
          To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
        • Java version OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)
        • All Android licenses accepted.
    
    [✓] Chrome - develop for the web [13ms]
        • Chrome at google-chrome
    
    [✓] Linux toolchain - develop for Linux desktop [722ms]
        • Ubuntu clang version 21.1.8 (6ubuntu1)
        • cmake version 4.2.3
        • ninja version 1.13.2
        • pkg-config version 2.5.1
        • OpenGL core renderer: llvmpipe (LLVM 21.1.8, 256 bits)
        • OpenGL core version: 4.5 (Core Profile) Mesa 26.0.3-1ubuntu1
        • OpenGL core shading language version: 4.50
        • OpenGL ES renderer: llvmpipe (LLVM 21.1.8, 256 bits)
        • OpenGL ES version: OpenGL ES 3.2 Mesa 26.0.3-1ubuntu1
        • OpenGL ES shading language version: OpenGL ES GLSL ES 3.20
        • GL_EXT_framebuffer_blit: yes
        • GL_EXT_texture_format_BGRA8888: yes
    
    [✓] Connected device (2 available) [1,096ms]
        • Linux (desktop) • linux  • linux-x64      • Ubuntu 26.04 LTS
          6.6.143-09589-gf9e106250941
        • Chrome (web)    • chrome • web-javascript • Google Chrome 150.0.7871.128
    
    [✓] Network resources [987ms]
        • All expected network resources are available.
    
    • No issues found!
  6. added
    in triagePresently being triaged by the triage team
    platform-linuxBuilding on or for Linux specifically
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-linuxOwned by the Linux platform team
    c: crashStack traces logged to the console
    and removed
    in triagePresently being triaged by the triage team
    on Oct 6, 2026
  7. Mk-strg commented on Oct 6, 2026

    @Mk-strg
    Author

    Thanks for confirming! Interesting that you also hit the SIGABRT variant. I saw that one in FluffyChat as well (libepoxy assertion, "Couldn't find current GLX or EGL context"), so both signatures seem to come from the same shutdown race. Happy to test a fix once there is one.

  8. siegblink commented on Oct 7, 2026

    @siegblink

    We hit the same segfault on quit (g_type_check_instance_cast+48, main thread) in a Linux app that uses window_manager 0.5.2 on Flutter 3.47.2, and looked into why fl_task_runner_dispose() doesn't remove the pending timeout. It removes timeout_source_id (L123-L136), but the pending source can be one the runner no longer tracks:

    1. The main thread starts dispatching the tracked timeout X, but fl_task_runner_on_expired_timeout hasn't taken self->mutex yet (L75-L83).
    2. Another thread posts a task. Holding the mutex, fl_task_runner_tasks_did_change_locked calls g_source_remove(X) (X is already dispatching, so its callback still runs), adds timeout Y and sets timeout_source_id = Y (L108-L120).
    3. X's callback gets the mutex and runs self->timeout_source_id = 0;. Y is still attached, but nothing tracks it.

    dispose then removes nothing, or only a later tracked timeout, and Y fires on the freed runner: FL_TASK_RUNNER(data) → g_type_check_instance_cast → SIGSEGV. When Y fires while the runner is alive, its callback zeroes the id again, so under continuous load the next timeout is orphaned in turn.

    With the default UI-thread policy Dart runs on the platform thread, so every dart:io, isolate or FFI wake-up posts to the runner from another thread. A standalone C copy of fl_task_runner.cc's logic on GLib 2.80 shows the race without any delay added: with two or four threads posting every 1.5 to 9 ms, 9 of 12 ten-second runs left untracked timeouts (up to 15 per run), each one from a poster removing the timeout the main thread had started; with one posting thread, none. One run under continuous posting carried an untracked timeout through 3,315 callbacks. A quit crashes only when the runner is freed while one is pending, which fits the crash being intermittent. Under gdb, adding an untracked task-runner timeout at the engine's dispose reproduces the top of the reported backtrace: g_type_check_instance_cast called from fl_task_runner_on_expired_timeout, from GLib's timeout dispatch.

    Possible fixes: in fl_task_runner_on_expired_timeout, clear timeout_source_id only when it is the id of the source being dispatched (g_source_get_id(g_main_current_source())); or let each timeout hold a reference to the runner, and stop rescheduling once the engine is gone.

    App-side workaround that avoids it for us: the runner keeps a reference to the engine and drops it after g_application_run() returns, so the engine, its task runner and the plugins are disposed when the main loop can no longer dispatch anything:

    // linux/runner/my_application.cc
    struct _MyApplication {
      GtkApplication parent_instance;
      char** dart_entrypoint_arguments;
      FlEngine* engine;
    };
    
    // my_application_activate, after fl_view_new():
    g_set_object(&self->engine, fl_view_get_engine(view));
    
    // my_application_dispose (runs when main()'s g_autoptr drops the app):
    g_clear_object(&self->engine);
  9. Mk-strg commented on Oct 7, 2026

    @Mk-strg
    Author

    @siegblink Thanks a lot for the detailed root-cause analysis and the workaround, that was really helpful! I can confirm both on Flutter 3.47.6 with the minimal reproducer:

    Workaround as applied to the default linux/runner/my_application.cc (everything else unchanged)
     struct _MyApplication {
       GtkApplication parent_instance;
       char** dart_entrypoint_arguments;
    +  FlEngine* engine;
     };
    
     // in my_application_activate():
       FlView* view = fl_view_new(project);
    +  g_set_object(&self->engine, fl_view_get_engine(view));
    
     // in my_application_dispose():
       g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
    +  g_clear_object(&self->engine);
       G_OBJECT_CLASS(my_application_parent_class)->dispose(object);

    main.cc is the unmodified template (g_autoptr(MyApplication)), so the app and thus the engine are released after g_application_run() returns.

    SIGSEGV SIGABRT
    Without workaround 5/200 0/200
    With workaround 0/200 0/200

    The FlutterEngineRemoveView ... kInvalidArguments message is still printed on every exit (200/200). The workaround doesn't change the teardown path; it only ensures the engine and its task runner outlive the main loop, so an orphaned timeout can no longer fire on a freed runner.

    That said, this is only a workaround and the underlying race in FlTaskRunner should still be fixed in the engine:

    • Any Linux app built from the default flutter create template is affected, since the template doesn't keep a reference to the engine. Most app developers won't know they need this.
    • The race isn't specific to window_manager. It only needs other threads posting tasks while the runner is disposed (dart:io, isolates, FFI); window_manager just makes the teardown timing worse.

    Either of the fixes suggested above would address the root cause: only clearing timeout_source_id in fl_task_runner_on_expired_timeout when it matches the source being dispatched (g_source_get_id(g_main_current_source())), or having each timeout hold a reference to the runner and stop rescheduling once the engine is gone.

    For completeness, one more crash signature from a run without the workaround. It's the timeout callback touching self->pending_tasks of the freed runner:

    #0 g_list_remove_link (libglib-2.0.so.0 + 0x3fb08)
    #1 libflutter_linux_gtk.so + 0x519402
    #2 libflutter_linux_gtk.so + 0x5195c4
    #3 g_timeout_dispatch
    ...
    #7 g_application_run
    
  10. added
    P2Important issues not at the top of the work list
    triaged-linuxTriaged by the Linux platform team
    on Oct 16, 2026
  11. github-actions commented on Nov 4, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  12. locked as resolved and limited conversation to collaborators on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listc: crashStack traces logged to the consoleengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-linuxBuilding on or for Linux specificallyteam-linuxOwned by the Linux platform teamtriaged-linuxTriaged by the Linux platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions