Repository navigation
Description
This bug was generated using AI. Please take it with a grain of salt.
Internal bug: b/536957736.
Summary
fl_view_present_layers() (fl_view.cc:291-300) is the FlRenderable::present_layers
implementation invoked on the raster thread. It schedules a GTK-thread callback via
g_idle_add(redraw_cb, g_object_ref(self)) and discards the source ID.
g_object_ref(self) keeps the FlView allocated, but does not prevent
gtk_widget_destroy() / g_object_run_dispose() from running. fl_view_dispose()
(fl_view.cc:598-641) clears self->compositor and chains to the parent
GtkBox/GtkContainer dispose, which destroys all children. The child
GtkDrawingArea (self->render_area) was created with a floating reference sunk by
gtk_container_add (fl_view.cc:800-803); FlView holds no extra reference and never
NULLs the field, so it is finalized and freed here while self->render_area still
points at it.
When the still-pending idle then dispatches, redraw_cb() (fl_view.cc:112-148)
dereferences the freed GtkDrawingArea at lines 123/125/132/145 with no liveness
check (it ignores self->cancellable, never checks self->compositor != NULL,
and the idle source was never removed in dispose).
Trigger Sequence (deterministic, no race needed)
- Create an
FlView. - Call
fl_renderable_present_layers(FL_RENDERABLE(view), nullptr, 0)— exactly what
the raster thread does. This queuesg_idle_add(redraw_cb, g_object_ref(view)). gtk_widget_destroy(view)— runsfl_view_dispose, which freesrender_areavia
the chained container dispose. (AGWeakRefin the test confirms it is finalized.)- Drain the GLib main context —
redraw_cbfires and reads/writes the freed
GtkDrawingArea.
In a real app the race window is one main-loop iteration: the raster thread queues the
idle at G_PRIORITY_DEFAULT_IDLE while the GTK thread processes a window-close event
at higher priority, so destroy → idle is the natural ordering. The unit test simply
sequences those two steps explicitly on one thread.
Patch Contents (repro.patch)
Two files are touched:
-
shell/platform/linux/fl_view_test.cc— adds
TEST(FlViewTest, RedrawAfterDisposeUseAfterFree)implementing the sequence above. -
shell/platform/linux/testing/mock_gtk.cc— the existing test harness stubs
gtk_widget_get_allocation()as a no-op that never toucheswidget. Real GTK
dereferenceswidget->priv->allocation. A one-linevolatileread of
((GTypeInstance*)widget)->g_classis added so the dereference happens in
ASAN-instrumented code (systemlibgobject/libgtkare not instrumented). This
changes nothing about the vulnerable production code path; it only lets ASAN observe
whatredraw_cbis doing.Without this mock change the bug still reproduces — GLib emits
invalid unclassed pointer in cast to 'GtkWidget'three times from
g_type_check_instance_cast()reading the freed instance — but the read happens
inside uninstrumented system GLib so ASAN stays silent.
Prerequisites
- Flutter engine
host_debug_unoptbuild with ASAN (--asan --lsan) xvfb,dbus-run-sessionG_SLICE=always-mallocso GObject instances go throughmalloc/freeand ASAN
can poison the freedGtkDrawingArea
Reproduction
cd "$FLUTTER_ROOT"
git apply repro.patch
cd engine/src
./flutter/tools/gn --runtime-mode=debug --unoptimized --asan --lsan --no-goma --no-rbe
ninja -C out/host_debug_unopt flutter_linux_unittests
G_SLICE=always-malloc \
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 \
ASAN_SYMBOLIZER_PATH="$PWD/flutter/buildtools/linux-x64/clang/bin/llvm-symbolizer" \
dbus-run-session -- xvfb-run -a \
./out/host_debug_unopt/exe.unstripped/flutter_linux_unittests \
--gtest_filter='FlViewTest.RedrawAfterDisposeUseAfterFree'Expected Result
AddressSanitizer: heap-use-after-free with the read frame at
redraw_cb → fl_view.cc:123 and the free frame at g_type_free_instance (the
GtkDrawingArea being finalized during fl_view_dispose's chained container destroy).
Output
** (flutter_linux_unittests:342): CRITICAL **: gboolean fl_compositor_present_layers(FlCompositor *, const FlutterLayer **, size_t): assertion 'FL_IS_COMPOSITOR(self)' failed
(flutter_linux_unittests:342): GLib-GObject-WARNING **: invalid unclassed pointer in cast to 'GtkWidget'
=================================================================
==342==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bf3e0e10bb0 at pc 0x57c9de59ebb3 bp 0x7ffebe35c140 sp 0x7ffebe35c138
READ of size 8 at 0x7bf3e0e10bb0 thread T0
#0 0x57c9de59ebb2 in gtk_widget_get_allocation .../flutter/shell/platform/linux/testing/mock_gtk.cc:242:49
#1 0x57c9de6b554b in redraw_cb(void*) .../flutter/shell/platform/linux/fl_view.cc:123:3
#2 0x7ed3e2cad65e in g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5465e)
#3 0x7ed3e2cada17 (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x54a17)
#4 0x7ed3e2cadaab in g_main_context_iteration (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x54aab)
#5 0x57c9de53c72c in FlViewTest_RedrawAfterDisposeUseAfterFree_Test::TestBody() .../flutter/shell/platform/linux/fl_view_test.cc:305:10
...
0x7bf3e0e10bb0 is located 240 bytes inside of 280-byte region [0x7bf3e0e10ac0,0x7bf3e0e10bd8)
freed by thread T0 here:
#0 0x57c9de065b56 in free .../compiler-rt/lib/asan/asan_malloc_linux.cpp:51:3
#1 0x7ed3e2dca1bb in g_type_free_instance (/lib/x86_64-linux-gnu/libgobject-2.0.so.0+0x381bb)
previously allocated by thread T0 here:
#0 0x57c9de065df4 in malloc .../compiler-rt/lib/asan/asan_malloc_linux.cpp:87:3
#1 0x7ed3e2cb3658 in g_malloc (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5a658)
SUMMARY: AddressSanitizer: heap-use-after-free .../flutter/shell/platform/linux/testing/mock_gtk.cc:242:49 in gtk_widget_get_allocation
The 280-byte region is the GtkDrawingArea GObject allocation (private data + instance
struct); offset 240 is ((GTypeInstance*)render_area)->g_class. The preceding
invalid unclassed pointer in cast to 'GtkWidget' warning is GTK_WIDGET() at
fl_view.cc:123 already reading the same freed memory inside (uninstrumented)
g_type_check_instance_cast().
Full raw output is in asan.log.
Bandaid Fix
bandaid-fix.patch adds an early-return guard at the top of redraw_cb:
if (self->compositor == nullptr) {
return G_SOURCE_REMOVE;
}fl_view_dispose always clears self->compositor (g_clear_object at fl_view.cc:631)
before chaining to the parent dispose that frees render_area, so a NULL compositor is
a reliable "view has been disposed" signal. With the fix applied the test passes cleanly
under ASAN with no invalid unclassed pointer warnings.
A more thorough fix would also store the idle source ID in fl_view_present_layers and
g_source_remove() it in fl_view_dispose.
Attached files
asan.log
dbus[328]: Unable to set up transient service directory: XDG_RUNTIME_DIR "/run/user/1519684234" not available: No such file or directory
[INFO:flutter/testing/test_timeout_listener.cc(75)] Test timeout of 300 seconds per test case will be enforced.
Note: Google Test filter = FlViewTest.RedrawAfterDisposeUseAfterFree
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from FlViewTest
[ RUN ] FlViewTest.RedrawAfterDisposeUseAfterFree
dbus-daemon[328]: [session uid=1519684234 pid=328] Activating service name='org.a11y.Bus' requested by ':1.0' (uid=1519684234 pid=342 comm="./out/host_debug_unopt/exe.unstripped/flutter_linu" label="unconfined")
(process:345): dconf-CRITICAL **: 15:55:10.045: unable to create directory '/run/user/1519684234/dconf': Permission denied. dconf will not work properly.
(process:345): dconf-CRITICAL **: 15:55:10.045: unable to create directory '/run/user/1519684234/dconf': Permission denied. dconf will not work properly.
(process:345): dconf-CRITICAL **: 15:55:10.045: unable to create directory '/run/user/1519684234/dconf': Permission denied. dconf will not work properly.
(process:345): dconf-CRITICAL **: 15:55:10.045: unable to create directory '/run/user/1519684234/dconf': Permission denied. dconf will not work properly.
dbus-daemon[328]: [session uid=1519684234 pid=328] Successfully activated service 'org.a11y.Bus'
(process:345): dconf-CRITICAL **: 15:55:10.046: unable to create directory '/run/user/1519684234/dconf': Permission denied. dconf will not work properly.
(process:345): dconf-CRITICAL **: 15:55:10.046: unable to create directory '/run/user/1519684234/dconf': Permission denied. dconf will not work properly.
** (flutter_linux_unittests:342): CRITICAL **: 15:55:10.110: gboolean fl_compositor_present_layers(FlCompositor *, const FlutterLayer **, size_t): assertion 'FL_IS_COMPOSITOR(self)' failed
dbus-daemon[350]: Activating service name='org.a11y.atspi.Registry' requested by ':1.0' (uid=1519684234 pid=342 comm="./out/host_debug_unopt/exe.unstripped/flutter_linu" label="unconfined")
(flutter_linux_unittests:342): GLib-GObject-WARNING **: 15:55:10.111: invalid unclassed pointer in cast to 'GtkWidget'
=================================================================
==342==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bf3e0e10bb0 at pc 0x57c9de59ebb3 bp 0x7ffebe35c140 sp 0x7ffebe35c138
READ of size 8 at 0x7bf3e0e10bb0 thread T0
dbus-daemon[350]: Successfully activated service 'org.a11y.atspi.Registry'
SpiRegistry daemon is running with well-known name - org.a11y.atspi.Registry
#0 0x57c9de59ebb2 in gtk_widget_get_allocation out/host_debug_unopt/../../flutter/shell/platform/linux/testing/mock_gtk.cc:242:49
#1 0x57c9de6b554b in redraw_cb(void*) out/host_debug_unopt/../../flutter/shell/platform/linux/fl_view.cc:123:3
#2 0x7ed3e2cad65e in g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5465e) (BuildId: edd2644598e8e0c7656687c30a7daede5f4b6e6d)
#3 0x7ed3e2cada17 (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x54a17) (BuildId: edd2644598e8e0c7656687c30a7daede5f4b6e6d)
#4 0x7ed3e2cadaab in g_main_context_iteration (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x54aab) (BuildId: edd2644598e8e0c7656687c30a7daede5f4b6e6d)
#5 0x57c9de53c72c in FlViewTest_RedrawAfterDisposeUseAfterFree_Test::TestBody() out/host_debug_unopt/../../flutter/shell/platform/linux/fl_view_test.cc:305:10
#6 0x57c9e27c227d in void testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:2682:10
#7 0x57c9e2781d4f in void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:2737:12
#8 0x57c9e274d32d in testing::Test::Run() out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:2757:5
#9 0x57c9e274e2bc in testing::TestInfo::Run() out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:2903:11
#10 0x57c9e274f3b5 in testing::TestSuite::Run() out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:3081:30
#11 0x57c9e2765e05 in testing::internal::UnitTestImpl::RunAllTests() out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:6073:44
#12 0x57c9e27c8fed in bool testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*) out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:2682:10
#13 0x57c9e2784f0f in bool testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*) out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:2737:12
#14 0x57c9e2764ec1 in testing::UnitTest::Run() out/host_debug_unopt/../../flutter/third_party/googletest/googletest/src/gtest.cc:5613:10
#15 0x57c9de6babc0 in RUN_ALL_TESTS() out/host_debug_unopt/../../flutter/third_party/googletest/googletest/include/gtest/gtest.h:2341:73
#16 0x57c9de6ba8e3 in main out/host_debug_unopt/../../flutter/testing/run_all_unittests.cc:72:17
#17 0x7ed3e2872249 (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId: 93ac61ec5a8eb1396f9fbd350e3169a558528a40)
#18 0x7ed3e2872304 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x27304) (BuildId: 93ac61ec5a8eb1396f9fbd350e3169a558528a40)
#19 0x57c9ddfc23a9 in _start (/home/awillia_google_com/flutter/engine/src/out/host_debug_unopt/exe.unstripped/flutter_linux_unittests+0x2cb83a9) (BuildId: bc66bb9428d1e056c75e04ef80c99d4008c37e5e)
0x7bf3e0e10bb0 is located 240 bytes inside of 280-byte region [0x7bf3e0e10ac0,0x7bf3e0e10bd8)
freed by thread T0 here:
#0 0x57c9de065b56 in free ../../../../../../../../../llvm-llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:51:3
#1 0x7ed3e2dca1bb in g_type_free_instance (/lib/x86_64-linux-gnu/libgobject-2.0.so.0+0x381bb) (BuildId: d56c5a278ee6e10faea0a792cbbf7a764701a962)
previously allocated by thread T0 here:
#0 0x57c9de065df4 in malloc ../../../../../../../../../llvm-llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:87:3
#1 0x7ed3e2cb3658 in g_malloc (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5a658) (BuildId: edd2644598e8e0c7656687c30a7daede5f4b6e6d)
SUMMARY: AddressSanitizer: heap-use-after-free out/host_debug_unopt/../../flutter/shell/platform/linux/testing/mock_gtk.cc:242:49 in gtk_widget_get_allocation
Shadow bytes around the buggy address:
0x7bf3e0e10900: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
0x7bf3e0e10980: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x7bf3e0e10a00: fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa
0x7bf3e0e10a80: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
0x7bf3e0e10b00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x7bf3e0e10b80: fd fd fd fd fd fd[fd]fd fd fd fd fa fa fa fa fa
0x7bf3e0e10c00: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x7bf3e0e10c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7bf3e0e10d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 fa fa fa
0x7bf3e0e10d80: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
0x7bf3e0e10e00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==342==ABORTING
bandaid-fix.patch
diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc
index 21df992b2bc..0c2ec57f21f 100644
--- a/engine/src/flutter/shell/platform/linux/fl_view.cc
+++ b/engine/src/flutter/shell/platform/linux/fl_view.cc
@@ -112,6 +112,14 @@ G_DEFINE_TYPE_WITH_CODE(
static gboolean redraw_cb(gpointer user_data) {
g_autoptr(FlView) self = FL_VIEW(user_data);
+ // This idle is queued from the raster thread by fl_view_present_layers().
+ // If the view was disposed before the idle dispatched, |compositor| has been
+ // cleared and |render_area| has been freed by the chained GtkContainer
+ // dispose. Bail out to avoid a use-after-free on render_area.
+ if (self->compositor == nullptr) {
+ return G_SOURCE_REMOVE;
+ }
+
if (!self->have_first_frame) {
self->have_first_frame = TRUE;
g_signal_emit(self, fl_view_signals[SIGNAL_FIRST_FRAME], 0);repro.patch
diff --git a/engine/src/flutter/shell/platform/linux/fl_view_test.cc b/engine/src/flutter/shell/platform/linux/fl_view_test.cc
index acd44f25e40..c21ff79f601 100644
--- a/engine/src/flutter/shell/platform/linux/fl_view_test.cc
+++ b/engine/src/flutter/shell/platform/linux/fl_view_test.cc
@@ -5,6 +5,7 @@
#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h"
#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
#include "flutter/shell/platform/linux/fl_engine_private.h"
+#include "flutter/shell/platform/linux/fl_renderable.h"
#include "flutter/shell/platform/linux/fl_view_private.h"
#include "flutter/shell/platform/linux/testing/fl_test.h"
#include "flutter/shell/platform/linux/testing/fl_test_gtk_logs.h"
@@ -251,3 +252,57 @@ TEST(FlViewTest, SizedToContent) {
fl_gtk_widget_destroy(GTK_WIDGET(secondary_view));
fl_gtk_widget_destroy(GTK_WIDGET(implicit_view));
}
+
+// POC: redraw_cb idle queued by fl_view_present_layers (raster thread) survives
+// fl_view_dispose and then dereferences the already-freed GtkDrawingArea
+// (self->render_area). Under ASAN this reports heap-use-after-free.
+TEST(FlViewTest, RedrawAfterDisposeUseAfterFree) {
+ flutter::testing::fl_ensure_gtk_init();
+
+ g_autoptr(FlDartProject) project = fl_dart_project_new();
+ FlView* view = fl_view_new(project);
+
+ // Locate the internal GtkDrawingArea (FlView -> GtkEventBox -> render_area)
+ // and take a weak reference so we can prove it is finalized during dispose.
+ GList* box_children = gtk_container_get_children(GTK_CONTAINER(view));
+ ASSERT_NE(box_children, nullptr);
+ GtkWidget* event_box = GTK_WIDGET(box_children->data);
+ g_list_free(box_children);
+ GList* eb_children = gtk_container_get_children(GTK_CONTAINER(event_box));
+ ASSERT_NE(eb_children, nullptr);
+ GtkWidget* render_area = GTK_WIDGET(eb_children->data);
+ g_list_free(eb_children);
+ ASSERT_TRUE(GTK_IS_DRAWING_AREA(render_area));
+
+ GWeakRef render_area_weak;
+ g_weak_ref_init(&render_area_weak, render_area);
+
+ // Simulate the raster thread presenting a frame: this queues
+ // g_idle_add(redraw_cb, g_object_ref(view)) on the default main context.
+ // self->compositor is NULL here so fl_compositor_present_layers() is a
+ // g_return_val_if_fail no-op, but the idle is still scheduled.
+ fl_renderable_present_layers(FL_RENDERABLE(view), nullptr, 0);
+
+ // GTK thread destroys the view (e.g. user closes the window) before the
+ // idle dispatches. fl_view_dispose runs and chains to GtkBox/GtkContainer
+ // dispose, which finalizes render_area (its only ref was the container's).
+ // The idle source is never removed because fl_view_present_layers discards
+ // the source ID and redraw_cb performs no liveness check.
+ fl_gtk_widget_destroy(GTK_WIDGET(view));
+
+ // Prove render_area has been freed.
+ gpointer p = g_weak_ref_get(&render_area_weak);
+ EXPECT_EQ(p, nullptr) << "render_area was not finalized during dispose";
+ if (p != nullptr) {
+ g_object_unref(p);
+ }
+ g_weak_ref_clear(&render_area_weak);
+
+ // Dispatch the pending idle. redraw_cb now runs against a disposed FlView
+ // whose ->render_area field points at freed memory. The very first
+ // GTK_WIDGET(self->render_area) cast at fl_view.cc:123 reads
+ // instance->g_class from the freed allocation -> ASAN heap-use-after-free.
+ while (g_main_context_iteration(nullptr, FALSE)) {
+ // Drain.
+ }
+}
diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc
index 61059dcf7d9..3b6cf8f5d7d 100644
--- a/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc
+++ b/engine/src/flutter/shell/platform/linux/testing/mock_gtk.cc
@@ -234,6 +234,13 @@ void gtk_widget_class_set_accessible_type(GtkWidget* widget, GType type) {
void gtk_widget_get_allocation(GtkWidget* widget, GtkAllocation* allocation) {
check_thread();
+ // Real gtk_widget_get_allocation() dereferences the widget (it reads
+ // priv->allocation). Do an equivalent instrumented read here so that ASAN
+ // can observe use-after-free on a stale widget pointer the same way the
+ // real GTK function would.
+ volatile GTypeClass* klass =
+ reinterpret_cast<GTypeInstance*>(widget)->g_class;
+ (void)klass;
allocation->x = 0;
allocation->y = 0;
allocation->width = 100;
Activity
loic-sharma commentedon Sep 16, 2026
cc @robert-ancell for awareness. I didn't copy all the attached files, so let me know if I missed anything!
[Linux] Handle FlView being destroyed before rendering is complete. (f…
[Linux] Handle FlView being destroyed before rendering is complete. (f…
[Linux] Handle FlView being destroyed before rendering is complete. (f…
fix(linux): avoid NVIDIA EGL atexit segfault on quit
github-actions commentedon Oct 9, 2026
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 -vand a minimal reproduction of the issue.