Setting the Thread_AutoRelease flag (default when using IThreader::MakeThread) caused a use-after-free after running the thread body.
		
			
				
	
	
		
			554 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			554 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /**
 | |
|  * vim: set ts=4 sw=4 tw=99 noet :
 | |
|  * =============================================================================
 | |
|  * SourceMod
 | |
|  * Copyright (C) 2004-2009 AlliedModders LLC.  All rights reserved.
 | |
|  * =============================================================================
 | |
|  *
 | |
|  * This program is free software; you can redistribute it and/or modify it under
 | |
|  * the terms of the GNU General Public License, version 3.0, as published by the
 | |
|  * Free Software Foundation.
 | |
|  * 
 | |
|  * This program is distributed in the hope that it will be useful, but WITHOUT
 | |
|  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 | |
|  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 | |
|  * details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU General Public License along with
 | |
|  * this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
|  *
 | |
|  * As a special exception, AlliedModders LLC gives you permission to link the
 | |
|  * code of this program (as well as its derivative works) to "Half-Life 2," the
 | |
|  * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
 | |
|  * by the Valve Corporation.  You must obey the GNU General Public License in
 | |
|  * all respects for all other code used.  Additionally, AlliedModders LLC grants
 | |
|  * this exception to all derivative works.  AlliedModders LLC defines further
 | |
|  * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
 | |
|  * or <http://www.sourcemod.net/license.php>.
 | |
|  *
 | |
|  * Version: $Id$
 | |
|  */
 | |
| #include <sm_platform.h>
 | |
| #include <amtl/am-deque.h>
 | |
| #include <amtl/am-maybe.h>
 | |
| #include <amtl/am-thread.h>
 | |
| #include <atomic>
 | |
| #include <chrono>
 | |
| #include <condition_variable>
 | |
| #include <memory>
 | |
| #include <mutex>
 | |
| #include <thread>
 | |
| #include "BaseWorker.h"
 | |
| #include "ThreadSupport.h"
 | |
| #include "common_logic.h"
 | |
| 
 | |
| static constexpr unsigned int DEFAULT_THINK_TIME_MS	= 20;
 | |
| 
 | |
| class CompatWorker final : public IThreadWorker
 | |
| {
 | |
|   public:
 | |
| 	explicit CompatWorker(IThreadWorkerCallbacks* callbacks);
 | |
| 	~CompatWorker();
 | |
| 
 | |
| 	void MakeThread(IThread *pThread) override;
 | |
| 	IThreadHandle *MakeThread(IThread *pThread, ThreadFlags flags) override;
 | |
| 	IThreadHandle *MakeThread(IThread *pThread, const ThreadParams *params) override;
 | |
| 	void GetPriorityBounds(ThreadPriority &max, ThreadPriority &min) override;
 | |
| 	unsigned int RunFrame() override;
 | |
| 	bool Pause() override;
 | |
| 	bool Unpause() override;
 | |
| 	bool Start() override;
 | |
| 	bool Stop(bool flush) override;
 | |
| 	WorkerState GetStatus(unsigned int *numThreads) override;
 | |
| 	void SetMaxThreadsPerFrame(unsigned int threads) override;
 | |
| 	void SetThinkTimePerFrame(unsigned int thinktime) override;
 | |
| 
 | |
|   private:
 | |
| 	void Flush();
 | |
| 	void Worker();
 | |
| 	void RunWork(SWThreadHandle* handle);
 | |
| 	void RunWorkLocked(std::unique_lock<std::mutex>* lock, SWThreadHandle* handle);
 | |
| 
 | |
|   private:
 | |
| 	IThreadWorkerCallbacks* callbacks_;
 | |
| 	WorkerState state_;
 | |
| 	std::mutex mutex_;
 | |
| 	std::condition_variable work_cv_;
 | |
| 	std::deque<SWThreadHandle*> work_;
 | |
| 	std::unique_ptr<std::thread> thread_;
 | |
| 	std::atomic<unsigned int> jobs_per_wakeup_;
 | |
| 	std::atomic<unsigned int> wait_between_jobs_;
 | |
| };
 | |
| 
 | |
| CompatWorker::CompatWorker(IThreadWorkerCallbacks* callbacks)
 | |
| 	: callbacks_(callbacks),
 | |
| 	  state_(Worker_Stopped),
 | |
| 	  jobs_per_wakeup_(SM_DEFAULT_THREADS_PER_FRAME),
 | |
| 	  wait_between_jobs_(DEFAULT_THINK_TIME_MS)
 | |
| {
 | |
| }
 | |
| 
 | |
| CompatWorker::~CompatWorker()
 | |
| {
 | |
| 	Stop(false /* ignored */);
 | |
| 	Flush();
 | |
| }
 | |
| 
 | |
| bool CompatWorker::Start()
 | |
| {
 | |
| 	std::lock_guard<std::mutex> lock(mutex_);
 | |
| 	if (state_ != Worker_Stopped)
 | |
| 		return false;
 | |
| 
 | |
| 	thread_ = ke::NewThread("SM CompatWorker Thread", [this]() -> void {
 | |
| 		Worker();
 | |
| 	});
 | |
| 
 | |
| 	state_ = Worker_Running;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| bool CompatWorker::Stop(bool)
 | |
| {
 | |
| 	{
 | |
| 		std::lock_guard<std::mutex> lock(mutex_);
 | |
| 
 | |
| 		if (state_ <= Worker_Stopped)
 | |
| 			return false;
 | |
| 
 | |
| 		state_ = Worker_Stopped;
 | |
| 		work_cv_.notify_all();
 | |
| 	}
 | |
| 
 | |
| 	thread_->join();
 | |
| 	thread_ = nullptr;
 | |
| 
 | |
| 	Flush();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| bool CompatWorker::Pause()
 | |
| {
 | |
| 	std::lock_guard<std::mutex> lock(mutex_);
 | |
| 	if (state_ != Worker_Running)
 | |
| 		return false;
 | |
| 
 | |
| 	state_ = Worker_Paused;
 | |
| 	work_cv_.notify_all();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| bool CompatWorker::Unpause()
 | |
| {
 | |
| 	std::lock_guard<std::mutex> lock(mutex_);
 | |
| 	if (state_ != Worker_Paused)
 | |
| 		return false;
 | |
| 
 | |
| 	state_ = Worker_Running;
 | |
| 	work_cv_.notify_all();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void CompatWorker::Flush()
 | |
| {
 | |
| 	while (!work_.empty()) {
 | |
| 		auto handle = ke::PopFront(&work_);
 | |
| 		handle->GetThread()->OnTerminate(handle, true);
 | |
| 		if (handle->m_params.flags & Thread_AutoRelease)
 | |
| 			delete handle;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void CompatWorker::Worker()
 | |
| {
 | |
| 	// Note: this must be first to ensure an ordering between Worker() and
 | |
| 	// Start(). It must also be outside of the loop to ensure the lock is
 | |
| 	// held across wakeup and retesting the predicates.
 | |
| 	std::unique_lock<std::mutex> lock(mutex_);
 | |
| 
 | |
| 	if (callbacks_) {
 | |
| 		lock.unlock();
 | |
| 		callbacks_->OnWorkerStart(this);
 | |
| 		lock.lock();
 | |
| 	}
 | |
| 
 | |
| 	typedef std::chrono::system_clock Clock;
 | |
| 	typedef std::chrono::time_point<Clock> TimePoint;
 | |
| 
 | |
| 	auto can_work = [this]() -> bool {
 | |
| 		return state_ == Worker_Running && !work_.empty();
 | |
| 	};
 | |
| 
 | |
| 	ke::Maybe<TimePoint> wait;
 | |
| 	unsigned int work_in_frame = 0;
 | |
| 	for (;;) {
 | |
| 		if (state_ == Worker_Stopped)
 | |
| 			break;
 | |
| 
 | |
| 		if (!can_work()) {
 | |
| 			// Wait for work or a Stop.
 | |
| 			work_cv_.wait(lock);
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (wait.isValid()) {
 | |
| 			// Wait until the specified time has passed. If we wake up with a
 | |
| 			// timeout, then the wait has elapsed, so reset the holder.
 | |
| 			if (work_cv_.wait_until(lock, wait.get()) == std::cv_status::timeout)
 | |
| 				wait = ke::Nothing();
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		assert(state_ == Worker_Running);
 | |
| 		assert(!work_.empty());
 | |
| 
 | |
| 		SWThreadHandle* handle = ke::PopFront(&work_);
 | |
| 		RunWorkLocked(&lock, handle);
 | |
| 		work_in_frame++;
 | |
| 
 | |
| 		// If we've reached our max jobs per "frame", signal that the next
 | |
| 		// immediate job must be delayed. We retain the old ThreadWorker
 | |
| 		// behavior by checking if the queue has more work. Thus, a delay
 | |
| 		// only occurs if two jobs would be processed in the same wakeup.
 | |
| 		if (work_in_frame >= jobs_per_wakeup_ && wait_between_jobs_ && can_work())
 | |
| 			wait = ke::Some(Clock::now() + std::chrono::milliseconds(wait_between_jobs_));
 | |
| 	}
 | |
| 
 | |
| 	assert(lock.owns_lock());
 | |
| 
 | |
| 	while (!work_.empty()) {
 | |
| 		SWThreadHandle* handle = ke::PopFront(&work_);
 | |
| 		RunWorkLocked(&lock, handle);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| unsigned int CompatWorker::RunFrame()
 | |
| {
 | |
| 	unsigned int nprocessed = 0;
 | |
| 	for (unsigned int i = 1; i <= jobs_per_wakeup_; i++) {
 | |
| 		SWThreadHandle* handle;
 | |
| 		{
 | |
| 			std::lock_guard<std::mutex> lock(mutex_);
 | |
| 			if (work_.empty())
 | |
| 				break;
 | |
| 			handle = ke::PopFront(&work_);
 | |
| 		}
 | |
| 
 | |
| 		RunWork(handle);
 | |
| 		nprocessed++;
 | |
| 	}
 | |
| 	return nprocessed;
 | |
| }
 | |
| 
 | |
| void CompatWorker::RunWorkLocked(std::unique_lock<std::mutex>* lock, SWThreadHandle* handle)
 | |
| {
 | |
| 	lock->unlock();
 | |
| 	RunWork(handle);
 | |
| 	lock->lock();
 | |
| }
 | |
| 
 | |
| void CompatWorker::RunWork(SWThreadHandle* handle)
 | |
| {
 | |
| 	bool autorelease = !!(handle->m_params.flags & Thread_AutoRelease);
 | |
| 	handle->m_state = Thread_Running;
 | |
| 	handle->GetThread()->RunThread(handle);
 | |
| 	handle->m_state = Thread_Done;
 | |
| 	handle->GetThread()->OnTerminate(handle, false);
 | |
| 	if (autorelease)
 | |
| 		delete handle;
 | |
| }
 | |
| 
 | |
| void CompatWorker::MakeThread(IThread *pThread)
 | |
| {
 | |
| 	ThreadParams params;
 | |
| 	params.flags = Thread_AutoRelease;
 | |
| 	MakeThread(pThread, ¶ms);
 | |
| }
 | |
| 
 | |
| IThreadHandle *CompatWorker::MakeThread(IThread *pThread, ThreadFlags flags)
 | |
| {
 | |
| 	ThreadParams params;
 | |
| 	params.flags = flags;
 | |
| 	return MakeThread(pThread, ¶ms);
 | |
| }
 | |
| 
 | |
| IThreadHandle *CompatWorker::MakeThread(IThread *pThread, const ThreadParams *params)
 | |
| {
 | |
| 	std::lock_guard<std::mutex> lock(mutex_);
 | |
| 
 | |
| 	ThreadParams def_params;
 | |
| 	if (!params)
 | |
| 		params = &def_params;
 | |
| 
 | |
| 	if (state_ <= Worker_Stopped)
 | |
| 		return nullptr;
 | |
| 
 | |
| 	SWThreadHandle* handle = new SWThreadHandle(this, params, pThread);
 | |
| 	work_.push_back(handle);
 | |
| 	work_cv_.notify_one();
 | |
| 	return handle;
 | |
| }
 | |
| 
 | |
| void CompatWorker::GetPriorityBounds(ThreadPriority &max, ThreadPriority &min)
 | |
| {
 | |
| 	min = ThreadPrio_Normal;
 | |
| 	max = ThreadPrio_Normal;
 | |
| }
 | |
| 
 | |
| void CompatWorker::SetMaxThreadsPerFrame(unsigned int threads)
 | |
| {
 | |
| 	jobs_per_wakeup_ = threads;
 | |
| }
 | |
| 
 | |
| void CompatWorker::SetThinkTimePerFrame(unsigned int thinktime)
 | |
| {
 | |
| 	wait_between_jobs_ = thinktime;
 | |
| }
 | |
| 
 | |
| WorkerState CompatWorker::GetStatus(unsigned int *numThreads)
 | |
| { 
 | |
| 	std::lock_guard<std::mutex> lock(mutex_);
 | |
| 
 | |
| 	// This number is meaningless and the status is racy.
 | |
| 	if (numThreads)
 | |
| 		*numThreads = jobs_per_wakeup_;
 | |
| 	return state_;
 | |
| }
 | |
| 
 | |
| class CompatThread final : public IThreadHandle
 | |
| {
 | |
| public:
 | |
| 	CompatThread(IThread* callbacks, const ThreadParams* params);
 | |
| 
 | |
| 	bool WaitForThread() override;
 | |
| 	void DestroyThis() override;
 | |
| 	IThreadCreator *Parent() override;
 | |
| 	void GetParams(ThreadParams *ptparams) override;
 | |
| 	ThreadPriority GetPriority() override;
 | |
| 	bool SetPriority(ThreadPriority prio) override;
 | |
| 	ThreadState GetState() override;
 | |
| 	bool Unpause() override;
 | |
| 
 | |
| private:
 | |
| 	void Run();
 | |
| 
 | |
| private:
 | |
| 	IThread* callbacks_;
 | |
| 	ThreadParams params_;
 | |
| 	std::unique_ptr<std::thread> thread_;
 | |
| 	std::mutex mutex_;
 | |
| 	std::condition_variable check_cv_;
 | |
| 	std::atomic<bool> finished_;
 | |
| };
 | |
| 
 | |
| CompatThread::CompatThread(IThread* callbacks, const ThreadParams* params)
 | |
| 	: callbacks_(callbacks),
 | |
| 	  params_(*params)
 | |
| {
 | |
| 	if (!(params_.flags & Thread_CreateSuspended))
 | |
| 		Unpause();
 | |
| }
 | |
| 
 | |
| bool CompatThread::Unpause()
 | |
| {
 | |
| 	std::unique_lock<std::mutex> lock(mutex_);
 | |
| 	if (thread_)
 | |
| 		return false;
 | |
| 
 | |
| 	thread_ = ke::NewThread("SM CompatThread", [this]() -> void {
 | |
| 		Run();
 | |
| 	});
 | |
| 
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void CompatThread::Run()
 | |
| {
 | |
| 	// Create an ordering between when the thread runs and when thread_ is assigned.
 | |
| 	std::unique_lock<std::mutex> lock(mutex_);
 | |
| 
 | |
| 	lock.unlock();
 | |
| 	callbacks_->RunThread(this);
 | |
| 	finished_ = true;
 | |
| 	callbacks_->OnTerminate(this, false);
 | |
| 
 | |
| 	if (params_.flags & Thread_AutoRelease) {
 | |
| 		// There should be no handles outstanding, so it's safe to self-destruct.
 | |
| 		thread_->detach();
 | |
| 		delete this;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	lock.lock();
 | |
| 	callbacks_ = nullptr;
 | |
| 	check_cv_.notify_all();
 | |
| }
 | |
| 
 | |
| bool CompatThread::WaitForThread()
 | |
| {
 | |
| 	std::unique_lock<std::mutex> lock(mutex_);
 | |
| 	for (;;) {
 | |
| 		// When done, callbacks are unset. If paused, this will deadlock.
 | |
| 		if (!callbacks_)
 | |
| 			break;
 | |
| 		check_cv_.wait(lock);
 | |
| 	}
 | |
| 
 | |
| 	thread_->join();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| ThreadState CompatThread::GetState()
 | |
| {
 | |
| 	std::unique_lock<std::mutex> lock(mutex_);
 | |
| 	if (!thread_)
 | |
| 		return Thread_Paused;
 | |
| 	return finished_ ? Thread_Done : Thread_Running;
 | |
| }
 | |
| 
 | |
| void CompatThread::DestroyThis()
 | |
| {
 | |
| 	delete this;
 | |
| }
 | |
| 
 | |
| ThreadPriority CompatThread::GetPriority()
 | |
| {
 | |
| 	return ThreadPrio_Normal;
 | |
| }
 | |
| 
 | |
| bool CompatThread::SetPriority(ThreadPriority prio)
 | |
| {
 | |
| 	return prio == ThreadPrio_Normal;
 | |
| }
 | |
| 
 | |
| IThreadCreator *CompatThread::Parent()
 | |
| {
 | |
| 	return g_pThreader;
 | |
| }
 | |
| 
 | |
| void CompatThread::GetParams(ThreadParams *ptparams)
 | |
| {
 | |
| 	*ptparams = params_;
 | |
| }
 | |
| 
 | |
| class CompatMutex : public IMutex
 | |
| {
 | |
| public:
 | |
| 	bool TryLock() {
 | |
| 		return mutex_.try_lock();
 | |
| 	}
 | |
| 	void Lock() {
 | |
| 		mutex_.lock();
 | |
| 	}
 | |
| 	void Unlock() {
 | |
| 		mutex_.unlock();
 | |
| 	}
 | |
| 	void DestroyThis() {
 | |
| 		delete this;
 | |
| 	}
 | |
| private:
 | |
| 	std::mutex mutex_;
 | |
| };
 | |
| 
 | |
| class CompatThreader final : public IThreader
 | |
| {
 | |
| public:
 | |
| 	void MakeThread(IThread *pThread) override;
 | |
| 	IThreadHandle *MakeThread(IThread *pThread, ThreadFlags flags) override;
 | |
| 	IThreadHandle *MakeThread(IThread *pThread, const ThreadParams *params) override;
 | |
| 	void GetPriorityBounds(ThreadPriority &max, ThreadPriority &min) override;
 | |
| 	IMutex *MakeMutex() override;
 | |
| 	void ThreadSleep(unsigned int ms) override;
 | |
| 	IEventSignal *MakeEventSignal() override;
 | |
| 	IThreadWorker *MakeWorker(IThreadWorkerCallbacks *hooks, bool threaded) override;
 | |
| 	void DestroyWorker(IThreadWorker *pWorker) override;
 | |
| } sCompatThreader;
 | |
| 
 | |
| void CompatThreader::MakeThread(IThread *pThread)
 | |
| {
 | |
| 	ThreadParams params;
 | |
| 	params.flags = Thread_AutoRelease;
 | |
| 	MakeThread(pThread, ¶ms);
 | |
| }
 | |
| 
 | |
| IThreadHandle *CompatThreader::MakeThread(IThread *pThread, ThreadFlags flags)
 | |
| {
 | |
| 	ThreadParams params;
 | |
| 	params.flags = flags;
 | |
| 	return MakeThread(pThread, ¶ms);
 | |
| }
 | |
| 
 | |
| IThreadHandle *CompatThreader::MakeThread(IThread *pThread, const ThreadParams *params)
 | |
| {
 | |
| 	ThreadParams def_params;
 | |
| 	if (!params)
 | |
| 		params = &def_params;
 | |
| 
 | |
| 	return new CompatThread(pThread, params);
 | |
| }
 | |
| 
 | |
| void CompatThreader::GetPriorityBounds(ThreadPriority &max, ThreadPriority &min)
 | |
| {
 | |
| 	min = ThreadPrio_Normal;
 | |
| 	max = ThreadPrio_Normal;
 | |
| }
 | |
| 
 | |
| IMutex *CompatThreader::MakeMutex()
 | |
| {
 | |
| 	return new CompatMutex();
 | |
| }
 | |
| 
 | |
| void CompatThreader::ThreadSleep(unsigned int ms)
 | |
| {
 | |
| 	std::this_thread::sleep_for(std::chrono::milliseconds(ms));
 | |
| }
 | |
| 
 | |
| class CompatEventSignal final : public IEventSignal
 | |
| {
 | |
| public:
 | |
| 	void Wait() override {
 | |
| 		std::unique_lock<std::mutex> lock(mutex_);
 | |
| 		cv_.wait(lock);
 | |
| 	}
 | |
| 	void Signal() override {
 | |
| 		std::lock_guard<std::mutex> lock(mutex_);
 | |
| 		cv_.notify_all();
 | |
| 	}
 | |
| 	void DestroyThis() override {
 | |
| 		delete this;
 | |
| 	}
 | |
| 
 | |
| private:
 | |
| 	std::mutex mutex_;
 | |
| 	std::condition_variable cv_;
 | |
| };
 | |
| 
 | |
| IEventSignal *CompatThreader::MakeEventSignal()
 | |
| {
 | |
| 	return new CompatEventSignal();
 | |
| }
 | |
| 
 | |
| IThreadWorker *CompatThreader::MakeWorker(IThreadWorkerCallbacks *hooks, bool threaded)
 | |
| {
 | |
| 	if (!threaded)
 | |
| 		return new BaseWorker(hooks);
 | |
|     return new CompatWorker(hooks);
 | |
| }
 | |
| 
 | |
| void CompatThreader::DestroyWorker(IThreadWorker *pWorker)
 | |
| {
 | |
| 	delete pWorker;
 | |
| }
 | |
| 
 | |
| IThreader *g_pThreader = &sCompatThreader;
 | |
| 
 | |
| class RegThreadStuff : public SMGlobalClass
 | |
| {
 | |
| public:
 | |
| 	void OnSourceModAllInitialized()
 | |
| 	{
 | |
| 		sharesys->AddInterface(NULL, g_pThreader);
 | |
| 	}
 | |
| } s_RegThreadStuff;
 | |
| 
 |