Skip to content

Commit f83f341

Browse files
authored
Avoid destroying the Task's operation while holding a lock (#5658)
This prevents deadlock that can occur if the currently executing task happens hold the last `shared_ptr<Firestore>` for an instance. The deadlock looked like this: the destruction of the closure would trigger the destruction of the instance, which would dispose the executor, triggering cancellation of all tasks, including the currently executing one. Since the `operation_` member was reassigned while holding the task's lock, the `Cancel` operation would block trying to acquire it. This fixes the deadlock by forcing all interaction with the `operation_` outside the lock.
1 parent e9135a6 commit f83f341

File tree

3 files changed

+64
-2
lines changed

3 files changed

+64
-2
lines changed

Firestore/core/src/util/task.cc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,15 @@ void Task::ExecuteAndRelease() {
101101
Defer relock([&] { lock.lock(); });
102102
operation_();
103103

104+
// Clear the operation while not holding the lock to avoid the
105+
// possibility that the destructor of something in the closure might
106+
// try to reference this task.
107+
operation_ = {};
108+
104109
TASK_TRACE("Task::Execute %s (completing)", this);
105110
}
106111

107112
state_ = State::kDone;
108-
operation_ = {};
109113

110114
// The callback to the executor must be performed after the operation
111115
// completes, otherwise the executor's destructor cannot reliably block
@@ -165,9 +169,17 @@ void Task::Cancel() {
165169
TASK_TRACE("Task::Cancel %s", this);
166170

167171
if (state_ == State::kInitial) {
172+
// Do not clear the `operation_` here because that might indirectly trigger
173+
// an interaction with this task through its destructor.
168174
state_ = State::kCancelled;
169175
executor_ = nullptr;
176+
177+
// Clear the operation while not holding the lock to avoid the
178+
// possibility that the destructor of something in the closure might
179+
// try to reference this task.
180+
lock.unlock();
170181
operation_ = {};
182+
171183
is_complete_.notify_all();
172184

173185
} else if (state_ == State::kRunning) {

Firestore/core/src/util/task.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,11 @@ class Task {
201201
Executor::Tag tag_ = 0;
202202
Executor::Id id_ = 0;
203203

204-
Executor::Operation operation_;
205204
std::thread::id executing_thread_;
205+
206+
// The operation to run, supplied by the caller. Make this the last member
207+
// just in case it refers to this task during its own destruction.
208+
Executor::Operation operation_;
206209
};
207210

208211
/**

Firestore/core/test/unit/util/task_test.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ TEST_F(TaskTest, OwnedExecuteThenRelease) {
210210
ASSERT_EQ(state.task_destroyed, 0);
211211

212212
task->Release();
213+
ASSERT_EQ(state.op_destroyed, 1);
213214
ASSERT_EQ(state.task_destroyed, 1);
214215
}
215216

@@ -269,6 +270,52 @@ TEST_F(TaskTest, OwnedExecuteThenExecute) {
269270
ASSERT_EQ(state.task_destroyed, 1);
270271
}
271272

273+
TEST_F(TaskTest, AvoidsDeadlockDuringOperationDestruction) {
274+
std::string steps;
275+
276+
// Arrange for a value to be captured inside the lambda capture of the Task's
277+
// operation.
278+
//
279+
// On destruction, Holder will try to cancel the task it owns.
280+
struct Holder {
281+
explicit Holder(std::string* steps) : steps_(steps) {
282+
}
283+
284+
~Holder() {
285+
*steps_ += "4";
286+
if (!copied_from_ && task_) {
287+
task_->Cancel();
288+
}
289+
}
290+
291+
Holder(const Holder& other) : task_(other.task_), steps_(other.steps_) {
292+
other.copied_from_ = true;
293+
}
294+
295+
mutable bool copied_from_ = false;
296+
Task* task_ = nullptr;
297+
std::string* steps_ = nullptr;
298+
};
299+
auto holder = std::make_shared<Holder>(&steps);
300+
301+
Task* task = Task::Create(nullptr, [holder, &steps] { steps += "3"; });
302+
303+
// Give the task to holder and remove our local copy of the holder.
304+
holder->task_ = task;
305+
holder.reset();
306+
steps += "1";
307+
308+
Expectation ran;
309+
Async([&] {
310+
steps += "2";
311+
task->ExecuteAndRelease();
312+
ran.Fulfill();
313+
});
314+
315+
Await(ran);
316+
ASSERT_EQ(steps, "1234");
317+
}
318+
272319
} // namespace util
273320
} // namespace firestore
274321
} // namespace firebase

0 commit comments

Comments
 (0)