Skip to content

Commit a18066b

Browse files
authored
Avoid capturing this unsafely in MultiFrameCodec (flutter#16824)
1 parent d41e25b commit a18066b

File tree

8 files changed

+265
-28
lines changed

8 files changed

+265
-28
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ FILE: ../../../flutter/lib/ui/painting/image.cc
306306
FILE: ../../../flutter/lib/ui/painting/image.h
307307
FILE: ../../../flutter/lib/ui/painting/image_decoder.cc
308308
FILE: ../../../flutter/lib/ui/painting/image_decoder.h
309+
FILE: ../../../flutter/lib/ui/painting/image_decoder_test.cc
310+
FILE: ../../../flutter/lib/ui/painting/image_decoder_test.h
309311
FILE: ../../../flutter/lib/ui/painting/image_decoder_unittests.cc
310312
FILE: ../../../flutter/lib/ui/painting/image_encoding.cc
311313
FILE: ../../../flutter/lib/ui/painting/image_encoding.h

lib/ui/BUILD.gn

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ if (current_toolchain == host_toolchain) {
162162
executable("ui_unittests") {
163163
testonly = true
164164

165+
configs += [ "//flutter:export_dynamic_symbols" ]
166+
165167
sources = [
168+
"painting/image_decoder_test.cc",
169+
"painting/image_decoder_test.h",
166170
"painting/image_decoder_unittests.cc",
167171
"window/pointer_data_packet_converter_unittests.cc",
168172
]
@@ -171,8 +175,14 @@ if (current_toolchain == host_toolchain) {
171175
":ui",
172176
":ui_unittests_fixtures",
173177
"//flutter/common",
178+
"//flutter/fml",
179+
"//flutter/lib/snapshot",
180+
"//flutter/runtime",
181+
"//flutter/shell/common",
174182
"//flutter/testing:dart",
175183
"//flutter/testing:opengl",
184+
"//flutter/third_party/tonic",
185+
"//third_party/dart/runtime/bin:elf_loader",
176186
]
177187
}
178188
}

lib/ui/fixtures/ui_test.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,11 @@
33
// Use of this source code is governed by a BSD-style license that can be
44
// found in the LICENSE file.
55

6+
import 'dart:ui';
7+
68
void main() {}
9+
10+
@pragma('vm:entry-point')
11+
void frameCallback(FrameInfo info) {
12+
print('called back');
13+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "flutter/lib/ui/painting/image_decoder_test.h"
6+
7+
namespace flutter {
8+
namespace testing {
9+
10+
ImageDecoderFixtureTest::ImageDecoderFixtureTest()
11+
: native_resolver_(std::make_shared<TestDartNativeResolver>()),
12+
assets_dir_(fml::OpenDirectory(GetFixturesPath(),
13+
false,
14+
fml::FilePermission::kRead)),
15+
aot_symbols_(LoadELFSymbolFromFixturesIfNeccessary()) {}
16+
17+
Settings ImageDecoderFixtureTest::CreateSettingsForFixture() {
18+
Settings settings;
19+
settings.leak_vm = false;
20+
settings.task_observer_add = [](intptr_t, fml::closure) {};
21+
settings.task_observer_remove = [](intptr_t) {};
22+
settings.isolate_create_callback = [this]() {
23+
native_resolver_->SetNativeResolverForIsolate();
24+
};
25+
settings.enable_observatory = false;
26+
SetSnapshotsAndAssets(settings);
27+
return settings;
28+
}
29+
30+
void ImageDecoderFixtureTest::SetSnapshotsAndAssets(Settings& settings) {
31+
if (!assets_dir_.is_valid()) {
32+
return;
33+
}
34+
35+
settings.assets_dir = assets_dir_.get();
36+
37+
// In JIT execution, all snapshots are present within the binary itself and
38+
// don't need to be explicitly supplied by the embedder. In AOT, these
39+
// snapshots will be present in the application AOT dylib.
40+
if (DartVM::IsRunningPrecompiledCode()) {
41+
FML_CHECK(PrepareSettingsForAOTWithSymbols(settings, aot_symbols_));
42+
} else {
43+
settings.application_kernels = [this]() {
44+
std::vector<std::unique_ptr<const fml::Mapping>> kernel_mappings;
45+
kernel_mappings.emplace_back(
46+
fml::FileMapping::CreateReadOnly(assets_dir_, "kernel_blob.bin"));
47+
return kernel_mappings;
48+
};
49+
}
50+
}
51+
52+
} // namespace testing
53+
} // namespace flutter
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef FLUTTER_LIB_UI_PAINTING_IMAGE_DECODER_TEST_H_
6+
#define FLUTTER_LIB_UI_PAINTING_IMAGE_DECODER_TEST_H_
7+
8+
#include <memory>
9+
10+
#include "flutter/common/settings.h"
11+
#include "flutter/runtime/dart_vm.h"
12+
#include "flutter/testing/elf_loader.h"
13+
#include "flutter/testing/test_dart_native_resolver.h"
14+
#include "flutter/testing/testing.h"
15+
#include "flutter/testing/thread_test.h"
16+
17+
namespace flutter {
18+
namespace testing {
19+
20+
class ImageDecoderFixtureTest : public ThreadTest {
21+
public:
22+
ImageDecoderFixtureTest();
23+
24+
Settings CreateSettingsForFixture();
25+
26+
private:
27+
std::shared_ptr<TestDartNativeResolver> native_resolver_;
28+
fml::UniqueFD assets_dir_;
29+
ELFAOTSymbols aot_symbols_;
30+
31+
void SetSnapshotsAndAssets(Settings& settings);
32+
33+
FML_DISALLOW_COPY_AND_ASSIGN(ImageDecoderFixtureTest);
34+
};
35+
36+
} // namespace testing
37+
} // namespace flutter
38+
39+
#endif // FLUTTER_LIB_UI_PAINTING_IMAGE_DECODER_TEST_H_

lib/ui/painting/image_decoder_unittests.cc

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
#include "flutter/fml/mapping.h"
77
#include "flutter/fml/synchronization/waitable_event.h"
88
#include "flutter/lib/ui/painting/image_decoder.h"
9+
#include "flutter/lib/ui/painting/image_decoder_test.h"
10+
#include "flutter/lib/ui/painting/multi_frame_codec.h"
11+
#include "flutter/runtime/dart_vm.h"
12+
#include "flutter/runtime/dart_vm_lifecycle.h"
13+
#include "flutter/testing/dart_isolate_runner.h"
14+
#include "flutter/testing/elf_loader.h"
15+
#include "flutter/testing/test_dart_native_resolver.h"
916
#include "flutter/testing/test_gl_surface.h"
1017
#include "flutter/testing/testing.h"
1118
#include "flutter/testing/thread_test.h"
@@ -14,8 +21,6 @@
1421
namespace flutter {
1522
namespace testing {
1623

17-
using ImageDecoderFixtureTest = ThreadTest;
18-
1924
class TestIOManager final : public IOManager {
2025
public:
2126
TestIOManager(fml::RefPtr<fml::TaskRunner> task_runner,
@@ -557,5 +562,96 @@ TEST(ImageDecoderTest, VerifySubpixelDecodingPreservesExifOrientation) {
557562
assert_image(decode({}, 100));
558563
}
559564

565+
TEST_F(ImageDecoderFixtureTest,
566+
MultiFrameCodecCanBeCollectedBeforeIOTasksFinish) {
567+
// This test verifies that the MultiFrameCodec safely shares state between
568+
// tasks on the IO and UI runners, and does not allow unsafe memory access if
569+
// the UI object is collected while the IO thread still has pending decode
570+
// work. This could happen in a real application if the engine is collected
571+
// while a multi-frame image is decoding. To exercise this, the test:
572+
// - Starts a Dart VM
573+
// - Latches the IO task runner
574+
// - Create a MultiFrameCodec for an animated gif pointed to a callback
575+
// in the Dart fixture
576+
// - Calls getNextFrame on the UI task runner
577+
// - Collects the MultiFrameCodec object before unlatching the IO task
578+
// runner.
579+
// - Unlatches the IO task runner
580+
auto settings = CreateSettingsForFixture();
581+
auto vm_ref = DartVMRef::Create(settings);
582+
auto vm_data = vm_ref.GetVMData();
583+
584+
auto gif_mapping = OpenFixtureAsSkData("hello_loop_2.gif");
585+
586+
ASSERT_TRUE(gif_mapping);
587+
588+
auto gif_codec = SkCodec::MakeFromData(gif_mapping);
589+
ASSERT_TRUE(gif_codec);
590+
591+
TaskRunners runners(GetCurrentTestName(), // label
592+
CreateNewThread("platform"), // platform
593+
CreateNewThread("gpu"), // gpu
594+
CreateNewThread("ui"), // ui
595+
CreateNewThread("io") // io
596+
);
597+
598+
fml::AutoResetWaitableEvent latch;
599+
fml::AutoResetWaitableEvent io_latch;
600+
std::unique_ptr<TestIOManager> io_manager;
601+
602+
// Setup the IO manager.
603+
runners.GetIOTaskRunner()->PostTask([&]() {
604+
io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
605+
latch.Signal();
606+
});
607+
latch.Wait();
608+
609+
auto isolate =
610+
RunDartCodeInIsolate(vm_ref, settings, runners, "main", {},
611+
GetFixturesPath(), io_manager->GetWeakIOManager());
612+
613+
// Latch the IO task runner.
614+
runners.GetIOTaskRunner()->PostTask([&]() { io_latch.Wait(); });
615+
616+
runners.GetUITaskRunner()->PostTask([&]() {
617+
fml::AutoResetWaitableEvent isolate_latch;
618+
fml::RefPtr<MultiFrameCodec> codec;
619+
EXPECT_TRUE(isolate->RunInIsolateScope([&]() -> bool {
620+
Dart_Handle library = Dart_RootLibrary();
621+
if (Dart_IsError(library)) {
622+
isolate_latch.Signal();
623+
return false;
624+
}
625+
Dart_Handle closure =
626+
Dart_GetField(library, Dart_NewStringFromCString("frameCallback"));
627+
if (Dart_IsError(closure) || !Dart_IsClosure(closure)) {
628+
isolate_latch.Signal();
629+
return false;
630+
}
631+
632+
codec = fml::MakeRefCounted<MultiFrameCodec>(std::move(gif_codec));
633+
codec->getNextFrame(closure);
634+
codec = nullptr;
635+
isolate_latch.Signal();
636+
return true;
637+
}));
638+
isolate_latch.Wait();
639+
640+
EXPECT_FALSE(codec);
641+
642+
io_latch.Signal();
643+
644+
latch.Signal();
645+
});
646+
latch.Wait();
647+
648+
// Destroy the IO manager
649+
runners.GetIOTaskRunner()->PostTask([&]() {
650+
io_manager.reset();
651+
latch.Signal();
652+
});
653+
latch.Wait();
654+
}
655+
560656
} // namespace testing
561657
} // namespace flutter

lib/ui/painting/multi_frame_codec.cc

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
namespace flutter {
1313

1414
MultiFrameCodec::MultiFrameCodec(std::unique_ptr<SkCodec> codec)
15+
: state_(new State(std::move(codec))) {}
16+
17+
MultiFrameCodec::~MultiFrameCodec() = default;
18+
19+
MultiFrameCodec::State::State(std::unique_ptr<SkCodec> codec)
1520
: codec_(std::move(codec)),
1621
frameCount_(codec_->getFrameCount()),
1722
repetitionCount_(codec_->getRepetitionCount()),
1823
nextFrameIndex_(0) {}
1924

20-
MultiFrameCodec::~MultiFrameCodec() = default;
21-
2225
static void InvokeNextFrameCallback(
2326
fml::RefPtr<FrameInfo> frameInfo,
2427
std::unique_ptr<DartPersistentValue> callback,
@@ -70,7 +73,7 @@ static bool CopyToBitmap(SkBitmap* dst,
7073
return true;
7174
}
7275

73-
sk_sp<SkImage> MultiFrameCodec::GetNextFrameImage(
76+
sk_sp<SkImage> MultiFrameCodec::State::GetNextFrameImage(
7477
fml::WeakPtr<GrContext> resourceContext) {
7578
SkBitmap bitmap = SkBitmap();
7679
SkImageInfo info = codec_->getInfo().makeColorType(kN32_SkColorType);
@@ -128,7 +131,7 @@ sk_sp<SkImage> MultiFrameCodec::GetNextFrameImage(
128131
}
129132
}
130133

131-
void MultiFrameCodec::GetNextFrameAndInvokeCallback(
134+
void MultiFrameCodec::State::GetNextFrameAndInvokeCallback(
132135
std::unique_ptr<DartPersistentValue> callback,
133136
fml::RefPtr<fml::TaskRunner> ui_task_runner,
134137
fml::WeakPtr<GrContext> resourceContext,
@@ -167,9 +170,16 @@ Dart_Handle MultiFrameCodec::getNextFrame(Dart_Handle callback_handle) {
167170
task_runners.GetIOTaskRunner()->PostTask(fml::MakeCopyable(
168171
[callback = std::make_unique<DartPersistentValue>(
169172
tonic::DartState::Current(), callback_handle),
170-
this, trace_id, ui_task_runner = task_runners.GetUITaskRunner(),
173+
weak_state = std::weak_ptr<MultiFrameCodec::State>(state_), trace_id,
174+
ui_task_runner = task_runners.GetUITaskRunner(),
171175
io_manager = dart_state->GetIOManager()]() mutable {
172-
GetNextFrameAndInvokeCallback(
176+
auto state = weak_state.lock();
177+
if (!state) {
178+
ui_task_runner->PostTask(fml::MakeCopyable(
179+
[callback = std::move(callback)]() { callback->Clear(); }));
180+
return;
181+
}
182+
state->GetNextFrameAndInvokeCallback(
173183
std::move(callback), std::move(ui_task_runner),
174184
io_manager->GetResourceContext(), io_manager->GetSkiaUnrefQueue(),
175185
trace_id);
@@ -179,11 +189,11 @@ Dart_Handle MultiFrameCodec::getNextFrame(Dart_Handle callback_handle) {
179189
}
180190

181191
int MultiFrameCodec::frameCount() const {
182-
return frameCount_;
192+
return state_->frameCount_;
183193
}
184194

185195
int MultiFrameCodec::repetitionCount() const {
186-
return repetitionCount_;
196+
return state_->repetitionCount_;
187197
}
188198

189199
} // namespace flutter

lib/ui/painting/multi_frame_codec.h

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,44 @@ class MultiFrameCodec : public Codec {
2626
Dart_Handle getNextFrame(Dart_Handle args) override;
2727

2828
private:
29-
const std::unique_ptr<SkCodec> codec_;
30-
const int frameCount_;
31-
const int repetitionCount_;
32-
int nextFrameIndex_;
33-
34-
// The last decoded frame that's required to decode any subsequent frames.
35-
std::unique_ptr<SkBitmap> lastRequiredFrame_;
36-
// The index of the last decoded required frame.
37-
int lastRequiredFrameIndex_ = -1;
38-
39-
sk_sp<SkImage> GetNextFrameImage(fml::WeakPtr<GrContext> resourceContext);
40-
41-
void GetNextFrameAndInvokeCallback(
42-
std::unique_ptr<DartPersistentValue> callback,
43-
fml::RefPtr<fml::TaskRunner> ui_task_runner,
44-
fml::WeakPtr<GrContext> resourceContext,
45-
fml::RefPtr<flutter::SkiaUnrefQueue> unref_queue,
46-
size_t trace_id);
29+
// Captures the state shared between the IO and UI task runners.
30+
//
31+
// The state is initialized on the UI task runner when the Dart object is
32+
// created. Decoding occurs on the IO task runner. Since it is possible for
33+
// the UI object to be collected independently of the IO task runner work,
34+
// it is not safe for this state to live directly on the MultiFrameCodec.
35+
// Instead, the MultiFrameCodec creates this object when it is constructed,
36+
// shares it with the IO task runner's decoding work, and sets the live_
37+
// member to false when it is destructed.
38+
struct State {
39+
State(std::unique_ptr<SkCodec> codec);
40+
41+
const std::unique_ptr<SkCodec> codec_;
42+
const int frameCount_;
43+
const int repetitionCount_;
44+
45+
// The non-const members and functions below here are only read or written
46+
// to on the IO thread. They are not safe to access or write on the UI
47+
// thread.
48+
int nextFrameIndex_;
49+
// The last decoded frame that's required to decode any subsequent frames.
50+
std::unique_ptr<SkBitmap> lastRequiredFrame_;
51+
52+
// The index of the last decoded required frame.
53+
int lastRequiredFrameIndex_ = -1;
54+
55+
sk_sp<SkImage> GetNextFrameImage(fml::WeakPtr<GrContext> resourceContext);
56+
57+
void GetNextFrameAndInvokeCallback(
58+
std::unique_ptr<DartPersistentValue> callback,
59+
fml::RefPtr<fml::TaskRunner> ui_task_runner,
60+
fml::WeakPtr<GrContext> resourceContext,
61+
fml::RefPtr<flutter::SkiaUnrefQueue> unref_queue,
62+
size_t trace_id);
63+
};
64+
65+
// Shared across the UI and IO task runners.
66+
std::shared_ptr<State> state_;
4767

4868
FML_FRIEND_MAKE_REF_COUNTED(MultiFrameCodec);
4969
FML_FRIEND_REF_COUNTED_THREAD_SAFE(MultiFrameCodec);

0 commit comments

Comments
 (0)