Media: Fix size math, Atrac decoder state and the SAS race

- The ringbuffer's packets * packetSize went straight into the media
  engine's allocations. It's clamped to what a savestate can hold, and
  the put callback's return value is clamped before it sizes the
  validation demuxer. A ringbuffer with no packets no longer divides by
  zero.
- sceMpegAvcCsc with a range starting past the frame gave a negative
  width. sceMpegBaseCsc* reject a stride that overflows the size math,
  and the MJPEG output size could overflow 32 bits.
- MpegDemux clamps the header's stream offset, and doesn't push PES
  payload past what's been read. read16/read24/readPts relied on the
  unspecified evaluation order of | for their byte order. A negative
  stream offset made every later savestate fail to load.
- Atrac: DecodeForSas clamps the split packet so the second copy stays
  in the stack buffer. A zero frame size from the file is rejected
  before it's divided by. decodeTemp_ is reallocated when SetData
  changes the channel count. The standalone decoder only has buffers
  for two channels, so it clamps the count it's given.
  AtracSasBindContextAndGetID indexed with a negative ID and
  dereferenced a missing context.
- Psmf's player tempbuf path could read past the 64KB buffer from the
  stream table or EP map offset.
- sceMpegDelete didn't forget the context's PMP state, so a new context
  at the same address skipped the PMP codec setup and crashed.

Releasing an Atrac ID or setting its data now waits for a SAS mix in
progress, which can be decoding from it on the SAS thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-31 22:59:11 +01:00
1 parent fd42bb6dcb
commit 1c980fc104
11 files changed
+118 -30

No files matched your search

+21 -4
View File
@@ -201,6 +201,10 @@ static int ComputeAtracStateAndInitSecondBuffer(SceAtracIdInfo *info, u32 readSi
}
int InitContextFromTrackInfo(SceAtracContext *ctx, const TrackInfo *wave, u32 bufferAddr, int readSize, int bufferSize) {
// The frame size comes from the file, and InitLengthAndLoop divides by it.
if (wave->blockAlign <= 0) {
return SCE_ERROR_ATRAC_BAD_CODEC_PARAMS;
}
(ctx->info).numChan = (char)wave->numChans;
const int extraSamples = (ctx->info).codec == 0x1001 ? 0x45 : 0x170;
(ctx->info).firstValidSample = extraSamples + wave->firstSampleOffset;
@@ -1020,6 +1024,9 @@ int Atrac2::SetData(const Track &track, u32 bufferAddr, u32 readSize, u32 buffer
CreateDecoder(info.codec, info.sampleSize, info.numChan, jointStereo_);
outputChannels_ = outputChannels;
// Sized for the old frame size and channel count.
delete[] decodeTemp_;
decodeTemp_ = nullptr;
INFO_LOG(Log::Atrac,
"Atrac: sampleSize: %d buffer: %08x bufferByte: %d firstValidSample: %d\n"
@@ -1150,6 +1157,8 @@ void Atrac2::InitLowLevel(const Atrac3LowLevelParams &params, int codecType) {
info.numChan = params.encodedChannels;
outputChannels_ = params.outputChannels;
info.sampleSize = params.bytesPerFrame;
delete[] decodeTemp_;
decodeTemp_ = nullptr;
info.dataOff = 0;
info.decodePos = 0;
info.state = ATRAC_STATUS_LOW_LEVEL;
@@ -1255,13 +1264,21 @@ void Atrac2::DecodeForSas(s16 *dstData, int *bytesWritten, int *finish) {
return;
}
// Compute the part sizes using the current size.
// Compute the part sizes using the current size. The game can shrink the buffer under us,
// so keep both parts inside the assembly buffer.
int part1Size = sas_.bufSize[sas_.curBuffer] - sas_.streamOffset;
int part2Size = info.sampleSize - part1Size;
_dbg_assert_(part1Size >= 0);
if (part1Size >= 0) {
part1Size = std::clamp(part1Size, 0, (int)info.sampleSize);
int part2Size = info.sampleSize - part1Size;
if (part1Size > 0) {
// Grab the partial packet, before we switch over to the other buffer.
Memory::Memcpy(assembly, sas_.bufPtr[sas_.curBuffer] + sas_.streamOffset, part1Size);
const u32 part1Addr = sas_.bufPtr[sas_.curBuffer] + sas_.streamOffset;
if (!Memory::IsValidRange(part1Addr, part1Size)) {
ERROR_LOG(Log::Atrac, "SAS: bad buffer %08x", part1Addr);
*finish = 1;
return;
}
Memory::MemcpyUnchecked(assembly, part1Addr, part1Size);
}
// Check if we hit the end.
+17 -2
View File
@@ -37,6 +37,7 @@
#include "Core/HLE/sceKernelInterrupt.h"
#include "Core/HLE/scePower.h"
#include "Core/HLE/sceAtrac.h"
#include "Core/HLE/sceSas.h"
#include "Core/HLE/sceAudiocodec.h"
#include "Core/HLE/AtracCtx.h"
#include "Core/HLE/AtracCtx2.h"
@@ -333,6 +334,8 @@ static int AllocAndRegisterAtrac(int codecType) {
static int UnregisterAndDeleteAtrac(int atracID) {
if (atracID >= 0 && atracID < PSP_MAX_ATRAC_IDS) {
if (atracContexts[atracID] != nullptr) {
// The SAS thread might be decoding from it.
__SasWaitForMix();
delete atracContexts[atracID];
atracContexts[atracID] = nullptr;
return 0;
@@ -776,6 +779,8 @@ static u32 sceAtracSetHalfwayBuffer(int atracID, u32 buffer, u32 readSize, u32 b
return hleLogError(Log::Atrac, SCE_ERROR_ATRAC_WRONG_CODECTYPE, "atracID uses different codec type than data");
}
// SetData replaces the decoder, which the SAS thread might be using.
__SasWaitForMix();
ret = atrac->SetData(track, buffer, readSize, bufferSize, 0, 2, false);
if (ret < 0) {
return AtracSetDataError(ret, track);
@@ -811,6 +816,8 @@ static u32 sceAtracSetData(int atracID, u32 buffer, u32 bufferSize) {
return hleLogError(Log::Atrac, SCE_ERROR_ATRAC_WRONG_CODECTYPE, "atracID uses different codec type than data");
}
// SetData replaces the decoder, which the SAS thread might be using.
__SasWaitForMix();
ret = atrac->SetData(track, buffer, bufferSize, bufferSize, 0, 2, false);
if (ret < 0) {
return AtracSetDataError(ret, track);
@@ -988,6 +995,8 @@ static int sceAtracSetMOutHalfwayBuffer(int atracID, u32 buffer, u32 readSize, u
return hleLogError(Log::Atrac, ret, "%s", error.c_str());
}
// SetData replaces the decoder, which the SAS thread might be using.
__SasWaitForMix();
ret = atrac->SetData(track, buffer, readSize, bufferSize, 0, 1, false);
if (ret < 0 && ret != SCE_ERROR_ATRAC_NOT_MONO) {
return AtracSetDataError(ret, track);
@@ -1012,6 +1021,8 @@ static u32 sceAtracSetMOutData(int atracID, u32 buffer, u32 bufferSize) {
return hleLogError(Log::Atrac, ret, "%s", error.c_str());
}
// SetData replaces the decoder, which the SAS thread might be using.
__SasWaitForMix();
ret = atrac->SetData(track, buffer, bufferSize, bufferSize, 0, 1, false);
if (ret < 0 && ret != SCE_ERROR_ATRAC_NOT_MONO) {
return AtracSetDataError(ret, track);
@@ -1225,8 +1236,8 @@ void AtracSasDecodeData(int atracID, u8* outbuf, int *SamplesNum, int *finish) {
// The context pointer is assumed to be valid.
int AtracSasBindContextAndGetID(u32 contextAddr) {
// Ugly hack, but needed to support both old and new contexts.
int atracID = (int)Memory::ReadUnchecked_U32(contextAddr + 0xfc);
if (atracID < PSP_MAX_ATRAC_IDS && atracContexts[atracID] && atracContexts[atracID]->GetContextVersion() == 1) {
int atracID = Memory::IsValid4AlignedAddress(contextAddr + 0xfc) ? (int)Memory::ReadUnchecked_U32(contextAddr + 0xfc) : -1;
if (atracID >= 0 && atracID < PSP_MAX_ATRAC_IDS && atracContexts[atracID] && atracContexts[atracID]->GetContextVersion() == 1) {
// We can assume the old atracID hack was used, and atracID is valid.
} else {
// Let's just loop around the contexts and find it by address.
@@ -1244,6 +1255,10 @@ int AtracSasBindContextAndGetID(u32 contextAddr) {
}
AtracBase *atrac = getAtrac(atracID);
if (!atrac) {
// The SAS voice treats a negative ID as no data.
return -1;
}
atrac->CheckForSas();
return atracID;
}
+3 -1
View File
@@ -287,7 +287,9 @@ static int DecodeJpeg(u32 jpegAddr, int jpegSize, u32 imageAddr, int &usec) {
usec += (width * height) / 14;
if (!Memory::IsValidRange(imageAddr, mjpegWidth * mjpegHeight * 4)) {
// CreateMJpeg doesn't bound the height, so this could overflow in 32 bits.
const u64 outSize = (u64)std::max(mjpegWidth, 0) * (u64)std::max(mjpegHeight, 0) * 4;
if (outSize > 0x3FFFFFFF || !Memory::IsValidRange(imageAddr, (u32)outSize)) {
free(jpegBuf);
return hleLogError(Log::ME, SCE_KERNEL_ERROR_INVALID_POINTER, "invalid output address");
}
+32 -11
View File
@@ -16,6 +16,7 @@
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
// This code is part shamelessly "inspired" from JPSCP.
#include <algorithm>
#include <map>
#include <memory>
@@ -274,6 +275,12 @@ static u32 getMpegVersion(u32 mpegRawVersion) {
}
}
// The game controls both fields, so keep the media engine's buffers to what a savestate can hold.
static int RingbufferStreamSize(const SceMpegRingBuffer *ringbuffer) {
const s64 size = (s64)ringbuffer->packets * ringbuffer->packetSize;
return (int)std::clamp<s64>(size, 0, MediaEngine::MAX_RINGBUFFER_SIZE);
}
static void AnalyzeMpeg(const u8 *buffer, u32 validSize, MpegContext *ctx) {
ctx->mpegMagic = *(u32_le*)buffer;
ctx->mpegRawVersion = *(u32_le*)(buffer + PSMF_STREAM_VERSION_OFFSET);
@@ -309,7 +316,7 @@ static void AnalyzeMpeg(const u8 *buffer, u32 validSize, MpegContext *ctx) {
// init mediaEngine
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ctx->mpegRingbufferAddr);
if (ringbuffer.IsValid()) {
ctx->mediaengine->loadStream(buffer, ctx->mpegOffset, ringbuffer->packets * ringbuffer->packetSize);
ctx->mediaengine->loadStream(buffer, ctx->mpegOffset, RingbufferStreamSize(ringbuffer));
} else {
// TODO: Does this make any sense?
ctx->mediaengine->loadStream(buffer, ctx->mpegOffset, 0);
@@ -592,6 +599,8 @@ static int sceMpegDelete(u32 mpeg) {
delete ctx;
g_mpegCtxs.erase(Memory::ReadUnchecked_U32(mpeg));
// A new context at the same address needs its PMP codec set up again.
pmp_ContextList.remove(mpeg);
return hleDelayResult(hleLogDebug(Log::Mpeg, 0), "mpeg delete", 40000);
}
@@ -1456,6 +1465,12 @@ static int sceMpegRingbufferAvailableSize(u32 ringbufferAddr) {
void PostPutAction::run(MipsCall &call) {
auto ringbuffer = PSPPointer<SceMpegRingBuffer>::Create(ringAddr_);
// The callback could have changed it.
if (!ringbuffer.IsValid() || (s32)ringbuffer->packets <= 0) {
ERROR_LOG(Log::Mpeg, "sceMpegRingbufferPut: bad ringbuffer %08x", ringAddr_);
call.setReturnValue(SCE_MPEG_ERROR_INVALID_VALUE);
return;
}
MpegContext *ctx = getMpegCtx(ringbuffer->mpeg);
int writeOffset = ringbuffer->packetsWritePos % (s32)ringbuffer->packets;
@@ -1471,13 +1486,19 @@ void PostPutAction::run(MipsCall &call) {
return;
}
// The count comes from the game's callback, so clamp it before using it for sizes.
if (packetsAddedThisRound > ringbuffer->packets - ringbuffer->packetsAvail) {
WARN_LOG(Log::Mpeg, "sceMpegRingbufferPut clamping packetsAdded old=%i new=%i", packetsAddedThisRound, ringbuffer->packets - ringbuffer->packetsAvail);
packetsAddedThisRound = ringbuffer->packets - ringbuffer->packetsAvail;
}
// It seems validation is done only by older mpeg libs.
if (mpegLibVersion < 0x0105 && packetsAddedThisRound > 0) {
// TODO: Faster / less wasteful validation.
auto demuxer = std::make_unique<MpegDemux>(packetsAddedThisRound * 2048, 0);
int readOffset = ringbuffer->packetsRead % (s32)ringbuffer->packets;
uint32_t bufSize = Memory::ClampValidSizeAt(ringbuffer->data + readOffset * 2048, packetsAddedThisRound * 2048);
const u8 *buf = Memory::GetPointerOrException(ringbuffer->data + readOffset * 2048);
uint32_t bufSize = Memory::ClampValidSizeAt(ringbuffer->data + readOffset * 2048, (u32)packetsAddedThisRound * 2048);
auto demuxer = std::make_unique<MpegDemux>(std::max(bufSize, 2048U), 0);
const u8 *buf = Memory::GetPointerUnchecked(ringbuffer->data + readOffset * 2048);
bool invalid = false;
for (uint32_t i = 0; i < bufSize / 2048; ++i) {
demuxer->addStreamData(buf, 2048);
@@ -1505,15 +1526,11 @@ void PostPutAction::run(MipsCall &call) {
if (ringbuffer->packetsRead == 0 && ctx->mediaengine && packetsAddedThisRound > 0) {
// init mediaEngine
AnalyzeMpeg(ctx->mpegheader, 2048, ctx);
ctx->mediaengine->loadStream(ctx->mpegheader, 2048, ringbuffer->packets * ringbuffer->packetSize);
ctx->mediaengine->loadStream(ctx->mpegheader, 2048, RingbufferStreamSize(ringbuffer));
}
if (packetsAddedThisRound > 0) {
if (packetsAddedThisRound > ringbuffer->packets - ringbuffer->packetsAvail) {
WARN_LOG(Log::Mpeg, "sceMpegRingbufferPut clamping packetsAdded old=%i new=%i", packetsAddedThisRound, ringbuffer->packets - ringbuffer->packetsAvail);
packetsAddedThisRound = ringbuffer->packets - ringbuffer->packetsAvail;
}
const u8 *data = Memory::GetPointerOrException(ringbuffer->data + writeOffset * 2048);
uint32_t dataSize = Memory::ClampValidSizeAt(ringbuffer->data + writeOffset * 2048, packetsAddedThisRound * 2048);
uint32_t dataSize = Memory::ClampValidSizeAt(ringbuffer->data + writeOffset * 2048, (u32)packetsAddedThisRound * 2048);
const u8 *data = dataSize ? Memory::GetPointerUnchecked(ringbuffer->data + writeOffset * 2048) : nullptr;
int actuallyAdded = ctx->mediaengine == NULL ? 8 : ctx->mediaengine->addStreamData(data, dataSize) / 2048;
if (actuallyAdded != packetsAddedThisRound) {
WARN_LOG(Log::Mpeg, "sceMpegRingbufferPut(): unable to enqueue all added packets, going to overwrite some frames (%d != %d)", actuallyAdded, packetsAddedThisRound);
@@ -1559,6 +1576,10 @@ static u32 sceMpegRingbufferPut(u32 ringbufferAddr, int numPackets, int availabl
return hleLogError(Log::Mpeg, -1, "invalid ringbuffer address");
}
if ((s32)ringbuffer->packets <= 0) {
return hleLogError(Log::Mpeg, SCE_MPEG_ERROR_INVALID_VALUE, "bad ringbuffer packet count");
}
numPackets = std::min(numPackets, available);
// Generally, program will call sceMpegRingbufferAvailableSize() before this func.
// Seems still need to check actual available, Patapon 3 for example.
+8
View File
@@ -595,6 +595,10 @@ static int MpegBaseCscRange(u32 bufferRGB, u32 bufferRGB2, u32 cscAddr, int buff
if (bufferWidth == 0) {
bufferWidth = g_mpegBaseBufferWidth;
}
// The stride goes into the output size and row addresses, so keep that math in range.
if (bufferWidth < 0 || bufferWidth > 4096) {
return hleLogError(Log::Mpeg, -1, "bad buffer width %d", bufferWidth);
}
u32 buffers[8]{};
for (int i = 0; i < 8; i++) {
@@ -731,6 +735,10 @@ static int sceMpegBaseCscVme(u32 bufferRGB, u32 bufferRGB2, int bufferWidth, u32
if (bufferWidth == 0) {
bufferWidth = g_mpegBaseBufferWidth;
}
// The stride goes into the output size and row addresses, so keep that math in range.
if (bufferWidth < 0 || bufferWidth > 4096) {
return hleLogError(Log::Mpeg, -1, "bad buffer width %d", bufferWidth);
}
auto csc = PSPPointer<SceMp4AvcCscStruct>::Create(cscAddr);
u32 buffers[6];
for (int i = 0; i < 6; i++) {
+7 -2
View File
@@ -302,8 +302,9 @@ public:
} else {
// No guest address to validate against (player tempbuf path,
// where the buffer is 64KB): cap so the reads below stay in bounds.
if (epMapBytes > 0x10000) {
psmf->EPMapEntriesNum = 0x10000 / EP_MAP_STRIDE;
const u32 available = psmf->EPMapOffset < 0x10000 ? 0x10000 - psmf->EPMapOffset : 0;
if (epMapBytes > available) {
psmf->EPMapEntriesNum = available / EP_MAP_STRIDE;
}
}
@@ -385,6 +386,10 @@ Psmf::Psmf(const u8 *ptr, u32 data) {
if (data != 0 && !Memory::IsValidRange(data, 0x82 + numStreams * 16)) {
ERROR_LOG(Log::ME, "Invalid PSMF stream count: %d", numStreams);
numStreams = Memory::ClampValidSizeAt(data + 0x82, numStreams * 16) / 16;
} else if (data == 0 && 0x82 + numStreams * 16 > 0x10000) {
// The player's 64KB tempbuf.
ERROR_LOG(Log::ME, "Invalid PSMF stream count: %d", numStreams);
numStreams = (0x10000 - 0x82) / 16;
}
for (int i = 0; i < numStreams; i++) {
+5 -2
View File
@@ -1,3 +1,5 @@
#include <algorithm>
#include "SimpleAudioDec.h"
#include "Common/LogReporting.h"
#include "ext/at3_standalone/at3_decoders.h"
@@ -16,10 +18,11 @@ inline int16_t clamp16(float f) {
class Atrac3Audio : public AudioDecoder {
public:
Atrac3Audio(PSPAudioType audioType, int channels, size_t blockAlign, const uint8_t *extraData, size_t extraDataSize)
: audioType_(audioType), channels_(channels) {
// The channel count can come straight from the game, and buffers_ only has room for two.
: audioType_(audioType), channels_(std::clamp(channels, 1, 2)) {
blockAlign_ = (int)blockAlign;
if (audioType_ == PSP_CODEC_AT3) {
at3Ctx_ = atrac3_alloc(channels, &blockAlign_, extraData, (int)extraDataSize);
at3Ctx_ = atrac3_alloc(channels_, &blockAlign_, extraData, (int)extraDataSize);
if (at3Ctx_) {
codecOpen_ = true;
} else {
+5 -2
View File
@@ -180,7 +180,7 @@ void MediaEngine::DoState(PointerWrap &p) {
// All straight from the file. The header ones index m_mpegheader, the ringbuffer size sizes
// buffers (the PSP's whole RAM is a generous limit.)
if (p.mode == p.MODE_READ && (m_mpegheaderSize < 0 || m_mpegheaderSize > (int)sizeof(m_mpegheader) ||
m_mpegheaderReadPos < 0 || m_mpegheaderReadPos > m_mpegheaderSize || m_ringbuffersize < 0 || m_ringbuffersize > 64 * 1024 * 1024)) {
m_mpegheaderReadPos < 0 || m_mpegheaderReadPos > m_mpegheaderSize || m_ringbuffersize < 0 || m_ringbuffersize > MAX_RINGBUFFER_SIZE)) {
ERROR_LOG(Log::SaveState, "MediaEngine: bad header size %d, position %d or ringbuffer size %d in savestate", m_mpegheaderSize, m_mpegheaderReadPos, m_ringbuffersize);
p.SetError(p.ERROR_FAILURE);
return;
@@ -479,7 +479,7 @@ int MediaEngine::addStreamData(const u8 *buffer, int addSize) {
if (!m_pFormatCtx && m_pdata->getQueueSize() >= 2048) {
m_mpegheaderSize = m_pdata->get_front(m_mpegheader, sizeof(m_mpegheader));
int streamOffset = (int)(*(s32_be *)(m_mpegheader + 8));
if (streamOffset <= m_mpegheaderSize) {
if (streamOffset >= 0 && streamOffset <= m_mpegheaderSize) {
m_mpegheaderSize = streamOffset;
m_pdata->pop_front(0, m_mpegheaderSize);
openContext();
@@ -1000,6 +1000,9 @@ int MediaEngine::writeVideoImageWithRange(u32 bufferPtr, int frameWidth, int vid
width = frameWidth;
if (height > m_desHeight - ypos)
height = m_desHeight - ypos;
// A range starting outside the frame clamps to nothing (or less).
if (xpos < 0 || ypos < 0 || width <= 0 || height <= 0)
return 0;
switch (videoPixelMode) {
case GE_CMODE_32BIT_ABGR8888:
+3
View File
@@ -52,6 +52,9 @@ bool InitFFmpeg();
class MediaEngine {
public:
// Also the most DoState accepts.
static constexpr int MAX_RINGBUFFER_SIZE = 64 * 1024 * 1024;
MediaEngine();
~MediaEngine();
+5 -2
View File
@@ -26,7 +26,8 @@ MpegDemux::MpegDemux(int size, int offset) : m_audioStream(size) {
m_buf = new u8[size];
m_len = size;
m_index = offset;
// The offset comes from the stream header.
m_index = std::clamp(offset, 0, size);
m_audioChannel = -1;
m_readSize = 0;
}
@@ -162,7 +163,9 @@ int MpegDemux::demuxStream(bool bdemux, int startCode, int length, int channel)
length = readPesHeader(pesHeader, length, startCode);
if (pesHeader.channel == channel || channel < 0) {
channel = pesHeader.channel;
m_audioStream.push(m_buf + m_index, length, pesHeader.pts);
// The header lengths come from the stream, so don't push past what's been read.
const int pushLength = std::clamp(length, 0, std::max(m_readSize - m_index, 0));
m_audioStream.push(m_buf + m_index, pushLength, pesHeader.pts);
}
skip(length);
} else {
+12 -4
View File
@@ -46,22 +46,30 @@ private:
// only cross-checked against the outer packet length, not against how much
// data is actually available - a crafted stream can claim more than that,
// so guard the actual buffer access here rather than in every caller.
if (m_index >= m_len) {
if (m_index < 0 || m_index >= m_len) {
return 0;
}
return m_buf[m_index++];
}
// The operands of | aren't sequenced, so read into locals to get the byte order right.
int read16() {
return (read8() << 8) | read8();
const int hi = read8();
const int lo = read8();
return (hi << 8) | lo;
}
int read24() {
return (read8() << 16) | (read8() << 8) | read8();
const int hi = read8();
const int mid = read8();
const int lo = read8();
return (hi << 16) | (mid << 8) | lo;
}
s64 readPts() {
return readPts(read8());
}
s64 readPts(int c) {
return (((s64) (c & 0x0E)) << 29) | ((read16() >> 1) << 15) | (read16() >> 1);
const int hi = read16();
const int lo = read16();
return (((s64) (c & 0x0E)) << 29) | ((hi >> 1) << 15) | (lo >> 1);
}
bool isEOF() const {
return m_index >= m_readSize;