fix(audio): harden AudioOut2 stack out-buffer writes against canary smash (#532)

Titles that stack-allocate AudioOut2 outs next to the frame canary were
corrupted by oversized or mistyped HLE writes; keep ContextPush pacing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MarcelMediaDev
2026-07-23 13:34:40 +01:00
committed by GitHub
parent 956da769a3
commit e13cb28267
5 changed files with 657 additions and 72 deletions
@@ -0,0 +1,86 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Audio;
using Xunit;
namespace SharpEmu.Libs.Tests.Audio;
public sealed class AudioOut2PortGetStateExportsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong StateAddress = MemoryBase + 0x100;
private static CpuContext CreateContext(out FakeCpuMemory memory)
{
memory = new FakeCpuMemory(MemoryBase, 0x1000);
return new CpuContext(memory, Generation.Gen5);
}
[Fact]
public void PortGetState_WritesFixedSizeIgnoringPollutedR9()
{
var ctx = CreateContext(out var memory);
// Paint the buffer so we can see the write footprint.
Span<byte> paint = stackalloc byte[0x100];
paint.Fill(0xAB);
Assert.True(memory.TryWrite(StateAddress, paint));
ctx[CpuRegister.Rdi] = 0xDE1FF6800001UL;
ctx[CpuRegister.Rsi] = StateAddress;
ctx[CpuRegister.Rdx] = StateAddress + 0x200;
// Polluted GetSize leftover — must NOT enlarge the write.
ctx[CpuRegister.R9] = 0x180;
var result = AudioOut2Exports.AudioOut2PortGetState(ctx);
Assert.Equal(0, result);
Span<byte> state = stackalloc byte[0x100];
Assert.True(memory.TryRead(StateAddress, state));
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state));
Assert.Equal(2, state[2]);
// Bytes past the fixed 0x20 header must remain untouched.
Assert.Equal(0xAB, state[0x20]);
Assert.Equal(0xAB, state[0x7F]);
}
[Fact]
public void PortGetState_SkipsGuestStackOutBuffer()
{
var ctx = CreateContext(out _);
const ulong stackOut = 0x00007FFFDE1FF688UL;
ctx[CpuRegister.Rdi] = 0xDE1FF688004DUL;
ctx[CpuRegister.Rsi] = stackOut;
ctx[CpuRegister.Rdx] = 0;
var result = AudioOut2Exports.AudioOut2PortGetState(ctx);
Assert.Equal(0, result);
}
[Fact]
public void GetSpeakerInfo_WritesFixedSizeToRdiNotRsiTypeFlag()
{
var ctx = CreateContext(out var memory);
Span<byte> paint = stackalloc byte[0x80];
paint.Fill(0xCD);
Assert.True(memory.TryWrite(StateAddress, paint));
ctx[CpuRegister.Rdi] = StateAddress;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = StateAddress + 0x200;
ctx[CpuRegister.R8] = 0x840;
ctx[CpuRegister.R9] = 0x10C;
var result = AudioOut2Exports.AudioOut2GetSpeakerInfo(ctx);
Assert.Equal(0, result);
Span<byte> info = stackalloc byte[0x80];
Assert.True(memory.TryRead(StateAddress, info));
Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(info));
Assert.Equal(48000u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
Assert.Equal(0xCD, info[0x20]);
}
}
@@ -0,0 +1,140 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Audio;
using Xunit;
namespace SharpEmu.Libs.Tests.Audio;
public sealed class AudioOut2SpeakerArrayExportsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong OutHandleAddress = MemoryBase + 0x100;
private const ulong ReservedAddress = MemoryBase + 0x120;
private const ulong ParamAddress = MemoryBase + 0x200;
private const ulong SpeakerMemoryAddress = MemoryBase + 0x400;
private static CpuContext CreateContext(out FakeCpuMemory memory)
{
memory = new FakeCpuMemory(MemoryBase, 0x2000);
return new CpuContext(memory, Generation.Gen5);
}
private static void WriteU64(FakeCpuMemory memory, ulong address, ulong value)
{
Span<byte> bytes = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
Assert.True(memory.TryWrite(address, bytes));
}
private static ulong ReadU64(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[8];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
private static uint ReadU32(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[4];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
}
[Fact]
public void GetSpeakerArrayMemorySize_NeverReturnsTheNotFoundSentinel()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = 8;
var result = AudioOut2Exports.AudioOut2GetSpeakerArrayMemorySize(ctx);
Assert.NotEqual((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
Assert.Equal(0x40 + 8 * 0x100 + 0x400, result);
Assert.Equal((ulong)result, ctx[CpuRegister.Rax]);
Assert.True(result < 0x10000);
}
[Fact]
public void GetSpeakerArrayMemorySize_TwoChannelsIsExactChannelScaledSize()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = 2;
var result = AudioOut2Exports.AudioOut2GetSpeakerArrayMemorySize(ctx);
Assert.Equal(0x40 + 2 * 0x100 + 0x400, result);
Assert.Equal(0x640UL, ctx[CpuRegister.Rax]);
}
[Fact]
public void SpeakerArrayCreate_PublishesObjectPointerAndLeavesReservedSizeAlone()
{
var ctx = CreateContext(out var memory);
// Stage a size in the reserved slot the way callers do before Create.
WriteU64(memory, ReservedAddress, 0x100);
ctx[CpuRegister.Rdi] = ParamAddress;
ctx[CpuRegister.Rsi] = OutHandleAddress;
ctx[CpuRegister.Rdx] = ReservedAddress;
ctx[CpuRegister.Rcx] = 2;
var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx);
Assert.Equal(0, result);
Assert.NotEqual(0UL, ctx[CpuRegister.Rax]);
Assert.NotEqual(0x100UL, ctx[CpuRegister.Rax]);
Assert.Equal(ctx[CpuRegister.Rax], ReadU64(memory, OutHandleAddress));
// Reserved/size slot must remain untouched — writing it corrupted canaries.
Assert.Equal(0x100UL, ReadU64(memory, ReservedAddress));
}
[Fact]
public void SpeakerArrayCreate_PublishesHandleForTypicalCallShape()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = ParamAddress;
ctx[CpuRegister.Rsi] = OutHandleAddress;
ctx[CpuRegister.Rdx] = ReservedAddress;
ctx[CpuRegister.Rcx] = 2;
var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx);
Assert.Equal(0, result);
Assert.NotEqual(0UL, ctx[CpuRegister.Rax]);
Assert.NotEqual(0x10000UL, ctx[CpuRegister.Rax]);
Assert.NotEqual((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, result);
}
[Fact]
public void SpeakerArrayCreate_IgnoresCorruptedParamBufferFields()
{
var ctx = CreateContext(out var memory);
// Simulate PortGetState having overwritten param+0x18 (size) with a
// state blob — Create must NOT adopt that as an in-place buffer.
WriteU64(memory, ParamAddress + 0x10, SpeakerMemoryAddress);
WriteU64(memory, ParamAddress + 0x18, 0x100);
ctx[CpuRegister.Rdi] = ParamAddress;
ctx[CpuRegister.Rsi] = OutHandleAddress;
ctx[CpuRegister.Rdx] = ReservedAddress;
ctx[CpuRegister.Rcx] = 2;
var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx);
Assert.Equal(0, result);
Assert.NotEqual(SpeakerMemoryAddress, ctx[CpuRegister.Rax]);
Assert.NotEqual(0x100UL, ctx[CpuRegister.Rax]);
}
[Fact]
public void SpeakerArrayDestroy_UnknownHandleStillSucceeds()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = 0xDEAD_BEEF;
var result = AudioOut2Exports.AudioOut2SpeakerArrayDestroy(ctx);
Assert.Equal(0, result);
}
}
+31 -1
View File
@@ -7,15 +7,18 @@ namespace SharpEmu.Libs.Tests;
// A single contiguous guest region backed by a byte[]. Enough to hand C strings and small
// structures to HLE exports under test without a live guest.
internal sealed class FakeCpuMemory : ICpuMemory
internal sealed class FakeCpuMemory : ICpuMemory, IGuestMemoryAllocator
{
private readonly ulong _base;
private readonly byte[] _storage;
private ulong _allocBump;
public FakeCpuMemory(ulong baseAddress, int size)
{
_base = baseAddress;
_storage = new byte[size];
// Bump from the top so test fixtures at low offsets stay intact.
_allocBump = baseAddress + (ulong)size;
}
public bool TryRead(ulong virtualAddress, Span<byte> destination)
@@ -40,6 +43,33 @@ internal sealed class FakeCpuMemory : ICpuMemory
return true;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
address = 0;
if (size == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0)
{
return false;
}
var alignedSize = (size + alignment - 1) & ~(alignment - 1);
if (alignedSize > _allocBump - _base)
{
return false;
}
var next = (_allocBump - alignedSize) & ~(alignment - 1);
if (next < _base)
{
return false;
}
_allocBump = next;
address = next;
return true;
}
public bool TryFreeGuestMemory(ulong address) => false;
public ulong WriteCString(ulong virtualAddress, string text)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(text);