mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 15:00:30 +02:00
libretro: fix OOB, UAF, data races and control flow in core glue
System_AudioPushSamples converts straight into the output buffer: the block loop never advanced the source pointer and copied numSamples*2 int16s out of a 2048-sample stack buffer, a stack OOB read for any push over 1024 frames. Realloc failure drops samples instead of crashing and log_cb is NULL-checked. retro_serialize refuses to write a state larger than the frontend's buffer, retro_get_memory_data/size return nothing until PSP memory is mapped, deferred unserialize no longer leaks the previous buffer, and the cheat entry points stop leaking a CWCheatEngine per call. The emu thread acknowledges a pause with a CAS so it cannot clobber a concurrent quit request. EmuThreadStop stops and joins from any live state, so unloading while paused no longer leaves a thread running over freed state. EmuThreadPause pumps queued frames until the thread parks, via a new non-blocking GLRenderManager::HasQueuedThreadWork() check, instead of draining one frame, which deadlocked when the thread was blocked on a readback sync in the next frame. retro_run resumes a paused thread and pauses it before check_variables() rewrites g_Config, retro_reset stops it before PSP_Shutdown(), and the sleep_ms(4) after resume is gone.
This commit is contained in:
1 parent
8abc5920c6
commit
b71fbaeb5c
5 files changed
+101
-59
No files matched your search
@@ -249,6 +249,11 @@ public:
|
||||
void ThreadStart(Draw::DrawContext *draw);
|
||||
void ThreadEnd();
|
||||
bool ThreadFrame(); // False means it's time to exit.
|
||||
// True if ThreadFrame() has queued work and will not block on an empty queue.
|
||||
bool HasQueuedThreadWork() {
|
||||
std::lock_guard<std::mutex> lock(pushMutex_);
|
||||
return !renderThreadQueue_.empty();
|
||||
}
|
||||
|
||||
void NotifyEmuThreadExit();
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
|
||||
void ThreadStart() override { renderManager_->ThreadStart(draw_); }
|
||||
bool ThreadFrame() override { return renderManager_->ThreadFrame(); }
|
||||
bool ThreadFramePending() override { return renderManager_->HasQueuedThreadWork(); }
|
||||
void ThreadEnd() override { renderManager_->ThreadEnd(); }
|
||||
|
||||
GPUCore GetGPUCore() override { return GPUCORE_GLES; }
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
|
||||
void ThreadStart() override { renderManager_->ThreadStart(draw_); }
|
||||
bool ThreadFrame() override { return renderManager_->ThreadFrame(); }
|
||||
bool ThreadFramePending() override { return renderManager_->HasQueuedThreadWork(); }
|
||||
void ThreadEnd() override { renderManager_->ThreadEnd(); }
|
||||
|
||||
// Call from emu thread
|
||||
|
||||
@@ -31,6 +31,8 @@ public:
|
||||
}
|
||||
virtual void SwapBuffers() = 0;
|
||||
void Resize() override {}
|
||||
// True if ThreadFrame() has queued work to run (emu thread backends only).
|
||||
virtual bool ThreadFramePending() { return false; }
|
||||
|
||||
virtual void GotBackbuffer();
|
||||
virtual void LostBackbuffer();
|
||||
|
||||
+92
-59
@@ -132,15 +132,24 @@ namespace Libretro
|
||||
static s64 runTicksLast = 0;
|
||||
|
||||
// Must be called with output_audio_buffer_mutex held.
|
||||
static void ensure_output_audio_buffer_capacity(int32_t capacity)
|
||||
static bool ensure_output_audio_buffer_capacity(int32_t capacity)
|
||||
{
|
||||
if (capacity <= output_audio_buffer.capacity) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
output_audio_buffer.data = (int16_t*)realloc(output_audio_buffer.data, capacity * sizeof(*output_audio_buffer.data));
|
||||
int16_t *data = (int16_t*)realloc(output_audio_buffer.data, capacity * sizeof(*output_audio_buffer.data));
|
||||
if (!data) {
|
||||
// Keep the old buffer, drop the new samples.
|
||||
if (log_cb)
|
||||
log_cb(RETRO_LOG_ERROR, "Failed to grow output audio buffer to %d samples\n", capacity);
|
||||
return false;
|
||||
}
|
||||
output_audio_buffer.data = data;
|
||||
output_audio_buffer.capacity = capacity;
|
||||
log_cb(RETRO_LOG_DEBUG, "Output audio buffer capacity set to %d\n", capacity);
|
||||
if (log_cb)
|
||||
log_cb(RETRO_LOG_DEBUG, "Output audio buffer capacity set to %d\n", capacity);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void init_output_audio_buffer(int32_t capacity)
|
||||
@@ -164,7 +173,8 @@ namespace Libretro
|
||||
static void upload_output_audio_buffer()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(output_audio_buffer_mutex);
|
||||
audio_batch_cb(output_audio_buffer.data, output_audio_buffer.size / 2);
|
||||
if (output_audio_buffer.data && output_audio_buffer.size > 0)
|
||||
audio_batch_cb(output_audio_buffer.data, output_audio_buffer.size / 2);
|
||||
output_audio_buffer.size = 0;
|
||||
}
|
||||
|
||||
@@ -1388,14 +1398,16 @@ namespace Libretro {
|
||||
SetCurrentThreadName("EmuThread");
|
||||
|
||||
for (;;) {
|
||||
switch ((EmuThreadState)emuThreadState)
|
||||
EmuThreadState state = emuThreadState;
|
||||
switch (state)
|
||||
{
|
||||
case EmuThreadState::RUNNING:
|
||||
EmuFrame();
|
||||
break;
|
||||
case EmuThreadState::PAUSE_REQUESTED:
|
||||
emuThreadState = EmuThreadState::PAUSED;
|
||||
[[fallthrough]];
|
||||
// CAS so a concurrent QUIT_REQUESTED is not clobbered; the loop re-reads the state.
|
||||
emuThreadState.compare_exchange_strong(state, EmuThreadState::PAUSED);
|
||||
break;
|
||||
case EmuThreadState::PAUSED:
|
||||
sleep_ms(1, "libretro-paused");
|
||||
break;
|
||||
@@ -1428,16 +1440,19 @@ namespace Libretro {
|
||||
}
|
||||
|
||||
void EmuThreadStop() {
|
||||
if (emuThreadState != EmuThreadState::RUNNING)
|
||||
// Stop and join from any live state, not just RUNNING: unloading while
|
||||
// PAUSED (the state retro_serialize_size() leaves behind) must not leave
|
||||
// a live thread over freed engine state or a joinable std::thread.
|
||||
if (!emuThread.joinable())
|
||||
return;
|
||||
|
||||
emuThreadState = EmuThreadState::QUIT_REQUESTED;
|
||||
if (emuThreadState != EmuThreadState::STOPPED)
|
||||
emuThreadState = EmuThreadState::QUIT_REQUESTED;
|
||||
|
||||
// Eat remaining frames.
|
||||
while (ctx->ThreadFrame()) {}
|
||||
|
||||
emuThread.join();
|
||||
emuThread = std::thread();
|
||||
ctx->ThreadEnd();
|
||||
}
|
||||
|
||||
@@ -1447,11 +1462,16 @@ namespace Libretro {
|
||||
|
||||
emuThreadState = EmuThreadState::PAUSE_REQUESTED;
|
||||
|
||||
// Is this safe?
|
||||
ctx->ThreadFrame(); // Eat 1 frame
|
||||
|
||||
while (emuThreadState != EmuThreadState::PAUSED)
|
||||
sleep_ms(1, "libretro-pause-poll");
|
||||
// Drain queued frames until the emu thread parks at a frame boundary.
|
||||
// A single frame is not enough when the emu thread is blocked on a
|
||||
// readback sync in the following frame, and ThreadFrame() blocks on an
|
||||
// empty queue, so only pump it when work is queued.
|
||||
while (emuThreadState != EmuThreadState::PAUSED) {
|
||||
if (ctx->ThreadFramePending())
|
||||
ctx->ThreadFrame();
|
||||
else
|
||||
sleep_ms(1, "libretro-pause-poll");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Libretro
|
||||
@@ -1563,6 +1583,11 @@ void retro_unload_game(void) {
|
||||
}
|
||||
|
||||
void retro_reset(void) {
|
||||
// EmuFrame() must not run while the core is torn down; retro_run()
|
||||
// restarts the thread.
|
||||
if (Libretro::useEmuThread)
|
||||
Libretro::EmuThreadStop();
|
||||
|
||||
PSP_Shutdown(true);
|
||||
|
||||
if (BootState::Complete != PSP_Init(PSP_CoreParameter(), &g_bootErrorString)) {
|
||||
@@ -1709,10 +1734,13 @@ void retro_run(void) {
|
||||
}
|
||||
|
||||
// Update setting if any have changed.
|
||||
bool updated;
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
|
||||
bool updated = false;
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated) {
|
||||
// The emu thread reads g_Config mid-frame; it is resumed below.
|
||||
if (useEmuThread)
|
||||
EmuThreadPause();
|
||||
check_variables(PSP_CoreParameter());
|
||||
else
|
||||
} else
|
||||
check_dynamic_variables(PSP_CoreParameter());
|
||||
|
||||
// Process input.
|
||||
@@ -1720,13 +1748,8 @@ void retro_run(void) {
|
||||
|
||||
// Handle thread pumping.
|
||||
if (useEmuThread) {
|
||||
if (emuThreadState == EmuThreadState::PAUSED ||
|
||||
emuThreadState == EmuThreadState::PAUSE_REQUESTED) {
|
||||
VsyncSwapIntervalDetect();
|
||||
ctx->SwapBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
// Also resumes from PAUSED, so a retro_serialize_size() without a
|
||||
// following retro_serialize() cannot freeze emulation.
|
||||
if (emuThreadState != EmuThreadState::RUNNING) {
|
||||
EmuThreadStart();
|
||||
}
|
||||
@@ -1780,16 +1803,24 @@ bool retro_serialize(void *data, size_t size) {
|
||||
EmuThreadPause(); // Does nothing if already paused
|
||||
}
|
||||
|
||||
size_t measuredSize;
|
||||
SaveState::SaveStart state;
|
||||
auto err = CChunkFileReader::MeasureAndSavePtr(state, (u8 **)&data, &measuredSize);
|
||||
bool retVal = err == CChunkFileReader::ERROR_NONE;
|
||||
bool retVal = false;
|
||||
|
||||
if (useEmuThread) {
|
||||
EmuThreadStart();
|
||||
sleep_ms(4, "libretro-serialize");
|
||||
// The buffer was sized by an earlier retro_serialize_size(); the state
|
||||
// may have grown since, so never write past 'size'.
|
||||
size_t measuredSize = CChunkFileReader::MeasurePtr(state);
|
||||
if (measuredSize <= size) {
|
||||
u8 *buffer = (u8 *)data;
|
||||
auto err = CChunkFileReader::MeasureAndSavePtr(state, &buffer, &measuredSize);
|
||||
retVal = err == CChunkFileReader::ERROR_NONE;
|
||||
} else {
|
||||
ERROR_LOG(Log::SaveState, "Savestate (%d bytes) does not fit in frontend buffer (%d bytes)",
|
||||
(int)measuredSize, (int)size);
|
||||
}
|
||||
|
||||
if (useEmuThread)
|
||||
EmuThreadStart();
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@@ -1797,9 +1828,13 @@ bool retro_unserialize(const void *data, size_t size) {
|
||||
// The HW renderer isn't ready on first pass.
|
||||
// So we save the data until we are ready to use it.
|
||||
if (!gpu) {
|
||||
unserialize_data = malloc(size);
|
||||
memcpy(unserialize_data, data, size);
|
||||
void *buffer = malloc(size);
|
||||
if (!buffer)
|
||||
return false;
|
||||
free(unserialize_data);
|
||||
unserialize_data = buffer;
|
||||
unserialize_size = size;
|
||||
memcpy(unserialize_data, data, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1812,31 +1847,32 @@ bool retro_unserialize(const void *data, size_t size) {
|
||||
bool retVal = CChunkFileReader::LoadPtr((u8 *)data, size, state, &errorString)
|
||||
== CChunkFileReader::ERROR_NONE;
|
||||
|
||||
if (useEmuThread) {
|
||||
if (useEmuThread)
|
||||
EmuThreadStart();
|
||||
sleep_ms(4, "libretro-unserialize");
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
void *retro_get_memory_data(unsigned id)
|
||||
{
|
||||
if ( id == RETRO_MEMORY_SYSTEM_RAM )
|
||||
return Memory::GetPointerWriteUnchecked(PSP_GetKernelMemoryBase()) ;
|
||||
// Frontends query this right after retro_load_game(), before the
|
||||
// deferred boot in retro_run() has mapped PSP memory.
|
||||
if (id == RETRO_MEMORY_SYSTEM_RAM && Memory::IsActive())
|
||||
return Memory::GetPointerWriteUnchecked(PSP_GetKernelMemoryBase());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t retro_get_memory_size(unsigned id)
|
||||
{
|
||||
if ( id == RETRO_MEMORY_SYSTEM_RAM )
|
||||
return Memory::g_MemorySize ;
|
||||
if (id == RETRO_MEMORY_SYSTEM_RAM && Memory::IsActive())
|
||||
return Memory::g_MemorySize;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void retro_cheat_reset(void) {
|
||||
// Init Cheat Engine
|
||||
CWCheatEngine *cheatEngine = new CWCheatEngine(g_paramSFO.GetDiscID());
|
||||
CWCheatEngine cheatEngineObj(g_paramSFO.GetDiscID());
|
||||
CWCheatEngine *cheatEngine = &cheatEngineObj;
|
||||
Path file=cheatEngine->CheatFilename();
|
||||
|
||||
// Output cheats to cheat file
|
||||
@@ -1858,7 +1894,8 @@ void retro_cheat_reset(void) {
|
||||
|
||||
void retro_cheat_set(unsigned index, bool enabled, const char *code) {
|
||||
// Initialize Cheat Engine
|
||||
CWCheatEngine *cheatEngine = new CWCheatEngine(g_paramSFO.GetDiscID());
|
||||
CWCheatEngine cheatEngineObj(g_paramSFO.GetDiscID());
|
||||
CWCheatEngine *cheatEngine = &cheatEngineObj;
|
||||
cheatEngine->CreateCheatFile();
|
||||
Path file=cheatEngine->CheatFilename();
|
||||
|
||||
@@ -2003,26 +2040,22 @@ inline int16_t Clamp16(int32_t sample) {
|
||||
void System_AudioPushSamples(const int32_t *audio, int numSamples, float volume) {
|
||||
// We ignore volume here, because it's handled by libretro presumably.
|
||||
|
||||
if (!audio || numSamples <= 0)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(output_audio_buffer_mutex);
|
||||
|
||||
// Convert to 16-bit audio for further processing.
|
||||
int16_t buffer[1024 * 2];
|
||||
int origSamples = numSamples * 2;
|
||||
|
||||
while (numSamples > 0) {
|
||||
int blockSize = std::min(1024, numSamples);
|
||||
for (int i = 0; i < blockSize; i++) {
|
||||
buffer[i * 2] = Clamp16(audio[i * 2]);
|
||||
buffer[i * 2 + 1] = Clamp16(audio[i * 2 + 1]);
|
||||
}
|
||||
|
||||
numSamples -= blockSize;
|
||||
int32_t values = numSamples * 2; // stereo
|
||||
if (output_audio_buffer.capacity - output_audio_buffer.size < values) {
|
||||
if (!ensure_output_audio_buffer_capacity((int32_t)((output_audio_buffer.capacity + values) * 1.5f)))
|
||||
return;
|
||||
}
|
||||
|
||||
if (output_audio_buffer.capacity - output_audio_buffer.size < origSamples)
|
||||
ensure_output_audio_buffer_capacity((output_audio_buffer.capacity + origSamples) * 1.5);
|
||||
memcpy(output_audio_buffer.data + output_audio_buffer.size, buffer, origSamples * sizeof(*output_audio_buffer.data));
|
||||
output_audio_buffer.size += origSamples;
|
||||
// Convert straight into the output buffer.
|
||||
int16_t *dst = output_audio_buffer.data + output_audio_buffer.size;
|
||||
for (int32_t i = 0; i < values; i++)
|
||||
dst[i] = Clamp16(audio[i]);
|
||||
output_audio_buffer.size += values;
|
||||
}
|
||||
|
||||
void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) buf[0] = '\0'; }
|
||||
|
||||
Reference in new issue
Block a user