Skip to content

Commit 65dbd9a

Browse files
committed
[webview_flutter_lwe] Fix use-after-free of disposed WebView in async callbacks
The navigation-delegate callbacks (onPageStarted/onUrlChange, onPageFinished, onProgress, onWebResourceError, navigationRequest) and the JavaScript-channel callback are dispatched to the main loop asynchronously, and the navigationRequest result is resolved asynchronously by the Dart side. Any of them could run after the WebView was disposed and dereference the destroyed WebView (and its now-freed method channels), causing a use-after-free. Add a shared is_alive_ flag that Dispose() clears; every deferred callback captures a copy and bails out when the WebView is gone, and NavigationRequestResult checks it before touching the WebView.
1 parent c658c05 commit 65dbd9a

3 files changed

Lines changed: 77 additions & 36 deletions

File tree

packages/webview_flutter_lwe/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
is replaced after the WebView is created.
66
* Fix a crash on the raster thread when a WebView is disposed while a frame is
77
still being composited.
8+
* Fix a use-after-free when navigation-delegate or JavaScript-channel callbacks
9+
run after the WebView has been disposed.
810
* Add 1 integration test case.
911

1012
## 0.4.1

packages/webview_flutter_lwe/tizen/src/webview.cc

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,15 @@ extern "C" size_t LWE_EXPORT createWebViewInstance(
3737

3838
class NavigationRequestResult : public FlMethodResult {
3939
public:
40-
NavigationRequestResult(std::string url, WebView* webview)
41-
: url_(url), webview_(webview) {}
40+
// |alive| is the WebView's is_alive_ flag. Dart resolves the
41+
// "navigationRequest" method call asynchronously (it round-trips through
42+
// the Dart navigation delegate), so this result's completion can run well
43+
// after the WebView that created it has been disposed; |webview_| would
44+
// then be a dangling pointer. Checking |alive| before dereferencing it
45+
// avoids a use-after-free in that case.
46+
NavigationRequestResult(std::string url, WebView* webview,
47+
std::shared_ptr<bool> alive)
48+
: url_(url), webview_(webview), alive_(std::move(alive)) {}
4249

4350
void SuccessInternal(const flutter::EncodableValue* should_load) override {
4451
if (std::holds_alternative<bool>(*should_load)) {
@@ -60,13 +67,17 @@ class NavigationRequestResult : public FlMethodResult {
6067

6168
private:
6269
void LoadUrl() {
70+
if (!*alive_) {
71+
return;
72+
}
6373
if (webview_ && webview_->GetWebViewInstance()) {
6474
webview_->GetWebViewInstance()->LoadURL(url_);
6575
}
6676
}
6777

6878
std::string url_;
6979
WebView* webview_;
80+
std::shared_ptr<bool> alive_;
7081
};
7182

7283
template <typename T>
@@ -153,35 +164,45 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id,
153164
flutter::EncodableMap args = {
154165
{flutter::EncodableValue("url"), flutter::EncodableValue(url)}};
155166

156-
dispatcher_->dispatchTaskOnMainThread([this, args]() {
157-
navigation_delegate_channel_->InvokeMethod(
158-
"onPageStarted", std::make_unique<flutter::EncodableValue>(args));
159-
// The lightweight web engine has no dedicated URL-change callback, so
160-
// report a URL change whenever a navigation starts.
161-
navigation_delegate_channel_->InvokeMethod(
162-
"onUrlChange", std::make_unique<flutter::EncodableValue>(args));
163-
});
167+
dispatcher_->dispatchTaskOnMainThread(
168+
[this, args, alive = is_alive_]() {
169+
if (!*alive) return;
170+
navigation_delegate_channel_->InvokeMethod(
171+
"onPageStarted",
172+
std::make_unique<flutter::EncodableValue>(args));
173+
// The lightweight web engine has no dedicated URL-change
174+
// callback, so report a URL change whenever a navigation
175+
// starts.
176+
navigation_delegate_channel_->InvokeMethod(
177+
"onUrlChange",
178+
std::make_unique<flutter::EncodableValue>(args));
179+
});
164180
});
165181
webview_instance_->RegisterOnPageLoadedHandler(
166182
[this](LWE::WebContainer* container, const std::string& url) {
167183
flutter::EncodableMap args = {
168184
{flutter::EncodableValue("url"), flutter::EncodableValue(url)}};
169185

170-
dispatcher_->dispatchTaskOnMainThread([this, args]() {
171-
navigation_delegate_channel_->InvokeMethod(
172-
"onPageFinished",
173-
std::make_unique<flutter::EncodableValue>(args));
174-
});
186+
dispatcher_->dispatchTaskOnMainThread(
187+
[this, args, alive = is_alive_]() {
188+
if (!*alive) return;
189+
navigation_delegate_channel_->InvokeMethod(
190+
"onPageFinished",
191+
std::make_unique<flutter::EncodableValue>(args));
192+
});
175193
});
176194
webview_instance_->RegisterOnProgressChangedHandler(
177195
[this](LWE::WebContainer* container, int progress) {
178196
flutter::EncodableMap args = {{flutter::EncodableValue("progress"),
179197
flutter::EncodableValue(progress)}};
180198

181-
dispatcher_->dispatchTaskOnMainThread([this, args]() {
182-
navigation_delegate_channel_->InvokeMethod(
183-
"onProgress", std::make_unique<flutter::EncodableValue>(args));
184-
});
199+
dispatcher_->dispatchTaskOnMainThread(
200+
[this, args, alive = is_alive_]() {
201+
if (!*alive) return;
202+
navigation_delegate_channel_->InvokeMethod(
203+
"onProgress",
204+
std::make_unique<flutter::EncodableValue>(args));
205+
});
185206
});
186207
webview_instance_->RegisterOnReceivedErrorHandler(
187208
[this](LWE::WebContainer* container, LWE::ResourceError error) {
@@ -193,11 +214,13 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id,
193214
{flutter::EncodableValue("failingUrl"),
194215
flutter::EncodableValue(error.GetUrl())},
195216
};
196-
dispatcher_->dispatchTaskOnMainThread([this, args]() {
197-
navigation_delegate_channel_->InvokeMethod(
198-
"onWebResourceError",
199-
std::make_unique<flutter::EncodableValue>(args));
200-
});
217+
dispatcher_->dispatchTaskOnMainThread(
218+
[this, args, alive = is_alive_]() {
219+
if (!*alive) return;
220+
navigation_delegate_channel_->InvokeMethod(
221+
"onWebResourceError",
222+
std::make_unique<flutter::EncodableValue>(args));
223+
});
201224
});
202225
webview_instance_->RegisterShouldOverrideUrlLoadingHandler(
203226
[this](LWE::WebContainer* view, const std::string& url) -> bool {
@@ -210,13 +233,16 @@ WebView::WebView(flutter::PluginRegistrar* registrar, int view_id,
210233
flutter::EncodableValue(true)},
211234
};
212235

213-
dispatcher_->dispatchTaskOnMainThread([this, args, url]() {
214-
auto result = std::make_unique<NavigationRequestResult>(url, this);
215-
navigation_delegate_channel_->InvokeMethod(
216-
"navigationRequest",
217-
std::make_unique<flutter::EncodableValue>(args),
218-
std::move(result));
219-
});
236+
dispatcher_->dispatchTaskOnMainThread(
237+
[this, args, url, alive = is_alive_]() {
238+
if (!*alive) return;
239+
auto result = std::make_unique<NavigationRequestResult>(
240+
url, this, alive);
241+
navigation_delegate_channel_->InvokeMethod(
242+
"navigationRequest",
243+
std::make_unique<flutter::EncodableValue>(args),
244+
std::move(result));
245+
});
220246
return true;
221247
});
222248
}
@@ -235,11 +261,13 @@ void WebView::RegisterJavaScriptChannelName(const std::string& name) {
235261
{flutter::EncodableValue("message"), flutter::EncodableValue(message)},
236262
};
237263

238-
dispatcher_->dispatchTaskOnMainThread([this, args]() {
239-
webview_channel_->InvokeMethod(
240-
"javaScriptChannelMessage",
241-
std::make_unique<flutter::EncodableValue>(args));
242-
});
264+
dispatcher_->dispatchTaskOnMainThread(
265+
[this, args, alive = is_alive_]() {
266+
if (!*alive) return;
267+
webview_channel_->InvokeMethod(
268+
"javaScriptChannelMessage",
269+
std::make_unique<flutter::EncodableValue>(args));
270+
});
243271
return "success";
244272
};
245273
webview_instance_->AddJavaScriptInterface(name, "postMessage", on_message);
@@ -261,6 +289,12 @@ std::string WebView::GetNavigationDelegateChannelName() {
261289
}
262290

263291
void WebView::Dispose() {
292+
// Flip this before anything else: any dispatcher_ callback already queued
293+
// on the main loop (e.g. a navigation event that fired just before this
294+
// WebView was disposed) checks it and bails out instead of touching a
295+
// WebView that may be fully destroyed by the time it runs.
296+
*is_alive_ = false;
297+
264298
// Stop the web engine first so its renderer thread stops writing into the
265299
// shared TBM surfaces before anything is torn down.
266300
if (webview_instance_) {

packages/webview_flutter_lwe/tizen/src/webview.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ class WebView : public PlatformView {
8181
bool is_mouse_lbutton_down_ = false;
8282
bool has_navigation_delegate_ = false;
8383
bool is_disposing_ = false;
84+
// Set to false at the start of Dispose(). Every dispatcher_ callback that
85+
// captures `this` also captures a copy of this shared_ptr, so a callback
86+
// still queued on the main-loop when the WebView is disposed can check it
87+
// and bail out instead of dereferencing an already-destroyed WebView.
88+
std::shared_ptr<bool> is_alive_ = std::make_shared<bool>(true);
8489
// Shared (not unique) so a pending dispose callback can keep the
8590
// dispatcher alive by holding its own reference after the WebView that
8691
// created it has been destroyed.

0 commit comments

Comments
 (0)