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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb
This commit is contained in:
Alberto Ruiz 2026-10-03 14:25:49 +02:00
parent d3ec1f15fb
commit 9879deca69
5 changed files with 61 additions and 8 deletions

2
3rdparty/LibAtrac9 vendored

@ -1 +1 @@
Subproject commit ec8899dadf393f655f2871a94e0fe4b3d6220c9a
Subproject commit 946e05a9212976626a9f5e52f29c0a7202871f29

View File

@ -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<short*>(pcm.data()), &used, 0); break;
case 1: decodeStatus = Atrac9DecodeS32(instance.decoder, input.data() + consumed, reinterpret_cast<int*>(pcm.data()), &used, 0); break;
default: decodeStatus = Atrac9DecodeF32(instance.decoder, input.data() + consumed, reinterpret_cast<float*>(pcm.data()), &used, 0); break;
case 0: decodeStatus = Atrac9Decode(instance.decoder, input.data() + consumed, static_cast<int>(instance.superframeRemaining), reinterpret_cast<short*>(pcm.data()), &used, 0); break;
case 1: decodeStatus = Atrac9DecodeS32(instance.decoder, input.data() + consumed, static_cast<int>(instance.superframeRemaining), reinterpret_cast<int*>(pcm.data()), &used, 0); break;
default: decodeStatus = Atrac9DecodeF32(instance.decoder, input.data() + consumed, static_cast<int>(instance.superframeRemaining), reinterpret_cast<float*>(pcm.data()), &used, 0); break;
}
if (decodeStatus != 0 || used <= 0 || static_cast<std::uint32_t>(used) > instance.superframeRemaining) {
result |= AJM_RESULT_INVALID_DATA;

View File

@ -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<float*>(pcm + result.produced), &used, 0)
: Atrac9Decode(handle, data + offset, reinterpret_cast<short*>(pcm + result.produced), &used, 0);
? Atrac9DecodeF32(handle, data + offset, static_cast<int>(superframeSize - offset), reinterpret_cast<float*>(pcm + result.produced), &used, 0)
: Atrac9Decode(handle, data + offset, static_cast<int>(superframeSize - offset), reinterpret_cast<short*>(pcm + result.produced), &used, 0);
if (status != 0 || used <= 0 || offset + static_cast<std::size_t>(used) > superframeSize) {
Reset();
return {DecodeStatus::InvalidData};

View File

@ -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<int>(atrac9.superframeBytes - consumed), atrac9.window.data() + end, &used, 0);
consumed += static_cast<std::size_t>(std::max(used, 0));
if (status != 0 || consumed > atrac9.superframeBytes) throw std::runtime_error("NGS2: ATRAC9 decode failed with " + Ngs2Hex(static_cast<std::uint32_t>(status)));
}

View File

@ -4,8 +4,20 @@
#include <cstdint>
#include <cstring>
#include <random>
#include <stdexcept>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#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<float> 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<int>(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<std::uint8_t*>(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<std::size_t>(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<std::uint8_t*>(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<std::uint8_t>(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;
}