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:
libretroadmin authored and Henrik Rydgård committed 2026-10-13 13:50:05 +02:00
1 parent 8abc5920c6
commit b71fbaeb5c
5 files changed
+101 -59

No files matched your search

+5
View File
@@ -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();
+1
View File
@@ -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; }
+1
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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'; }