From 9879deca69e22057b937532990b7b12bed7a6488 Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Sat, 3 Oct 2026 14:25:49 +0200 Subject: [PATCH] fix(audio): bound ATRAC9 decoding to the superframe LibAtrac9 read its bitstream without a length, so a corrupt ATRAC9 frame made the decoder read past the end of the guest's buffer. Update the submodule to 946e05a ("Prevent bitstream overruns"), whose decode functions take the buffer size, and pass the bytes left in the superframe from NGS2, AJM and Audiodec. The callers already reject a frame that consumes more than the superframe holds. ngs2_atrac9 places a synthetic corrupt superframe right before an inaccessible page and expects the render to report the decode failure; with the previous LibAtrac9 it crashes with a segmentation fault. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb --- 3rdparty/LibAtrac9 | 2 +- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 6 +- core/libs/prx/libSceAudiodec/src/Codecs.cpp | 4 +- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 2 +- .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 55 ++++++++++++++++++- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/3rdparty/LibAtrac9 b/3rdparty/LibAtrac9 index ec8899dad..946e05a92 160000 --- a/3rdparty/LibAtrac9 +++ b/3rdparty/LibAtrac9 @@ -1 +1 @@ -Subproject commit ec8899dadf393f655f2871a94e0fe4b3d6220c9a +Subproject commit 946e05a9212976626a9f5e52f29c0a7202871f29 diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 65c6748e1..48fbaf898 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -359,9 +359,9 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c } int used = 0; switch (encoding) { - case 0: decodeStatus = Atrac9Decode(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; - case 1: decodeStatus = Atrac9DecodeS32(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; - default: decodeStatus = Atrac9DecodeF32(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; + case 0: decodeStatus = Atrac9Decode(instance.decoder, input.data() + consumed, static_cast(instance.superframeRemaining), reinterpret_cast(pcm.data()), &used, 0); break; + case 1: decodeStatus = Atrac9DecodeS32(instance.decoder, input.data() + consumed, static_cast(instance.superframeRemaining), reinterpret_cast(pcm.data()), &used, 0); break; + default: decodeStatus = Atrac9DecodeF32(instance.decoder, input.data() + consumed, static_cast(instance.superframeRemaining), reinterpret_cast(pcm.data()), &used, 0); break; } if (decodeStatus != 0 || used <= 0 || static_cast(used) > instance.superframeRemaining) { result |= AJM_RESULT_INVALID_DATA; diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.cpp b/core/libs/prx/libSceAudiodec/src/Codecs.cpp index 36d021763..dfa428f9d 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.cpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.cpp @@ -162,8 +162,8 @@ public: for (int frame = 0; frame < info.framesInSuperframe; ++frame) { int used = 0; const int status = wordSize == WORD_SIZE_FLOAT - ? Atrac9DecodeF32(handle, data + offset, reinterpret_cast(pcm + result.produced), &used, 0) - : Atrac9Decode(handle, data + offset, reinterpret_cast(pcm + result.produced), &used, 0); + ? Atrac9DecodeF32(handle, data + offset, static_cast(superframeSize - offset), reinterpret_cast(pcm + result.produced), &used, 0) + : Atrac9Decode(handle, data + offset, static_cast(superframeSize - offset), reinterpret_cast(pcm + result.produced), &used, 0); if (status != 0 || used <= 0 || offset + static_cast(used) > superframeSize) { Reset(); return {DecodeStatus::InvalidData}; diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index e9b180347..3a59cf433 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -81,7 +81,7 @@ static void DecodeSuperframe(Ngs2Voice& voice, Ngs2Block& block) { std::size_t consumed = 0; for (std::uint32_t frame = 0; frame < atrac9.framesInSuperframe; frame++, end += frameValues) { int used = 0; - const int status = Atrac9DecodeF32(atrac9.decoder.get(), superframe + consumed, atrac9.window.data() + end, &used, 0); + const int status = Atrac9DecodeF32(atrac9.decoder.get(), superframe + consumed, static_cast(atrac9.superframeBytes - consumed), atrac9.window.data() + end, &used, 0); consumed += static_cast(std::max(used, 0)); if (status != 0 || consumed > atrac9.superframeBytes) throw std::runtime_error("NGS2: ATRAC9 decode failed with " + Ngs2Hex(static_cast(status))); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 0026cf7d8..739d8ae3c 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -4,8 +4,20 @@ #include #include +#include +#include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#endif + static constexpr std::uint32_t Config = 0xFE7007F0; static constexpr std::uint32_t SuperframeBytes = 256; static constexpr std::uint32_t SuperframeSamples = 1024; @@ -37,7 +49,7 @@ static std::vector Reference() { int offset = 0; for (std::uint32_t frame = 0; frame < 4; frame++) { int used = 0; - Require(Atrac9DecodeF32(decoder, Superframe + offset, pcm.data() + frame * 256, &used, 0) == 0); + Require(Atrac9DecodeF32(decoder, Superframe + offset, static_cast(SuperframeBytes) - offset, pcm.data() + frame * 256, &used, 0) == 0); offset += used; } Atrac9ReleaseHandle(decoder); @@ -178,11 +190,52 @@ static void TestCalcBlock() { Require(sceNgs2CalcWaveformBlock(&silent, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static std::uint8_t* SuperframeBeforeGuardPage() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t page = info.dwPageSize; + auto* region = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + DWORD previous = 0; + Require(region != nullptr && VirtualProtect(region + page, page, PAGE_NOACCESS, &previous) != 0); +#else + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* mapping = mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapping != MAP_FAILED); + auto* region = static_cast(mapping); + Require(mprotect(region + page, page, PROT_NONE) == 0); +#endif + auto* superframe = region + page - SuperframeBytes; + std::mt19937 random(50); + for (std::uint32_t i = 0; i < SuperframeBytes; i++) superframe[i] = static_cast(random()); + return superframe; +} + +static void TestCorruptSuperframeAtPageEnd() { + const auto* superframe = SuperframeBeforeGuardPage(); + const auto system = CreateSystem(); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}}); + const Ngs2WaveformBlock block{0, SuperframeBytes, 0, 0, SuperframeSamples, 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, superframe, 0, 1, &block}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + Patch(voice, Mastering(system, 1)); + bool rejected = false; + try { + Render(system, Grain); + } catch (const std::runtime_error& error) { + rejected = std::strstr(error.what(), "ATRAC9 decode failed") != nullptr; + } + Require(rejected); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { const auto reference = Reference(); TestSkipAndBlockEnd(reference); TestRepeatAndState(reference); TestParse(); TestCalcBlock(); + TestCorruptSuperframeAtPageEnd(); return 0; }