Skip to content

Commit bc32eb8

Browse files
authored
Merge pull request #483 from GeneralsOnlineDevelopmentTeam/seer/bugfix/thread-list-cs-robustness
bugfix(threading): Ensure thread list critical section remains valid during CRT shutdown
2 parents 8503c3b + c089284 commit bc32eb8

1 file changed

Lines changed: 64 additions & 12 deletions

File tree

Core/Libraries/Source/WWVegas/WWLib/Except.cpp

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,72 @@ int ExceptionRecursions = -1;
114114
DynamicVectorClass<ThreadInfoType*> ThreadList;
115115

116116
/*
117-
** Critical section to protect ThreadList from concurrent access.
118-
** This prevents race conditions when threads register/unregister while
119-
** another thread is accessing the list (e.g., during exception handling or shutdown).
117+
** Returns the CRITICAL_SECTION used to protect ThreadList.
120118
**
121-
** Intentionally heap-allocated and never freed: threads may call Unregister_Thread_ID
122-
** after C++ global destructors have run, so a static-lifetime object would already be
123-
** destroyed by that point, causing a use-after-free crash.
119+
** Allocated from the Windows process heap (not the CRT heap) and never freed.
120+
** The CRT heap can be torn down during application shutdown before all threads
121+
** have exited. If a thread calls Unregister_Thread_ID after the CRT heap is
122+
** destroyed, any CRITICAL_SECTION allocated via _aligned_malloc (which uses
123+
** the CRT heap) would already be invalid memory, causing an access violation
124+
** when EnterCriticalSection dereferences it.
125+
**
126+
** The Windows process heap (GetProcessHeap) outlives the CRT heap and is only
127+
** reclaimed when the process terminates, so this CRITICAL_SECTION remains valid
128+
** for the entire process lifetime regardless of CRT shutdown order.
129+
**
130+
** Thread-safe one-time initialization is achieved via
131+
** InterlockedCompareExchangePointer, which works on all supported Windows versions.
124132
*/
125-
static CriticalSectionClass& GetThreadListLock()
133+
static CRITICAL_SECTION* GetThreadListCS()
126134
{
127-
static CriticalSectionClass* lock = new CriticalSectionClass();
128-
return *lock;
135+
static CRITICAL_SECTION* volatile s_cs = nullptr;
136+
137+
if (s_cs == nullptr) {
138+
CRITICAL_SECTION* cs = reinterpret_cast<CRITICAL_SECTION*>(
139+
HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CRITICAL_SECTION)));
140+
if (cs != nullptr) {
141+
InitializeCriticalSection(cs);
142+
}
143+
// Race-free handoff: only one thread's allocation wins; the loser is discarded.
144+
if (InterlockedCompareExchangePointer(
145+
reinterpret_cast<volatile PVOID*>(&s_cs), cs, nullptr) != nullptr) {
146+
// Another thread initialized it first; discard our copy.
147+
if (cs != nullptr) {
148+
DeleteCriticalSection(cs);
149+
HeapFree(GetProcessHeap(), 0, cs);
150+
}
151+
}
152+
}
153+
154+
return s_cs;
129155
}
130156

157+
/*
158+
** RAII lock guard for the raw ThreadList CRITICAL_SECTION.
159+
** Replaces CriticalSectionClass::LockClass for the thread-list lock so that
160+
** the CriticalSectionClass wrapper (and its _aligned_malloc-based handle) is
161+
** never used for this particular, shutdown-sensitive critical path.
162+
*/
163+
struct ScopedThreadListLock
164+
{
165+
explicit ScopedThreadListLock(CRITICAL_SECTION* cs) : cs_(cs)
166+
{
167+
if (cs_ != nullptr) {
168+
EnterCriticalSection(cs_);
169+
}
170+
}
171+
~ScopedThreadListLock()
172+
{
173+
if (cs_ != nullptr) {
174+
LeaveCriticalSection(cs_);
175+
}
176+
}
177+
private:
178+
CRITICAL_SECTION* cs_;
179+
ScopedThreadListLock(const ScopedThreadListLock&);
180+
ScopedThreadListLock& operator=(const ScopedThreadListLock&);
181+
};
182+
131183
/*
132184
** Definitions to allow run-time linking to the Imagehlp.dll functions.
133185
**
@@ -905,7 +957,7 @@ void Register_Thread_ID(unsigned long thread_id, char *thread_name, bool main_th
905957
{
906958
WWMEMLOG(MEM_GAMEDATA);
907959
if (thread_name) {
908-
CriticalSectionClass::LockClass lock(GetThreadListLock());
960+
ScopedThreadListLock lock(GetThreadListCS());
909961

910962
/*
911963
** See if we already know about this thread. Maybe just the thread_id changed.
@@ -1016,7 +1068,7 @@ HANDLE Get_Thread_Handle(int thread_index)
10161068
*=============================================================================================*/
10171069
void Unregister_Thread_ID(unsigned long thread_id, char *thread_name)
10181070
{
1019-
CriticalSectionClass::LockClass lock(GetThreadListLock());
1071+
ScopedThreadListLock lock(GetThreadListCS());
10201072

10211073
for (int i=0 ; i<ThreadList.Count() ; i++) {
10221074
if (strcmp(thread_name, ThreadList[i]->ThreadName) == 0) {
@@ -1046,7 +1098,7 @@ void Unregister_Thread_ID(unsigned long thread_id, char *thread_name)
10461098
*=============================================================================================*/
10471099
unsigned long Get_Main_Thread_ID()
10481100
{
1049-
CriticalSectionClass::LockClass lock(GetThreadListLock());
1101+
ScopedThreadListLock lock(GetThreadListCS());
10501102

10511103
for (int i=0 ; i<ThreadList.Count() ; i++) {
10521104
if (ThreadList[i]->Main) {

0 commit comments

Comments
 (0)