Skip to content

Commit f5c7cae

Browse files
committed
[Profiler] Add EngineActiveGuard unit tests
Covers both halves of the guard directly: not-yet-initialized, already-shut-down (the confirmed crash scenario), a writer blocking a would-be reader (non-blocking try_to_lock, callbacks never wait), a reader blocking a writer until released (in-flight callbacks finish before DisposeServices() can run), and concurrent readers not serializing against each other. All 583 tests in the native suite pass (1 pre-existing, unrelated skip).
1 parent c0467c1 commit f5c7cae

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
3+
4+
#include "gtest/gtest.h"
5+
6+
#include "EngineActiveGuard.h"
7+
8+
#include <atomic>
9+
#include <chrono>
10+
#include <future>
11+
#include <memory>
12+
#include <shared_mutex>
13+
#include <thread>
14+
15+
using namespace std::chrono_literals;
16+
17+
// These tests cover the fix for a shutdown-time use-after-free: an ICorProfilerCallback method
18+
// used to check _isInitialized.load() - a bare, lock-free atomic read - then, several lines
19+
// later, dereference a service pointer that CorProfilerCallback::DisposeInternal() could
20+
// concurrently null out and destroy on another thread. A real crash (ExecutionEngineException,
21+
// a ThreadsCpuManager use-after-free deep inside std::unordered_map's internals) was traced to
22+
// exactly this gap: the check and the use were never serialized against each other at all.
23+
//
24+
// EngineActiveGuard replaces that single check with two: isInitialized (has Initialize()
25+
// finished? - a plain atomic read is fine, this direction never destroys anything concurrently)
26+
// and isServicesShutdown (has teardown started? - only ever answered while holding the same
27+
// mutex the teardown path holds exclusively). These tests exercise both halves directly, without
28+
// needing a full CorProfilerCallback.
29+
30+
TEST(EngineActiveGuardTest, ActiveWhenInitializedNotShutdownAndUncontended)
31+
{
32+
std::atomic<bool> isInitialized{true};
33+
std::shared_mutex mutex;
34+
bool isServicesShutdown = false;
35+
36+
EngineActiveGuard guard(isInitialized, mutex, isServicesShutdown);
37+
38+
ASSERT_TRUE(guard.IsActive());
39+
}
40+
41+
TEST(EngineActiveGuardTest, NotActiveBeforeInitialization)
42+
{
43+
// Mirrors the "has not yet initialized" half of the original
44+
// `if (false == _isInitialized.load()) return S_OK;` check - a callback firing before
45+
// Initialize() has finished constructing the services must still no-op, exactly as before.
46+
std::atomic<bool> isInitialized{false};
47+
std::shared_mutex mutex;
48+
bool isServicesShutdown = false;
49+
50+
EngineActiveGuard guard(isInitialized, mutex, isServicesShutdown);
51+
52+
ASSERT_FALSE(guard.IsActive());
53+
}
54+
55+
TEST(EngineActiveGuardTest, NotActiveOnceServicesShutdownFlagIsSet)
56+
{
57+
std::atomic<bool> isInitialized{true};
58+
std::shared_mutex mutex;
59+
bool isServicesShutdown = true;
60+
61+
// No writer holds the mutex here - a bare try_to_lock would succeed. This is exactly the gap
62+
// the fix closes: acquiring the lock is not enough on its own, IsActive() must also observe
63+
// isServicesShutdown while still holding it, or a callback arriving right after teardown
64+
// releases the lock would sail through and touch already-destroyed service pointers.
65+
EngineActiveGuard guard(isInitialized, mutex, isServicesShutdown);
66+
67+
ASSERT_FALSE(guard.IsActive());
68+
}
69+
70+
TEST(EngineActiveGuardTest, NotActiveWhileWriterHoldsTheExclusiveLock)
71+
{
72+
std::atomic<bool> isInitialized{true};
73+
std::shared_mutex mutex;
74+
bool isServicesShutdown = false;
75+
76+
std::promise<void> writerHasTheLock;
77+
std::promise<void> readerHasChecked;
78+
auto readerHasCheckedFuture = readerHasChecked.get_future();
79+
80+
// Mirrors DisposeInternal() holding the exclusive lock for the duration of DisposeServices().
81+
std::thread writer(
82+
[&]
83+
{
84+
std::unique_lock<std::shared_mutex> exclusiveLock(mutex);
85+
writerHasTheLock.set_value();
86+
readerHasCheckedFuture.wait();
87+
});
88+
89+
writerHasTheLock.get_future().wait();
90+
91+
// A callback arriving while teardown is in progress must fail to acquire the lock at all
92+
// (non-blocking try_to_lock), not block waiting for it - callbacks must never wait.
93+
EngineActiveGuard guard(isInitialized, mutex, isServicesShutdown);
94+
ASSERT_FALSE(guard.IsActive());
95+
96+
readerHasChecked.set_value();
97+
writer.join();
98+
}
99+
100+
TEST(EngineActiveGuardTest, WriterBlocksUntilReaderGuardIsReleased)
101+
{
102+
std::atomic<bool> isInitialized{true};
103+
std::shared_mutex mutex;
104+
bool isServicesShutdown = false;
105+
106+
auto guard = std::make_unique<EngineActiveGuard>(isInitialized, mutex, isServicesShutdown);
107+
ASSERT_TRUE(guard->IsActive());
108+
109+
std::atomic<bool> writerAcquired{false};
110+
std::thread writer(
111+
[&]
112+
{
113+
std::unique_lock<std::shared_mutex> exclusiveLock(mutex);
114+
writerAcquired.store(true);
115+
});
116+
117+
// The writer (teardown) must not be able to proceed while an in-flight callback still holds
118+
// its shared lock - this is what guarantees a callback that already passed its guard check
119+
// gets to finish using service pointers before DisposeServices() can destroy them.
120+
std::this_thread::sleep_for(50ms);
121+
ASSERT_FALSE(writerAcquired.load());
122+
123+
guard.reset(); // release the reader's shared lock
124+
writer.join();
125+
126+
ASSERT_TRUE(writerAcquired.load());
127+
}
128+
129+
TEST(EngineActiveGuardTest, MultipleReadersCanBeActiveConcurrently)
130+
{
131+
std::atomic<bool> isInitialized{true};
132+
std::shared_mutex mutex;
133+
bool isServicesShutdown = false;
134+
135+
// Callbacks legitimately run concurrently on different threads in normal operation - the
136+
// guard must not serialize them against each other, only against the writer.
137+
EngineActiveGuard first(isInitialized, mutex, isServicesShutdown);
138+
EngineActiveGuard second(isInitialized, mutex, isServicesShutdown);
139+
140+
ASSERT_TRUE(first.IsActive());
141+
ASSERT_TRUE(second.IsActive());
142+
}

0 commit comments

Comments
 (0)