Skip to content

Commit ea9e6d0

Browse files
committed
bug dynamic-configs: fix deadlock
commit_hash:f1aa24a0663877e2f846af376abb98ae54247a56
1 parent 147aecc commit ea9e6d0

File tree

3 files changed

+38
-6
lines changed

3 files changed

+38
-6
lines changed

core/src/dynamic_config/config_test.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,4 +545,32 @@ UTEST(DynamicConfig, DeadlockOnSubscribeInSendEvent) {
545545
subscriber.cb = {}; // cleanup to avoid UAF in dtr (in debug)
546546
}
547547

548+
UTEST(DynamicConfig, DeadlockOnSubscribeInSendEventDiff) {
549+
struct LocalSubscriber {
550+
void OnDiffUpdate(const dynamic_config::Diff&) {
551+
if (cb) cb();
552+
}
553+
554+
std::function<void()> cb;
555+
};
556+
557+
dynamic_config::StorageMock storage{{kDummyConfig, {42, "what"}}, {kIntConfig, 5}};
558+
auto source = storage.GetSource();
559+
LocalSubscriber subscriber;
560+
561+
auto scope = source.UpdateAndListen(&subscriber, "test", &LocalSubscriber::OnDiffUpdate);
562+
563+
LocalSubscriber subscriber2;
564+
concurrent::AsyncEventSubscriberScope subscriber2_scope;
565+
subscriber.cb = [&] {
566+
// Subscribe inside of callback
567+
subscriber2_scope = source.UpdateAndListen(&subscriber2, "test2", &LocalSubscriber::OnDiffUpdate);
568+
};
569+
570+
// emit SendEvent() which calls callback which subscribes
571+
storage.Extend({});
572+
573+
subscriber.cb = {}; // cleanup to avoid UAF in dtr (in debug)
574+
}
575+
548576
USERVER_NAMESPACE_END

core/src/dynamic_config/storage_data.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,22 @@ StorageData::StorageData() : StorageData(SnapshotData{}) {}
3232
rcu::ReadablePtr<SnapshotData> StorageData::Read() const { return config_.Read(); }
3333

3434
void StorageData::Update(SnapshotData config, AfterAssignHook after_assign_hook) {
35-
const std::lock_guard lock(update_mutex_);
35+
std::unique_lock lock(update_mutex_);
3636

3737
std::optional<Snapshot> previous_config;
3838
{
39-
Snapshot current_config(*this);
39+
auto current_config = GetSnapshot();
4040
if (!current_config.GetData().IsEmpty()) previous_config = std::move(current_config);
4141
}
4242

4343
config_.Assign(std::move(config));
4444
after_assign_hook();
45-
4645
const Diff diff{std::move(previous_config), GetSnapshot()};
46+
47+
lock.release();
48+
update_mutex_.unlock_and_lock_shared();
49+
std::shared_lock tmp_lock{update_mutex_, std::adopt_lock};
50+
4751
diff_channel_.SendEvent(diff);
4852
snapshot_channel_.SendEvent(GetSnapshot());
4953
}
@@ -73,7 +77,7 @@ StorageData::DoUpdateAndListen(concurrent::FunctionId id, std::string_view name,
7377
//
7478
// As a result, the subscriber receives two `Diff` object with 'new_config' as
7579
// a `current` field.
76-
const std::lock_guard lock(update_mutex_);
80+
std::shared_lock lock(update_mutex_);
7781

7882
auto updater = [&, func_copy = func] {
7983
const Diff diff{std::nullopt, GetSnapshot()};

core/src/dynamic_config/storage_data.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include <userver/concurrent/async_event_channel.hpp>
44
#include <userver/dynamic_config/impl/snapshot.hpp>
55
#include <userver/dynamic_config/snapshot.hpp>
6-
#include <userver/engine/mutex.hpp>
6+
#include <userver/engine/shared_mutex.hpp>
77
#include <userver/rcu/rcu.hpp>
88
#include <userver/utils/function_ref.hpp>
99

@@ -41,7 +41,7 @@ class StorageData final {
4141
SnapshotChannel snapshot_channel_;
4242
DiffChannel diff_channel_;
4343

44-
engine::Mutex update_mutex_;
44+
engine::SharedMutex update_mutex_;
4545
};
4646

4747
} // namespace dynamic_config::impl

0 commit comments

Comments
 (0)